170

I would like to store a command to use at a later time in a variable (not the output of the command, but the command itself).

I have a simple script as follows:

command="ls";
echo "Command: $command"; #Output is: Command: ls

b=`$command`;
echo $b; #Output is: public_html REV test... (command worked successfully)

However, when I try something a bit more complicated, it fails. For example, if I make

command="ls | grep -c '^'";

The output is:

Command: ls | grep -c '^'
ls: cannot access |: No such file or directory
ls: cannot access grep: No such file or directory
ls: cannot access '^': No such file or directory

How could I store such a command (with pipes/multiple commands) in a variable for later use?

Peter Mortensen
  • 30,030
  • 21
  • 100
  • 124
Benjamin
  • 2,468
  • 5
  • 19
  • 19

11 Answers11

201

Use eval:

x="ls | wc"
eval "$x"
y=$(eval "$x")
echo "$y"
Gilles 'SO- stop being evil'
  • 98,216
  • 36
  • 202
  • 244
Erik
  • 84,860
  • 12
  • 192
  • 185
  • 33
    $(...) is now recommended instead of `backticks`. y=$(eval $x) http://mywiki.wooledge.org/BashFAQ/082 – James Broadhead Mar 11 '12 at 20:35
  • while that has been confusing at start, it's running as expected in both ways. `eval` is fine. Thank you. – m3nda May 21 '15 at 03:09
  • 22
    `eval` is an acceptable practice **only** if you trust your variables' contents. If you're running, say, `x="ls $name | wc"` (or even `x="ls '$name' | wc"`), then this code is a fast track to injection or privilege escalation vulnerabilities if that variable can be set by someone with less privileges. (Iterating over all subdirectories in `/tmp`, for instance? You'd better trust every single user on the system to not make one called `$'/tmp/evil-$(rm -rf $HOME)\'$(rm -rf $HOME)\'/'`). – Charles Duffy Jun 01 '16 at 15:22
  • 18
    `eval` is a huge bug magnet that should never be recommended without a warning about the risk of unexpected parsing behavior (even without malicious strings, as in @CharlesDuffy's example). For example, try `x='echo $(( 6 * 7 ))'` and then `eval $x`. You might expect that to print "42", but it probably won't. Can you explain why it doesn't work? Can you explain why I said "probably"? If the answers to those questions aren't obvious to you, you should never touch `eval`. – Gordon Davisson Mar 25 '18 at 07:03
  • @GordonDavisson why... I have completely no idea (and I do follow what you said.. i took away `eval` in my script!) – Student Jun 24 '19 at 20:50
  • 1
    @Student, try running `set -x` beforehand to log the commands run, which will make it easier to see what's happening. – Charles Duffy Jun 27 '19 at 13:10
  • @CharlesDuffy that is a spectacular trick! I am pretty new to this, and did not learn it systematically. I think I should study some theory behind it for a while until I start making scripts again.. any good suggestions? – Student Jun 27 '19 at 13:20
  • The [POSIX sh specification](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html) is the canonical source. Beyond that, I'm fond of the Wooledge wiki, which beyond the [BashGuide](http://mywiki.wooledge.org/BashGuide) also hosts background such as the [BashParser](https://mywiki.wooledge.org/BashParser) page. – Charles Duffy Jun 27 '19 at 13:29
  • 2
    @Student I'd also recommend [shellcheck.net](https://www.shellcheck.net) for pointing out common mistakes (and bad habits you shouldn't pick up). – Gordon Davisson Jul 02 '19 at 16:39
89

Do not use eval! It has a major risk of introducing arbitrary code execution.

BashFAQ-50 - I'm trying to put a command in a variable, but the complex cases always fail.

Put it in an array and expand all the words with double-quotes "${arr[@]}" to not let the IFS split the words due to Word Splitting.

cmdArgs=()
cmdArgs=('date' '+%H:%M:%S')

and see the contents of the array inside. The declare -p allows you see the contents of the array inside with each command parameter in separate indices. If one such argument contains spaces, quoting inside while adding to the array will prevent it from getting split due to Word-Splitting.

declare -p cmdArgs
declare -a cmdArgs='([0]="date" [1]="+%H:%M:%S")'

and execute the commands as

"${cmdArgs[@]}"
23:15:18

(or) altogether use a bash function to run the command,

cmd() {
   date '+%H:%M:%S'
}

and call the function as just

cmd

POSIX sh has no arrays, so the closest you can come is to build up a list of elements in the positional parameters. Here's a POSIX sh way to run a mail program

# POSIX sh
# Usage: sendto subject address [address ...]
sendto() {
    subject=$1
    shift
    first=1
    for addr; do
        if [ "$first" = 1 ]; then set --; first=0; fi
        set -- "$@" --recipient="$addr"
    done
    if [ "$first" = 1 ]; then
        echo "usage: sendto subject address [address ...]"
        return 1
    fi
    MailTool --subject="$subject" "$@"
}

Note that this approach can only handle simple commands with no redirections. It can't handle redirections, pipelines, for/while loops, if statements, etc

Another common use case is when running curl with multiple header fields and payload. You can always define args like below and invoke curl on the expanded array content

curlArgs=('-H' "keyheader: value" '-H' "2ndkeyheader: 2ndvalue")
curl "${curlArgs[@]}"

Another example,

payload='{}'
hostURL='http://google.com'
authToken='someToken'
authHeader='Authorization:Bearer "'"$authToken"'"'

now that variables are defined, use an array to store your command args

curlCMD=(-X POST "$hostURL" --data "$payload" -H "Content-Type:application/json" -H "$authHeader")

and now do a proper quoted expansion

curl "${curlCMD[@]}"
Community
  • 1
  • 1
Inian
  • 71,145
  • 9
  • 121
  • 139
  • This does not work for me, I have tried `Command=('echo aaa | grep a')` and `"${Command[@]}"`, hoping it runs literally the command `echo aaa | grep a`. It doesn't. I wonder if there's a safe way replacing `eval`, but seems that each solution that has the same force as `eval` could be dangerous. Isn't it? – Student Jun 24 '19 at 21:10
  • In short, how does this work if the original string contains a pipe '|'? – Student Jun 24 '19 at 21:19
  • 2
    @Student, if your original string contains a pipe, then that string needs to go through the unsafe parts of the bash parser to be executed as code. Don't use a string in that case; use a function instead: `Command() { echo aaa | grep a; }` -- after which you can just run `Command`, or `result=$(Command)`, or the like. – Charles Duffy Jun 24 '19 at 22:24
  • @CharlesDuffy problem is it fails if I want to do `Command() {$1}; Command "echo aaa | grep a"` – Student Jun 24 '19 at 22:26
  • 1
    @Student, right; but that fails **intentionally**, because what you're asking to do *is inherently insecure*. – Charles Duffy Jun 24 '19 at 22:50
  • 2
    @Student: I've added a note at the last to mention it doesn't work under certain conditions – Inian Jun 25 '19 at 04:55
  • 1
    `with no redirections. It can't handle redirections, pipelines, for/while loops, if statements,`, but there is an if: if [ "$first" = 1 ]; then.. – Timo Nov 12 '20 at 19:15
  • 1
    @Timo It's saying that you can't pass in an `if` statement to `sendto`; the fact that the code of the implementation contains an `if` statement and some other logic is unrelated to this restriction. – tripleee Feb 18 '21 at 12:54
  • @tripleee, you mean pass logic as param to `sendto()` or as $1? – Timo Feb 18 '21 at 18:39
  • 2
    Yes, exactly. For example, you can't `sendto if true; then echo poo; fi` because it looks like you are sending `if true`, which in isolation is obviously a syntax error, and the following statements are unrelated to the `sendto` call. – tripleee Feb 18 '21 at 18:40
  • 2
    @tripleee: a bit kudos for offering the bounty, wish I could share it with you and other useful contributors ;) – Inian Feb 19 '21 at 10:29
