Scripts That Run Without You
cron runs a script from your home folder, with a short PATH, no terminal and no keyboard, so a script that works when you type it can fail at 18:00; give it full paths and a PATH of its own, write everything to a log with the date and time, never ask a question, and take a lock with flock so two copies never run at once.
Freelancer · Unix · Linux · Updated
After this lesson you can
- Try a script in cron's environment with env -i, and fix what fails with full paths and a PATH of its own
- Make an unattended script log what it did with the date and time, and stop a second copy with flock
Key takeaways
- cron starts a job in your home folder with only a few variables and PATH=/usr/bin:/bin. env -i with those few variables lets you try a script the same way.
- Give an unattended script full paths: cd to its folder with a full path, set its own PATH, and use the script's full path in the crontab.
- exec >> backup.log 2>&1 sends everything the script prints, errors included, into a log; a log function puts the date and time on each line.
- Nobody is there to answer, so a script for cron never asks: read gets nothing, and rm -i deletes nothing.
- exec 9> backup.lock and flock -n 9 let only one copy run: a second copy finds the lock taken and stops.
You scheduled backup.sh in Scheduled Jobs: cron, and it works perfectly when you type ./backup.sh. The next morning the backup folder hasn't changed, and the log says:
cp: cannot stat 'school-work/*': No such file or directory
Backed up school-work.
It failed, and then said it had worked. Nothing is wrong with cron: the script ran in a different world from your terminal. This happens to everyone who schedules a script for the first time, at home and on the servers that run websites, so administrators write scripts for cron in a particular way. This lesson shows it, with the school folder from Scheduled Jobs: cron.
Run it the way cron does: env -i
Your terminal has dozens of variables, set up by your startup files. cron gives a job only a few, and starts it in your home folder. env -i (read "env, empty") runs a command with no variables but the ones you list, so you can try a script in cron's world any time. Save this as as-cron.sh in the school folder, and chmod +x as-cron.sh:
#!/usr/bin/env bash
# as-cron.sh: runs a command the way cron does: from your home folder,
# with only cron's few variables, its short PATH and no keyboard.
cd ~ || exit 1
env -i HOME="$HOME" LOGNAME="$USER" PATH=/usr/bin:/bin SHELL=/bin/sh /bin/sh -c "$1" < /dev/null
Here's everything a job gets:
./as-cron.sh env
HOME=/home/sam
LOGNAME=sam
PATH=/usr/bin:/bin
SHELL=/bin/sh
PWD=/home/sam
Now run the backup there, and the morning's log appears on your screen:
./as-cron.sh ~/terminal-practice/school/backup.sh
cp: cannot stat 'school-work/*': No such file or directory
Backed up school-work.
Full paths, and a PATH of its own
Two things broke. The script starts in /home/sam, where there's no school-work folder, so its short names lead nowhere. And cron's PATH has two folders, so commands in /usr/local/bin or your ~/bin aren't found: ./as-cron.sh 'backup.sh' prints /bin/sh: 1: backup.sh: not found. The fix is full paths: names that start at / and work from anywhere. The new backup.sh in the next section sets its own PATH near the top, then cds to its own folder by its full path ($HOME is one of the few variables cron does set). The crontab line names the script by its full path too, as in Scheduled Jobs: cron.
On a Mac: cron's PATH doesn't include Homebrew's folder, so #!/usr/bin/env bash finds the Mac's own bash 3.2. Name Homebrew's bash in the crontab line, as in 0 18 * * * /opt/homebrew/bin/bash /Users/sam/terminal-practice/school/backup.sh (/usr/local/bin/bash on an Intel Mac), and put /opt/homebrew/bin at the front of the script's PATH line.
Nobody there: no questions, and a log
At 18:00 nobody is at the keyboard. A question gets an empty answer, so read gets nothing and rm -i deletes nothing:
./as-cron.sh 'read -r answer; echo "You said: $answer"'
You said:
So a script for cron never asks; it decides, and writes down what it did. Here's the new backup.sh:
#!/usr/bin/env bash
# backup.sh: copies everything in school-work into backup. Safe to run from cron.
PATH=/usr/local/bin:/usr/bin:/bin
cd "$HOME/terminal-practice/school" || exit 1
exec >> backup.log 2>&1
log() {
echo "$(date '+%F %T') $*"
}
log "Backup started"
if cp -r school-work/* backup/; then
log "Backup finished"
else
log "Backup failed"
exit 1
fi
exec >> backup.log 2>&1 is read "from here on, everything this script prints, errors included, goes into backup.log": the log file is the script's diary. log puts the date and the time (%T) in front of each line, and the if makes the last line tell the truth. Try it the cron way:
./as-cron.sh ~/terminal-practice/school/backup.sh
cat backup.log
2026-11-09 07:35:06 Backup started
2026-11-09 07:35:06 Backup finished
Yours shows your own date and time. Nothing appears on the screen: it all went into the log.
One at a time: flock
A backup of a big folder can take longer than you think. If cron starts tonight's copy while you're running one by hand, two copies write into the same folder at once. A file lock works like the "occupied" sign on a bathroom door: the first copy turns it on, and a second copy sees it and leaves. flock (read "file lock") holds one. Add these lines above log "Backup started":
exec 9> backup.lock
if ! flock -n 9; then
log "Another backup is still running. Skipping."
exit 0
fi
exec 9> backup.lock opens the file backup.lock as file number 9 (as 2 is the errors), and flock -n 9 takes the lock on it, or fails at once if someone else has it (-n is "no waiting"). The lock lasts until the script ends. To try it, let flock hold the lock while it runs the script, so the script finds it taken:
flock backup.lock ./backup.sh
tail -n 1 backup.log
2026-11-09 07:35:06 Another backup is still running. Skipping.
flock comes with Ubuntu. A Mac doesn't have it: brew install flock adds it.
Check yourself
2 questions to try on the lesson page.
So what
A script that runs while you sleep has to bring everything it needs with it: full paths, its own PATH, a log that tells the truth, and a lock so it never trips over itself.