Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Run piped commands with eval

I have the following command line to check free space of a file system:

fs_used=`df -h /u01 | sed '1d' | sed '1d' | awk '{print $4}' | cut -d'%' -f1`

It works fine. It returns the percentage of the used space on the file system (without the % symbol).

Now I need to make it variable and run it with the eval command. I tried the following but it doesn't work (exit with df: invalid option -- 'd')

df_cmnd="df -h $fs1 | sed '1d' | sed '1d' | awk '{print $4}' | cut -d'%' -f1"
fs_used=eval $df_cmnd

The problem, I guess, is that eval cannot run piped commands. Is that true? is there any workaround or alternative to make this code run?

like image 748
Ould Abba Avatar asked Dec 13 '17 13:12

Ould Abba


2 Answers

Backslash-escape the $, and use $():

#              V                                         V
df_cmnd="df -h \$fs1 | sed '1d' | sed '1d' | awk '{print \$4}' | cut -d'%' -f1"
fs_used=$(eval "$df_cmnd")
#       ^^               ^

This will use the value of fs1 at the time you eval.

But, in reality, please don't use eval! Make it a shell function instead:

df_cmnd(){
    df -h "$1" | sed '1d' | sed '1d' | awk '{print $4}' | cut -d'%' -f1
}
fs_used=$(df_cmnd /u01)

Then you don't have to worry about escaping.

Explanation

Look at how bash interprets your df_cmnd assignment:

$ df_cmnd="df -h $fs1 | sed '1d' | sed '1d' | awk '{print $4}' | cut -d'%' -f1"
$ echo $df_cmnd
df -h | sed '1d' | sed '1d' | awk '{print }' | cut -d'%' -f1
#    ^                                   ^

In my case, fs1 was empty, so I just got df -h for the df part. In your case and mine, bash replaced $4 with its value, here, empty since I wasn't running in a script with four arguments. Therefore, awk will print the whole line rather than just the fourth field.

like image 94
cxw Avatar answered Oct 30 '22 15:10

cxw


I had a legitimate need for eval-ing a string: a helper scripts runs a complicated sequence of pipes with double-quoting (e.g. --json-override='"key": "value"'), 460+ characters long. But when I run it with --noop, I want to see the command that would be executed, in a copy-pasteable form for manual execution. I came up with this:

echo_eval () {
    cmd=$(cat)
    if ((noop)); then
        echo "$cmd"
    else
        eval "$cmd"
    fi
}

foo () { for arg; do echo "[$arg]"; done; }

echo_eval <<EOF
foo 1 2 "3 4" '"5 6"' " 7 " "\"8 9\"" | tac
EOF
exit 1

The main purpose of the here-document is to skip all the pitfalls of using the ' and " quotes in an already ' or "-quoted string (pain and suffering).

While I haven't tested it extensively, it seems to hit the relevant check-boxes, like pipes, quoting, quoting space-containing arguments, preserving quoted quotes, escaped quotes, leading/trailing space in arguments, etc.:

["8 9"]
[ 7 ]
["5 6"]
[3 4]
[2]
[1]

When noop is true, I get:

foo 1 "2" "3 4" '"5 6"' " 7 " "\"8 9\"" | tac

which is the command verbatim, copy-pasteable for manual execution and yielding the same output.

We can also use <<'EOF' to disable substitution of any $s before echo_eval, but if any variable/command substitution does need to happen before calling echo_eval, we have to use <<EOF and escape all $s that shouldn't be evaluated before.


Having said all this, I'd gladly (a) hear if there are pitfalls (apart from the $ mentioned above) and (b) see a better solution to the problem if there is one.

like image 42
Irfy Avatar answered Oct 30 '22 17:10

Irfy