3

In a Bash script, I pass a command with arguments. This works fine, except when the command includes a redirection. In that case, the redirection character is treated as an ordinary character.

$ cat foo
#!/bin/bash

f() { echo "command: $@" $@ }

f echo a-one a-two f 'echo b-one b-two' f 'echo c-one c-two > c.tmp'

I don't want to do f echo d-one d-two > d.tmp because I want to redirect the

output of the passed command, not the output of the f() function.

$ ./foo command: echo a-one a-two a-one a-two command: echo b-one b-two b-one b-two command: echo c-one c-two > c.tmp c-one c-two > c.tmp

As you see, this prints "c-one c-two > c.tmp" when I wanted to print "c-one c-two" to file c.tmp. Is there a way to do that?

k314159
  • 445

3 Answers3

2
f() {
  echo "command: $@"
  $@
}

In general, you should use "$@", here, too, for the usual reasons.

But you're right, running a command like that doesn't process shell operators, be it redirections or stuff like &&. You could change the function to eval the command it gets, which would process redirections, but also everything else.

In particular, passing arbitrary filenames would become difficult again. E.g. this would end up running ls on the two files foo and bar:

run_with_eval() {
    eval "$@"
}
file='foo bar'
run_with_eval ls -l "$file" 

You'd have to arrange for the filename to be passed quoted for the shell, which is somewhat awkward and error-prone.

But if you only want redirection, and even better, only output redirection with >, you could have the function handle it manually:

#!/bin/bash
run_with_redir() {
    local redir=
    if [[ $1 = '>' ]]; then
        redir=$2
        shift 2
    fi
    ## whatever else you do
    if [[ $redir ]]; then
        "$@" > "$redir"
    else
        "$@"
    fi
}

run_with_redir echo "normal command, no redirection" run_with_redir ">" output.txt echo "this gets redirected to output.txt"

That is, invoking the function with > and a filename as the first two arguments runs the rest as a command with the output redirected. Without the >, it runs the command normally. Note that the ">" must be quoted when calling the function, otherwise we'd again get the output of the whole function redirected. Expanding that to support e.g. >> should be straightforward, even if repetitive.

That should also work in the trickier cases:

run_with_redir ">" "output file with space.txt" ls -l "other file with space"
ilkkachu
  • 138,973
0

eval "$@" would do the job:

#!/bin/bash

f() { echo "command: $@" eval "$@" }

but in general it's very unsafe to use it even though you'll see it here and there.

0

A way to not use eval but it's very unsafe too.

#!/bin/bash

f() { echo "command: $@" bash -c "$@" }

f echo a-one a-two f 'echo b-one b-two' f 'echo c-one c-two > c.tmp'

ctac_
  • 1,960
  • Using eval and running another full shell aren't that different. In both cases there's a full round of shell expansions etc. done. Of course, an eval happens in the same shell environment, while starting another shell starts another environment. But all the quoting complications and issues with files called $(reboot) are there. Also, here, you probably should use bash -c "$*" because -c only takes one argument. Note e.g. how the f echo a-one a-two only prints an empty line with function here. – ilkkachu Apr 14 '21 at 19:58
  • @ilkkachu Yes, you are right. I don't look for all cases. Only want to tell the OP that's there is another way. – ctac_ Apr 14 '21 at 20:21