0

In bash, if I want to produce periodical status messages, I can replace the previous one by emitting a hotkey combination, e.g. as seen here, here or here. When a user executes the script directly, this is nice, because it keeps them informed without cluttering their screen.

However, if they want to use the same script in an automated context, the output will (must) be redirected into a file:

./test.sh >log.txt

At this point, none of \r, \b, \033[0K work.

How do I make this work for both modes?

(E.g. it would be straightforward enough to omit the intermediate messages, iff I could detect whether the output stream is a file.)

  • If possible, I would like to avoid introducing an argument to the script. The real-world version already comes with several.
  • If possible, I would like to avoid handling the output streams inside the script itself, e.g. as per this question, so it does not express surprising behaviour when embedded into other scripts.

Sample code test.sh:

#!/bin/bash

PREVIOUS_JOB_STATUS=""
SECONDS=0          # auto-incremented by Bash; divide by INTERVAL_SECONDS to get number of retries
INTERVAL_SECONDS=3 # interval between polls to the server
TIMEOUT_SECONDS=10 # how long until we give up, because the server is clearly busy (or dead)
while true; do
  RETRY_SECONDS=$SECONDS
  if [ $RETRY_SECONDS -lt $INTERVAL_SECONDS ]; then # in the real world, here would be      the polling operation
    JOB_STATUS='queued'
  else
    JOB_STATUS='processing'
  fi
  RESULT=1                                          # in the real world, here would be ? of the polling operation
  if [ $RESULT -eq 0 ]; then                            # success
    exit $RESULT
  elif [ $RETRY_SECONDS -gt $TIMEOUT_SECONDS ]; then    # failure (or at least surrender)
    echo "Timeout exceeded waiting for job to finish. Current Job Status is '$JOB_STATUS'. Current result code is '$RESULT'."
    exit $RESULT
  elif [ "$PREVIOUS_JOB_STATUS" = "$JOB_STATUS" ]; then # no change yet,      replace last status message and try again
    echo -en '\033[1A\r\033[K' # move one line up                                : \033[1A ,
                               # move to pos1                                    : \r      ,
                               # delete everything between cursor and end-of-line: \033[K  ,
                               # omit line break                                 : -n
  else                                                  # job status changed, write   new  status message and try again
    PREVIOUS_JOB_STATUS=$JOB_STATUS
  fi
  SLEEP_MESSAGE='Waiting for job to finish. Waited '`printf "%02g" $SECONDS`" s. Current job status is '$JOB_STATUS'. Current result code is '$RESULT'."
  echo $SLEEP_MESSAGE
  sleep $INTERVAL_SECONDS
done

Final output of ./test.sh:

Waiting for job to finish. Waited 00 s. Current job status is 'queued'. Current result code is '1'.
Waiting for job to finish. Waited 09 s. Current job status is 'processing'. Current result code is '1'.
Timeout exceeded waiting for job to finish. Current Job Status is 'processing'. Current result code is '1'.

Final output of ./test.sh >log.txt 2>&1:

Waiting for job to finish. Waited 00 s. Current job status is 'queued'. Current result code is '1'.
Waiting for job to finish. Waited 03 s. Current job status is 'processing'. Current result code is '1'.
^[[1A^M^[[KWaiting for job to finish. Waited 06 s. Current job status is 'processing'. Current result code is '1'.
^[[1A^M^[[KWaiting for job to finish. Waited 09 s. Current job status is 'processing'. Current result code is '1'.
Timeout exceeded waiting for job to finish. Current Job Status is 'processing'. Current result code is '1'.

Desired final output of ./test.sh >log.txt 2>&1: equal to final output of ./test.sh.

5
  • "At this point, none of \r, \b, \033[0K work" – They do work. cat the file to a terminal and you will see the bytes/sequences saved in the file do their job. Commented Jan 24 at 16:45
  • Yes, fair, but I consider it more not-working than working, considering that less, nano, MonoDevelop, etc. pp. cannot handle it. I'd rather have an effect like en.cppreference.com/w/c/io/fseek than write out a string that cannot be read without post-processing.
    – Zsar
    Commented Jan 24 at 18:23
  • Unfortunately(?) terminals do not work by letting you print to a seekable "screen buffer". They read a stream of bytes and if you redirect the stream to elsewhere then this is what you get. If you don't want some characters/sequences in the stream, you need to suppress printing them or you need to filter them out afterwards. Commented Jan 24 at 18:27
  • In general, status/progress messages should be written to stderr, not stdout (e.g. echo "some status" >&2). This won't solve the problem if both are redirected to a file, but is is basic scripting hygene. Also, echo with options will fail in some shells and/or bash modes, so I recommend using printf instead (e.g. printf '\033[1A\r\033[K' >&2). Commented Jan 24 at 19:28
  • Status and progress messages should most emphatically not be written to stderr. Errors should go there, with context in tow, if necessary. Warnings are so-so, depending on how bad the issue they hint at may be. This is basic logging hygiene.
    – Zsar
    Commented Jan 24 at 19:41

1 Answer 1

2

Use test -t 1 or test -t 2 to test if (respectively) stdout or stderr is associated with a terminal.

-t file_descriptor
True if file descriptor number file_descriptor is open and is associated with a terminal. False if file_descriptor is not a valid file descriptor number, or if file descriptor number file_descriptor is not open, or if it is open but is not associated with a terminal.

(source)

Proof of concept:

sh -c '
test -t 1 && echo foo 
echo bar
'

Run the above code as-is and you will see foo and bar. Redirect the output away from the terminal (e.g. to a regular file, or pipe to cat) and foo will not be printed.

To avoid testing each time when you need to print an intermediate message, use this trick:

if test -t 1; then exec 5>&1; else exec 5>/dev/null; fi

Now print every intermediate message to the file descriptor 5 (e.g. echo baz >&5). It will go to stdout if it's a terminal, to /dev/null otherwise.

3
  • Mmh, this does not seem to do The Right Thing(TM) if piped into tee . I did not expect that, but apparently 1 is not a terminal (and the terminal output of tee behaves like file output) in this case. ... Oh well, I can live with that, I suppose - not seeing a use case for that, right now.
    – Zsar
    Commented Jan 24 at 18:15
  • 1
    @Zsar What do you mean? If you pipe a shell script to tee, the stdout of the script will not be a terminal. The script has no means to know where tee prints to, it only knows its own stdout is not a terminal. Commented Jan 24 at 18:18
  • Well, as I said, I can live with that. Would have been nice, if those pipes were transparent and only the end destination mattered (which is, besides the file, also a terminal - the same terminal as without tee), but if that is not the case, it will not cause me to lose sleep.
    – Zsar
    Commented Jan 24 at 18:25

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .