11. CLI commands

← Standard library · Index · Next: Recipes →

Twelve commands: four that work on PDFs, four on scripts, two for distribution and two for licensing.

Command What it does
run Validates a PDF with a script
compare Compares two versions of a PDF
watch Watches a folder and validates what arrives
fix Applies corrections and saves a new PDF
inspect Quick summary of a PDF
lint Analyzes a script without running it
fmt Formats a script
doc Generates documentation from a script
pack Packages profiles and data files
add Installs a package
fingerprint Device fingerprint
license License verification

Exit codes

Every validating command uses the same convention:

Code Meaning
0 Everything passed
1 Warnings only
2 Validation errors, or unreadable PDF
3 Syntax error in the script

In shell scripts:

pdfl run profile.pdfl file.pdf > report.json
case $? in
  0) echo "approved" ;;
  1) echo "approved with warnings" ;;
  2) echo "rejected — see report.json" ;;
  3) echo "error in the validation script" ;;
esac

pdfl run

Validates a PDF with a script.

pdfl run <script.pdfl> <input.pdf> [options]
Option Default What it does
--output json|csv|html|pdf json Report format
--output-file <file> Writes to a file instead of stdout
--fail-on error|warning error With warning, warnings also exit 2
--verbose Extra information on stderr
# JSON report in the terminal
pdfl run prepress.pdfl magazine.pdf

# HTML to send back to the client
pdfl run prepress.pdfl magazine.pdf --output html --output-file report.html

# Audit PDF (the pdf format always writes to a file)
pdfl run prepress.pdfl magazine.pdf --output pdf --output-file report.pdf

# CSV for a spreadsheet
pdfl run prepress.pdfl magazine.pdf --output csv --output-file findings.csv

# Strict: warnings fail too
pdfl run prepress.pdfl magazine.pdf --fail-on warning

The JSON report

{
  "script_name": "prepress.pdfl",
  "input_file": "magazine.pdf",
  "profile": "offset-magazine",
  "status": "FAIL",
  "total_pages_analyzed": 120,
  "error_count": 2,
  "warning_count": 0,
  "info_count": 0,
  "diagnostics": [
    {
      "id": "PDFL-001",
      "severity": "error",
      "check_name": "Ink coverage",
      "message": "page 7: 324% ink (limit 300%)",
      "line": 12
    }
  ]
}

The same PDF with the same script always produces the same report, byte for byte — so it can be versioned and diffed in CI.


pdfl compare

Compares two versions of a PDF: text, structure and metadata.

pdfl compare <v1.pdf> <v2.pdf> [options]
Option Default What it does
--output json|csv|html|pdf json Format
--output-file <file> Writes to a file
--normalize Ignores case and spacing
--ignore-dates Masks dates before comparing
--similarity-threshold <0-100> 100 Minimum acceptable similarity
# Straight comparison
pdfl compare approved_v1.pdf new_v2.pdf

# Tolerating small formatting and date differences
pdfl compare approved_v1.pdf new_v2.pdf --normalize --ignore-dates

# Accepts up to 1% difference; below that it becomes an error
pdfl compare v1.pdf v2.pdf --similarity-threshold 99 \
  --output html --output-file diff.html

How it works

  • Pages are aligned by content, not by number: if a page was inserted in the middle, the comparison notices instead of flagging everything after it as different. It scales to documents of over a thousand pages.
  • Each aligned page gets a similarity score and a sample of the lines that changed (- removed, + added).
  • Changed metadata becomes a warning; changed text becomes an error when it falls below the threshold and a warning when above it.
  • The report carries a similarity field with the overall score.
page 4 → 4: similarity 97.8% | -original title | +revised title

pdfl watch

Watches a folder and validates every PDF that arrives or changes.

pdfl watch <folder> --script <script.pdfl> [options]
Option Default What it does
--pattern <glob> *.pdf Which files to process
--exclude <glob> Which to skip
--output-dir <folder> next to the PDF Where to write reports
--depth <n> 1 Subfolder levels
--debounce <ms> 1000 Waits for the file to stop being copied
--report json|csv|html|pdf json Report format
--fail-fast Stops at the first error
--once Processes what is already there and exits
# The print shop's inbox, running continuously
pdfl watch inbox/ --script preflight.pdfl --output-dir reports/ --report html

# Batch mode for CI: process everything and exit with the worst code
pdfl watch inbox/ --script preflight.pdfl --once
echo "result: $?"

# Skipping drafts
pdfl watch inbox/ --script preflight.pdfl \
  --pattern "*.pdf" --exclude "*_draft*"

Debounce exists because large files arrive in pieces: watch only processes a file once it stops changing, so it never reads half a PDF.

Reports are written as <name>.report.json (or .csv, .html, .pdf).


pdfl fix

Applies fix:: operations and saves a new PDF. Details in chapter 8.

pdfl fix <input.pdf> <script.pdfl> --output <output.pdf> [options]
Option What it does
--output <file> Output PDF (required)
--dry-run Lists the operations without saving
--report json|csv|html|pdf Report format
--report-file <file> Writes the report to a file
# See what would happen, touching nothing
pdfl fix original.pdf normalize.pdfl --output out.pdf --dry-run

# Apply for real
pdfl fix original.pdf normalize.pdfl --output fixed.pdf

pdfl inspect

Quick summary of a PDF, no script needed.

pdfl inspect <file.pdf>
File:     magazine.pdf
Size:     26 KB (27284713 bytes)
SHA-256:  af1029842e5bfeae338ead82fb449ef851be742b1d63117c12596e3ea123a616

