Reputation: 12249
The following code works fine
#!/bin/bash
while true; do
echo $(pwd | awk '{print "dir: "$1}')
sleep 1
done
But I need the command to be in a variable, like this.
#!/bin/bash
COMMAND=pwd | awk '{print "dir: "$1}'
while true; do
echo $($COMMAND)
sleep 1
done
The first snippet executes fine, and prints something like dir: /present/directory
. The second snippet just prints blank lines. Why is that and how do I fix it?
There's no significance in the commands I used. I just needed to use a pipe and chose these commands to demonstrate it.
Upvotes: 3
Views: 2448
Reputation: 753725
If you like living dangerously, the answer is eval
:
#!/bin/bash
COMMAND="pwd | awk '{print \"dir: \"\$1}'"
while true
do
echo $(eval $COMMAND)
sleep 1
done
Why dangerously? It's hard to control what's going on with eval
, doubly so if you accept the command string from a naïve (let alone malicious) user. Also note how carefully I had to quote the string containing the command.
Why didn't version 2 in the question work?
COMMAND=pwd | awk '{print "dir: "$1}'
This line doesn't do what you think it does, put bluntly.
It is a pipeline, with the first 'command' being the string assignment COMMAND=pwd
treated as an environment variable for the empty command, which sends no data to awk
. After completion, the value in $COMMAND
is an empty string (both because of the sub-shell used with the pipeline and because the COMMAND=pwd
only applied while the (empty) command was executed), so the loop echoes the result of executing the empty string — which is also an empty string. Hence the line of blanks.
This works for me but I'm having an issue with adding a tab in the AWK printout, and I think it's because of the escaping. If I have
COMMAND="ls -l | awk '{print \$8 \$9}'"
, how would I insert a tab\t
between the$8
and$9
?
Ouch, and Yuck!
If you had a tab in the source between $8
and $9
, awk
would treat it as generic white space and paste $8
and $9
together in the output. To get a tab in the output, you'll need either a quoted string containing a tab, or a quoted string containing \t
in between the two fields, or you'll use a printf()
with a suitable format string.
So, in principle, you'll probably want:
COMMAND='ls -l | awk '\''{printf "%s\t%s\n", $8, $9}'\'
I've switched to single quotes around the string because they're mostly easier to understand. Whether you use single or double quotes, the command itself contains both, so you have to worry about how to do the escaping. The '\''
sequence is how you embed a single quote into a single quoted string; the first single quote ends the current single quoted string; the backslash single quote adds a single quote, and the last single quote restarts a single string. The sequence '\'
at the end could also be written '\'''
, but the last two quotes are a zero-length single quoted string, so I dropped them.
This produces the nice output with tabs; you can verify by running:
eval $COMMAND
The echo $(eval $COMMAND)
completely undoes all the careful spacing, flattening the entire list of files and times into a single line with each 'word' separated from the next by spaces.
You can work around that issue by forcing echo
to respect the spacing (and newlines) with:
echo "$(eval $COMMAND)"
but this raises the question of 'why not use just eval $COMMAND
?'
(Testing: bash 3.2.48 Mac OS X 10.7.4.)
Upvotes: 5
Reputation: 1108
The use of a single quote makes everything within it be literal. This means that the $ does not get evaluated.
In the second example, however, you are forcing re-evaluation by saying $($COMMAND). This causes the '...$1' to be processed again, this time without quotes. So then - voila! it works!
You can fix this by switching the " and ' , using double quotes (and using \" inside , or single quotes).
Hope this helps,
J
Upvotes: 0