40
var=$(echo "asdf")
echo $var
# => asdf

Using this method, the command is immediately evaluated and its return value is stored.

stored_date=$(date)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015

The same with backtick

stored_date=`date`
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015

Using eval in the $(...) will not make it evaluated later:

stored_date=$(eval "date")
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015

Using eval, it is evaluated when eval is used:

stored_date="date" # < storing the command itself
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:05 EST 2015
# (wait a few seconds)
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:16 EST 2015
#                     ^^ Time changed

In the above example, if you need to run a command with arguments, put them in the string you are storing:

stored_date="date -u"
# ...

For Bash scripts this is rarely relevant, but one last note. Be careful with eval. Eval only strings you control, never strings coming from an untrusted user or built from untrusted user input.

Peter Mortensen
  • 30,030
  • 21
  • 100
  • 124
Nate
  • 12,486
  • 4
  • 57
  • 77
  • This does not solve the original problem where the command contains a pipe '|'. – Student Jun 24 '19 at 21:20
  • @Nate, note that `eval $stored_date` may be fine enough when `stored_date` only contains `date`, but `eval "$stored_date"` is much more reliable. Run `str=$'printf \' * %s\\n\' *'; eval "$str"` with and without the quotes around the final `"$str"` for an example. :) – Charles Duffy Jun 24 '19 at 22:55
  • @CharlesDuffy Thanks, I forgot about quoting. I'll bet my linter would have complained had I bothered to run it. – Nate Jun 27 '19 at 13:04
  • Tangentially, that's a [useless `echo`](http://www.iki.fi/era/unix/award.html#echo) – tripleee Feb 12 '21 at 11:30
