Automating Daily Tasks with Shell Scripts: 6 Easy Fixes

Automating daily tasks with shell scripts is simple to start and easy to get subtly wrong. The earlier version of this post gave four short scripts (backup, disk check, file sorter, cron) without running them. When I ran them on Debian 13 (Bash 5.2.37 with the cron package), each had a failure that would only show up weeks later: a backup that nests itself, a check that fails silently, a sorter that overwrites files, and a cron line that never runs. This rewrite on automating daily tasks with shell shows each failure, the output, and a tested fix.

Your first script for automating daily tasks with shell

The first step in automating daily tasks with shell is a script file: create it, make it executable and run it. This part of the old post works as written:

$ printf '#!/bin/bash\necho "Hello, World!"\n' > myscript.sh; chmod +x myscript.sh; ./myscript.sh
Hello, World!

The first line, #!/bin/bash, tells the system which interpreter runs the file. Everything below this point is about making real scripts safe to run unattended.

Backup script: it nests the copy on the second run

Backups are the classic case of automating daily tasks with shell. The old script created $DEST/$DATE by running cp -r $SRC $DEST/$DATE. The first run copies the files into a new folder named after the date. A second run on the same day finds the folder already exists, so cp copies the source into it. I ran it twice with two files in the source:

/backup
`-- documents_backup
    `-- DATE
        |-- a.txt
        |-- documents
        |   |-- a.txt
        |   `-- my\ notes.txt
        `-- my\ notes.txt

The second run created a nested documents folder, a mess that grows with every run, and the unquoted variables would break if a path contained a space. A tested fix quotes everything, copies the contents with cp -a "$SRC/." ... and puts the time in the folder name so each run gets its own directory:

#!/bin/bash
set -euo pipefail
SRC="/home/user/documents"
DEST="/backup/documents_backup"
DATE=$(date +%Y-%m-%d_%H%M%S)
mkdir -p "$DEST/$DATE"
cp -a "$SRC/." "$DEST/$DATE/"
echo "Backup completed for $SRC on $DATE"

Two runs, one second apart, produced two folders, each with a.txt and my notes.txt. The line set -euo pipefail stops the script on the first error, on unset variables and on failures inside pipes, which matters for anything run by cron.

Disk check: it hides failures

Monitoring is the next candidate for automating daily tasks with shell. The old monitoring script reads the percentage from df / | tail -1 | awk and compares it with 80. On a healthy disk (74% used in the lab) it printed nothing, and with the limit changed to 1 it printed Warning: Disk usage has exceeded 1%. The logic works. The problem appears when df cannot read the path:

df: /missing: No such file or directory
/root/disk_missing.sh: line 4: [: -gt: unary operator expected
[exit 0]

The variable was empty and unquoted, the test failed, and the script still exited with 0, so a monitoring system would see success. Here is a version that quotes, validates and uses exit codes (1 for "over the limit", 2 for "cannot read"):

#!/bin/bash
threshold=${1:-80}; mount=${2:-/}
usage=$(df --output=pcent "$mount" 2>/dev/null | tail -n 1 | tr -dc "0-9")
[ -n "$usage" ] || { echo "cannot read usage of $mount" >&2; exit 2; }
if [ "$usage" -gt "$threshold" ]; then echo "Warning: $mount is at ${usage}% (limit ${threshold}%)"; exit 1; fi
$ bash disk2.sh 1;  echo "[exit $?]"
Warning: / is at 74% (limit 1%)
[exit 1]
$ bash disk2.sh 99; echo "[exit $?]"
[exit 0]
$ bash disk2.sh 80 /missing; echo "[exit $?]"
cannot read usage of /missing
[exit 2]

File sorter: directories, names without extensions and overwrites

File organization is a popular example of automating daily tasks with shell. The sorter loops over * and moves each item into a folder named after its extension, computed with ${file##*.}. I put four files and a folder in the source: report.txt, photo.jpg, Makefile, my file.txt and subdir. This is the result:

/sorted/Makefile/Makefile
/sorted/jpg/photo.jpg
/sorted/subdir/subdir
/sorted/txt/my file.txt
/sorted/txt/report.txt

For a name without a dot, ${file##*.} returns the whole name, so Makefile got a folder called Makefile, and the directory subdir was moved into a folder with the same name. Worse, I then created a new report.txt with different content and ran the script again. It replaced the sorted copy without a word, and cat sorted/txt/report.txt printed NEW CONTENT. Silent overwrite is the worst failure of automating daily tasks with shell, because nobody notices until the old data is needed. The fixed script skips non-files, handles missing extensions and uses mv -n so that it never overwrites:

for file in *; do
    [ -f "$file" ] || continue
    case $file in *.*) ext=${file##*.} ;; *) ext=no_extension ;; esac
    mkdir -p "$dest/$ext"
    mv -n -- "$file" "$dest/$ext/" || echo "skipped: $file" >&2
done

With the same test data, the second run left the new report.txt in the source folder and the sorted copy still contained the original a. Note that mv -n did not print my "skipped" message, because it exits with 0 even when it does not move the file. If you need to know, test for the file's existence before the move.

Terminal output from automating daily tasks with shell: the backup nested twice and the sorter overwriting a file
Failures of the old scripts, from the lab.

Cron: a minimal environment and the percent sign

Scheduling completes the picture of automating daily tasks with shell, and cron runs jobs with a small environment. I added a job that wrote its variables to a file:

PATH=/usr/bin:/bin SHELL=/bin/sh HOME=/root
$ echo "interactive shell PATH: $PATH"
interactive shell PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

The job sees only /usr/bin:/bin and uses /bin/sh, so a script that works in your terminal can fail from cron because a command lives in /usr/local/bin. Use full paths or set PATH at the top of the script. The second trap is the percent sign. In a crontab, an unescaped % means a newline, so a job with date +%Y-%m-%d does not do what it says. I put two lines in the crontab and waited a minute:

* * * * * date +%Y-%m-%d >> /tmp/cron_date.txt 2>&1
* * * * * date +\%Y-\%m-\%d >> /tmp/cron_date_ok.txt 2>&1

cat: /tmp/cron_date.txt: No such file or directory
2026-11-05
2026-11-05

The unescaped line never created its output file, because everything after the first percent sign was cut off and passed to the command as input. The escaped line wrote the date every minute. The crontab format is described in the crontab(5) manual page. Better still, put the date call inside a script and avoid percent signs in the crontab entirely.

Checklist for automating daily tasks with shell

  • When automating daily tasks with shell, quote every variable, and use set -euo pipefail in unattended scripts.
  • Make backups idempotent: a new directory per run, cp -a "$SRC/.".
  • Return meaningful exit codes so a scheduler can tell failure from success.
  • Never overwrite silently: use mv -n or check first.
  • In cron, use full paths and escape %, or hide it inside a script.

For speed and process counts in loops, see my tested post on optimizing Bash scripts.

Comments

Popular posts from this blog

How to Play Minecraft Bedrock Edition on Linux: A Comprehensive Guide for Tech Professionals

Linux desktop frozen? My 5 go-to tricks to try - before forcing a hard reboot

The Ultimate Guide: How to Set Up DXVK in Wine on Linux for Enhanced Gaming Performance