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
  • 133,243
  • 15
  • 236
  • 397
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
  • 320,670
  • 36
  • 633
  • 936
  • 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