Execute command parsed from a string containing arguments with spaces - bash

I would like to execute a command which is given by a variable (Variable cmd in this example):
cmd="echo 'First argument'"
$cmd
Expected result would be:
First argument
BUT ... actual result is:
'First argument'
What? I don't understand why I can see single quotes in the output. After all, if the command (=content of variable $cmd) would be issued directly, then no quotes leak into the output, it behaves as desired:
$ echo 'First argument'
First argument
To illustrate what I am trying to achieve in real life: in my deploy script there is a code block like this (strongly simplified, but you get the point):
#!/bin/bash
function execute {
cmd=$1
echo "Executing $cmd ..."
# execute the command:
$cmd
}
VERSION=1.0.2
execute "git tag -a 'release-$VERSION'"
Now, Git would create a tag which contains single quotes:
git tag
'1.0.2'
which is not what I want ...
What to do?
(Bash version: GNU bash 3.1.0)
(I found a very similar issue, here, but the answer would not apply to my problem)

cmd="echo 'First argument'"
$cmd
What happens there is word splitting and the actual resulting command is:
echo "'First" "argument'"
Double-parsing with the single quotes inside would never happen.
Also, it's better to use arrays:
#!/bin/bash
function execute {
cmd=("$#") ## $# is already similar to an array and storing it to another is just optional.
echo "Executing ${cmd[*]} ..."
# execute the command:
"${cmd[#]}"
}
VERSION=1.0.2
execute git tag -a "release-$VERSION"
For eval is a difficult choice in that kind of situation. You may not only get unexpected parsing results, but also unexpectedly run dangerous commands.

I think this is what you want:
cmd="echo 'First arg'"
eval $cmd
First arg

Related

Appending command line arguments to a Bash array

