2

I have following bash script:

flag=false
command_name \
$(  flag == false  && printf %s '>/dev/null') 

I expect no output at Terminal but I still get some. If I redirect output to /dev/null on the same line as command-name without that expansion then it gets suppressed.

Command is dx tool from android SDK

Edit 1: Here the code from script

dx \
    --dex \
    $( ( (( flag_v == 1 )) || (( flag_v == 'd' ))) && printf %s '--verbose') \
    --no-strict \
    --output="../"$app_name.jar \
    $(find . -type f -name '*.class') \
    $( $dexflag == false && printf %s '>/dev/null')

As I run the tool and it works as expected. I do not think that it could be an error stream.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
Harshiv
  • 376
  • 2
  • 17
  • 1
    Run your code thru https://shellcheck.net, (use #!/bin/bash as the first line), then if you still have problems, update your Q with code that doesn't have syntax problems. Also, do you know about the concept of `std-error` . You'll need to redirect that stream to `/dev/null` as well. Try `... > /dev/null 2>&1` (I haven't downvoted your Q). Good luck. – shellter Nov 29 '17 at 12:52
  • The results of an expansion, such as `$( )`, aren't parsed as code. Thus, if those results contain expansions themselves, they won't be honored. This is a feature, not a bug -- writing shell scripts processing untrusted data would be a nightmare otherwise. – Charles Duffy Nov 29 '17 at 13:14
  • ...and the above assumes that `$dexflag == false` is a valid statement, which it isn't. (If `dexflag` is empty it'll run `== false` as a command; if it's `true`, it'll run `true == false` as a command; since the `true` command ignores your arguments, the result will always be true; if `$dexflag` contains `false`, then `false == false` will always return false, because the `false` command likewise doesn't look at its arguments at all). – Charles Duffy Nov 29 '17 at 13:16
  • And `(( ))` is *arithmetic* context; you can't do string comparisons there (when it *is* allowed, it'll evaluate strings as the numeric value of the variables they name). – Charles Duffy Nov 29 '17 at 13:17
  • I wish to append the `/dev/null` redirection when the flag is false. – Harshiv Nov 29 '17 at 13:19
  • Yes, it's clear that's what you want, but what your code *actually does* is append `>/dev/null` as an extra argument to `dx`, not running it as a redirection. – Charles Duffy Nov 29 '17 at 13:20
  • Is `dx` the last command in the script? Can we run a redirection that impacts the whole rest of the script, or does it need to be just the one command? – Charles Duffy Nov 29 '17 at 13:21
  • Yes, dx is a script. and I wish to redirect only the output of dx command – Harshiv Nov 29 '17 at 13:23
  • That's not what I asked. I asked if this script ran any other commands after it runs `dx`. – Charles Duffy Nov 29 '17 at 13:23
  • Yes It runs other commands. It's a bit of a mess there. When the dx tools successfully processes the files I attempt to run the dexed hello world program. I thought that posting entire script would make it less readable. – Harshiv Nov 29 '17 at 13:31
  • Agreed that posting the whole script would be the wrong thing -- we do want a [mcve], and inasmuch as this depends on variables it doesn't set (and has lines unrelated to the core question about conditional redirection) it's already not as minimal, or complete and verifiable, as it should be; just needed that detail to be explicit, as to just suppress the entire rest of the script's output (if "the rest of the script's output" was only the output of `dx`) would be a much simpler line: `[[ $dexflag = false ]] && exec >/dev/null`. – Charles Duffy Nov 29 '17 at 13:35
  • BTW, as a general note on why string-splitting is often a source of bugs -- consider what happens when running `$(find . -type f -name '*.class')` if a less-privileged user can run `dir=$'./\n/etc/passwd\n/'; mkdir -p "$dir" && touch "$dir"/evil.class`. (Even if you change `IFS` to not split on regular spaces, newlines are valid in filenames -- so it's impossible to tell the difference between a newline printed by `find` because it's part of the name and one printed by `find` to separate two names). – Charles Duffy Nov 29 '17 at 13:43
  • btw, in `--output="../"$app_name.jar`, the only part that needs to be in double quotes for correctness is the expansion of `app_name`; there's no need to quote the `../` separately -- `--output="../$app_name.jar"` would do, as would `"--output=../$app_name.jar"` or `--output=../"$app_name".jar`. – Charles Duffy Nov 29 '17 at 13:51
  • 1
    (see the flagged dupe, btw -- it's a good example of what a question *should* look like, with the simplest possible code that showcases a problem, runnable without any tools/setup being needed). – Charles Duffy Nov 29 '17 at 13:57

1 Answers1

9

Conditionally Redirecting Stdout

Redirections are shell syntax -- they have to be recognized at a parsing phase that comes before parameter expansion, so you can't generate them via variable expansion (without committing evil).

What you can do (in bash 4.1 or later) is have an unconditional redirection, but have the thing it redirects to change:

# Create an out_fd variable that points to stdout (FD 1) if dexflag != "false", or to a new
# handle on /dev/null otherwise
if [[ $dexflag = false ]]; then
  exec {out_fd}>/dev/null # maybe put 2>&1 as well to suppress stderr
else
  out_fd=1 # use FD 1 (stdout)
fi

# run dex with its stdout redirected to the FD number in "out_fd"
dex ... >&"$out_fd"

# if out_fd is not stdin/stdout/stderr, then go ahead and close it when done.
(( out_fd > 2 )) && exec {out_fd}>&-

Note:

  • A string comparison is done in the form [[ $var = $pattern ]] (or [[ $var = "$string" ]] to do an exact match). See the bash-hackers' wiki on the conditional expression.
  • In bash 4.1 or later, exec {fd_varname}>file opens file, and puts the file descriptor number pointing to that file in the variable fd_varname. exec {fd_varname}>&- closes the file descriptor whose number is stored in fd_varname.
  • With older versions of bash, you can still do this logic, but instead of having a file descriptor number automatically assigned, you'll need to do so by hand, manually assigning an otherwise-unused FD number that isn't any of 0, 1 or 2 (which are reserved for stdin, stdout and stderr). Thus, in that case, it might be exec 3>/dev/null or exec 3>&1 in the if branches, >&3 on the dex command, and exec 3>&- to close it.

Safely Generating Argument Lists Conditionally

See BashFAQ #50 for a long discussion. In short, though: For everything but the redirection to /dev/null, there's one simple change needed to bring this in line with best practices: Use an array.

#!/bin/bash

args=( )

case $flag_v in
  1|d) args+=( --verbose ) ;;
esac

while IFS= read -r -d '' filename; do
  args+=( "$filename" )
done < <(find . -type f -name '*.class' -print0)

dx --dex --no-strict --output="../$app_name.jar" "${args[@]}"
  • See BashPitfalls #1 describing why $(find ...) (like $(ls ...)) is unsafe, and Using Find going into best practices.
  • See BashFAQ #24 to understand why while read ...; do ...; done < <(find ...) is used instead of find ... | while read ...; do ...; done.
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441