atxdba
atxdba

Reputation: 5216

Escaping a literal asterisk as part of a command

Sample bash script

QRY="select * from mysql"
CMD="mysql -e \"$QRY\""
`$CMD`

I get errors because the * is getting evaluated as a glob (enumerating) files in my CWD.

I Have seen other posts that talk about quoting the "$CMD" reference for purposes of echo output, but in this case

"$CMD"

complains the whole literal string as a command.

If I

echo "$CMD"

And then copy/paste it to the command line, things seems to work.

Upvotes: 2

Views: 3398

Answers (2)

mklement0
mklement0

Reputation: 437238

Note: anubhava's answer has the right solution.
This answer provides background information.

As for why your approach didn't work:

"$CMD" doesn't work, because bash sees the entire value as a single token that it interprets as a command name, which obviously fails.


`$CMD` 

i.e., enclosing $CMD in backticks, is pointless in this case (and will have unintended side effects if the command produces stdout output[1]); using just:

$CMD

yields the same - broken - result (only more efficiently - by enclosing in backticks, you needlessly create a subshell; use backticks - or, better, $(...) only when embedding one command in another - see command substitution).


$CMD doesn't work,

  • because unquoted use of * subjects it to pathname expansion (globbing) - among other shell expansions.
  • \-escaping glob chars. in the string causes the \ to be preserved when the string is executed.

While it may seem that you've enclosed the * in double quotes by placing it (indirectly) between escaped double quotes (\"$QRY\") inside a double-quoted string, the shell does not see what's between these escaped double quotes as a single, double-quoted string.

Instead, these double quotes become literal parts of the tokens they abut, and the shell still performs word splitting (parsing into separate arguments by whitespace) on the string, and expansions such as globbing on the resulting tokens.

If we assume for a moment that globbing is turned off (via set -f), here is the breakdown of the arguments passed to mysql when the shell evaluates (unquoted) $CMD:

  • -e # $1 - all remaining arguments are the unintentionally split SQL command.
  • "select # $2 - note that " has become a literal part of the argument
  • * # $3
  • from # $4
  • mysql" # $5 - note that " has become a literal part of the argument

The only way to get your solution to work with the existing, single string variable is to use eval as follows:

eval "$CMD"

That way, the embedded escaped double-quoted string is properly parsed as a single, double-quoted string (to which no globbing is applied), which (after quote removal) is passed as a single argument to mysql.

However, eval is generally to be avoided due to its security implications (if you don't (fully) control the string's content, arbitrary commands could be executed).

Again, refer to anubhava's answer for the proper solution.


[1] A note re using `$CMD` as a command by itself:
It causes bash to execute stdout output from $CMD as another command, which is rarely the intent, and will typically result in a broken command or, worse, a command with unintended effects.
Try running `echo ha` (with the backticks - same as: $(echo ha)); you'll get -bash: ha: command not found, because bash tries to execute the command's output - ha - as a command, which fails.

Upvotes: 1

anubhava
anubhava

Reputation: 784998

You can just use:

qry='select * from db'
mysql -e "$qry"

This will not subject to * expansion by shell.

If you want to store mysql command line also then use BASH arrays:

cmd=(mysql -e "$qry")
"${cmd[@]}"

Upvotes: 4

Related Questions