I am trying to write a Bash script that appends a string to a Bash array, where the string contains the path to a Python script together with the arguments passed into the Bash script, enclosed in double quotes.
If I call the script using ./script.sh -o "a b", I would like a CMD_COUNT of 1, but I am getting 2 instead.
script.sh:
#!/bin/bash
declare -a COMMANDS=()
COMMANDS+=("/path/to/myscript.py \"${#}\"")
CMD_COUNT=${#COMMANDS[*]}
echo $CMD_COUNT
How can I ensure that the appended string is /path/to/myscript.py "-o" "a b"?
EDIT: The full script is actually like this:
script.sh:
#!/bin/bash
declare -a COMMANDS=()
COMMANDS+=("/path/to/myscript2.py")
COMMANDS+=("/path/to/myscript.py \"${#}\"")
CMD_COUNT=${#COMMANDS[*]}
echo $CMD_COUNT
for i in ${!COMMANDS[*]}
do
echo "${0} - command: ${COMMANDS[${i}]}"
${COMMANDS[${i}]}
done
It's a bad idea, but if it's what you really want, printf %q can be used to generate a string that, when parsed by the shell, will result in a given list of arguments. (The exact escaping might not be identical to what you'd write by hand, but the effect of evaluating it -- using eval -- will be).
#!/bin/bash
declare -a COMMANDS=( )
printf -v command '%q ' "/path/to/myscript" "$#"
COMMANDS+=( "$command" )
CMD_COUNT=${#COMMANDS[#]}
echo "$CMD_COUNT"
...but, as I said, this is all a bad idea.
Best-practice ways to encapsulate code as data in bash involve using functions, or arrays with one element per argument.
eval results in code that's prone to security bugs.

bash "$#" not working with arguments starting with '-'

I am working on an option driven bash script that will use getopts. The script has cases where it can accept multiple options and specific cases where only one option is accepted. While testing a few cases out I ran into this issue which I have reduced down to pseudo-code for this question.
for arg in "$#"; do
echo ${arg}
done
echo "end"
Running below returns:
$ ./test.sh -a -b
-a
end
I am running bash 4.1.2, why isn't the -b returned on the empty line? I assume this has to do with the '-'.
I cannot reproduce your exact error, but this is the risk of using echo: if $arg is a valid option, it will be treated as such, not a string to print. Use printf instead:
printf '%s\n' "$arg"
Also check if you have applied any "shift" commands that might remove the arguments before you test then (typical in a argument collection block that might include a case statement)

Passing argument to script invoked by exec producing undesired result

I'm trying to pass an argument to a shell script via exec, within another shell script. However, I get an error that the script does not exist in the path - but that is not the case.
$ ./run_script.sh
$ blob has just been executed.
$ ./run_script.sh: line 8: /home/s37syed/blob.sh test: No such file or directory
For some reason it's treating the entire execution as one whole absolute path to a script - it isn't reading the string as an argument for blob.sh.
Here is the script that is being executed.
#!/bin/bash
#run_script.sh
blobPID="$(pgrep "blob.sh")"
if [[ -z "$blobPID" ]]
then
echo "blob has just been executed."
#execs as absolute path - carg not read at all
( exec "/home/s37syed/blob.sh test" )
#this works fine, as exepcted
#( exec "/home/s37syed/blob.sh" )
else
echo "blob is currently running with pid $blobPID"
ps $blobPID
fi
And the script being invoked by run_script.sh, not doing much, just emulating a long process/task:
#!/bin/bash
#blob.sh
i=0
carg="$1"
if [[ -z "$carg" ]]
then
echo "nothing entered"
else
echo "command line arg entered: $carg"
fi
while [ $i -lt 100000 ];
do
echo "blob is currently running" >> test.txt
let i=i+1
done
Here is the version of Bash I'm using:
$ bash --version
GNU bash, version 4.2.37(1)-release (x86_64-pc-linux-gnu)
Any advice/comments/help on why this is happening would be much appreciated!
Thanks in advance,
s37syed
Replace
exec "/home/s37syed/blob.sh test"
(which tries to execute a command named "/home/s37syed/blob.sh test" with no arguments)
by
exec /home/s37syed/blob.sh test
(which executes "/home/s37/syed/blob.sh" with a single argument "test").
Aside from the quoting problem Cyrus pointed out, I'm pretty sure you don't want to use exec. What exec does is replace the current shell with the command being executed (rather than running the command as a subprocess, as it would without exec). Putting parentheses around it makes it execute that section in a subshell, thus effectively cancelling out the effect of exec.
As chepner said, you might be thinking of the eval command, which performs an extra parsing pass before executing the command. But eval is a huge bug magnet. It's incredibly easy to use eval in unsafe ways (see BashFAQ #48). If you need to construct a command, see BashFAQ #50 for better ways to do it.

Using spaces in bash scripts

I have a script foo.sh
CMD='export FOO="BAR"'
$CMD
echo $FOO
It works as expected
>./foo.sh
"BAR"
Now I want to change FOO variable to BAR BAR. So I get script
CMD='export FOO="BAR BAR"'
$CMD
echo $FOO
When I run it I expect to get "BAR BAR", but I get
./foo.sh: line 2: export: `BAR"': not a valid identifier
"BAR
How I can deal with that?
You should not use a variable as a command by just calling it (like in your $CMD). Instead, use eval to evaluate a command stored in a variable. Only by doing this, a true evaluation step with all the shell logic is performed:
eval "$CMD"
(And use double quotes to pass the command to eval.)
Just don't do that.
And read Bash FAQ #50
I'm trying to save a command so I can run it later without having to repeat it each time
If you want to put a command in a container for later use, use a
function. Variables hold data, functions hold code.
pingMe() {
ping -q -c1 "$HOSTNAME"
}
[...]
if pingMe; then ..
The proper way to do that is to use an array instead:
CMD=(export FOO="BAR BAR")
"${CMD[#]}"

All arguments into files with correct quoting using "$#"

I need my bashscript to cat all of its parameters into a file. I tried to use cat for this because I need to add a lot of lines:
#!/bin/sh
cat > /tmp/output << EOF
I was called with the following parameters:
"$#"
or
$#
EOF
cat /tmp/output
Which leads to the following output
$./test.sh "dsggdssgd" "dsggdssgd dgdsdsg"
I was called with the following parameters:
"dsggdssgd dsggdssgd dgdsdsg"
or
dsggdssgd dsggdssgd dgdsdsg
I want neither of these two things: I need the exact quoting which was used on the command line. How can I achieve this? I always thought $# does everything right in regards to quoting.
Well, you are right that "$#" has the args including the whitespace in each arg. However, since the shell performs quote removal before executing a command, you can never know how exactly the args were quoted (e.g. whether with single or double quotes, or backslashes or any combination thereof--but you shouldn't need to know, since all you should care for are the argument values).
Placing "$#" in a here-document is pointless because you lose the information about where each arg starts and ends (they're joined with a space inbetween). Here's a way to see just this:
$ cat test.sh
#!/bin/sh
printf 'I was called with the following parameters:\n'
printf '"%s"\n' "$#"
$ ./test.sh "dsggdssgd" "dsggdssgd dgdsdsg"
I was called with the following parameters:
"dsggdssgd"
"dsggdssgd dgdsdsg"
Try:
#!/bin/bash
for x in "$#"; do echo -ne "\"$x\" "; done; echo
To see what's interpreted by Bash, use:
bash -x ./script.sh
or add this to the beginning of your script:
set -x
You might want add this on the parent script.

Resources