I have a deployment script, it must add something to a user crontab (trigger a script that clean logs every XXX days), however this must only be done during the first deployment, or when it needs to be updated.

(I can run xxx.py deploy env or xxx.py update env)

so I have to do this:

Check if my cronJob already exist
Put my cronJob if it does not already exist
update my cronjob if one of the parameter of the command is different

I don't see how to add/check/remove something to the crontab without using crontab -e or editing the crontab file (download it, rewrite it, re-upload it)

PS : this is a user specific cronjob, "webadmin" is going to do it and he should not use sudo to do it.

  • 1
    Does it have to be in a user-specific crontab? Most prepackaged cron jobs go into one of the /etc/cron.* directories. – user Jul 21 '16 at 13:26
  • Does CentOS have /etc/cron.d? If so, put your script in there using a name unique to your application – roaima Jul 21 '16 at 13:26
  • yes it's user specific. I can't add it to /etc/cron.d because it's a root file, thus only root's job are alowed inside (I could sudo but that a bad practice I've been told) – sliders_alpha Jul 21 '16 at 13:28
  • 1
    same as /etc/crontab, files in /etc/cron.d/ have an extra field for the username, immediately after the schedule spec. e.g. * * * * * username /path/to/script. See man 5 crontab and search for SYSTEM CRON. – cas Jul 21 '16 at 13:30
  • See also stackoverflow.com/questions/610839/… – rogerdpack Apr 17 '18 at 19:29

my best idea so far

to check first if the content matches what should be in there and only update if it doesn't:

if [[ $(crontab -l | egrep -v "^(#|$)" | grep -q 'some_command'; echo $?) == 1 ]]
    set -f
    echo $(crontab -l ; echo '* 1 * * * some_command') | crontab -
    set +f

but this gets complicated enough to build a separate script around that cron task.

other ideas

you could send the string via stdin to crontab (beware, this clears out any previous crontab entries):

echo "* 1 * * * some_command" | crontab -

this should even work right through ssh:

echo "* 1 * * * some_command" | ssh user@host "crontab -"

if you want to append to the file you could use this:

# on the machine itself
echo "$(echo '* 1 * * * some_command' ; crontab -l)" | crontab -
# via ssh
echo "$(echo '* 1 * * * some_command' ; ssh user@host crontab -l)" | ssh user@host "crontab -"
  • that depends: do you need what would have been in there? :) – Phillip -Zyan K Lee- Stockmann Jul 21 '16 at 13:18
  • awww ... not running as root? ... I'll rewrite it ... – Phillip -Zyan K Lee- Stockmann Jul 21 '16 at 13:30
  • eh, I like that :D – sliders_alpha Jul 21 '16 at 15:39
  • I had two issues with this solution: 1) echo '*... expanded the * to a list of files. 2) the line endings in the crontab got deleted. – Heath Raftery Sep 8 '19 at 1:05
  • 1
    See my answer further down for a working fix to the * (wildcard) expansion issue. – Yngvar Kristiansen Feb 26 '20 at 7:57

For the record I'm going to suggest using /etc/cron.d/. Only root can write files here but the entries can be configured to run as any user (without need for sudo at run-time),

echo '0 0 * * 0 webadmin /usr/local/bin/tidy_logfiles' >/etc/cron.d/my_webadmin

An important part is that the my_webadmin should be unique to you (not necessarily unique for the run, though) because any installation package can also write files here and you want to avoid a clash. Having this uniqueness constraint, you can update my_webadmin with a simple overwrite, since you know it's "yours" and won't contain entries for anyone/anything else.

Furthermore, with this approach it becomes trivial to remove the cron entry

rm -f /etc/cron.d/my_webadmin

Possibly outside the scope of your question, but if you have remote access to the root account (or via sudo) you can even provision remotely,

echo '0 0 * * 0 webadmin /usr/local/bin/tidy_logfiles' > ~/webadmin.cron
scp -p ~/webadmin.cron root@remote_host:/etc/cron.d/my_webadmin


echo '0 0 * * 0 webadmin /usr/local/bin/tidy_logfiles' |
    ssh -q root@remote_host 'cat >/etc/cron.d/my_webadmin'

and remove the provisioning,

ssh -nq root@remote_host rm -f /etc/cron.d/my_webadmin

