193

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,738
  • 21
  • 105
  • 131
Benjamin
  • 2,718
  • 5
  • 21
  • 20
  • 14
    Use a function! – gniourf_gniourf Jan 15 '15 at 16:14
  • 6
    See this post: [Why should eval be avoided in Bash, and what should I use instead?](https://stackoverflow.com/questions/17529220/why-should-eval-be-avoided-in-bash-and-what-should-i-use-instead). – codeforester Jan 25 '18 at 22:11
  • See also [Why does shell ignore quoting characters in arguments passed to it through variables?](https://stackoverflow.com/questions/12136948/why-does-shell-ignore-quoting-characters-in-arguments-passed-to-it-through-varia) and the similar https://mywiki.wooledge.org/BashFAQ/050 – tripleee Mar 02 '23 at 05:33

12 Answers12

224

Use eval:

x="ls | wc"
eval "$x"
y=$(eval "$x")
echo "$y"
Gilles 'SO- stop being evil'
  • 104,111
  • 38
  • 209
  • 254
Erik
  • 88,732
  • 13
  • 198
  • 189
  • 37
    $(...) 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
  • 24
    `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
  • 20
    `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
110

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
  • 80,270
  • 14
  • 142
  • 161
  • 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
44
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,738
  • 21
  • 105
  • 131
Nate
  • 12,963
  • 4
  • 59
  • 80
  • 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
7

For bash, store your command like this:

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

Run your command like this:

echo $command | bash
Derek Hazell
  • 109
  • 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
  • 2
    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
  • to be clear `echo "$command" | bash` – Julien Sep 21 '22 at 13:48
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,551
  • 1
  • 15
  • 30
Cyberience
  • 972
  • 10
  • 15
  • There are many problems with aliases. For one thing, they do not expand in scripts, which is obviously a bummer if you are writing a script (and thus your question is on-topic on Stack Overflow). For another, you can't pass positional arguments to aliases, just have them eat up the rest of your command line with no access to it. Also, the parsing rules are finicky, so you can't enable alias expansion and use the alias in the same command line. Bottom line, use a function, like everyone was telling you all along. – tripleee Mar 02 '23 at 05:31
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,551
  • 1
  • 15
  • 30
-1

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
  • 272,448
  • 266
  • 850
  • 1,236
  • 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
-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,738
  • 21
  • 105
  • 131
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
-1

As you don't specify any scripting language, I would recommand tcl, the Tool Command Language for this kind of purpose.

Then in the first line, add the appropriate shebang:

#!/usr/local/bin/tclsh

with appropriate location you can retrieve with which tclsh.

In tcl scripts, you can call operating system commands with exec.

lalebarde
  • 1,684
  • 1
  • 21
  • 36
-2

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,738
  • 21
  • 105
  • 131
Ivan
  • 6,188
  • 1
  • 16
  • 23
  • 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
  • Your bash -c $cmd ad bash -c "$cmd" cases do two VERY different things! Try it each way with cmd='echo "$(pwd)"; cd test2; echo "$(pwd)"' To see why, do 'bash -x -c $cmd'. – Bob Kerns Apr 21 '23 at 10:13
  • Also: Functions are not a solution, They may be an alternative in some cases, but you have to construct the function! If you get the pieces of $cd as a series of $1, $2, etc., have fun turning it into a function, and then have even more fun not using eval to make use of it in another shell. – Bob Kerns Apr 21 '23 at 10:19
-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,738
  • 21
  • 105
  • 131
kurumi
  • 25,121
  • 5
  • 44
  • 52
  • 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