3

I have defined the following shell functions:

success() {
  printf "[\033[32mSUCCESS\033[0m]\n"
}

failure() {
  printf "[\033[31mFAILURE\033[0m]\n"
}

try() {
  result=$($* 2>&1)
  if [ $? -ne 0 ]; then
    failure
    echo $result
    exit 1
  fi
  success
}

This allows me to silently execute any command, capturing all the output, and display a SUCCESS or FAILURE message. Only if the command fails, the command output is displayed for debugging:

try rm -r $tmp

But I just realized that it's limited to a single command, and fails miserably for a command piped to another with |:

try git archive --format=tar HEAD | tar xf - -C $tmp

Because try is executed only on the git command, and then the output of try is piped to tar, instead of the output of git.

Is it possible to pass the two commands git ... | tar ... as a single parameter to try, capturing the output of both, and check if both git and tar returned 0?

Any other consideration maybe, to achieve my goal?

BenMorel
  • 4,587
  • what happens if you do try git archive --format=tar HEAD \| tar xf - -C $tmp? Though you almost definitely don't want the output of the first - if you caught it the second would have nothing to do. Still, if you mean diagnostic messages you'll want to change your command slightly like: $({ $* ; } 2>&1). You might also consider switching the $* to "$@" which should be more robust. – mikeserv Sep 12 '14 at 14:46

2 Answers2

1

You'll have to add some quoting on the command line, there's no way to avoid that

try "git archive --format=tar HEAD | tar xf - -C $tmp"

Then in the function, since shell metachars (like |) are not special when stored in a variable, so you'll need eval

try() {
  if result=$(eval "$*" 2>&1); then
    success
  else
    failure
    echo $result
    exit 1
  fi
}

I have a feeling I haven't got that quite right. Hopefully someone will correct me.

glenn jackman
  • 85,964
  • Is it correct to use $* if you have one argument (because it is quoted in the call) to try? See Gilles' answer. I would use "$1". – Timo Nov 26 '17 at 21:30
  • 1
    it's not incorrect. Since there's only 1 argument, joining the arguments together with space separator is the same as "$1". – glenn jackman Nov 27 '17 at 19:18
  • You need eval here because you have a string with commands (git and tar). The metachar | is part of the string and some kind of command as well. – Timo Nov 28 '17 at 08:22
1

If you want to pass a pipeline like git … | tar … (it's a single command which contains two subcommands, not two separate commands) directly as an argument to a function, you'll need build a string containing that command, and use the eval builtin in the function to execute this string as a shell command.

Take care of proper quoting. For the argument, you're building a string which is a mini-shell-script using the same variables as the enclosing script. In particular, if a variable contains a file name (like tmp here), you need to pass the variable expansion, not the value of the variable, because a file name isn't a shell snippet, so you need '…"$tmp"…', not "…$tmp…". When evaluating, you need to pass the exact string to eval, not a string that has already undergone expansion. In particular, $* is pretty much always wrong; read Why does my shell script choke on whitespace or other special characters?

try () {
  result=$(eval "$1" 2>&1)
  if [ $? -ne 0 ]; then
    failure
    echo $result
    exit 1
  fi
  success
}
try 'git archive --format=tar HEAD | tar xf - -C "$tmp"'

An alternative approach is to stuff the compound command in a function. Again, take care of proper quoting. Use "$@" to evaluate the parameters of the function as a simple command (alias, function, builtin or external command with arguments).

try () {
  result=$(eval "$1" 2>&1)
  if [ $? -ne 0 ]; then
    failure
    echo $result
    exit 1
  fi
  success
}
archive_to_directory () {
  git archive --format=tar HEAD | tar xf - -C "$1"'
}
try archive_to_directory "$tmp"

The status of a pipeline is the status of its right-hand side; the status of the left-hand side is ignored. In bash (but not in sh), you can access the status of all the commands in a pipeline through the PIPESTATUS variable.

try () {
  result=$(eval "$1" 2>&1)
  if [ $? -ne 0 ]; then
    failure
    echo $result
    exit 1
  fi
  success
}
archive_to_directory () {
  git archive --format=tar HEAD | tar xf - -C "$1"'
  [[ -n ${PIPESTATUS[*]//[0 ]/} ]]
}
try archive_to_directory "$tmp"