简体   繁体   中英

Adding a redirection to a bash command array

I am storing a command to execute in a bash array, example:

declare -a cmd=("sudo" "dnf" "update")
"${cmd[@]}"

Last metadata expiration check: 0:24:45 ago on Fri 07 Jan 2022 03:35:34 PM EST.
Dependencies resolved.
Nothing to do.
Complete!

Now, say I want to redirect the output to make it less noisy. This works:

"${cmd[@]}" &>/dev/null

But I would prefer to store the redirect with the command array so it can be added/removed like any other command in the array:

declare -a cmd=("sudo" "dnf" "update" "&>/dev/null")
"${cmd[@]}"

Last metadata expiration check: 0:29:14 ago on Fri 07 Jan 2022 03:35:34 PM EST.
No match for argument: &>/dev/null

The output isn't being redirected, the final array element is just being passed like a normal argument. Is there any way to get this work (ie judicious use of eval) or a better strategy?

XY statement: I am trying to use conditionals to make my program output silent. I can do this with:

silent=true
cmd=("sudo" "dnf" "update")
if silent; then
  "${cmd[@]}" &>/dev/null
else # Be noisy
  "${cmd[@]}"
fi

This results in lots of duplicated code over the course of my program (every debug operation needs multiple command execution lines). Instead I would prefer appending the redirection to the array, such as:

silent=true
cmd=("sudo" "dnf" "update")
$silent && cmd+=("&>/dev/null")
"${cmd[@]}"

This strategy works great for functions and arguments but not for redirections. While I can apply --quiet flags to some programs to achieve this, in some cases I would like to redirect stderr, redirect to file, etc.

Dynamic silent redirection

#!/usr/bin/env sh

cmd() {
  silent=$1
  if [ true = "$silent" ]
    then out=/dev/null
    else out=/dev/stdout
  fi

  sudo dnf update > "$out"
}

How about prepending to the array?

# provide a function that wraps the content
silence() { "$@" >/dev/null 2>&1; }

if [ "$silent" = true ]; then
  cmd=( silence "${cmd[@]}" )
fi
"${cmd[@]}"

Of course, you could just use that wrapper unconditionally and make it responsible for the work:

maybe_silence() {
  if [ "$silent" = true ]; then
    "$@" >/dev/null 2>&1
  else
    "$@"
  fi
}

maybe_silence "${cmd[@]}"

If you really want to be able to support arbitrary redirections (and other shell syntax), then it makes sense to have a wrapper that just applies one redirection and leaves everything else unmodified.

with_redirection() {
  local redirections=$1     # first argument contains redirections to perform
  shift || return           # remove it from "$@"
  local cmd                 # create a local variable to store our command
  printf -v cmd '%q ' "$@"  # generate a string that evals to our argument list
  eval "$cmd $redirections" # run the string with the redirection following
}

...so you can run:

cmd=( with_redirection '&>/dev/null' sudo dnf update )
"${cmd[@]}"

...and only &>/dev/null is subject to eval -like behavior, while other contents are passed normally. You can even nest this:

testfunc() { echo "this is on stderr" >&2; }
cmd=( with_redirection '>out.txt' with_redirection '2>&1' testfunc

...and you end up with this is on stderr in out.txt (though of course, you could also run with_redirection '>out.txt 2>&1' testfunc to get the same effect).

You have to use eval to process shell operations in the variable expansion.

declare -a cmd=("sudo" "dnf" "update" "&>/dev/null")
eval "${cmd[@]}"

If you want to avoid duplicate "${cmd[@]}" code, you can use a subshell:

(
    [[ $silent == true ]] && exec &>/dev/null
    exec "${cmd[@]}"
)

I suggest the use of exec if cmd will always be an external command.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM