The command watch
in FreeBSD has a completely different function than the popular GNU-command with the same name. Since I find the GNU-watch
convenient I wrote a short shell-script to provide that functionality for my systems. The script is a nice way to show off some basics as well as some advanced shell-scripting features.
To resolve the ambiguity with watch(8)
I called it observe
on my system. My observe
command takes the time to wait between updates as the first argument. Successive arguments are interpreted as commands to run. The following listing is the complete code:
#!/bin/sh set -f sleep=$1 clear= shift runcmd() { tput cm 0 0 (eval "$@") tput AL `tput li` } trap 'runcmd "$@"; tput ve; exit' EXIT INT TERM trap 'clear=1' HUP INFO WINCH tput vi clear runcmd "$@" while sleep $sleep; do eval ${clear:+clear;clear=} runcmd "$@" done
Careful observers may notice that there is no parameter checking and the code is not commented. These shortcomings are part of what makes it a convenient example in a tutorial.
Turning Off Glob-Pattern Expansion
The second line already shows a good convention:
#!/bin/sh set -f
The set
builtin can be used to set parameters as if they were provided on the command line. It is also able to turn them off again, e.g. set +x
would turn off tracing. The -f
option turns off glob pattern expansion for command arguments. This is a good habit to pick up, glob pattern expansion is very dangerous in scripts. Of course the -f
option could be set as part of the shebang, e.g. #!/bin/sh -f
, but that would allow the script user to override it. By canlling bash ./observe 2 ccache -s
the shell could be invoked without setting the option, which is dangerous for options with safety-implications.
Global Variable Initialisation
The next block initialises some global variables:
sleep=$1 clear= shift
Initialising global variables at the beginning of a script is not just good style (because there is one place to find them all), it also protects the script from whatever the caller put into the environment using export
or the interactive shell's equivalent.
The shift
builtin can be a very useful feature. It throws away the first argument, so what was $2
becomes $1
, $3
turns into $2
etc.. With an optional argument the number of arguments to be removed can be specified.
The runcmd
Function
The runcmd
function is responsible for invoking the command in a fashion that overwrites its last output:
runcmd() { tput cm 0 0 (eval "$@") tput AL `tput li` }
The tput(1)
command is handy to directly talk to the terminal. What it can do depends on the terminal it is run in, so it is good practice to test it in as many terminals as possible. A list of available commands is provided by the terminfo(5)
manual page. The following commands were used here:
cm
:cursor_address #row #col
Used to position the cursor in the top-left cornerAL
:parm_insert_line #lines
Used to push any garbage on the terminal (e.g. random key inputs) out of the terminalli
:lines
Returns the number of terminal lines onstdout
The tput AL `tput li`
basically acts as a clear below the cursor command.
The eval "$@"
command executes all the arguments (apart from the one that was shifted away) as shell commands. The command is enclosed by parenthesis to invoke it in a subshell. That effectively prevents it from affecting the script. It is not able to change signal handlers or variables of the script, because it is run in its own process.
Signal Handlers
Signal handlers provide a method of overriding the shell's default actions. The trap
builtin takes the code to execute as the first argument, followed by a list of signals to catch. Providing a dash as the first argument can be used to invoke the default action:
trap 'runcmd "$@"; tput ve; exit' EXIT INT TERM trap 'clear=1' HUP INFO WINCH
The INT
signal represents a user interrupt, usually caused by the user pressing CTRL+C
. The TERM
signal is a request to terminate. E.g. it is sent when the system shuts down. The EXIT
is a pseudosignal that occurs when the shell terminates regularly, i.e. by reaching the end of the script (in this case if sleep would fail) or an exit
call.
The HUP
signal is frequently used to reconfigure daemons without terminating them. WINCH
occurs when the terminal is resized. The INFO
signal is a very useful BSDism. It is usually invoked by pressing CTRL+T
and causes a process to print status information.
The Output Cycle
The output cycle heavily interacts with the signal handlers:
tput vi clear runcmd "$@" while sleep $sleep; do eval ${clear:+clear;clear=} runcmd "$@" done
The tput vi
command hides the cursor, tput ve
turns it back on.
The clear
command clears up the terminal before the command is run the first time.
The runcmd "$@"
call occurs once before the loop, because the first call within the loop occurs after the first sleep
interval.
The clear
global is set by the HUP/WINCH/INFO
handler. The eval ${clear:+clear;clear=}
line runs the clear
command if the variable is set and resets it afterwards. The clear
command is not run every cycle, because it would cause flickering. The ability to trigger it is required to clean up the screen in case a command does not override all the characters from a previous cycle.
Conclusion
If you made it here, thank you for reading this till the end! You probably already knew a lot of what you read. But maybe you also learned a trick or two. That's what I hope.