Pages:    120
Page size: 496 x 709 pt
Boxes:    MediaBox, TrimBox, BleedBox

Metadata:
  Title: Example Magazine
  Creator: Adobe InDesign 19.3

Fonts:    26
  ABCDEF+Helvetica — embedded
  Arial — NOT embedded
Images:   81 (minimum DPI 136, spaces: DeviceCMYK, Indexed)
Max. estimated TAC: 300% (RGB render approximation)

Warnings:
  ! there are non-embedded fonts
  ! 3 image(s) below 300 DPI

This is the first command to run when a new file lands: within seconds you know whether it is worth opening.


pdfl lint

Analyzes a script without running it, reporting quality issues.

pdfl lint <script.pdfl>

It detects:

  • variables, block parameters and functions that are declared and never used (prefix with _ to silence: _page)
  • duplicate or empty checks
  • unknown namespaces (text::, struct::, visual::, prepress::, codes::, fix::, data::)
  • assert/require outside any check
  • use of fix:: (which only runs under pdfl fix)
$ pdfl lint profile.pdfl
profile.pdfl: warning: variable 'LIMIT' declared and never used
profile.pdfl: warning: check "Fonts" declared 2 times

Exits with 1 when there are warnings — usable in CI.


pdfl fmt

Formats the script: two-space indentation, consistent spacing, collapsed blank lines. Comments and units are preserved (3mm stays 3mm).

pdfl fmt <script.pdfl>            # formats in place
pdfl fmt <script.pdfl> --check    # changes nothing; exits 1 if unformatted
# In CI, enforcing a team standard
for f in profiles/*.pdfl; do pdfl fmt "$f" --check || exit 1; done

pdfl doc

Generates documentation for a script from the code itself.

pdfl doc <script.pdfl> [--output markdown|html]

It produces: the profile, a table of constants, functions, imports and — for each check — its tags and what it validates (the assert messages become the description).

# Markdown for the repository
pdfl doc prepress.pdfl > docs/prepress-profile.md

# HTML to hand to people who do not read code
pdfl doc prepress.pdfl --output html > profile.html

This is the artifact that lets a production manager understand what a profile validates without opening the script.


pdfl pack

Packages scripts and data files into a distributable .pdflpkg.

pdfl pack <folder> [--name <name>] [--version <version>] [--output <file>]

It includes .pdfl, .csv, .txt, .json and .xlsx files from the folder (recursively), plus a manifest.json recording the SHA-256 of each file. The package is deterministic: the same folder produces identical bytes.

pdfl pack profiles/print-shop --name print-profile --version 1.0.0
# creates print-profile.pdflpkg

pdfl add

Installs a local package, verifying the manifest hashes.

pdfl add <package.pdflpkg> [--dir <folder>]
pdfl add print-profile.pdflpkg
# installs into ./pdfl_profiles/print-profile@1.0.0/

pdfl run pdfl_profiles/print-profile@1.0.0/prepress.pdfl file.pdf

If any file's hash differs from the recorded one, installation is refused — a corrupted or tampered package never lands.

A remote repository and digital signatures are not part of this version: add installs from local files.


pdfl fingerprint

This machine's fingerprint — the value you send to your vendor to be issued a license.

pdfl fingerprint generate

It is printed alone on stdout; the surrounding text goes to stderr, so pdfl fingerprint generate 2>/dev/null gives you a value ready to pipe.

$ pdfl fingerprint generate
Device fingerprint for this machine:

1a7553e711c53306be083ea8927c9cbe

Send it to your vendor to be issued a license.
It changes if the operating system is reinstalled.

The fingerprint comes from the system's machine identifier (/etc/machine-id, IOPlatformUUID or MachineGuid). It does not survive an operating-system reinstall, and virtual machines cloned from the same image share the value. For CI and containers — where the machine is destroyed on every run — there is the seatless license, which does not look at the device.


pdfl license

Verifies a license you received. pdfl only verifies: licenses are issued by the vendor, with the private key, which does not ship with the binary.

pdfl license check <token>
$ pdfl license check "$(cat license.txt)"
signature:  ok
customer:   Example Print Shop
expires:    2027-01-01
features:   all
device:     1a7553e711c53306be083ea8927c9cbe
this machine: matches

Exits 0 when the license is valid for this machine and 2 when it is not — including the case where the signature is fine but the license belongs to another machine, where the message says exactly that.

Option What it does
--pubkey <base64> Public key to use, instead of the one compiled in

Where a license is required

run, fix, watch, compare and inspect stop with code 2 when there is no valid license for this machine. fingerprint generate never requires one — it is what produces the value you need to request a license. lint, fmt, doc and pack only touch scripts, never a PDF, and do not require one either.

The token is looked up in this order:

  1. $PDFL_LICENSE — the CI path, where it becomes a secret
  2. ~/.config/pdfl/license
  3. pdfl.license next to the executable
$ pdfl run profile.pdfl file.pdf
error: no license found

Run 'pdfl fingerprint generate' and send the value to your vendor
to be issued a license. Then set PDFL_LICENSE, or save the token to
/home/you/.config/pdfl/license.

A seatless license has a run cap, counted in ~/.config/pdfl/state. The same file records when the last run happened and refuses to run if the clock moves backwards — with one hour of tolerance, so clock synchronisation does not trip it.


← Standard library · Index · Next: Recipes →