# 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 November 10, 2026

## 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:

```text
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`:

```bash
#!/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:

```bash
./as-cron.sh env
```

```text
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:

```bash
./as-cron.sh ~/terminal-practice/school/backup.sh
```

```text
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 `cd`s 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:

```bash
./as-cron.sh 'read -r answer; echo "You said: $answer"'
```

```text
You said:
```

So a script for cron never asks; it decides, and writes down what it did. Here's the new `backup.sh`:

```bash
#!/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:

```bash
./as-cron.sh ~/terminal-practice/school/backup.sh
cat backup.log
```

```text
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"`:

```bash
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:

```bash
flock backup.lock ./backup.sh
tail -n 1 backup.log
```

```text
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

- [Absolute Path](https://www.ask-codey.com/glossary/absolute-path)
- [PATH](https://www.ask-codey.com/glossary/path-unix)
- [Environment Variable](https://www.ask-codey.com/glossary/environment-variable-unix)
- [Log File](https://www.ask-codey.com/glossary/log-file)
- [flock](https://www.ask-codey.com/glossary/flock)
- [File Lock](https://www.ask-codey.com/glossary/file-lock)

## Cheat sheets

- [Scripts That Run Without You](https://www.ask-codey.com/cheat-sheets/bash-unattended-scripts-cheat-sheet)

## Related

- [Scheduled Jobs: cron](https://www.ask-codey.com/lessons/bash-cron)
- [Backups That Work: rsync](https://www.ask-codey.com/lessons/bash-rsync)
- [Environment Variables: export and env](https://www.ask-codey.com/lessons/bash-environment-variables)

Source: https://www.ask-codey.com/lessons/bash-unattended-scripts
