53

Let's say that I want to run a command through Bash like this:

/bin/bash -c "ls -l"

According to Bash man page, I could also run it like this:

#               don't process arguments after this one
#               |   pass all unprocessed arguments to command
#               |   |
#               V   V
/bin/bash -c ls -- -l

except it doesn't seem to work (it seems ignored). Am I doing something wrong, or am I interpreting man page wrong?

Relevant quotes from man:

If the -c option is present, then commands are read from string. If there are arguments after the string, they are assigned to the positional parameters, starting with $0.

and

A -- signals the end of options and disables further option processing. Any arguments after the -- are treated as filenames and arguments.

3 Answers 3

67

You're interpreting the man page wrong. Firstly, the part about -- signalling the end of options is irrelevant to what you're trying to do. The -c overrides the rest of the command line from that point on, so that it's no longer going through bash's option handling at all, meaning that the -- would be passed through to the command, not handled by bash as an end of options marker.

The second mistake is that extra arguments are assigned as positional parameters to the shell process that's launched, not passed as arguments to the command. So, what you're trying to do could be done as one of:

/bin/bash -c 'echo "$0" "$1"' foo bar
/bin/bash -c 'echo "$@"' bash foo bar

In the first case, passing echo the parameters $0 and $1 explicitly, and in the second case, using "$@" to expand as normal as "all positional parameters except $0". Note that in that case we have to pass something to be used as $0 as well; I've chosen "bash" since that's what $0 would normally be, but anything else would work.

As for the reason it's done this way, instead of just passing any arguments you give directly to the command you list: note that the documentation says "commands are read from string", plural. In other words, this scheme allows you to do:

/bin/bash -c 'mkdir -p -- "$1" && cd -P -- "$1" && touch -- "$2"' bash dir file

But, note that a better way to meet your original goal might be to use env rather than bash:

/usr/bin/env -- "ls" "-l"

If you don't need any of the features that a shell is providing, there's no reason to use it - using env in this case will be faster, simpler, and less typing. And you don't have to think as hard to make sure it will safely handle filenames containing shell metacharacters or whitespace.

1
  • 5
    I think it's worthwhile to emphasize that you surrounded the script argument with single quotes and not double quotes, otherwise the shell in which you run /bin/bash would resolve any positional parameters in the script argument in the context of the active shell. For example, /bin/bash -c "echo '$0' '$1'" foo bar results in bash instead of foo bar. Commented Apr 18, 2020 at 20:22
7

I’m not sure what your objective is, but if you’re simply trying to build a Rube Goldberg machine – “a contraption, invention, device or apparatus that is deliberately over-engineered or overdone to perform a very simple task in a very complicated fashion” – then try

sh -c 'ls $0' -l

or

sh -c 'ls $1' supercalifragilisticexpialidocious -l

or even

sh -c 'ls -$0' l

You should be able to understand how these work from godlygeek’s answer.  As Derek Mahar commented, it’s important to emphasize that the script argument (the command string after -c) must be enclosed with single quotes and not double quotes.  Otherwise the parent shell (in which you type the above command lines) would resolve any positional parameters in the script argument in the context of the parent shell.  For example,

$ set -- The quick brown fox
$ sh -c 'echo "$0" "$1"' foo bar baz
foo bar
$ sh -c "echo '$0' '$1'" foo bar baz
bash The
-2

It's simpler than all that, and you were close.

Escape your whitespace so the whole command is passed as a single string. Your script will look like something this:

bash -c "ls\ -l\ $@"

Additional commands can be added separated by unescaped whitespace, e.g.

#                  ↓            unescaped
bash -c "cd\ ~/junk ls\ -l\ $@"
#          ↑↑         ↑↑  ↑↑    escaped

The space between junk and ls in that command is the only one that is unescaped, and so it serves to separate the two commands.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .