Raja G
Raja G

Reputation: 6633

How to execute commands stored in array

I am trying to execute a shell script like below

cmds=('uptime' 'free -m' 'nproc')
for cmd in ${cmds[@]};
do
echo $($cmd)
done

Execution is breaking when it comes to free -m execution because of space.

vagrant@vagrant-ubuntu-trusty-64:~$ bash for_my_script.sh
 03:42:50 up 56 min,  1 user,  load average: 0.00, 0.00, 0.00
              total        used        free      shared  buff/cache   available
Mem:         499928      108516       43204        1844      348208      366140
Swap:             0           0           0
for_my_script.sh: line 5: -m: command not found

1
vagrant@vagrant-ubuntu-trusty-64:~$

I tried iterating with for by storing commands in variable

vagrant@vagrant-ubuntu-trusty-64:~$ cmds="uptime,free -m"
vagrant@vagrant-ubuntu-trusty-64:~$ for cmd in "${cmds//,/ }"; do echo "$($cmd)"; done
uptime: invalid option -- 'm'
vagrant@vagrant-ubuntu-trusty-64:~$ cmds="uptime,'free -m'"
vagrant@vagrant-ubuntu-trusty-64:~$ for cmd in "${cmds//,/ }"; do echo "$($cmd)"; done
uptime: invalid option -- 'm'

With no success.

is touching IFS is the only way for this type of problem? any inputs are much appreciated.

Upvotes: 0

Views: 256

Answers (2)

dimo414
dimo414

Reputation: 48794

The (edit: previously) accepted answer still involves unquoted strings, which are discouraged and error-prone. You could use eval as @Fravadona suggests, but eval is also hard to work with properly/safely.

Instead, consider creating functions to wrap the commands you want to run; then you don't need to deal with nested arguments within strings. Functions also makes it easy to compose more complex behavior (pipelines, conditionals, etc.) without complicating the execution loop. Something like:

do_free() { free -m; }

cmds=(uptime do_free nproc)
for cmd in "${cmds[@]}"; do
  "$cmd"  # notice that you don't need echo $("$cmd"); it's generally redundant
done

You could create wrappers for uptime and nproc too for consistency, but it's not necessary as long as you don't need to pass any arguments.

For a more complete example of what I'm suggesting see this heartbeat script and the COMMANDS array in particular.

Upvotes: 3

Fravadona
Fravadona

Reputation: 16865

In this specific case the right way would be to use eval:

cmds=('uptime' 'free -m' 'nproc')
for cmd in "${cmds[@]}"
do
    eval "$cmd"
done

But you need to make sure that the content of cmd is exactly like you would type it in the terminal

eval is evil, when you don't know the right way to use it


Let's see a few examples:

# cmd='echo "a     b"'
#
# eval "$cmd"
a     b
#
# $cmd
"a b"
#
# cmd='echo a; echo b'
#
# eval "$cmd"
a
b
# $cmd
a; echo b
#
# cmd='echo;'
#
# eval "$cmd"

# $cmd
bash: echo;: command not found...
#

Upvotes: 1

Related Questions