Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A bash script to list all cron tasks on a computer.
#!/bin/bash
# This script is designed to work only with recent bash implementations, not ash/dash etc.
# @file show-cron-jobs.sh
#
# published at:
# https://gist.github.com/myshkin-uk/d667116d3e2d689f23f18f6cd3c71107
#
# version 13
#
# This version is functionally equivalent to version 12 - but if it detects jobs associated with the 'at'
# command it emits a list of them for completeness.
#
# @param Any parameter suppresses 'helpful output' and so emits only the result table.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
# OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# @author DGC after yukondude
# email me at from_scj_snippet@myshkin.me
# @FIXME DGC 20-Feb-2019
# On consideration, the way we handle 'dummy anacron' is not right.
# The resulting entries are labelled 'anacron' - as if they really were using it.
# We should either:
# Do a prepass, and decide the situation is an operative 'dummy-anacron'
# ( anacron exe not executable
# though in fact further facts are needed to make anacron operate
# so you could test them too
# presence of 0hourly
# presence of 0anacron
# )
# then label the resulting tasks accordingly.
# or
# Just treat dummy anacon crontab entries as 'standard fare'
# ( which involves adding further 'special case' code where we test the executability
# of one file, but actually execute a different thing. )
# so label them as "main crontab"
# @FIXME DGC 8-Jan-2019
# This file is based on a much simpler example picked up from
# https://stackoverflow.com/questions/134906/how-do-i-list-all-cron-jobs-for-all-users/53892185#53892185
# authored by yukondude.
#
# It has now evolved into a perfect example of tasks which should not be done in bash.
#
# It uses a pile of intermediate temporary files, which we do our best to clear up on exit
# but may still be left cluttering the place up after errors and problems.
#
# The right way to do it is to store the info about tasks in an array of structs.
# The bash code could be improved to use a one-dimensional array, but it can't go that far.
#
# This really needs re-writing in a more capable language.
# Version changes
# 11 Tidier comments
# Drop empty columns in output when no numeric timings are shown.
# 12 Fix case-disregard on dow and moy strings.
# Fix detection of untranslated numeric timings.
if [ "root" != "$(whoami)" ]
then
echo "This script can only run as root - quitting."
exit
fi
# Any parameter on the command line requests quiet mode.
#
quiet=false
if [ '0' != "$#" ]
then
quiet=true
fi
showProgress=false
#set -e
#set -x
screenColumns="$(tput cols )"
screenRows="$( tput lines)"
screenWidthSeparator="$(printf "%0.s-" $(seq 1 ${screenColumns}))"
if ! ${quiet}
then
cat << EOS
Add any parameter to the command to suppress this message.
WARNING - this script is clever, but not as clever as you might hope.
It now examines the executables invoked by each cron line, and tries to warn you if they are
not executable.
not present where they are expected to be.
Now there is another thing which can go wrong we we do not yet check,
which is that the executable may be present and executable by root
but not by the user which is scheduled to run it.
You might hope that something on the lines of:
if [ 'yes' = "\$(sudo -u ${user} bash -c "if [ -x ${executableAPAFN} ] ; then echo 'yes' ; else echo 'no'; fi")" ]
would accomplish the required test. But it doesn't.
It turns out that [ -x ] only ever tests the permission bit, and ignores the directory and file rights
to say nothing of the fact that entire filesystems can be mounted as 'non-executable'.
So it is not within our reasonable aspirations to spot a situation where a cron task cannot run for
anything but the simplest reasons.
=============================================================================================================
EOS
fi
# System-wide crontab file and cron job directory. Should be standard.
#
mainCrontabAPAFN='/etc/crontab'
# A file which controls the action of 'real' anacron, but which is missing in the dummied-up version.
anacrontabAPAFN='/etc/anacrontab'
# I don't know where to mention this, but I'll do it here....
# files in /etc/cron.d are crontabs, not scripts,
# and are textually included in the main crontab
# so the executable status of the file is not significant - it goes in regardless.
#
cronAdditionsAPADN='/etc/cron.d'
# Definitions which are only utilised when anacon is in use.
anacronHourlyAPARN='/etc/cron.hourly/'
anacron0anacronAPAFN="${anacronHourlyAPARN}0anacron"
anacronDailyAPARN='/etc/cron.daily/'
anacronWeeklyAPARN='/etc/cron.weekly/'
anacronMonthlyAPARN='/etc/cron.monthly/'
# Single tab character.
tab=$(echo -en "\t")
# Given a stream of crontab lines:
# replace whitespace characters with a single space
# remove any spaces from the beginning of each line.
# exclude non-cron job lines
# replace '@monthly', 'Wed' and 'May' type of tokens with corresponding numeric sequences
# so they can be processed by the rest of the code.
#
# Reads from stdin, and writes to stdout.
# SO DO NOT INSERT ECHO STATEMENTS FOR DEBUGGING HERE!!
#
# @param prefix A string to be prepended to each line we output.
#
function cleanCronLines()
{
prefix="$1"; shift
# @FIXME DGC 28-Jan-2019
# I think we should drop all leading whitespace - this just seems to do one.
setMatchMonthField='matchMonthField="s#^(((((\*|[0-9]+-[0-9]+|[0-9]+)(/[0-9]+)?|[0-9]+(,[0-9]+)+) *){3}) ${month} )#\2 ${monthNum} #I"'
setMatchDowField=' matchDowField=" s#^(((((\*|[0-9]+-[0-9]+|[0-9]+)(/[0-9]+)?|[0-9]+(,[0-9]+)+) *){4}) ${dow} )#\2 ${dowNum} #I"'
while read line
do
if ${showProgress} ; then echo -n "c" 1>&2 ; fi
# sed "s/\s+/ /g" convert all multiple-spaces to single space
# sed "s/^ //g" remove any number of leading whitespaces ( not tabs - should really )
#
# grep
# --invert-match emit only lines NOT matching the following pattern...
# ^(|||) match any of these alternatives, but only at start of line
# $ blank line
# # comment line [ disregard leading whitespace. ]
# [[:alnum:]_]+= <identifier>= line [ disregard leading whitespace. ]
#
# grep
# ignore lines like
# 25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.<period> )
#
# ( we test for those separately. )
#
# sed "s/^@xxxx/0 0 1 1 9/" convert the conventional @xxx tags
# to roughly corresponding 'standard' timing settings.
# the '9' in reboot will be spotted later, and treated specially.
#
# In the following lines, each field can have one of the formats:
# \* meaning *
# [0-9]+-[0-9]+ meaning 4-7
# [0-9]+ meaning 23
# each of which can have an optional /123 type divider appended.
# [0-9]+(,[0-9]+)* meaning 2,8,12
#
# sed "s/ ... Jan /... 1/I"
# convert month-of-year tokens ( in either case ) into numeric equivalents
# sed "s/ ... Mon /... 1/I"
# convert day-of-week tokens ( in either case ) into numeric equivalents
#
# sed "s/ ... 0/... 7/"
# force Sunday to be represented by 7, not zero, as it helps during sorting later.
#
# insert the required prefix.
#
# Skip the line completely if it is
# empty or comment
# crontab parameter setting
# dummy anacron run-parts
# line.
line="$(echo "${line}" | \
sed -r "s/\s+/ /g ; s/^ //g" | \
grep -E -v '^$|^#|[[:alnum:]_]+=|test *-x */usr/sbin/anacron *\|\| *\( *cd */ *&& *run-parts.*/etc/cron\.' \
)"
if [ -z "${line}" ]
then
#if ${showProgress} ; then echo -n ">" 1>&2 ; fi
continue
fi
if [ "@" = "${line:0:1}" ]
then
if ${showProgress} ; then echo -n "@" 1>&2 ; fi
line="$(echo "${line}" | \
sed -r "s/^@reboot/0 0 1 1 9/ ; \
s/^@hourly/0 * * * */ ; \
s/^@daily/0 0 * * */ ; \
s/^@midnight/0 0 * * */ ; \
s/^@weekly/0 0 * * 7/ ; \
s/^@monthly/0 0 1 * */ ; \
s/^@annually/0 0 1 1 */ ; \
s/^@yearly/0 0 1 1 */ " \
)"
fi
if echo "${line}" | grep -qiE "Jan|Feb|Mar|Apr|May|Jun|Jly|Aug|Sep|Oct|Nov|Dec|Mon|Tue|Wed|Thu|Fri|Sat|Sun"
then
if ${showProgress} ; then echo -n "M" 1>&2 ; fi
line="$(echo "${line}" | \
(month="Jan"; monthNum="1" ; eval "${setMatchMonthField}"; sed -r "${matchMonthField}") | \
(month="Feb"; monthNum="2" ; eval "${setMatchMonthField}"; sed -r "${matchMonthField}") | \
(month="Mar"; monthNum="3" ; eval "${setMatchMonthField}"; sed -r "${matchMonthField}") | \
(month="Apr"; monthNum="4" ; eval "${setMatchMonthField}"; sed -r "${matchMonthField}") | \
(month="May"; monthNum="5" ; eval "${setMatchMonthField}"; sed -r "${matchMonthField}") | \
(month="Jun"; monthNum="6" ; eval "${setMatchMonthField}"; sed -r "${matchMonthField}") | \
(month="Jly"; monthNum="7" ; eval "${setMatchMonthField}"; sed -r "${matchMonthField}") | \
(month="Aug"; monthNum="8" ; eval "${setMatchMonthField}"; sed -r "${matchMonthField}") | \
(month="Sep"; monthNum="9" ; eval "${setMatchMonthField}"; sed -r "${matchMonthField}") | \
(month="Oct"; monthNum="10"; eval "${setMatchMonthField}"; sed -r "${matchMonthField}") | \
(month="Nov"; monthNum="11"; eval "${setMatchMonthField}"; sed -r "${matchMonthField}") | \
(month="Dec"; monthNum="12"; eval "${setMatchMonthField}"; sed -r "${matchMonthField}") | \
(dow="Mon"; dowNum="1" ; eval "${setMatchDowField}" ; sed -r "${matchDowField}" ) | \
(dow="Tue"; dowNum="2" ; eval "${setMatchDowField}" ; sed -r "${matchDowField}" ) | \
(dow="Wed"; dowNum="3" ; eval "${setMatchDowField}" ; sed -r "${matchDowField}" ) | \
(dow="Thu"; dowNum="4" ; eval "${setMatchDowField}" ; sed -r "${matchDowField}" ) | \
(dow="Fri"; dowNum="5" ; eval "${setMatchDowField}" ; sed -r "${matchDowField}" ) | \
(dow="Sat"; dowNum="6" ; eval "${setMatchDowField}" ; sed -r "${matchDowField}" ) | \
(dow="Sun"; dowNum="7" ; eval "${setMatchDowField}" ; sed -r "${matchDowField}" ) | \
(dow="0"; dowNum="7" ; eval "${setMatchDowField}" ; sed -r "${matchDowField}" ) \
)"
fi
# You could emit a trace character here to show something got through
# but as it happens all lines we pass on are processed by the routine below
# which will emit one for us.
#
echo "${prefix} | ${line}"
done;
}
# Given a stream of cleaned crontab lines,
# if they don't include the run-parts command
# echo unchanged
# if they do
# show each job file in the run-parts directory as if it were scheduled explicitly.
#
# Reads from stdin, and writes to stdout.
# SO DO NOT INSERT ECHO STATEMENTS FOR DEBUGGING HERE!!
#
function lookupRunParts()
{
while read line
do
#echo "#running lookupRunParts on '${line}'"
match=$(echo "${line}" | grep -Eo 'run-parts (-{1,2}\S+ )*\S+' )
if [ -z "${match}" ]
then
if ${showProgress} ; then echo -n "l" 1>&2 ; fi
echo "${line}"
else
if ${showProgress} ; then echo -n "e" 1>&2 ; fi
# This is awkward code - it needs to know how many fields there are on the line.
# It would be better to split the line in two at the token "run-parts"
#
prefixCronAndUserFields=$(echo "${line}" | cut -f1-8 -d' ' )
cronJobDir=$( echo "${match}" | awk '{print $NF}')
#echo "#expanding run-parts in '${line}' with prefix cron and user fields '${prefixCronAndUserFields}'"
if [ -d "${cronJobDir}" ]
then
for cronJobFile in "${cronJobDir}"/*
do
if [ -f "${cronJobFile}" ]
then
echo "${prefixCronAndUserFields} ${cronJobFile}"
fi
done
fi
fi
done
}
# Temporary files for crontab lines.
#
# The following lines must match the deletion lines in the function below.
#
# A better scheme which created them all in a subdirectory of /tmp,
# and set an exit trap to remove the subdirectory and contents,
# would be better.
#
if ${showProgress} ; then echo -n "1"; fi
keepWorkFiles=true
if ${keepWorkFiles}
then
cleanCronLinesAPAFN="/tmp/cleanCronLines.txt"
cronLinesAPAFN="/tmp/cronLines.txt"
cronForUserAPAFN="/tmp/cronForUser.txt"
sortedLinesAPAFN="/tmp/sortedLines.txt"
annotatedSortedLinesAPAFN="/tmp/annotatedSortedLines.txt"
else
# We just assume, and depend on, the presence and normal operation of mktemp.
#
cleanCronLinesAPAFN="$(mktemp)" || exit 1
cronLinesAPAFN="$(mktemp)" || exit 1
cronForUserAPAFN="$(mktemp)" || exit 1
sortedLinesAPAFN="$(mktemp)" || exit 1
annotatedSortedLinesAPAFN="$(mktemp)" || exit 1
fi
deleteTempFiles()
{
# The following lines must match the creation lines above.
# We run a delete on each of the files even if they were in fact never created.
#
rm -f "${cleanCronLinesAPAFN}"
rm -f "${cronLinesAPAFN}"
rm -f "${cronForUserAPAFN}"
rm -f "${sortedLinesAPAFN}"
rm -f "${annotatedSortedLinesAPAFN}"
}
if ${keepWorkFiles}
then
# We will keep these after running, but we have no interest in any previous versions that may be lying about now.
deleteTempFiles
else
trap deleteTempFiles EXIT
fi
if ${showProgress} ; then echo -n "2"; fi
# Add all of the jobs from the main crontab file,
# except for the 4 supporting dummy-up for anacron.
cat "${mainCrontabAPAFN}" | cleanCronLines "main-crontab" > "${cleanCronLinesAPAFN}"
cat "${cleanCronLinesAPAFN}" | lookupRunParts > "${cronLinesAPAFN}"
if ${showProgress} ; then echo "3"; fi
# Add all of the jobs from files in the system-wide cron.d directory.
for cronDotDFileAPAFN in "${cronAdditionsAPADN}"/*
do
fileName="$(basename ${cronDotDFileAPAFN})"
cat ${cronDotDFileAPAFN} | cleanCronLines "cron.d-${fileName}" | lookupRunParts >> "${cronLinesAPAFN}"
done
if ${showProgress} ; then echo "4"; fi
#echo "Main crontab and cron.d files contain:"
#cat ${cronLinesAPAFN} | sed "s#${tab}#<tab>#g"
#echo "-----------"
#echo
# Anacron is a scheme for implementing hourly/daily/weekly and monthly cron tasks.
# but with a bit more sophistication than simply scheduling them in the main cron tab ( or cron.d dirs ).
#
# I find that it is invoked as follows:
# Searching /etc/cron.d is the default behaviour of the cron daemon
# and files there simply extend the main cron file.
#
# We find there is a script
# /etc/cron.d/0hourly
# which simply runs, as root, all scripts in /etc/cron.hourly
# NB there are no further scripts for other (longer) intervals,
# any such intervals are cascaded from with the hourly handling.
#
# In fact we often find just one hourly script, though others can be added if desired.
# /etc/cron.hourly/0anacron
# NB - any other hourly scripts added are NOT skipped by the logic described below to do with AC power.
#
# Now I find that 0anacron in fact has logic to:
# Quit without acting if the daily tasks have already run today.
# Quit without acting if a script
# /usr/bin/on_ac_power
# exists and when executed reports that we are not ( so I guess we are on battery )
# If it finds no reason not to, it runs anacron, which then processes the /etc/anacrontab file.
# this script just assumes that file contains the dafault instructions for daily/weekly/monthly actions.
#
# We now try to handle main crontabs of this format - which we find on Raspberry Pi units
# ( and other debian machines I assume ).
# They dummy up the action of the 'standard' /etc/anacrontab when no anacron is in use,
# supporting daily/weekly/monthly actions - though without the other clever features of 0anacron.
#
# 17 * * * * root cd / && run-parts --report /etc/cron.hourly
# 25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
# 47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
# 52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
#
# after pre-testing executability using "command -v"
#
# Note that in fact that logic is inadequate - in that what we REALLY want to look for is
# -x /etc/cron.hourly/0anacron
# ( and then if you want perfection 'the anacron invoked by it', which is normally
# -x /usr/sbin/anacron
# as above. )
#
usesAnacron=false
if cat "${mainCrontabAPAFN}" | grep -qP "root.*run-parts.*${anacronHourlyAPARN}" \
&& [ -f "${anacron0anacronAPAFN}" ]
then
# We are on one of the systems ( like RPi ) which dummy-up anacron in the main crontab.
#
# We simply assume that if the line we have found is present in /etc/crontab.
# it will be accompanied by 3 further lines formatted similarly to:
# 25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
#
usesAnacron=true
elif cat "${cronLinesAPAFN}" | grep -qP "root.*etc/cron\..*/0anacron" \
&& [ -f "${anacrontabAPAFN}" ]
then
usesAnacron=true
# Remove from our list of tasks the line which invokes anacron hourly
# that isn't a task we need to report.
# though we will report everything it will cause to happen.
#
sed -i '/hourly\/0anacron/d' "${cronLinesAPAFN}"
if cat "${cronLinesAPAFN}" | grep -q "0anacron"
then
echo "This script has detected that anacron has been altered to run at some other interval than 'hourly'."
echo "It would be simple to extend of this script to handle that, but we have not done so."
echo "Script will quit."
exit
fi
fi
if ${showProgress} ; then echo "5"; fi
# Get a list of users on this machine. Most of whom will never run a cron task.
declare -a users
knownUsers=0
while read user
do
users[${knownUsers}]="${user}"
(( knownUsers++ ))
done < <(cut --fields=1 --delimiter=: /etc/passwd)
# This only works because user names cannot contain spaces or semicolons.
#
# sevUsers means "semicolon-encapulated-values"
# the list starts with, ends with, and is separated using, semicolons.
#
# bash does not have a proper 'does this entry exists in the array' function
# ( though you can test for an empty string if you aren't trapping undefined variables. )
# but we can grep for a user name in a long string of them.
#
sevUsers=";${users[@]};"; sevUsers="${sevUsers// /;}"
# debug.
#echo "sevUsers = '${sevUsers}'"
if ${showProgress} ; then echo "6"; fi
# Examine each user's crontab (if it exists). Insert the user's name between the
# five time fields and the command, so the lines match the main crontab ones.
checkUser=0
while [ "${checkUser}" -lt "${knownUsers}" ]
do
user="${users[${checkUser}]}"
# Note that this edit will fail on a malformed line.
# We have to make sure we don't create double-spaces, as that messes up line splitting
# but ensuring that results in awkward-looking code.
#
crontab -l -u "${user}" 2>/dev/null | \
cleanCronLines "${user}-crontab" | \
sed -r "s/^(\S+) \| ((\S+ +){5})(.+)$/\1 | \2${user} \4/" | \
lookupRunParts \
> ${cronForUserAPAFN}
while IFS= read -r cronLine
do
echo "${cronLine}" >> "${cronLinesAPAFN}"
done < ${cronForUserAPAFN}
(( checkUser++ ))
done
if ${showProgress} ; then echo "7"; fi
#echo "Main crontab plus user crontabs contain:"
#cat ${cronLinesAPAFN} | sed "s#${tab}#<tab>#g"
#echo "-----------"
#echo
# The following section simply assumes that no-one has altered the standard /etc/anacrontab file
# We do not completely deal with the
# START_HOURS_RANGE
# and
# RANDOM_DELAY
# parameters.
# However we do now carry them through, and print them rather cryptically on the output.
#
# I think each task can set a further timing setting ( called 'base delay' below )
# which we have not read up about, and completely ignore.
#
# Use of the START_HOURS_RANGE setting
# makes the assumption that jobs run under this system are limited to 'regular housekeeping'
# tasks which it is reasonable to suppress or put-off-till-later during certain periods of the day.
#
# That file on a server we looked at read:
#
# # /etc/anacrontab: configuration file for anacron
#
# # See anacron(8) and anacrontab(5) for details.
#
# SHELL=/bin/sh
# PATH=/sbin:/bin:/usr/sbin:/usr/bin
# MAILTO=root
# # the maximal random delay added to the base delay of the jobs
# RANDOM_DELAY=45
# # the jobs will be started during the following hours only
# START_HOURS_RANGE=3-22
#
# #period in days delay in minutes job-identifier command
# 1 5 cron.daily nice run-parts /etc/cron.daily
# 7 25 cron.weekly nice run-parts /etc/cron.weekly
# @monthly 45 cron.monthly nice run-parts /etc/cron.monthly
#
if ${usesAnacron}
then
# We use the prefix 'anacron' for 'real anacron' and also 'dummied-up-anacron'
anacronPrefix="anacron"
# If we are dummying-up anacron we will not have an /etc/anacrontab file to look at.
if [ -f "${anacrontabAPAFN}" ]
then
# These settings can legitimately be absent.
rangeSetting="$(cat "${anacrontabAPAFN}" | grep "START_HOURS_RANGE" | sed 's/START_HOURS_RANGE=//')"
delaySetting="$(cat "${anacrontabAPAFN}" | grep "RANDOM_DELAY" | sed 's/RANDOM_DELAY=//' )"
if [ -n "${rangeSetting}" ]
then
anacronPrefix="anacron_${rangeSetting}"
fi
if [ -n "${delaySetting}" ]
then
anacronPrefix="${anacronPrefix}[~${delaySetting}]"
fi
fi
# The following code inserts impossible value strings including '98',
# which will sort anacron tasks after non-anacron tasks
# in the task list we print out.
# ( you could try using -1 to sort them 'before'. )
#
# We expect to spot those impossible values and replace them in the final output.
# In a non-systemd unit, apparently anacron is only run daily.
# ( which of course is good enough to run it's sub-tasks daily/weekly/monthly )
#
# In a systemd machine it is run hourly. See:
# https://unix.stackexchange.com/questions/478803/is-it-true-that-cron-daily-runs-anacron-everyhour
#
# However even if run hourly it does not take control of other tasks in /etc/cron.hourly.
# Apparently all anacron tasks run as root.
# The logic here DOES NOT examine whether the things it finds are executable.
# If we were simply building a list of things which WILL happen, we could add
# -executable to the find command.
# [ "x" = "$( ls -l $file | cut -c4-4 ) ] before we echo the lines to $cronLinesAPAFN
#
# HOWEVER - one of the features of this script is that it brings to your attention
# anything which WOULD have run, if only the executable flag was set
# - but won't because it isn't.
#
# So we ignore the executable status here, and add it to the list regardless.
if [ -d "${anacronDailyAPARN}" ]
then
# The read command will return success if any output is produced by 'find',
# the find command itself does NOT flag whether it found anything.
#
if find "${anacronDailyAPARN}" -mindepth 1 -type f | read
then
for file in $(ls "${anacronDailyAPARN}"* )
do
# Note these timing parameters are not EXACTLY what anacron
# does with such 'daily' tasks, but they are an approximately equivalent stand-in.
#
echo "${anacronPrefix} | 98 98 * * * root ${file}" >> "${cronLinesAPAFN}"
done
fi
fi
if [ -d "${anacronWeeklyAPARN}" ]
then
# See comment above.
if find "${anacronWeeklyAPARN}" -mindepth 1 -type f | read
then
for file in $(ls "${anacronWeeklyAPARN}"* )
do
# Note these timing parameters are not EXACTLY what anacron
# does with such 'weekly' tasks, but they are an approximately equivalent stand-in.
#
echo "${anacronPrefix} | 98 98 * * 98 root ${file}" >> "${cronLinesAPAFN}"
done
fi
fi
if [ -d "${anacronMonthlyAPARN}" ]
then
# See comment above.
if find "${anacronMonthlyAPARN}" -mindepth 1 -type f | read
then
for file in $(ls "${anacronMonthlyAPARN}"* )
do
# Note these timing parameters are not EXACTLY what anacron
# does with such 'monthly' tasks, but they are an approximately equivalent stand-in.
#
echo "${anacronPrefix} | 98 98 1 * * root ${file}" >> "${cronLinesAPAFN}"
done
fi
fi
fi
if ${showProgress} ; then echo "8"; fi
#echo "All cron lines from all sources:"
#cat ${cronLinesAPAFN} | sed "s#${tab}#<tab>#g"
#echo "-----------"
#echo
#
# cron lines consist of six fields with predictable formats, followed by a command line with optional embedded spaces.
#
# Output the collected crontab lines.
# Replace the single spaces between the 6 fields with tab characters.
# Sort the lines by hour and minute.
# Insert the header line.
# Format the results as a table.
#
#example:
# root-crontab | 10 1 * * * /usr/local/sbin/hostmaker fred george
tabbedLines=$(cat "${cronLinesAPAFN}" | \
sed -r "s/^(\S+) \| (\S+) +(\S+) +(\S+) +(\S+) +(\S+) +(\S+) +(\S+) *(.*)$/\1\t|\t\2\t\3\t\4\t\5\t\6\t\7\t\8 \9/" \
)
#echo "tabbedLines ="
#echo "${tabbedLines}" | sed "s#${tab}#<tab>#g"
#echo "-------------------"
if ${showProgress} ; then echo "9"; fi
# Replace asterisk values with 99 - which is normally bigger than any legal value
#
# We expect to spot those impossible values and replace them in the final output
# usually with a meaningful word like 'weekly' but failing that, back to asterisk.
#
echo "${tabbedLines}" | \
sed -r "s/\*\t/99\t/" | \
sort -t"${tab}" -k7,7n -k6,6n -k5,5n -k4,4n -k3,3n | \
sed -r "s/99\t/*\t/" \
> ${sortedLinesAPAFN}
#echo "Sorted lines ="
#cat "${sortedLinesAPAFN}" | sed "s#${tab}#<tab>#g"
#echo "-------------------"
#echo "users and executables="
#cat "${sortedLinesAPAFN}" | cut -d"$tab" -f 8- | cut -d' ' -f1-2 | sed "s#${tab}#<tab>#g"
#echo "-------------------"
if ${showProgress} ; then echo "A"; fi
: > ${annotatedSortedLinesAPAFN}
while read sortedLine
do
user=$( echo -e "${sortedLine}" | cut -d"$tab" -f 8 )
executable=$( echo -e "${sortedLine}" | cut -d"$tab" -f 9 | cut -d' ' -f1)
executableAndParams=$(echo -e "${sortedLine}" | cut -d"$tab" -f 9-99 )
#debug
# echo
# echo "'${sevUsers}'"
# echo "';${user};'"
# echo "';${executable};'"
# echo
# We will label the lines 'MALFORMED' so they shouldn't be missed,
# the moans here are suppressed by the 'quiet' flag.
if [ -z "${executable}" ]
then
if ! ${quiet}
then
echo
echo "ERROR!!!!! A cron entry is malformed - probably too few fields - making it seem to have no executable command."
echo "The line was ( similar to ): '${sortedLine}'"
echo
fi
executableTag="MALFORMED\t"
elif [ "${user}" = "*" ]
then
if ! ${quiet}
then
echo "ERROR!!!!! A cron entry is malformed - probably too many fields - making it seem to use a user of star."
echo "The line was ( similar to ): '${sortedLine}'"
echo
fi
executableTag="MALFORMED\t"
elif [[ ! "${sevUsers}" =~ ";${user};" ]] # See if ;<user>; is anywhere in the list of known users.
then
if ! ${quiet}
then
echo
echo "ERROR!!!!! User '${user}' given in a cron entry is not a known user on this machine."
echo "The line was ( similar to ): '${sortedLine}'"
echo
fi
executableTag="MALFORMED\t"
elif [ "/" = "${executable:0:1}" ]
then
executableAPAFN="${executable}"
# @FIXME DGC 18-Feb-2019
# This does not make use of the PATH= directive in the file,
# so will currently wrongly fail if one is utilised.
if [ -f "${executableAPAFN}" ]
then
# See comment at the top of the file about how we would like to know if $user can execute this
# but testing that is too difficult to try here.
#
if [ -x "${executable}" ]
then
executableTag="executable\t"
else
executableTag="DISABLED !!!\t"
fi
else
executableTag="MISSING !!!\t"
fi
else
# We have so far noticed two 'encapsulated commands' in crontab files,
# which we previously did not have logic to handle,
# but now have a go at.
#
# Of course any old bash command could be placed in a crontab command.
#
# If a command starts with a name that can be 'located'
# by the 'which' command, ( as all non-built-in commands will do )
# it is not currently considered a special case.
# This script will report itself 'happy' as the executable can be found.
#
# Built-in commands like 'if', '[' and 'command' cannot be 'located' by 'which'
# and if we do not detect them and treat them specially, they will be labelled as 'missing'
# - which of course they aren't.
#
# However in the case of the two we have encountered they are simply 'encapulating'
# some actual command which is to be used, and it is that command which we
# would like to tell the user of this script about.
#
# One takes the form
# [ -x fred ] && fred <params>
# the other
# command -v fred <params>
#
# For the first of these we now remove the [ ... ] && section
# and allow our later code to examine and label fred as missing or non-executable, in the normal way.
#
# For the second of these we simply ignore the 'command [-v]' part as if it weren't there.
#
# Similar-but-not-identical situations will be labelled as 'not analysed'.
# The following line is not very sophisticated in what it matches - we could do better.
executableAndParams="$(echo "${executableAndParams}" | sed -r "s#\[ \-x .* \] && ##" )"
# We should probably tolerate this without the -v, but we don't at present.
executableAndParams="$(echo "${executableAndParams}" | sed -r "s#command -v ##" )"
executable="$( echo "${executableAndParams}" | cut -d' ' -f1)"
# We will just pragmatically expand this test to cover other built-in commands
# if we encounter other variations.
#
if [ "[" = "${executable}" ] \
|| [ "if" = "${executable}" ] \
|| [ "command" = "${executable}" ]
then
executableTag="not analysed\t"
elif which ${executable} > /dev/nul 2>&1
then
executableTag="executable\t"
else
executableTag="?? on a custom path ??\t"
fi
fi
# echo "${executableTag} '${sortedLine}'"
echo "${annotatedSortedLines}${executableTag}| ${sortedLine}" >> ${annotatedSortedLinesAPAFN}
done < ${sortedLinesAPAFN}
#echo "annotatedSortedLines ="
#cat "${annotatedSortedLinesAPAFN}" | sed "s#${tab}#<tab>#g"
#echo "-------------------"
if ${showProgress} ; then echo "B"; fi
# We treat a repeat at any number of minutes past the hour as 'hourly' for our purposes.
# These lines convert
# executable | anacron_3-22[~45] | daily at 98:98 root /etc/cron.daily/logrotate
# into
# executable | anacron_3-22[~45] | daily root /etc/cron.daily/logrotate
#
# It would be more sophisticated to convert them to
# executable | anacron | daily < 45 mins after 03:00 root /etc/cron.daily/logrotate
# Remove all leading zeroes on numbers
# Substitute human-readable versions of common numeric sequences.
#
# It is much faster to give sed 50 things to do than to load-and-run sed 50 times. Though it does look clunky.
#
sortedLinesTranslated=$(cat "${annotatedSortedLinesAPAFN}" | \
sed -r "s# 0([0-9])# \1#g ; \
s#\t0([0-9])#\t\1#g ; \
s#-0([0-9])#-\1#g ; \
s#,0([0-9])#,\1#g ; \
\
s#\|\t0\t0\t1\t1\t9#|\t@reboot\t \t \t \t #g ; \
\
s#\|\t\*\/1\t\*\t\*\t\*\t\*#|\tevery minute\t \t \t \t #g ; \
s#\|\t\*\t\*\t\*\t\*\t\*#|\teach minute\t \t \t \t #g ; \
s#\|\t\*\/([0-9]*)\t\*\t\*\t\*\t\*#|\tevery \1 minutes\t \t \t \t #g ; \
\
s#\|\t0\t\*\t\*\t\*\t\*#|\ton the hour\t \t \t \t #g ; \
s#\|\t([0-9][0-9]?)\t\*\t\*\t\*\t\*#|\thourly at \1 mins past\t \t \t \t #g ; \
s#\|\t\0\t\*\/([0-9]*)\t\*\t\*\t\*#|\ton the hour every \1 hours\t \t \t \t #g ; \
\
s#\|\t0\t0\t\*\t\*\t\*#|\t@start of each day\t \t \t \t #g ; \
s#\|\t98\t98\t\*\t\*\t\*#|\tdaily (anacron)\t \t \t \t #g ; \
s#\|\t([0-9]*)\t([0-9]*)\t\*\t\*\t\*#|\tdaily at \2:\1\t \t \t \t #g ; \
\
s#\|\t0\t0\t\*\t\*\t1#|\tstart of each Monday\t \t \t \t #g ; \
s#\|\t0\t0\t\*\t\*\t2#|\tstart of each Tuesday\t \t \t \t #g ; \
s#\|\t0\t0\t\*\t\*\t3#|\tstart of each Wednesday\t \t \t \t #g ; \
s#\|\t0\t0\t\*\t\*\t4#|\tstart of each Thursday\t \t \t \t #g ; \
s#\|\t0\t0\t\*\t\*\t5#|\tstart of each Friday\t \t \t \t #g ; \
s#\|\t0\t0\t\*\t\*\t6#|\tstart of each Saturday\t \t \t \t #g ; \
s#\|\t0\t0\t\*\t\*\t7#|\tstart of each Sunday\t \t \t \t #g ; \
s#\|\t([0-9]*)\t([0-9]*)\t\*\t\*\t0*1#|\teach Monday at \2:\1\t \t \t \t #g ; \
s#\|\t([0-9]*)\t([0-9]*)\t\*\t\*\t0*2#|\teach Tuesday at \2:\1\t \t \t \t #g ; \
s#\|\t([0-9]*)\t([0-9]*)\t\*\t\*\t0*3#|\teach Wednesday at \2:\1\t \t \t \t #g ; \
s#\|\t([0-9]*)\t([0-9]*)\t\*\t\*\t0*4#|\teach Thursday at \2:\1\t \t \t \t #g ; \
s#\|\t([0-9]*)\t([0-9]*)\t\*\t\*\t0*5#|\teach Friday at \2:\1\t \t \t \t #g ; \
s#\|\t([0-9]*)\t([0-9]*)\t\*\t\*\t0*6#|\teach Saturday at \2:\1\t \t \t \t #g ; \
s#\|\t([0-9]*)\t([0-9]*)\t\*\t\*\t0*7#|\teach Sunday at \2:\1\t \t \t \t #g ; \
\
s#\|\t98\t98\t\*\t\*\t7#|\tweekly (anacron)\t \t \t \t #g ; \
\
s#\|\t0\t0\t1\t\*\t\*#|\t@start of each month\t \t \t \t #g ; \
s#\|\t98\t98\t1\t\*\t\*#|\tmonthly (anacron)\t \t \t \t #g ; \
s#\|\t([0-9]*)\t([0-9]*)\t([0-9]*)\t\*\t\*#|\tday \3 of month at \2:\1\t \t \t \t #g ; \
\
s#\|\t0\t0\t1\t1\t\*#|\t@start of each year\t \t \t \t # " \
)
#echo "sortedLinesTranslated (unescaped) ="
#echo "${sortedLinesTranslated}" | sed "s#${tab}#<tab>#g"
#echo "-------------------"
#echo "sortedLinesTranslated (escaped) ="
#echo -e "${sortedLinesTranslated}" | sed "s#${tab}#<tab>#g"
#echo "-------------------"
if ${showProgress} ; then echo "C"; fi
if ! ${quiet}
then
cat <<EOS
Common timing intervals below are converted to more readable form.
However some more obscure possible combinations
( such as "run at 3AM on the first Friday of each month" )
are not handled, and will show using the original
min hour day-of-month month day-of-week
format
Anacron timings are printed in a rather cryptic code.
executable | anacron_3-22[~45] | daily root /etc/cron.daily/logrotate
( meaning tasks can only start between 03:00 and 22:00 hours,
and will be randomly delayed up to 45 minutes ).
If we are only using daily/weekly/monthly anacron lists.
you would think tasks would start somewhere near the beginning of the permitted window.
( which is what we observe ).
It seems the 22:00 info is only useful for situations when mains power is restored,
and all the cron tasks we didn't want to run on battery become 'eligible'.
It may also have an influence on the per-anacron-task delay we currently ignore.
should be read as:
executable | anacron | daily < 45 mins after 03:00 root /etc/cron.daily/logrotate
------------------------------ Cron tasks on this machine ------------------------------
EOS
fi
#
# Narrow the empty area in front of the user names when possible
# We now drop the 3 or 4 empty tabbed columns completely if they are present on all lines.
#
if echo "${sortedLinesTranslated}" | grep -Eq "\|${tab}[0-9]|\|${tab}\*/" # All numeric formats start with some digit or */ I think
then
# Some entry remains in numeric format
sortedLinesHdr="$(echo " notes${tab}| location${tab}|${tab}min${tab}hr${tab}dom${tab}mo${tab}dow${tab}user${tab}command" )";
else
# All entries are in summarised format
sortedLinesHdr="$(echo " notes${tab}| location${tab}|${tab}description${tab}user${tab}command" )";
sortedLinesTranslated="$(echo "${sortedLinesTranslated}" | sed 's# \t \t \t \t##g')"
fi
# We have to include the header line when aligning columns.
sortedLinesXlHdr="$(echo "${sortedLinesHdr}"; \
echo -e "${sortedLinesTranslated}" \
)"
#echo "sortedLinesXlHdr (unescaped) ="
#echo "${sortedLinesXlHdr}" | sed "s#${tab}#<tab>#g"
#echo "-------------------"
#echo "sortedLinesXlHdr (escaped) ="
#echo -e "${sortedLinesXlHdr}" | sed "s#${tab}#<tab>#g"
#echo "-------------------"
if ${showProgress} ; then echo "->"; fi
captured="$(echo -e "${sortedLinesXlHdr}" | \
sed 's#|\t#| #g' | \
column -s"${tab}" -t | \
cut -c1-${screenColumns} \
)"
echo "$captured" | head -n1
echo "${screenWidthSeparator}"
echo "$captured" | tail -n +2
echo
if [ -n "$(atq)" ]
then
echo "There are one or more jobs waiting to be executed by the 'at' command."
echo "While they don't form part of what this script was created to examine,"
echo " they are drawn to your attention:"
echo
atq
echo
fi
@gh4chris

This comment has been minimized.

Copy link

gh4chris commented Aug 30, 2019

I tried the script, but it seems to miss something:

`

cat: /etc/cron.d/*: No such file or directory
notes | location | description user command

executable | main-crontab | -*/15 * * * * root test -x /usr/lib/cron/run-crons && /usr/lib/cron/ru

`

Here is more to find (/var/spool/atjobs can hold some jobs as well)

`

ls /etc/cron*
/etc/cron.deny /etc/crontab

/etc/cron.d:

/etc/cron.daily:
mdadm suse-clean_catman suse-do_mandb suse.de-backup-rc.config suse.de-backup-rpmdb suse.de-check-battery suse.de-cron-local suse.de-snapper

/etc/cron.hourly:
suse.de-snapper

/etc/cron.monthly:
btrfs-scrub

/etc/cron.weekly:
btrfs-balance btrfs-trim

`

@myshkin-uk

This comment has been minimized.

Copy link
Owner Author

myshkin-uk commented Sep 2, 2019

Sadly on my PC I get the 'right' output in a similar situation. It's hard to debug your experience from here.

You might like to look at the 'temp files' the script creates - it is set by default to leave them for examination:

"/tmp/cleanCronLines.txt"
"/tmp/cronLines.txt"
"/tmp/cronForUser.txt"
"/tmp/sortedLines.txt"
"/tmp/annotatedSortedLines.txt"

@gh4chris

This comment has been minimized.

Copy link

gh4chris commented Sep 3, 2019

Sadly on my PC I get the 'right' output in a similar situation. It's hard to debug your experience from here.
Might be because the you presented code is checking those dirs only in case it finds a dedicated anachron install?
I guess SuSe Linux might have the anachron functionality built in or in case it is actually using anachron, it can't be identified as such, because it doesn't use that name.

A work around could be, not to distinguish between cron / anacron, but just scan the dirs in case they exist.

@gh4chris

This comment has been minimized.

Copy link

gh4chris commented Sep 3, 2019

I did get some more lines, by changing the line usesAnacron=false to usesAnacron=true

but just the cron.daily - those guys below are still missing

/etc/cron.hourly:
suse.de-snapper

/etc/cron.monthly:
btrfs-scrub

/etc/cron.weekly:
btrfs-balance btrfs-trim

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.