Last active
December 22, 2025 10:47
-
-
Save ifthenelse/4fdd962f92258c8c5c1c7e559c5ee40c to your computer and use it in GitHub Desktop.
ZSH script to unlock all PDF files in the current directory using a single password
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env zsh | |
| set -u | |
| setopt extendedglob nullglob | |
| SCRIPT_NAME=${0:t} | |
| usage() { | |
| cat <<EOF | |
| Usage: $SCRIPT_NAME [options] [input_dir] | |
| Unlock all PDF files in input_dir (or current directory if omitted). | |
| Unlocked files are written to: input_dir/unlocked/ | |
| Options: | |
| -i, --input DIR Input directory (optional; can also be given positionally) | |
| -h, --help Show this help message and exit | |
| Requirements: | |
| - zsh | |
| - qpdf | |
| Notes: | |
| - Matching is case-insensitive (*.pdf, *.PDF, ...) | |
| - Only regular files are processed | |
| - Already-unlocked PDFs are skipped | |
| - Existing output files are not overwritten | |
| EOF | |
| } | |
| # --- Parse args --- | |
| indir="." | |
| while (( $# > 0 )); do | |
| case "$1" in | |
| -h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| -i|--input) | |
| shift | |
| if [[ -z "${1:-}" ]]; then | |
| echo "Error: --input requires a directory path." | |
| exit 1 | |
| fi | |
| indir="$1" | |
| shift | |
| ;; | |
| --) # end of options | |
| shift | |
| break | |
| ;; | |
| -*) | |
| echo "Unknown option: $1" | |
| echo | |
| usage | |
| exit 1 | |
| ;; | |
| *) | |
| # positional input dir (only one supported) | |
| indir="$1" | |
| shift | |
| ;; | |
| esac | |
| done | |
| # --- Validate input directory --- | |
| if [[ ! -d "$indir" ]]; then | |
| echo "Error: input directory not found: $indir" | |
| exit 1 | |
| fi | |
| # --- Dependency check --- | |
| if ! command -v qpdf >/dev/null 2>&1; then | |
| echo "Error: qpdf is not installed or not in PATH." | |
| echo "Install it with:" | |
| echo " macOS (brew): brew install qpdf" | |
| echo " Debian/Ubuntu: sudo apt install qpdf" | |
| exit 1 | |
| fi | |
| # --- Output directory is unlocked subdir of input dir --- | |
| outdir="$indir/unlocked" | |
| mkdir -p -- "$outdir" | |
| # --- Collect PDFs from input dir (case-insensitive, regular files only) --- | |
| pdfs=( "$indir"/*.(#i)pdf(.N) ) | |
| if (( ${#pdfs} == 0 )); then | |
| echo "No PDF file found in: $indir" | |
| exit 0 | |
| fi | |
| echo "Input directory: $indir" | |
| echo "Output directory: $outdir" | |
| echo "Found ${#pdfs} PDF file(s):" | |
| for f in "${pdfs[@]}"; do | |
| print -- " ${f:t}" | |
| done | |
| echo | |
| # --- Prompt for password --- | |
| read -rs "PW?Password: " | |
| echo | |
| skipped=0 | |
| unlocked=0 | |
| failed=0 | |
| # --- Process each PDF --- | |
| for f in "${pdfs[@]}"; do | |
| out="$outdir/${f:t:r}_unlocked.pdf" | |
| encinfo="$(qpdf --show-encryption "$f" 2>/dev/null || true)" | |
| if [[ "$encinfo" == *"File is not encrypted"* ]]; then | |
| echo "Skipping (already unlocked): ${f:t}" | |
| ((skipped++)) | |
| continue | |
| fi | |
| if [[ -f "$out" ]]; then | |
| echo "Skipping (output already exists): ${f:t} -> ${out:t}" | |
| ((skipped++)) | |
| continue | |
| fi | |
| echo "Unlocking: ${f:t} -> ${out:t}" | |
| if qpdf --password="$PW" --decrypt "$f" "$out" 2>/dev/null; then | |
| ((unlocked++)) | |
| else | |
| echo "Failed to unlock: ${f:t}" | |
| rm -f -- "$out" 2>/dev/null || true | |
| ((failed++)) | |
| fi | |
| done | |
| echo | |
| echo "Outcome:" | |
| echo " Unlocked: $unlocked" | |
| echo " Skipped : $skipped" | |
| echo " Failed : $failed" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment