1

I would like to run the following command:

tar  --sort=name --owner=root:0 --group=root:0 --mtime='UTC 2020-01-01' -cvf api.tar api

But need the following fallback for macOS:

gtar --sort=name --owner=root:0 --group=root:0 --mtime='UTC 2020-01-01' -cvf api.tar api

(Note: the arguments are identical.)

Can I call gtar first, and then tar as a fallback, as a one-liner, writing the arguments only once?

  • You can put the argument string in a variable, say ARGS, then call gtar $ARGS followed by tar $ARGS. If you use Bash interactively, you should also look up "quick substitution". – berndbausch Sep 19 '21 at 22:11
  • @berndbausch, putting that argument list in a single string variable will fail immediately (because of the space in UTC 2020-01-01). See: https://unix.stackexchange.com/q/444946/170373 – ilkkachu Sep 20 '21 at 10:25

2 Answers2

2

Can I call gtar first, and then tar as a fallback, as a one-liner, writing the arguments only once?

To answer this as asked: this can be done as a simple implementation of storing the arguments in an array. (Bash/ksh/zsh. See How can we run a command stored in a variable? for the issues and the POSIX-compatible workaround.)

args=(--sort=name --owner=root:0 --group=root:0 --mtime='UTC 2020-01-01' -cvf api.tar api)
if ! tar "${args[@]}"; then
    echo "using 'tar' failed, retrying with 'gtar'" >&1
    gtar "${args[@]}"
fi

or a as a one-liner, if you insist:

tar "${args[@]}" || gtar "${args[@]}"

Though this doesn't tell why it failed, and would try to retry with the other tar even if the problem is something like a non-accessible directory.

Another alternative would be to only rerun the command if the first one errors with "command not found". The shells usually set $? to 127 in that case. Of course, this requires flipping gtar first, since tar probably exists, in some form.

gtar "${args[@]}"
ret=$?
if [ "$ret" = 127 ]; then
    tar "${args[@]}"
    ret=$?
fi

The test [ "$? = 127 ] trashes the value of $?, hence the extra variable to hold the actual exit status if needed.

In the specific case of the two tars, Kusalananda's answer about checking beforehand is also a good solution.

ilkkachu
  • 138,973
1

If you install GNU tar by means of installing the gnu-tar package using Homebrew on macOS, you will notice the following message in the terminal:

GNU "tar" has been installed as "gtar".
If you need to use it as "tar", you can add a "gnubin" directory
to your PATH from your bashrc like:
PATH="/usr/local/opt/gnu-tar/libexec/gnubin:$PATH"

This means that your tar command at the start of your question will work as expected if you first set PATH as shown in the gnu-tar installation message above.

PATH="/usr/local/opt/gnu-tar/libexec/gnubin:$PATH"
tar  --sort=name --owner=root:0 --group=root:0 --mtime='UTC 2020-01-01' -cvf api.tar api

Updating PATH conditionally:

if [ "$(uname)" = Darwin ]; then
    PATH="/usr/local/opt/gnu-tar/libexec/gnubin:$PATH"
fi

tar --sort=name --owner=root:0 --group=root:0 --mtime='UTC 2020-01-01' -cvf api.tar api

You could also test with command -v:

if command -v gtar >/dev/null 2>&1; then
    tar=gtar
elif command -v tar >/dev/null 2>&1 && tar --version | grep -q -F GNU 2>/dev/null; then
    tar=tar
else
    echo 'No GNU tar available' >&2
    exit 1
fi

"$tar" --sort=name --owner=root:0 --group=root:0 --mtime='UTC 2020-01-01' -cvf api.tar api

This tests whether gtar exists as a command. If it does, the variable tar is set to the string gtar. If it doesn't exist, we test for tar, and if tar exists we test whether tar --version returns something that contains the substring GNU and assign the variable tar the string tar. But if the tests fail, we bail out with a diagnostic message.

Later, if we didn't bail out with an error message, we use "$tar" as the command.

You could also choose to use a test on the output of uname, obviously,

if [ "$(uname)" = Darwin ]; then
    # Assumes GNU tar is gtar on macOS and that it's available
    tar=gtar
else
    # Assumes GNU tar is tar on this system, and that it's available
    tar=tar
fi

"$tar" --sort=name --owner=root:0 --group=root:0 --mtime='UTC 2020-01-01' -cvf api.tar api

Kusalananda
  • 333,661
  • Re. your suggestion for setting on the global $PATH: I had considered this, but I'm keen to avoid modifying OS commands. For example, another script (not written by me) might do something like if [[ "$OSTYPE" == "darwin"* ]]; and then continue to use tar expecting it to behave like BSD tar. – Lawrence Wagerfield Sep 19 '21 at 22:28
  • @LawrenceWagerfield I left you with a number of suggestions. Pick one that fits your needs. You can also modify PATH in a subshell: ( PATH=...; tar ... ). That way you use a locally modified PATH. Alternatively, make tar a shell function that does this for you, and declare PATH as local in there (make sure to call the real tar using command tar or command gtar in this function as to not make a recursive call). I mean, there are so many ways you can solve this. – Kusalananda Sep 19 '21 at 22:31
  • Thank you @Kusalananda :+1: – Lawrence Wagerfield Sep 19 '21 at 22:38