(Note that in many cases you cannot provide root's password for the scp/ssh commands because the root account is constrained to prevent password-based logins. Instead you need to have set up public/private key certificates. Also, by implication the local account (whatever it is) will have full root access to the remote server.)

  • It's my client server, I cannot log as root, I can sudo into it BUT, if I do that they'll kill me. This is a webadlmin job so it should only be in the webadmin stuff, that's what the sysadmin told me. – sliders_alpha Jul 21 '16 at 15:40
  • @sliders_alpha the job only runs as webadmin. It's the provisioning that requires root equivalence. However, I'll look for a non-root solution too. – roaima Jul 21 '16 at 15:44
  • 2
    +1. /etc/cron.d/ exists for exactly this purpose - so that packages / deployments can just drop a crontab file in here. – cas Jul 22 '16 at 0:20
  • cas and roaima are spot on. this is why /etc/cron.d/ exists -- so you can "install" a crontab for a specific app without having to append / detect anything – CompEng88 Apr 15 at 15:28

I highly recommend using Ansible* for this rather than rolling your own. Or Puppet or Chef — but Ansible is well-suited for zero-infrastructure deploy scripts like this.

That's because there are already modules meant to solve problems like this, and config management tools have have idempotence as a basic design goal — that's the property of only changing when it needs to even if you accidentally (or intentionally) run it again.

In particular, Ansible's cron module can modify user crontabs. As a bonus, if you want to later adjust to use system crontabs, it'll be a very easy tweak rather than a rewrite.

* disclaimer: I work for Red Hat, and Ansible is a Red Hat sponsored project.

  • Yup, thing is, I didn't knew about ansible 2 month ago, and now we have a massive python deployer script (but he is MAGNIFICIENT, readable, maintanable, ;)) Next time, I'll use ansible but right now going back is impossible (money money money) – sliders_alpha Jul 21 '16 at 15:38

If you want to add a cron job via the target account, run crontab -e. This command passes the crontab through an editor. Tell it to use an editor command that modifies the crontab as you desire. The editor command is executed as a shell snippet with the name of a temporary file appended.

unset VISUAL
EDITOR='update_crontab () {
  set -e
  if <"$1" grep -v "^#" | grep -w do_stuff; then
    # Remove existing entries containing do_stuff
    grep -v -w do_stuff "$1" >"$new"
    cp "$1" "$new"
  # Add the new entry
  echo "1 2 3 4 5 do_stuff --new-options" >>"$new"
  mv "$new" "$1"
update_crontab' crontab -e

This approach is more reliable than the native crontab -l | … | crontab - because this one is vulnerable to a race condition if the crontab is edited concurrently: modifications made between the call to crontab -l and the call to crontab - would be undone.


This is an adaptation of what @phillip-zyan-k-lee-stockmann offered, based on his "Best idea so far" code.

My changes from his (excellent and helpful snippet) are basically:

  • Regex for not just the command name, but also the whole entry including time strings. That way it could support adding a command even if there are same-named or overlapping-named commands in other entries. (It still won't add the same command on the same schedule twice.)
  • A bit of logging
  • I switched (and named) mine to hourly for various reasons; easy to adjust it back per crontab syntax

And so here's my code for what I called crontab-add-hourly.sh:


# To allow simple, programmatic addition of commands/entries into the crontab (if not already present)

entry="0 * * * * $cmd"
printf "we want to add this entry:\n$entry\n\n" 
escapedEntry=$(printf '%s\n' "$entry" | sed 's:[][\/.^$*]:\\&:g') #from: https://unix.stackexchange.com/a/129063/320236
printf "but first we'll see if it's already in there using this regex pattern:\n$escapedEntry\n\n"

if [[ $(crontab -l | egrep -v '^(#|$)' | grep -q "$escapedEntry"; echo $?) == 1 ]] # from: https://unix.stackexchange.com/a/297377/320236
    printf "all clear; pattern was not already present; adding command to crontab hourly:\n$cmd\n\n"
    (crontab -l ; printf "$entry\n\n") | crontab -
    printf "pattern already present; no action taken\n\n"

Example usage and output:

$ ./crontab-add-hourly.sh my-script.bash

we want to add this entry:
0 * * * * my-script.bash

but first we'll see if it's already in there using this regex pattern:
0 \* \* \* \* my-script\.bash

all clear; pattern was not already present; adding command to crontab hourly:
  • If the $cmd or $entry contains % it will trigger unexpected formatting in the printf statement. Don't do printf "$var/n" but instead use printf "%s/n" "$var" – roaima Feb 9 at 6:20

TL;DR: This actually works, tested in Bash 4.4.

if [[ $(crontab -l | egrep -v "^(#|$)" | grep -q 'some_command'; echo $?) == 1 ]]
    set -f
    printf "$(crontab -l ; echo '* * * * * some_command')\n" | crontab -
    set +f

As noted in @Phillip -Zyan K Lee- Stockmann answer's comments, that solution expands * into all files in the current directory. I couldn't get the comments' suggestion to work. set -f disables the wildcard expansion, see https://stackoverflow.com/a/11456496/915441.

  • If the crontab contains % it will trigger unexpected formatting in the printf statement. Don't do printf "$(...)/n" but instead use printf "%s/n" "$(...)" – roaima Feb 9 at 6:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.