I am trying to check if all the non POSIX commands that my script depends on are present before my script proceeds with its main job. This will help me to ensure that my script does not generate errors later due to missing commands.
I want to keep the list of all such non POSIX commands in a variable called DEPS
so that as the script evolves and depends on more commands, I can edit this variable.
I want the script to support commands with spaces in them, e.g. my program
.
This is my script.
#!/bin/sh
DEPS='ssh scp "my program" sftp'
for i in $DEPS
do
echo "Checking $i ..."
if ! command -v "$i"
then
echo "Error: $i not found"
else
echo "Success: $i found"
fi
echo
done
However, this doesn't work, because "my program"
is split into two words while the for
loop iterates: "my
and program"
as you can see in the output below.
# sh foo.sh
Checking ssh ...
/usr/bin/ssh
Success: ssh found
Checking scp ...
/usr/bin/scp
Success: scp found
Checking "my ...
Error: "my not found
Checking program" ...
Error: program" not found
Checking sftp ...
/usr/bin/sftp
Success: sftp found
The output I expected is:
# sh foo.sh
Checking ssh ...
/usr/bin/ssh
Success: ssh found
Checking scp ...
/usr/bin/scp
Success: scp found
Checking my program ...
Error: my program not found
Checking sftp ...
/usr/bin/sftp
Success: sftp found
How can I solve this problem while keeping the script POSIX compliant?
This happens because the steps after parameter expansion are string-splitting and glob-expansion -- not syntax-level parsing (such as handling quoting). To go all the way back to the beginning of the parsing process, you need to use
eval
.Frankly, the best approaches are to either:
...there's a reason proper array support is ubiquitous in modern shells; writing unambiguously correct code, particularly when handling untrusted data, is much harder without it.
That said, you have the option of using
$@
to store your contents, which can be set, albeit dangerously, usingeval
:If you do this inside of a function, you'll override only the function's argument list, leaving the global list unmodified.
Alternately,
eval "yourfunction $deps"
will have the same effect, setting the argument list within the function to the results of running all the usual parsing and expansion phases on the contents of$deps
.I'll repeat the answer I gave to your previous question: use a while loop with a here document rather than a for loop. You can embed newlines in a string, which is all you need to separate command names in a string if those command names might contain whitespace. (If your command names contain newlines, strongly consider renaming them.)
For maximum POSIX compatibility, use
printf
, since the POSIX specification ofecho
is remarkably lax due to differences in howecho
was implemented in various shells prior to the definition of the standard.Because the script is in your controll, you can use the
eval
with reasonable safety, so @Charles Duffy's answer is an simple and good solution. Use it. :)Also, consider to use the
autoconf
for generating the usualconfigure
script what is doing good job for what you need - e.g. checking commands and much more... At least, check some configure scripts for ideas how to solvle common problems...If you want play with your own implementation:
sed
,cat
cp
and such. Those programs doesn't contains spaces in their names, nor in the $PATH.for
loop for space delimited elements unless you getting them as the function arguments - so you can use the"$@"
As starting script could be something like the following:
The above will blow up on non-existent "hooloovoo" command. :)
Now you can safely continue, all core commands needed for the install script are available. In the next step, you can check other strange dependencies.
Some ideas:
One more thing :), (or two)
echo
. The POSIX isn't defines how it should act when contains escaped characters (e.g.echo "some\nwed"
). Use: