5

I have a bash application that is producing some result, and I'd like to echo the result to either stdout or to a user chosen file. Because I also echo other interactive messages going to the screen, requiring the user to explicitly use the > redirection when he wants to echo the result to a file is not an option (*), as those messages would also appear in the file.

Right now I have a solution, but it's ugly.

if [ -z $outfile ]
then
    echo "$outbuf"    # Write output buffer to the screen (stdout)
else
    echo "$outbuf" > $outfile  # Write output buffer to file
fi

I tried to have the variable $outfile to be equal to stdout, to &1 and perhaps something else but it would just write to file having that name and not actually to stdout. Is there a more elegant solution?

(*) I could cheat and use stderr for that purpose, but I think it's also quite ugly, isn't it?

Bregalad
  • 1,025
  • 3
  • 11
  • 14

4 Answers4

10

First, you should avoid echo to output arbitrary data.

On systems other than Linux-based ones, you could use:

logfile=/dev/stdout

For Linux, that works for some types of stdout, but that fails when stdout is a socket or worse, if stdout is a regular file, that would truncate that file instead of writing at the current position stdout is in the file.

Other than that, in Bourne-like shell, there's no way to have conditional redirection, though you could use eval:

eval 'printf "%s\n" "$buf" '${logfile:+'> "$logfile"'}

Instead of a variable, you could use a dedicated file descriptor:

exec 3>&1
[ -z "$logfile" ] || exec 3> "$logfile"

 printf '%s\n' "$buf" >&3

A (small) downside with that is that except in ksh, that fd 3 would be leaked to every command run in the script. With zsh, you can do sysopen -wu 3 -o cloexec -- "$logfile" || exit in place of exec 3> "$logfile" but bash has no equivalent.

Another common idiom is to use a function like:

log() {
  if [ -n "$logfile" ]; then
    printf '%s\n' "$@" >> "$logfile"
  else
    printf '%s\n' "$@"
  fi
}

log "$buf"
  • So basically, using /dev/stdout works in some case, but is not a good idea or can be problematic in some cases, then I'm better off doing the ugly if/else solution ?! – Bregalad Jan 10 '17 at 10:50
  • @Bregalad Looks like it. – Kusalananda Jan 10 '17 at 10:52
  • Very nicely done with that example of a log file that shouldn't be truncated. – Kusalananda Jan 10 '17 at 10:55
  • I do not understand this syntax : [ -z "$logfile" ] || exec 3> "$logfile" It looks like some shorthand if/then/else. Would you care to explain ? Thanks. – Bregalad Jan 10 '17 at 14:02
  • @Bregalad || is or. Either $logfile is empty or you run the exec command. Check your shell man page. – Stéphane Chazelas Jan 10 '17 at 14:11
  • @StéphaneChazelas So basically, [ A ] || B is just a shorthand for if [ ! A ]; then B; fi that have absolutely nothing to do with boolean logic or, right ? – Bregalad Jan 11 '17 at 07:46
  • @Bregalad, A || B is a bit like if ! A; then B; fi in that it executes the B command only if the A command returns false (note that you can use any command in the condition part of an if statement, not just the [ command). It has everything to do with boolean logic. It's just that in shells, booleans are the exit status of commands (0 for true, anything else for false). The [ command exits with 0 when the test expression it evaluates resolves to true. You can do if A || B; then echo Either A or B returned true; fi for instance (B would not be run if A returned true, like in C). – Stéphane Chazelas Jan 11 '17 at 09:39
6
  1. Set outfile to "/dev/stdout".
  2. Let user choose filename, overriding outfile, or keep default value.
  3. printf '%s\n' "$outbuf" >"$outfile"

I used printf because of "Why is printf better than echo?".

For caveats to this solution, see Stéphane Chazelas' answer.

Kusalananda
  • 333,661
0

Try this

#!/bin/bash
TOSCREEN="1" # empty OR 0 to log to file
if [[ ! -z "$TOSCREEN" && $TOSCREEN == "1" ]]; then
    echo "$outbuf"    # Write output buffer to the screen (stdout)
else
    if [[ ! -f $outfile ]]; then
      #echo -e "Making file "
      touch "$outfile"
    fi  
    echo "$outbuf" >> $outfile  # Write output buffer to file
fi
0
#!/bin/bash -ue

function stdout_or_file()
{
    DUMP_FILE=${1:-}

    if [ -z "${DUMP_FILE}" ]; then
        # print stdin to stdout
        cat
    else
        # dump stdin to a file
        sed -n "w ${DUMP_FILE}"
    fi
}

echo "foo" | stdout_or_file "${outfile}"

Note that ${1:-} is bash specific and especially useful in "-u" mode. A generic one-line that even works in other shells might be

echo "foo" | if [ -z "${outfile}" ]; then cat; else sed -n "w ${outfile}"; fi
  • ${1:-} is actually POSIX standard, not bash specific. Using -u with sh (or with set -u) is also standard. – Kusalananda Jul 14 '21 at 13:45
  • So are you using a non-POSIX shell then and that explains why you get a - there? – Axel Heider Jul 15 '21 at 00:49
  • No I'm not, I'm instead not reading or thinking properly in the middle of the night in my +30 degree apartment. My apologies. I've deleted the irrelevant comments. – Kusalananda Jul 15 '21 at 05:35