Bash -c with positional parameters
Usually, $0
in a script is set to the name of the script, or to whatever it was invoked as (including the path). However, if I use bash
with the -c
option, $0
is set to the first of the arguments passed after the command string:
bash -c 'echo $0' foo bar
# foo
In effect, it seems like positional parameters have been shifted, but including $0
. However shift
in the command string doesn’t affect $0
(as normal):
bash -c 'echo $0; shift; echo $0' foo bar
# foo
# foo
Why this apparently odd behaviour for command strings?
Note that I am looking for the reason, the rationale, behind implementing such odd behaviour.
One could speculate that such a command string wouldn’t need the $0
parameter as usually defined, so for economy it is also used for normal arguments. However, in that case the behaviour of shift
is odd. Another possibility is that $0
is used to define the behaviour of programs (a la bash
called as sh
or vim
called as vi
), but that cannot be, since $0
here is only seen in the command string and not by programs called within it. I cannot think of any other uses for $0
, so I am at a loss to explain this.
This behaviour is defined by POSIX:
sh -c command_name [argument…]
Read commands from the command_string operand. Set the value of special parameter 0 (see Special Parameters) from the value of the command_name operand and the positional parameters ($1, $2, and so on) in sequence from the remaining argument operands.
As for why you’d want that behaviour: this smooths out the gap between a script and a -c
string. You can directly convert between the two without any change of behaviour. Other areas rely these being identical.
It’s also in line with how program arguments work in general: this ultimately comes down to calling one of the exec
functions, in which the first provided argument is also $0
, and equally commonly that argument is the same as the executable you’re running. Sometimes, though, you want a special value there, and there’d just be no other way to get it. Given that the argument exists, it has to map to something, and the user needs to be able to set what that is.
This consistency (and likely historical accident) leads to the situation you find.
That gives you an opportunity to set/choose $0
when using an inline script. Otherwise, $0
would just be bash
.
Then you can do for instance:
$ echo foo > foo
$ bash -c 'wc -c < "${1?}"' getlength foo
4
$ rm -f bar
$ bash -c 'wc -c < "${1?}"' getlength bar
getlength: bar: No such file or directory
$ bash -c 'wc -c < "${1?}"' getlength
getlength: 1: parameter not set
Not all shells used to do that. The Bourne shell did. The Korn (and Almquist) shell chose to have the first parameter go to $1
instead. POSIX eventually went for the Bourne way, so ksh
and ash
derivatives reverted to that later (more on that at http://www.in-ulm.de/~mascheck/various/find/#shell). That meant that for a long time for sh
(which depending on the system was based on the Bourne, Almquist or Korn shell), you didn’t know whether the first argument went into $0
or $1
, so for portability, you had to do things like:
sh -c 'echo foo in "$1"' foo foo
Or:
sh -c 'shift "$2"; echo txt files are "$@"' tentative-arg0 3 2 *.txt
Thankfully, POSIX has specified the new behavior where the first argument goes in $0
, so we can now portably do:
sh -c 'echo txt files are "$@"' meaningful-arg0-for-error *.txt