5

I know about $*, $@, "$@" and even ${1+"@"} and what they mean.

I need to get access to the EXACT command-line arguments string from a shell script. Please pay attention to the quotes in the example. Anything like "$@" saves parameters but removes quotes and I don't see how to recover from this.

Example:

./my-shell.sh "1 2" 3

And I need to retrieve the EXACT parameter string without any processing:

"1 2" 3

Any idea how to achieve this?

Kirk Roybal
  • 17,273
  • 1
  • 29
  • 38
Roman Nikitchenko
  • 12,800
  • 7
  • 74
  • 110
  • Are you capable of getting the command line arguments `1 2` and `3` separately? If you can, then just add the quotes back to the first parameter, and then mash the two parameters back together. – Robert Harvey May 28 '14 at 16:01
  • possible duplicate of [Preserving quotes in bash function parameters](http://stackoverflow.com/questions/3260920/preserving-quotes-in-bash-function-parameters) – rthbound May 28 '14 at 16:12
  • @RobertHarvey But how could I distinguish between quoted and unquoted parameter in case of `"1 2" 3` ? I just need 'raw' string. And if I receive `1 2 4` I really need `1 2 4`, not `1 2 4`. – Roman Nikitchenko May 28 '14 at 16:25
  • If `1 2` is quoted, don't you get that as a separate parameter anyway? – Robert Harvey May 28 '14 at 16:26
  • @rthbound Thank you for point. Not duplicate but close. Too bad solution actually VERY specific, depends on quota type (single or dable) and does not solve `1 2 4` case as it is based on position parameters. – Roman Nikitchenko May 28 '14 at 16:26
  • @RobertHarvey ... but if it is not quoted? I just need plain arguments line, not even arguments separated. – Roman Nikitchenko May 28 '14 at 16:28
  • The shell won't give you the whole string. You'll have to figure out how to reconstruct it the way you want from whatever the shell gives you. – Robert Harvey May 28 '14 at 16:28
  • @RobertHarvey Sad but looks true. Just impossible to print original CLI string ... – Roman Nikitchenko May 28 '14 at 16:30
  • `while ([ "$1" ]); do check_for_space $1; shift;done` if any parameter has white space, it must have been quoted (no way to tell if it is single or double) or you can `cat /proc/$$/cmdline` ... where $$ is the shell's pid. The /proc/pid/cmdline will work with other shells (perl, python) too - even other processes that you know the pid for. – technosaurus May 28 '14 at 17:30
  • 2
    It's inherently impossible, the shell that calls the shell script interprets the command-line string and passes arguments to the shell script accordingly. To put it another way, no Unix program ever has access to the exact command-line by which it was invoked, only to the arguments that command-line contained. To have a look at how this works in practice, I recommend `strace -f`ing your shell as it launches a shell script; pay particular attention to the `execve` call that transfers control of the child process to the script. – Aaron Davies May 28 '14 at 19:22

2 Answers2

10

In bash, you can get this from the shell history:

set -o history
shopt -s expand_aliases

function myhack {
  line=$(history 1)
  line=${line#*[0-9]  }
  echo "You wrote: $line"
}
alias myhack='myhack #'

Which works as you describe:

$ myhack --args="stuff" * {1..10}    $PATH
You wrote: myhack --args="stuff" * {1..10}    $PATH

Also, here's a handy diagram:

that other guy
  • 116,971
  • 11
  • 170
  • 194
  • 1
    Both aliases and history can be enabled in a script; they are just turned off by default. – chepner May 28 '14 at 19:36
  • 1
    Cute diagram. But I need to exactly reproduce a user-entered command line to be able to programmatically re-execute it in case of failure. I think that falls into “other”. Oh, sure, I *could* write my own algorithm to correctly reproduce (nested) quotes as necessary. But if that idea doesn’t set off alarm bells I don’t know what would. (And unfortunately your solution doesn’t work for me since history is turned off and turning it on inside the script is too late.) – Konrad Rudolph Nov 16 '16 at 11:22
  • @KonradRudolph It might if you want to capture non-parameters (like redirections and pipelines) or you want to re-evaluate code (so that rerunning `yourscript "$(date +%F).$RANDOM.txt"` will use a new date and random number than the original run). – that other guy Nov 16 '16 at 16:43
  • @KonradRudolph, why do you need to reproduce the input, as opposed to reproducing the array that input parses to, or generating a different possible input that parses to that same array? – Charles Duffy Apr 04 '19 at 17:35
  • @CharlesDuffy Because said array does not contain redirection nor other parts of a command pipeline. – Konrad Rudolph Apr 04 '19 at 19:14
  • @KonradRudolph, the program that should be responsible for executing the full pipeline on failure is the same program that started the pipeline in the first place. Otherwise, your two executions are happening at different places in the process tree, invoked by programs with different internal state, potentially different signal handlers, etc; you talk about ideas "setting off alarm bells", and that one's ringing them for me. – Charles Duffy Apr 04 '19 at 19:59
  • @KonradRudolph, think about it: If you run `foo | bar | baz`, and if `bar` fails expect it to run `foo | bar | baz` itself, then what you get in the case of that failure is `foo | { foo | bar | baz; } | baz`: When `bar` is first started, it inherits the stdin coming from `foo` and stdout going to `baz`, and doesn't *have* a file handle on the original stdin (from which `foo` would need to read to be faithful to its original instance) or the original stdout (to which `baz` should write). – Charles Duffy Apr 04 '19 at 20:00
  • @KonradRudolph, ...by contrast, if you just want to keep the original redirections *exactly as they are*, all you need to do is re-exec yourself, and *that* you can do with only `exec "$@"`; you aren't changing the redirections, so you don't need to know them. – Charles Duffy Apr 04 '19 at 20:06
  • @KonradRudolph, ...even if you wanted to capture-and-replay stdin, even *then* you don't need to know where it came from, you only need to know *what its contents are* (or, if it's seekable, you can just call `lseek()` back to whatever position you record as the original start-of-input and you're back at the beginning without needing to capture a copy at all). – Charles Duffy Apr 04 '19 at 20:11
  • @CharlesDuffy I get it. All this means that Bash makes it fundamentally impossible to use some rather nice abstractions. For example, in other languages there are ways of implementing a function such as `run_in_path path cmd`. In Bash this is essentially impossible to do robustly. The only way is to put all of `cmd` in quotes. This isn’t an academic problem, either. For instance, virtually every Unix job scheduler has this issue and solves it more or less robustly. Usually less. – Konrad Rudolph Apr 05 '19 at 10:22
  • @KonradRudolph, eh? The constraints I describe above are fundamental to the UNIX process model -- you can't reexec your whole pipeline *from a component inside that pipeline* in C either; as a single process, you can only reexec yourself. Thus, it's necessary to restart a pipeline from its parent process (which is how the UNIX job schedulers with restart support you mention work), no matter what language it's written in. – Charles Duffy Apr 05 '19 at 15:09
  • @KonradRudolph, ...and I don't know why you'd claim that `run_in_path` can't be done robustly in bash. `run_in_path() { local path; path=$1; shift; "$@"; }` and there you are. – Charles Duffy Apr 05 '19 at 15:11
  • @CharlesDuffy Serves me right for not being more precise. Yes, your `run_in_path` works for single commands. But an incredibly common use-case is something like this: `run_in_path sub/dir {cmd1; cmd2}`. – Konrad Rudolph Apr 05 '19 at 15:22
  • @KonradRudolph, ...sure; at that point, you use `myfunc() { cmd1; cmd2; }; run_in_path sub/dir myfunc`. I'll grant that you can't pass around *anonymous* blocks in bash (not the only language to lack anonymous functions, as valuable as they are!), but it's easy enough to name them. – Charles Duffy Apr 05 '19 at 15:28
  • @CharlesDuffy My point exactly. Obviously limitations can be worked around. That doesn’t mean said limitations don’t exist or aren’t vexing. – Konrad Rudolph Apr 05 '19 at 15:29
  • Yup. That said, if we were going to spend this comment thread complaining about languages that don't have anonymous functions, we'd have a whole lot of complaining to do. Heck, Java didn't have them before 1.8, and I didn't hear its users complaining (except for the ones who just migrated over to LISPs that compiled to JVM bytecode; incidentally, I count myself a member of that set). Almost every language that *isn't* a LISP (or similarly extensible) is hobbled by its choices around abstractions, from where I stand; room to complain about them is nearly endless, but doing so changes few minds. – Charles Duffy Apr 05 '19 at 15:30
0

But I need to exactly reproduce a user-entered command line to be able to programmatically re-execute it in case of failure

You don't need your exact command line for that; you can reconstruct an equivalent (even if not identical!) shell command yourself.

#!/usr/bin/env bash
printf -v cmd_q '%q ' "$@"
echo "Executed with a command identical to: $cmd_q"

...though you don't even need that, because "$@" can be re-executed as-is, without knowing what the command that started it was:

#!/usr/bin/env bash
printf "Program is starting; received command line equivalent to: "
printf '%q ' "$@"
printf '\n'

if [[ ! $already_restarted ]]; then
  echo "About to restart ourselves:
  exec "$@" # restart the program
fi
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441