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.

Key terms

Cheat sheets

Related