Simple.guy
Simple.guy

Reputation: 374

Bash quirk when passing string from command line

This is an example I created to demonstrate the quirky behavior. I expected bash to pass the quoted command line argument as is.

john@doe:~/tmp$ cat script.sh 
#! /bin/bash
set -o xtrace
$1 sleep 3
john@doe:~/tmp$ ./script.sh "echo"
+ echo sleep 3
sleep 3
john@doe:~/tmp$ ./script.sh "echo -n"
+ echo -n sleep 3
sleep 3john@doe:~/tmp$

But sometimes bash edits my string argument, and add ticks around symbols like ; or &&

john@doe:~/tmp$ ./script.sh "echo hello ;"
+ echo hello ';' sleep 3
hello ; sleep 3
john@doe:~/tmp$ ./script.sh "echo hello && "
+ echo hello '&&' sleep 3
hello && sleep 3

What is this bash rule and how to circumvent it? thanks a lot :)

Upvotes: 4

Views: 395

Answers (2)

gniourf_gniourf
gniourf_gniourf

Reputation: 46903

The rule is outlined in the manual: Section Shell Operation.

You'll read:

The following is a brief description of the shell’s operation when it reads and executes a command. Basically, the shell does the following:

  1. Reads its input from a file (see Shell Scripts), from a string supplied as an argument to the -c invocation option (see Invoking Bash), or from the user’s terminal.
  2. Breaks the input into words and operators, obeying the quoting rules described in Quoting. These tokens are separated by metacharacters. Alias expansion is performed by this step (see Aliases).
  3. Parses the tokens into simple and compound commands (see Shell Commands).
  4. Performs the various shell expansions (see Shell Expansions), breaking the expanded tokens into lists of filenames (see Filename Expansion) and commands and arguments.
  5. Performs any necessary redirections (see Redirections) and removes the redirection operators and their operands from the argument list.
  6. Executes the command (see Executing Commands).
  7. Optionally waits for the command to complete and collects its exit status (see Exit Status).

So, with you script:

#! /bin/bash

$1 sleep 3

What happens when you execute ./script "echo hello ;"?

  1. Bash reads your script.
  2. It breaks input into words and operators. Note that at this point, Bash doesn't see the control operator ; that is in your argument echo hello ;. At this point, it only sees the three tokens $1, sleep and 3.
  3. It parses the tokens into simple and compound commands. There are no simple nor compound commands yet.
  4. It performs the shell and filename expansions: at this point, your token $1 is expanded into echo, hello and ; (not that the semicolon here just becomes an argument, it's not understood as a control operator, as the scanning of control operators was already done in step 2 and will not be performed anymore).
  5. There are no redirections to perform here.
  6. Bash now executes the command: it sees the command echo with arguments hello, ;, sleep and 3. So it happily echoes hello ; sleep 3.
  7. Collects exit status (very likely success) after executing echo.

There are 2 ways to circumvent this: the first one is to use eval, but this is dangerous and has many caveats: replace your script with

#!/bin/bash

eval "$1 sleep 3"

With this script:

$ ./script "echo hello;"
hello
$

(and the final prompt is displayed after a delay of 3 seconds). Same for ./script "echo hello &&". But this is not a good idea. Read on.

As a general rule, you should avoid (at least in Bash) mixing data and code: a variable is here to hold data (strings), not code. So this technique isn't a good one. I'm saying it's not a good one not only because someone once said that you shouldn't mix data with code, but because it will become unusable if you're starting to use special characters in your “code”. For example, if you want to echo the * symbol, you'll have to use the clunky call ./script "echo \"*\";". And there are many other caveats.

Code is held in functions or scripts. Using a function:

say_hello_and_execute() {
    echo hello; "$@"
}

Then export this function: export -f say_hello_and_execute and use it as an argument to your script: ./script say_hello_and_execute.

The same can be achieved with a script: call the following script say_hello_and_execute:

#!/bin/bash

echo hello; "$@"

then chmod +x say_hello_and_execute and run your script as ./script ./say_hello_and_execute.


This answer tries to answer your question. Though, it would be better if you could tell us exactly what you're trying to achieve, as there might be a better design to achieve that.

Upvotes: 1

Jens
Jens

Reputation: 72746

The rule is to quote metacharacters that would otherwise have a different interpretation. A semicolon is a statement separator, as is &&. The quoting makes it easy to cut and paste the particular line and have it reproduce the original effect.

Think about the difference of

echo hello '&&'

versus

echo hello &&

The first echos two words, the second waits for more input. The quoting you call quirky works as designed, is what you actually want, and no, there's no way around it short of hacking bash source code.

Upvotes: 1

Related Questions