1

I'm very confused about calling a command in bash with paths as parameters, coming from bash variables.

I've narrowed down the question to this:

Given a directory with the following contents:

KOSTUNRIX:i18n jand$ ls -al
total 40
drwxr-xr-x  6 jand  staff   204 Mar  6 15:53 .
drwxr-xr-x  3 jand  staff   102 Mar  6 10:27 ..
-rw-r--r--  1 jand  staff  3159 Mar  6 10:47 README.md
-rwxr--r--  1 jand  staff  4504 Mar  6 15:47 diff.sh
-rwxr--r--  1 jand  staff  4080 Mar  6 13:43 takeIn.sh
-rwxr--r--  1 jand  staff   558 Mar  6 15:51 test.sh

The script test.sh:

#!/bin/bash

while [[ $# -gt 1 ]]
do
  key="$1"

  case ${key} in
      --from)
      echo "--from" $2
      from="$2"
      shift
      ;;
      --to)
      echo "--to" $2
      to="$2"
      shift
      ;;
      -c|--command)
      echo "--command" $2
      command="$2"
      shift
      ;;
      *)
      # unknown option
      echo ${key} "- huh?"
      ;;
  esac
  shift
done

pwd
echo $command
iCommand=${command/"\$from"/\"$from\"}
echo $iCommand
iCommand=${iCommand/"\$to"/\"$to\"}
echo ${iCommand}
${iCommand}
cp "./README.md" "./README.mdFORCED"

executing test.sh gives:

KOSTUNRIX:i18n jand$ ./test.sh --command "cp \$from \$to" --from ./README.md --to ./README.md2
--command cp $from $to
--from ./README.md
--to ./README.md2
[...]/i18n
cp $from $to
cp "./README.md" $to
cp "./README.md" "./README.md2"
cp: "./README.md": No such file or directory
KOSTUNRIX:i18n jand$ ls -al
total 48
drwxr-xr-x  7 jand  staff   238 Mar  6 15:57 .
drwxr-xr-x  3 jand  staff   102 Mar  6 10:27 ..
-rw-r--r--  1 jand  staff  3159 Mar  6 10:47 README.md
-rw-r--r--  1 jand  staff  3159 Mar  6 15:57 README.mdFORCED
-rwxr--r--  1 jand  staff  4504 Mar  6 15:47 diff.sh
-rwxr--r--  1 jand  staff  4080 Mar  6 13:43 takeIn.sh
-rwxr--r--  1 jand  staff   558 Mar  6 15:51 test.sh

After gathering the arguments (--from, --to, --command), I output for debugging:

  • pwd --> [...]/i18n
  • echo $command --> cp $from $to
  • echo $iCommand --> cp "./README.md" $to (from replaced)
  • echo $iCommand --> cp "./README.md" "./README.md2" (to replaced)

Next, I execute the last output. What bugs me is that this doesn't work. The response is:

cp: "./README.md": No such file or directory

Yet, when I copy/paste the previous string in the terminal, it works. And as a double check, I pasted a variation of that command in the script too, at the last line, and that works too, as you can see in the ls afterwards (README.mdFORCED exists).

Whatever a try with backquotes, ${}, $(), I can't get this to work.

(Note that is the simplest version of this question I came up with. The actual issue arose with doing this with paths that contain spaces -- but I gather that when this becomes clear, that might be resolved too).

Using absolute paths gives the same result:

KOSTUNRIX:i18n jand$ ./test.sh --command "cp \$from \$to" --from `pwd`/README.md --to `pwd`/README.md2
--command cp $from $to
--from /[...]/i18n/README.md
--to /[...]/i18n/README.md2
/[...]/i18n
cp $from $to
cp "/[...]/i18n/README.md" $to
cp "/[...]/i18n/README.md" "/[...]/i18n/README.md2"
cp: "/[...]/i18n/README.md": No such file or directory

Again, executing the copy/pasted command by hand

cp "/[...]/i18n/README.md" "/[...]/i18n/README.md2"

does work.

What am I missing?

8
  • Can you print the result of pwd? Commented Mar 6, 2015 at 15:25
  • It's in the output both times above. pwd --> [...]/i18n both times ([..] is the path to the directory on my disk). Commented Mar 6, 2015 at 15:37
  • 1
    Notice the quotes in the cp error? They aren't getting stripped. See mywiki.wooledge.org/BashFAQ/001 for why this approach (command in a string) is problematic. Commented Mar 6, 2015 at 15:40
  • Etan, is that link correct? That page doesn't seem to address this issue at all, nor "command in a string"? Commented Mar 6, 2015 at 16:06
  • 2
    BashFAQ #48 (on why eval is risky to use, mywiki.wooledge.org/BashFAQ/048) and BashFAQ #50 (for best practices on programatically assembling commands, at mywiki.wooledge.org/BashFAQ/050) are more on-point than #1 (which is focused on file IO). Commented Mar 6, 2015 at 22:47

2 Answers 2

3

Doing string substitution on code is always Bad Idea. Just as Bobby Tables can wreck havoc on SQL, his cousin Jimmy /tmp/$(rm -rf /)/ can wreck havoc on shell.

Now, there's a slightly safer way to do what you want: Don't mutate the code; instead, pass arguments to it through the environment.

That is to say:

from="$from" to="$to" eval "$command"

...WITHOUT changing $command from what a trusted user gave you. Then, it all depends on $command to be written safely: Should the value be 'cp "$from" "$to"', you'll be safe. (However, this is how that command would be correctly written in shell in general -- if folks are in the habit of writing cp $from $to in your organization, y'all have bigger problems).


Now, if you insist on doing string substitution on code, you can do that too -- but you'll need your shell to be bash, or something else with the printf %q extension, to sanitize the variables to be eval-safe, and you'll need to ensure that the places where substitution is taking place is not quoted in the command given. (In this case, 'cp $from $to' would actually be desirable).

printf -v from_str %q "$from"
printf -v to_str %q "$to"
iCommand=${command/'$from'/$from_str}
iCommand=${iCommand/'$to'/$to_str}
eval "$iCommand"

I rather strongly advise against this approach, since you're requiring users to write code that would be buggy in any other context -- anyone doing a security audit would be prone to flagging this script's users unless they dug in far enough to see what was being done to make the usage safe. Alternately, you could use different substitution variables -- ie. @from@ and @to@ -- to make it clear that the semantics at play aren't those of upstream bash.

Sign up to request clarification or add additional context in comments.

Comments

-1

Placing a variable on a line by itself is not typically enough to execute a command stored inside that variable.

Try eval $iCommand instead.

4 Comments

No, don't. It's bad advice.
Using eval on user-supplied input strings is a massive security hole. Even if you don't care about security in particular, it adds a huge complication in that many things will not work as expected unless you add correspondingly huge sanitization steps.
Also, it is in fact not true that a variable alone on a line cannot be used to execute a simple command. ideone.com/hcmN3E
"typically", I think, was enough of a hedge -- as the robust way to execute a simple command from a variable (without using eval) is to have that variable be an array rather than a scalar.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.