6

How to escape quotes and pipe?

#!/bin/bash
set -x
MYCMD="VBoxManage showvminfo --machinereadable $1 \| grep \'VMState=\"poweroff\"\'"
echo "`$MYCMD`"

Executed command :

++ VBoxManage showvminfo --machinereadable d667 '|' grep '\'\''VMState="poweroff"\'\'''

And finally getting this error:

Syntax error: Invalid parameter '|'
Jonas
  • 4,683
  • 4
  • 45
  • 81
  • Capturing the output of the command (using backquotes) and then `echo`ing that is an antipattern. Just run the command directly, and let its output go to the usual place. BTW, what's the actual reason you're storing the command in a variable, rather than just executing it even more directly? – Gordon Davisson Jan 17 '17 at 22:55
  • Related: *[How to escape single quotes within single quoted strings](https://stackoverflow.com/questions/1250079/)* – Peter Mortensen Aug 16 '23 at 19:20

2 Answers2

10

You don't; you would need to use eval to embed an arbitrary pipeline in a regular string parameter.

MYCMD="VBoxManage showvminfo --machinereadable \"$1\" | grep 'VMState=\"poweroff\"'"
eval "$MYCMD"

However, this is not recommended unless you are certain that the value of $1 will not cause problems. (If you need an explanation of what those risks might be, then you should not be using eval.)

Instead, define a shell function:

mycmd () {
    VBoxManage showvminfo --machinereadable "$1" | grep 'VMState="poweroff"'
}

mycmd "$1"
chepner
  • 497,756
  • 71
  • 530
  • 681
0

One really simple way to do it is by using an array or positional parameters.

Array-based solution :

# Build command
declare -a CMD_AND_ARGS=(command and args with normal quoting)

# Append arguments
CMD_AND_ARGS+=(more arguments quoted the normal way)

# Execute command
"${CMD_AND_ARGS[@]}"

Positional parameter-based solution:

# Create command
set -- command and args with normal quoting

# Append arguments
set -- "$@" more arguments quoted the normal way

# Execute command
"$@"

The nice thing about both solutions is you do not need to put quotes inside quotes, because expanding positional parameters or an array surrounded by double quotes does not cause word-splitting and expansion to be performed again.

Examples:

declare -a CMD=()
CMD=(ls "/dir/with spaces/in its name")
"$CMD"

set -- ls "/dir/with spaces/in its name"
"$@"

Note that in both cases, you get to build your command incrementally, for instance having conditional expressions (e.g. if/case) choosing to add different arguments depending on the flow of your script.

If you want to pipe a command to another, you will have to build each command separately (e.g. two arrays), as the | symbol cannot be used inside an array declaration unquoted, and once quoted will be treated as a string argument and will not cause piping to occur.

Fred
  • 6,590
  • 9
  • 20
  • 2
    Unfortunately, while the array approach works well for complex parameters to a single command, it won't work for pipes (or other redirects or compound commands) such as the one here. – Gordon Davisson Jan 17 '17 at 22:50
  • As I suggested in my post, you can use two arrays. Worst case, you can always force a pipe, but have the second array contain only "cat" when you want to do nothing at the second step. – Fred Jan 17 '17 at 23:41
  • By the way, while a function is a very good way to approach many simple problems, the general problem of programatically assembling command lines and passing them around as arguments is something very useful in my experience, so that approach is well worth learning, even if not always the right tool for the job. – Fred Jan 17 '17 at 23:43