gsgx
gsgx

Reputation: 12249

Executing a piped command in a subshell doesn't work if the command is in a variable

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

Answers (2)

Jonathan Leffler
Jonathan Leffler

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

Technologeeks
Technologeeks

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

Related Questions