General impressions - nice neat code, well commented. It's good that you use trap
to clean up on exit.
Shellcheck picks up a couple of oversights:
206596.sh:11:11: note: Check exit code directly with e.g. 'if mycmd;', not indirectly with $?. [SC2181]
206596.sh:22:1: warning: Use 'cd ... || exit' or 'cd ... || return' in case cd fails. [SC2164]
206596.sh:27:5: note: read without -r will mangle backslashes. [SC2162]
206596.sh:44:1: warning: Use 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow non-alphanumeric filenames. [SC2011]
Let's do a walk-through:
#!/bin/bash
Why bash
? I don't think there's anything here that can't be achieved with standard POSIX shell.
NUM_PROCS=4
What's special about 4? If it's the number of cores you have available, then we could automate that:
NUM_PROCS=$(getconf _NPROCESSORS_ONLN)
PIDFILE="/tmp/cleanup.pid"
I'm not sure about CentOS, but on the platforms I'm more familiar with, pidfiles normally live under /run
(or /var/run
on older installations).
PID_NOW="$$"
Never modified; I think it's clearer to simply use $$
where it's needed.
echo "$1" > "$PIDFILE"
if [[ "$?" -ne 0 ]]; then
Instead of testing $?
, we should use the result directly (and since we never call this with anything other than our own pid, let's just embed that in the function rather than passing as argument). Give the function a POSIX-compliant name, and declare it without the deprecated function
keyword:
save_pid() {
if ! echo $$ >"$PIDFILE"
then
# If we couldn't save the PID to the lockfile...
echo >&2 "Failed to create PID file for PID $$ in $PIDFILE; exiting"
exit 1
fi
}
Note that the message is printed to standard error (stream 2) rather than standard output.
cd /scratch
There's no checking that this command succeeded. We could append || exit 1
or similar, but we can catch more errors by setting shell flags -e
and -u
at the beginning of the script.
There's really no need to change working directory at all; we could just omit this command, and use the directory name where we need it.
if [[ -f "$PIDFILE" ]]
The comment says we're testing for existence, so we should be using -e
(or change the comment). And we can use the standard [
command; there's nothing to be gained from Bash-specific [[
.
read PID_SAVED < "$PIDFILE"
Shellcheck complains about using read
without -r
option. Whilst we don't expect to see shell escapes in this file, there's no harm in pacifying the warning. Or, we could just interpolate the file directly:
if ps --quick-pid "$(cat $PIDFILE)" &> /dev/null
Or just test for the existence in procfs:
if [ -e "/proc/$(cat $PIDFILE)" ]
We could add further tests to ensure that we don't have a re-used pid that's running a different program, but I won't pursue that here.
This structure can be simplified:
if a
then
if b
then exit 1
else do-c
fi
else do-c
fi
It's exactly equivalent to
if a && b
then exit 1
fi
do-c
That simplification gives us:
if [ -e "$PIDFILE" ] && [ -e "/proc/$(cat $PIDFILE)" ]
then
# A process exists with our saved PID
echo >&2 "Cleanup script is already running with PID $PID_SAVED; exiting"
exit 1
fi
save_pid
trap clean-up HUP INT QUIT ILL ABRT BUS SEGV PIPE TERM ERR
Instead of listing all these signals, we just want to clean up however the shell exits:
trap clean_up EXIT
ls | xargs -P "$NUM_PROCS" -I{} \
It's dangerous to pipe the output of ls
like that. Files not under your control can contain almost anything, including newlines and other significant space. I don't think we really want to ignore dot-files, either. I recommend find -maxdepth 1 -mindepth 1
, which can then write safely to xargs
using --print0
. We might want to eliminate symlinks from the listing, although we know that the inner find
won't follow them without -H
or -L
.
save_pid
and clean_up
are only used once each now, so could be usefully inlined.
Modified code
#!/bin/sh
set -eu
LIMIT_TIME=30 # days
NUM_PROCS=$(getconf _NPROCESSORS_ONLN)
PIDFILE="/var/run/cleanup.pid"
# check pidfile
if [ -e "$PIDFILE" ] && [ -e "/proc/$(cat $PIDFILE)" ]
then
# A process exists with our saved PID
echo >&2 "Cleanup script is already running with PID $PID_SAVED; exiting"
exit 1
fi
# write pidfile
if ! echo $$ >"$PIDFILE"
then
# If we couldn't save the PID to the lockfile...
echo >&2 "Failed to create PID file for PID $$ in $PIDFILE; exiting"
exit 1
fi
trap 'rm "$PIDFILE"' EXIT
# Now start working
find /scratch -maxdepth 1 -mindepth 1 \! -type l --print0 \
| xargs -0 -P "$NUM_PROCS" -I{} \
find {} -xdev -type f -mtime "+$LIMIT_TIME" -delete
After all that, consider that you might be reinventing a wheel. Most likely, you have a tmpreaper
package available that can be configured to do exactly this task.