21

I need to run a command with a syntax like this:
runuser -l userNameHere -c '/path/to/command arg1 arg2'

Unfortunately, I have to nest additional ' characters into the command itself and I can't tell bash to interpret these correctly. The command I would like to run is actually:

runuser -l miner -c 'screen -S Mine -p 0 -X eval 'stuff "pwd"\015''

Unfortunately, bash seems to be hitting the second ' and puking. This is the error:
-bash: screen -S Mine -p 0 -X eval stuff: No such file or directory, so obviously it's not getting past the '.

How can I nest this as one command? Thank you!

user1833028
  • 865
  • 1
  • 9
  • 18

5 Answers5

22

The command itself doesn't see the outer quotes. The shell uses those to avoid wordsplitting, but doesn't let the command know they were there. So if you have:

./command foo bar "baz biz"

The command sees three args, foo, bar and baz biz with whitespace intact, but no quotes. Thus, if you need to actually send quotes, you can do that by wrapping the argument with the other kind of quote:

./command "foo'bar"

The command sees one arg: foo'bar. But if you need to send both kinds of quotes, you have a harder problem to solve. You can solve it with leaning toothpicks, quote-swapping or variables:

Quote swapping

Even though the shell uses quotes to avoid wordsplitting, if you put the quoted arguments next to each other without whitespace the command will see it as one word:

./command "foo""bar"

The command sees one arg: foobar. So if you use two different kinds of quotes:

./command 'foo"bar'"baz'quux"

The command sees one arg: foo"barbaz'quux.

Leaning toothpicks

There are two kinds of leaning toothpicks. One is really just quote swapping except you don't use quotes to wrap one of the …quotes.

./command 'foo"barbaz'\'quux

The command sees one arg: foo"barbaz'quux. The other is (hat tip: chepner) to use the special $'string' form of word expansion, which allows ANSI C strings:

./command $'foo"barbaz\'quux'

The command sees one arg: foo"barbaz'quux.

Variables

doublequote=\" singlequote=\'
./command "foo${doublequote}barbaz${singlequote}quux"

The command sees one arg: foo"barbaz'quux.

Community
  • 1
  • 1
kojiro
  • 74,557
  • 19
  • 143
  • 201
15

You can use another type of quoting supported by bash, $'...'. This can contain escaped single quotes.

runuser -l miner $'screen -S Mine -p 0 -X eval \'stuff "pwd"\015\''

Note that within $'...', the \015 will be treated replaced with the actual ASCII character at codepoint 015, so if that's not what you want, you'll need to escape the backslash as well.

runuser -l miner $'screen -S Mine -p 0 -X eval \'stuff "pwd"\\015\''

I think you can take advantage of the $'...' to remove the need for eval as well:

runuser -l miner $'screen -S Mine -p 0 -X stuff "pwd"\015'
chepner
  • 497,756
  • 71
  • 530
  • 681
8

You cannot nest single quotes within single quotes. You can nest double quotes within double quotes by escaping them though.

The best you can do with single quotes is to use '\'' wherever you need an embedded single quote.

That's a single quote to end the single quoted string. An unquoted, but escaped, single quote. And a single quote to start the next single quoted string.

Etan Reisner
  • 77,877
  • 8
  • 106
  • 148
5

Instead of escaping things manually, you can ask the shell to do it for you and avoid the trouble (if your shell is bash; this is an extension not available in POSIX sh, so your shebang needs to be #!/bin/bash, not #!/bin/sh).

For instance:

printf -v command '%q ' /path/to/command arg1 arg2
printf -v outer_command '%q ' runasuser -l userNameHere -c "$command"
printf -v ssh_command '%q ' ssh hostname "$outer_command"

...will put a string in "$ssh_command" which, if evaluated, will run the inner command inside of runasuser inside ssh.


To provide some examples of how to use these variables, once constructed:

# this evaluates the command in the current shell in a sane way
ssh hostname "$outer_command"

# this evaluates the command in the current shell in an inefficient way
eval "$ssh_command"

# this evaluates the command in a new shell
bash -c "$ssh_command"

# this evaluates the command in a new shell as a different user
sudo -u username bash -c "$ssh_command"

# this writes the command to a script, and makes it executable
printf '%s\n' "#!/bin/bash" "$ssh_command" >run_ssh
chmod +x run_ssh

To provide some counterexamples of how NOT to use these variables, once constructed:

# DO NOT DO THESE: WILL NOT WORK
$ssh_command        # see http://mywiki.wooledge.org/BashFAQ/050
"$ssh_command"      # likewise
sudo "$ssh_command" # likewise
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • It is exactly what I looking for. Unfortunately it does not work: ```printf -v command '%q ' /usr/bin/date +'%F %X'; printf -v outer_command '%q ' sudo -u root "$command"...``` produce error: `bash: line 2: sudo -u root /usr/bin/date\ +%F\\\ %X\ : No such file or directory` – Hubbitus Apr 18 '16 at 22:55
  • 1
    @Hubbitus, yeah, `sudo -u root "$command"` isn't a correct invocation. `sudo -u root bash -c "$command"`, on the other hand, would work correctly. (Mind you, it's more layers of indirection than there's a good reason to have, but I presume that you're trying to demonstrate a point). – Charles Duffy Apr 18 '16 at 22:59
  • 1
    @Hubbitus, ...that's true because `sudo` passes its argv array directly through to its child (after parsing off leading arguments), whereas `ssh` (which the sample code here was built for) concatenates its arguments into a string which it passes through for remote `eval`. – Charles Duffy Apr 18 '16 at 23:02
  • fast reaction! But it still the same: http://paste.fedoraproject.org/357171/10205191, error: `bash: line 2: sudo -u root -c date\ +%F\\\ %X\ : command not found` – Hubbitus Apr 18 '16 at 23:02
  • `$ssh_command` is not `eval "$ssh_command"`. – Charles Duffy Apr 18 '16 at 23:02
  • ...which is to say: details matter. I'll amend the answer to demonstrate how to actually use the variables it creates. – Charles Duffy Apr 18 '16 at 23:04
  • Sure. On `eval "$ssh_command"` I got: sudo usage: `usage: sudo -h | -K | -k | -V...` – Hubbitus Apr 18 '16 at 23:04
  • `printf -v outer_command '%q ' sudo -u root bash -c "$command"` works! Thank you! Meantime could you please describe slightly more why? – Hubbitus Apr 18 '16 at 23:06
  • 1
    Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/109484/discussion-between-charles-duffy-and-hubbitus). – Charles Duffy Apr 18 '16 at 23:10
  • 1
    @Hubbitus, does the explanation in the chat session linked above help? – Charles Duffy Apr 18 '16 at 23:15
  • Mostly yes. Very thank you again. And sorry, I first time use chat there. – Hubbitus Apr 18 '16 at 23:26
2

See my answer here. Basically, you can't put single quotes inside single quotes - but you don't have to, because quotation marks aren't word delimiters, so you can step out of quotes with a close ', add a literal single quote now that you're outside of the quotes with \', and then go back into quotes with a new open ', all without terminating the current shell word.

Mark Reed
  • 91,912
  • 16
  • 138
  • 175