21

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.

poige
  • 6,231
muru
  • 72,889
  • As a side note, I have seen the use of - for the $0 argument as an idiom, like in sh -c 'foo $1 $2' - a b. This way it looks pretty normal (once you found out what that - means, that is) – Volker Siegel Aug 27 '14 at 11:21
  • You can also echo 'echo the other side of this pipe globs "$@"' | sh -s -- *, though, unfortunately, $0 is generally not a settable parameter with the -stream option... It can be used in many of the same ways xargs generally is, though. And others besides. – mikeserv Aug 27 '14 at 11:48
  • @VolkerSiegel More normal would have been --, then that could have had the usual interpretation of 'from here starts the arguments', seen in some other programs. Then again, that could be confuse those unfamiliar with -c to think -- actually does have that interpretation. – muru Aug 27 '14 at 12:00

2 Answers2

16

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
  • I just ran: bash -c 'echo txt files are "$@"' meaningful-arg0-for-error *.txt on Ubuntu 14.04, bash version 4.3.11(1)-release, and I got: txt files are *.txt. O.o – muru Aug 27 '14 at 10:51
  • 3
    @muru, which is correct (and you probably don't have txt files in the current directory). See also bash -c 'echo "${1?}"' foo – Stéphane Chazelas Aug 27 '14 at 10:59
  • Ah, yes. That's an practically useful example. – muru Aug 27 '14 at 11:19
  • 2
    sh -c 'shift "$2"; echo txt files are "$@"' tentative-arg0 3 2 *.txt is fantastically inventive! – iruvar Aug 27 '14 at 13:02
  • Or you use a completely robust workaround (suggested by Stéphane Chazelas in comp.unix.shell) SHELL -c 'shift $1; command' 2 1 arg1 arg2 ...

    haha...

    – mikeserv Aug 27 '14 at 15:57
  • @StéphaneChazelas what is the purpose of adding ? directly after ${1? Bash has notation ${…:?…} which is documented and look similarly. – poige May 22 '20 at 09:38
  • @poige, bash has both ${var?error} and ${var:?error} like all Bourne-like shells. Note the if the colon is omitted, the operator tests only for existence in the bash manual. – Stéphane Chazelas May 22 '20 at 10:31
  • @StéphaneChazelas I don't see the quoted text in any bash manual I have — neither in macOS, nor in Linux. man bash | grep -i existence finds no occurrences. – poige May 22 '20 at 12:31
  • 1
    @poige, check in info bash 'expansion, parameter'. In the man page on Ubuntu, I find Omitting the colon results in a test only for a parameter that is unset – Stéphane Chazelas May 22 '20 at 12:41
5

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.

Michael Homer
  • 76,565
  • Can you give an (hopefully real-world) example of the second para? – muru Aug 27 '14 at 10:40
  • Careful with the analogy with argv[0]. $0 is only set to the argv[0] passed to execve() when it's like sh or bash. For scripts, $0 is set to the path given as argument 1, 2... to the interpreter (and for scripts executed directly, that comes from the first path argument to execve()). – Stéphane Chazelas Aug 27 '14 at 11:06