encrypt-file

Dissection of the encrypt-file wrapper script — bash patterns for RHCSA study: strict mode, file tests, argument handling, arithmetic edge cases, regex matching, and secure deletion.

What encrypt-file Does

A wrapper around age -e that handles the common workflow: encrypt one or more files, append .age, optionally shred originals. Located at ~/.secrets/bin/encrypt-file, found via PATH.

Basic usage
encrypt-file secret.txt                    # → secret.txt.age
encrypt-file file1.txt file2.txt file3.txt # Batch encrypt

Script Dissection

Each section below maps to a bash concept worth learning.

Strict Mode

#!/bin/bash
set -euo pipefail
Flag Meaning

set -e

Exit immediately on any command failure (non-zero exit)

set -u

Treat unset variables as errors (prevents silent bugs from typos)

set -o pipefail

Pipe fails if ANY stage fails, not just the last

set -e has edge cases. Arithmetic count))` returns 1 when count is 0, triggering exit. The script handles this with `((encrypted_count \|\| true.

Argument Validation

if [ $# -lt 1 ]; then
    echo "Usage: encrypt-file <file1> [file2] [file3] ..."
    exit 1
fi
Element Concept

$#

Number of positional parameters passed to the script

$@

All positional parameters as separate words (used later in for loop)

$1, $2

Individual positional parameters

exit 1

Non-zero exit = failure (convention: 1 = general error, 2 = usage error)

File Test Operators

if [ ! -f "$PUBKEY" ]; then
    echo "✗ Public key not found: $PUBKEY"
    exit 1
fi
Test Meaning

-f file

True if file exists AND is a regular file

-d dir

True if directory exists

-e path

True if path exists (any type)

-r file

True if readable

-w file

True if writable

-x file

True if executable

! -f

Negation — true if file does NOT exist

For RHCSA: always quote "$variable" inside [ ] to prevent word splitting on paths with spaces.

Looping Over Arguments

for INPUT in "$@"; do
    # Skip if file doesn't exist
    if [ ! -f "$INPUT" ]; then
        echo "✗ Skipping: $INPUT (file not found)"
        continue
    fi

    # Skip if already an .age file
    if [[ "$INPUT" == *.age ]]; then
        echo "✗ Skipping: $INPUT (already encrypted)"
        continue
    fi
    ...
done
Element Concept

"$@"

All arguments, individually quoted (preserves spaces in filenames)

"$*"

All arguments as ONE string (rarely what you want)

continue

Skip to next iteration (like Python’s continue)

[[ …​ == *.age ]]

Glob pattern match — [[ ]] supports *, ?, […​] without quoting the pattern

[ …​ ] vs [[ …​ ]]

[[ ]] is bash-specific, safer (no word splitting, supports &&, ||, regex, globs)

String Manipulation

OUTPUT="$INPUT.age"

Simple concatenation. More complex patterns use parameter expansion:

"${INPUT%.txt}.age"    # Strip .txt suffix, append .age
"${INPUT##*/}"         # Strip path prefix (basename)
"${INPUT%/*}"          # Strip filename (dirname)

The Core Encryption

age --encrypt -r "$(cat "$PUBKEY")" -o "$OUTPUT" "$INPUT"
Element Concept

$(cat "$PUBKEY")

Command substitution — reads the public key file and inlines its content as an argument

-r

Recipient — the public key string (not a file path)

-R

Recipients file — reads keys from a file (alternative approach)

-o "$OUTPUT"

Output file path

"$INPUT"

Input file to encrypt (last argument)

The script uses -r "$(cat …​)" to pass the key as a string. The age codex entry shows -R file which reads the file directly — equivalent but cleaner.

Arithmetic with set -e

((encrypted_count++)) || true

Arithmetic expansion returns exit code 1 when the result is 0. With set -e active, count++ when count=0 would kill the script. The || true absorbs the false exit.

Alternatives
encrypted_count=$((encrypted_count + 1))   # Assignment always succeeds
let "encrypted_count+=1" || true           # Same issue as (())
: $((encrypted_count++))                   # : is no-op, absorbs exit code

Interactive Confirmation

read -r response
if [[ "$response" =~ ^[Yy]$ ]]; then
    ...
fi
Element Concept

read -r

Read one line from stdin. -r prevents backslash interpretation.

=~

Regex match operator (only inside [[ ]])

^[Yy]$

Regex: exactly one character, either Y or y

== vs =~

== does glob matching, =~ does regex matching

Secure Deletion

shred -u "$INPUT"
Flag Meaning

shred

Overwrite file content with random data before unlinking

-u

Unlink (delete) the file after overwriting

-n 10

Number of overwrite passes (default: 3). The encrypt-file script uses default; the data/ README templates specify -n 10

shred is less effective on journaling filesystems (ext4, btrfs) and SSDs with wear leveling. For SSD/NVMe, the data may persist in spare cells. For true security on SSDs, use LUKS full-disk encryption so plaintext never hits raw flash.

How encrypt-file Gets Into PATH

The script lives at ~/.secrets/bin/encrypt-file. The shell finds it because ~/.secrets/bin is in $PATH (set in .zshrc or .bashrc).

# Verify
type encrypt-file
# → encrypt-file is /home/evanusmodestus/.secrets/bin/encrypt-file

echo "$PATH" | tr ':' '\n' | grep secrets
# → /home/evanusmodestus/.secrets/bin

See PATH Resolution for the full breakdown of which, type, aliases, and when PATH resolution breaks.