5

For bash, store your command like this:

command="ls | grep -c '^'"

Run your command like this:

echo $command | bash
Derek Hazell
  • 89
  • 1
  • 3
  • 3
    Not sure but perhaps this way of running the command has the same risks that the use of 'eval' has. – Derek Hazell Sep 19 '19 at 06:20
  • 1
    In addition, you are wrecking the contents of the variable by not quoting it when you `echo` it. If `command` was the string `cd /tmp && echo *` it will echo the files in the current directory, not in `/tmp`. See also [When to wrap quotes around a shell variable](https://stackoverflow.com/questions/10067266/when-to-wrap-quotes-around-a-shell-variable) – tripleee Feb 12 '21 at 11:28
1

Not sure why so many answers make it complicated! use alias [command] 'string to execute' example:

alias dir='ls -l'

./dir
[pretty list of files]
Dudi Boy
  • 4,105
  • 1
  • 13
  • 26
Cyberience
  • 784
  • 9
  • 15
0

I tried various different methods:

printexec() {
  printf -- "\033[1;37m$\033[0m"
  printf -- " %q" "$@"
  printf -- "\n"
  eval -- "$@"
  eval -- "$*"
  "$@"
  "$*"
}

Output:

$ printexec echo  -e "foo\n" bar
$ echo -e foo\\n bar
foon bar
foon bar
foo
 bar
bash: echo -e foo\n bar: command not found

As you can see, only the third one, "$@" gave the correct result.

mpen
  • 256,080
  • 255
  • 805
  • 1,172
  • What is the explanation for that? Why only the third one? Please respond by [editing (changing) your answer](https://stackoverflow.com/posts/58615498/edit), not here in comments (***without*** "Edit:", "Update:", or similar - the answer should appear as if it was written today). – Peter Mortensen Dec 05 '21 at 03:10
  • Not sure I care enough to investigate the intricacies of `eval`. IMO one of those 2 should have worked. – mpen Dec 05 '21 at 18:25
0

First of all, there are functions for this. But if you prefer variables then your task can be done like this:

$ cmd=ls

$ $cmd # works
file  file2  test

$ cmd='ls | grep file'

$ $cmd # not works
ls: cannot access '|': No such file or directory
ls: cannot access 'grep': No such file or directory
 file

$ bash -c $cmd # works
file  file2  test

$ bash -c "$cmd" # also works
file
file2

$ bash <<< $cmd
file
file2

$ bash <<< "$cmd"
file
file2

Or via a temporary file

$ tmp=$(mktemp)
$ echo "$cmd" > "$tmp"
$ chmod +x "$tmp"
$ "$tmp"
file
file2

$ rm "$tmp"
Peter Mortensen
  • 30,030
  • 21
  • 100
  • 124
Ivan
  • 4,442
  • 1
  • 11
  • 14
  • 1
    I suppose many people didn't notice the "First of all there are functions for this" you mentioned that is a correct pointer to the right direction IMHO: "Variables hold data. Functions hold code" – Pedro Apr 20 '21 at 13:04
0

I faced this problem with the following command:

awk '{printf "%s[%s]\n", $1, $3}' "input.txt"

I need to build this command dynamically:

The target file name input.txt is dynamic and may contain space.

The awk script inside {} braces printf "%s[%s]\n", $1, $3 is dynamic.

Challenge:

  1. Avoid extensive quote escaping logic if there are many " inside the awk script.
  2. Avoid parameter expansion for every $ field variable.

The solutions bellow with eval command and associative arrays do not work. Due to bash variable expansions and quoting.

Solution:

Build bash variable dynamically, avoid bash expansions, use printf template.

 # dynamic variables, values change at runtime.
 input="input file 1.txt"
 awk_script='printf "%s[%s]\n" ,$1 ,$3'

 # static command template, preventing double-quote escapes and avoid variable  expansions.
 awk_command=$(printf "awk '{%s}' \"%s\"\n" "$awk_script" "$input")
 echo "awk_command=$awk_command"

 awk_command=awk '{printf "%s[%s]\n" ,$1 ,$3}' "input file 1.txt"

Executing variable command:

bash -c "$awk_command"

Alternative that also works

bash << $awk_command
Dudi Boy
  • 4,105
  • 1
  • 13
  • 26
-1

Be careful registering an order with the: X=$(Command)

This one is still executed. Even before being called. To check and confirm this, you can do:

echo test;
X=$(for ((c=0; c<=5; c++)); do
sleep 2;
done);
echo note the 5 seconds elapsed
Peter Mortensen
  • 30,030
  • 21
  • 100
  • 124
Azerty
  • 1
-1
#!/bin/bash
#Note: this script works only when u use Bash. So, don't remove the first line.

TUNECOUNT=$(ifconfig |grep -c -o tune0) #Some command with "Grep".
echo $TUNECOUNT                         #This will return 0 
                                    #if you don't have tune0 interface.
                                    #Or count of installed tune0 interfaces.
  • This stores the static string output from the command in a variable, not the command itself. – tripleee Feb 12 '21 at 11:22
  • `grep -c -o` is not entirely portable; you would perhaps expect it to return the number of actual number of occurrences of the search expression, but at least GNU `grep` does not do that (it's basically equivalent to `grep -c` without the `-o`). – tripleee Feb 12 '21 at 11:23
  • The Bash-only comment is weird; there is nothing in this simple script which isn't compatible with any Bourne-family shell. – tripleee Apr 27 '21 at 16:48
-8

It is not necessary to store commands in variables even as you need to use it later. Just execute it as per normal. If you store in variables, you would need some kind of eval statement or invoke some unnecessary shell process to "execute your variable".

Peter Mortensen
  • 30,030
  • 21
  • 100
  • 124
kurumi
  • 24,217
  • 4
  • 43
  • 49
  • 2
    The command I will store will depend on options I send in, so instead of having tons of conditional statements in the bulk of my program it's a lot easier to store the command I need for later use. – Benjamin Apr 11 '11 at 00:54
  • 1
    @Benjamin, then at least store the options as variables, and not the command. eg `var='*.txt'; find . -name "$var"` – kurumi Apr 11 '11 at 01:00