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
.
\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.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 usingprintf
instead (e.g.printf '\033[1A\r\033[K' >&2
).