Stack Overflow is a great tool for developers as it allows to quickly investigate how common hurdles are addressed by our peers. We all know that, right? Unfortunately, it sometimes serves as a vehicle to perpetuate kludges, dirty hacks or misconceptions. I recently came across an issue that had around it an inordinate amount of such ill-advised prose.
Since the issue in question relates to UNIX literacy, I would like to dedicate this post to the memory of Dennis Ritchie, father of C and UNIX, who died earlier this week (the 13th of October, 2011).
I will first expose the question, offer some best practices and finally comment on the common pitfalls. So here is the question: how do you run rvm scripts as cron jobs? (1)
First, the shebang line in your script should not refer directly to a ruby executable, but to rvm's ruby:
This instructs the script to load the environment and run ruby as we would on the command line with rvm loaded (2). Easy. When you run a cron job, however, the environment is not your local environment. Not only are your environment variables missing, but cron jobs may be spawned in a shell other (3) than bash or zsh, the shells that work with rvm (as of this date).
On many UNIX derived systems, crontabs can have a configuration section before the actual lines that define the jobs to be run. If this is the case, you would then specify:
This will ensure that the cron job will be spawned from bash. Still, your environment is missing, so to instruct bash to load your environment, you will want to add to the configuration section the following:
BASH_ENV=/path/to/environment (typically .bash_profile or .bashrc)
HOME is automatically derived from the /etc/passwd line of the crontab owner, but you can override it.
After this, a cron job might look like this:
15 14 1 * * $HOME/rvm_script.rb
What if your crontab doesn't support the configuration section? Well, you will have to give all the environment directives in one line, with the job itself. For example,
15 14 1 * * export BASH_ENV=/path/to/environment && /full/path/to/bash -c '/full/path/to/rvm_script.rb'
On Stack Overflow, I have seen the following advice:
Run bash with -l switch. The --login switch instructs bash to run as a login shell. Therefore, it will try to load your profile. Please don't do that, even if things appear to work. Cron jobs are by nature non-interactive, non-login shells. Invoking them as if they were is just bad practice. Also, when bash starts a login shell, it first loads the system environment (/etc/profile), and anything that prints to the screen (like motd - message of the day) will cause your cron job to report nasty messages like this:
stty: TIOCGETD: Inappropriate ioctl for device
Note: your login profile is meant for one-time setup relevant to the session (like running ssh-agent). All the rest should go to ~/.bashrc. A common practice is to source ~/.bashrc from the profile, avoiding duplication.
Another often seen advice is to write a runner script (or a wrapper) that loads the environment first, then runs the rvm script. This might work as well, but is unnecessary and makes you look foolish.
2, The assumption here is that you installed rvm as a function and not as a binary.
3, Often sh.
4, If you are using zsh instead and you need everything in one directive, this is what I'd do:
15 14 1 * * export HOME=/path/to/home && /path/to/zsh -c 'source $HOME/.zshrc && $HOME/script.rb'
P.S. Follow me on Twitter.