1

My script contains many mysqldump blabla > dump.sql and mysq balbla < dump.sql in order to make it possible to run it in dry-run mode.

Actually the point is to create a funtion run to run anything I ask it to.

  run echo 'hello world'
  run mysqldump blabla > dump.sql
  run mysql blabla < dump.sql
  run ssh blabla
  # etc


run() {
    if [[ "$(printenv DRY_RUN)" = "yes" ]]
    then
        echo "${@}"
    else
        ${@}
    fi
}

However, this is doesn't work:

run "mysqldump -uuser -ppass dbase > dump.sql"

I get this error:

mysqldump: couldn't find table: ">"

smarber
  • 1,181
  • 2
  • 12
  • 25
  • @HaukeLaging I did try it and when it worked I accepted it... https://unix.stackexchange.com/questions/433987/wrap-bash-functions-into-a-runner-function#comment783613_433993 – smarber Mar 28 '18 at 08:27
  • `$(printenv DRY_RUN)`? Why not just `$DRY_RUN`? – camh Mar 29 '18 at 00:15

3 Answers3

5

You should use "${@}" instead of ${@} (like with echo "${@}") but that is not the reason for your problem.

The reason is that redirection takes places very early in command line parsing, before parameter substitution. Thus after the variable has put > in the command line, the shell is not looking for > any more.

An important point which I noticed after I published my answer: With a call like

run mysqldump blabla > dump.sql

the function run does not see > and dump.sql. That is probably not what you want because it prevents you from changing all the redirections with a single environment variable, as the output of echo "${@}" is redirected to the file then, too. Thus, you should use something like run --redirect dump.sql mysqldump blabla, see below.

There are two possibilities:

  1. Stick with "$@" and use eval. This may take you to a quoting nightmare, of course. You have to quote everything except for the > so that the shell sees a bare > in the command line before it does quote removal.

  2. Handle the redirection separately:

    run --redirect dump.sql mysqldump blabla
    
    run() {
        if [ "$1" == '--redirect' ]; then
            shift
            redirect_target="$1"
            shift
        else
            redirect_target='/dev/stdout' # works at least with Linux
        fi
    
        if [[ "$(printenv DRY_RUN)" = "yes" ]]
        then
            echo "${@}"
        else
            "${@}" > "$redirect_target"
        fi
    }
    

    You can avoid the redirect_target='/dev/stdout' if you put the if [ "$1" == '--redirect' ] in the else branch of if [[ "$(printenv DRY_RUN)" = "yes" ]].

        if [[ "$(printenv DRY_RUN)" = "yes" ]]
        then
            if [ "$1" == '--redirect' ]; then
                # do not output "--redirect file"
                shift
                shift
            fi
            echo "${@}"
        else
            if [ "$1" == '--redirect' ]; then
                shift
                redirect_target="$1"
                shift
                "${@}" > "$redirect_target"
            else
                "${@}"
            fi
        fi
    
Jeff Schaller
  • 66,199
  • 35
  • 114
  • 250
Hauke Laging
  • 88,146
  • 18
  • 125
  • 174
  • 2
    This answer presumes 1) the redirection will always be output redirection, when it could be input redirection; 2) that only output redirection will ever be desired, when both output and input redirection may be desirable for some future command. – user1404316 Mar 28 '18 at 09:06
  • @user1404316 That is right but on the one hand it would be easy to modify my code accordingly and on the other hand it makes sense to assume that you want to be able to switch the output but not the input because a command which works on input just doesn't work right without this input. Thus you just might stick to `run mysql blabla < dump.sql` – Hauke Laging Mar 30 '18 at 11:36
0

Like in another recent answer I posted, word splitting happens too late to initiate redirection (but not too late to affect redirection).

You can run the command using eval, like suggested in another answer in the linked post, or using bash -c "$*", while still having to quote the entire command line including redirections.

Though if you want to avoid having to quote the commands, one option is to set -nv (noexec and verbose), so that bash doesn't run the commands but merely prints them. So, instead of a wrapper run, at the start of the script, do:

[[ $DRY_RUN = yes ]] && set -nv
muru
  • 69,900
  • 13
  • 192
  • 292
  • What does this have to do with word splitting? – Hauke Laging Mar 28 '18 at 08:26
  • @HaukeLaging OP using an unquoted `$@`, presumably in the hopes of having bash see `>` and do something with it. – muru Mar 28 '18 at 08:27
  • Sure but that is not word splitting but parameter expansion. There is no word splitting to be done on `>` anyway. – Hauke Laging Mar 28 '18 at 08:32
  • @HaukeLaging Obviously OP knows to use a quoted `"$@"` elsewhere, but they used an *unquoted* `$@` there, and passed the command line including redirections as a single string. That's why it's about word splitting. (Also, isn't word splitting part of parameter expansion, anyway?) – muru Mar 28 '18 at 08:34
-1

Use "$@" (with the double quotes):

run() {
    if [[ "$(printenv DRY_RUN)" = "yes" ]]
    then
        echo "${@}"
    else
    "$@"
    fi
}

Without them, $@ expands to a single token.

xenoid
  • 8,648
  • 1
  • 24
  • 47
  • Everything about this answer is wrong. – Hauke Laging Mar 28 '18 at 08:22
  • @HaukeLaging it works for me, what's wrong with this solution? – smarber Mar 28 '18 at 08:25
  • @smarber Neither `"$@"` nor `$@` performs redirection. See my answer. If you have tested it then you misunderstand the test result. The second wrong part: Neither `"$@"` nor `$@` is expanded to a single token (`"$*"` is). The difference is that `$@` is expanded to several unquoted tokens and `"$@"` is expanded to quoted tokens. – Hauke Laging Mar 28 '18 at 08:29
  • @HaukeLaging when I run `run mysqldump blabla > dump.sql` I get a dump file filled with the right data. However when I run `run mysqldump blabla` I get the output of `mysqldump`. This is how I hoped the `run` function behave. – smarber Mar 28 '18 at 08:38
  • 1
    @smarber and did you test it in dry run mode? – muru Mar 28 '18 at 08:47
  • @smarber It makes a big difference whether you want redirection performed inside or outside your `run` function. I would think that it makes little sense to do that outside if you already have a `run` function which should be flexible. You could use two levels of dry run: one showing the command and another running the command without redirection. – Hauke Laging Mar 28 '18 at 08:55
  • @muru I just did and it does not work, indeed. So only the not dry run mode work. Thanks for point that out – smarber Mar 28 '18 at 08:59
  • @HaukeLaging sorry I was just talking about the non dry run mode. So now I see (I hope) your point and you're right... – smarber Mar 28 '18 at 09:00