0

So, I'm trying to retry failing or command-line with errors, BUT with different flags each time it retry...

I already know how to retry command on failure:

while ! "$@"
do
  :
  sleep 1
done

As an example, I'll use the function above on a failing command on purpose to illustrate:

retry ls test # here this will usually fail and thus retry infinitely

I'm aware this:

  • Retry infinitely (until the retried command succeed or return an exit code that is interpreted by the shell as successful)
  • Doesn't take into consideration any kind of errors/exit code other than failure whatsover

And all of the above is the wanted/expected effect.

Only thing I'm trying to add to that is support for additional flag when "retrying" N command (which I'll need to either supply as argument to the function or hardcode in the function depending on cases...)

Here what I tried:

bash
while ! "$(echo "$@" | sed "s/^pattern/pattern --otherflag/g")"
do
  :
  sleep 1
done

Usage:

retry ls pattern

Where the new command will become ls pattern --otherflag (which will obviously fail, but this is beside the point)

2nd Method:

while ! "${@/pattern/pattern --otherflag}"
do
  :
  sleep 1
done

Same usage as above...

Here it will give the same result as above.

Now this seems to work on first glance, but if the supplied/failing command contain any kind of quotes, which usually come unquoted, the output/retried command will then have the quotes removed/ignored...

So following above two method/example, a command such as ls pattern "file" will become ls pattern --otherflag file where file doesn't have any quotes (same happen with single quotes).

To fix this I tried this answer's quote function but didn't manage to get satisfying result. Furthermore, it also seems like it doesn't wait/use the added flag only on failure (eg: when the retry function kicks in) but instead run it before it even fail, with the additional flags.

I don't want to have to use a if block/condition if possible and prefer to stay as close as possible to the above snippet. Prefer bash or/and sh alternative. Any feedback/answer is welcome.

1 Answers1

1

Assuming your script is called retry.sh, and its called from the shell with the command line retry.sh ls "foo bar", the positional parameters (arguments, i.e. $1, $2...) within the script will be ls and foo bar. Note that while quotes are a way to protect white space and other special characters on the shell command line, the quotes don't exist any more within the launched script. Instead, what you have is basically an array of strings.

Now, "$@" treats the distinct parameters properly: it expands each to a distinct word. But if you do "$(echo "$@" | sed "s/^pattern/pattern --otherflag/g")", the echo concatenates its arguments to one string, and the quotes around the whole command substitution keep it as one string, instead of having word splitting split it on white space. Not that splitting would help, since at this point the separation between the original arguments is lost, and the one argument foo bar would be the same as the two arguments foo and bar.

Also, "${@/pattern/pattern --otherflag}" will do the replacement within each positional parameter individually, so with the parameters ls and foo bar, "${@/ls/ls --otherflag}" would result in the two words ls --otherflag. This would look for a command literally called ls --otherflag, the same as if you were to run "ls --otherflag" on the shell command line.


What you want is ls, --otherflag, foo bar, i.e. to add the flag as an additional argument.

While you could take all the arguments from $@, escape them for shell processing and join them together to make a valid command line, then edit that command line, and feed it back to the shell with eval, it's safer to just deal with the list as a list.

Adding an argument to the end would be relatively easy:

#!/bin/sh
if ! "$@"; then
    echo "failed, retrying..."
    if ! "$@" --otherflag; then
        echo "nope, still failed"
    fi
fi

Adding one to the middle requires arrays or slicing $@. E.g. to add the flag to the second position:

#!/bin/bash
if ! "$@"; then
    echo "failed, retrying..."
    if ! "$1" --otherflag "${@:2}"; then
        echo "nope, still failed"
    fi
fi

"${@:n:m}" expands to the first m positional parameters starting at index n, or until the end of the list m is not given. So "$1" is the first argument (same as "${@:1:1}"), and "${@:2}" the rest.

Somewhat related: How can we run a command stored in a variable?

ilkkachu
  • 138,973
  • It's a great answer, (and I appreciate the detailed info too) but it seems it didn't kept the "retry infinitely" part of the original snippet? I know i could use a while True but preferred to "retry infinitely until success" like the original snippet did if possible :) (eg: using the while ! method) – Nordine Lotfi May 15 '21 at 18:50
  • Also is there really no easy way to keep the quoting done on certain existing arguments? This is the main thing i struggled with, since adding a flag at the end using the original snippet in my post is easier (as you mentioned too) – Nordine Lotfi May 15 '21 at 18:51
  • @NordineLotfi, err, I'm not sure what you mean. The way to keep the arguments intact is to use "$@" or, for one or more args, but not the whole list, something like "$3" or "${@:2:3}". There are no quotes at this point. And yes, I switched to if, since I'm not sure what to do from the third try on. But you could do if ! "$@"; then while ! "$@" --otherarg; do sleep 1; done; fi to keep looping on the modified command line. – ilkkachu May 16 '21 at 09:41
  • I meant to keep the specific quote that could be placed (as the example used in my post) on the command passed to the retry function. I know how to escape them (as mentioned and linked to a solution that do this in my post) but don't know how to keep them intact...my guess is that placing single quote around double quote would work but not sure how to do this – Nordine Lotfi May 19 '21 at 07:53
  • @NordineLotfi, if you call a script on the shell command line as retry.sh ls pattern "file name", then you get ls, pattern, and file name in $1, $2 and $3. "$@" expands to those three words, the same as if the script contained "$1" "$2" "$3" (except that "$@" doesn't require knowing the number of arguments). If you call the script as retry.sh ls pattern file\ name, you get the same. The quotes and escapes are removed when the command line is processed, so when you're in the script, you don't have any quotes to deal with, but individual positional parameters. – ilkkachu May 19 '21 at 12:50
  • and of course if that third argument doesn't contain any white space, then you can enter it on the command line as file or "file" and it makes no difference. In any case, if you want to add a new command-line argument, there's no need to touch the existing words you have in the positional parameters, in $@, just add a new one. – ilkkachu May 19 '21 at 12:54