Run a Command on Every Image with pamexec
pamexec runs one shell command once per image in a Netpbm stream, turning a script that needs a manual loop into a single pipeline. This guide checks the install, proves the stream splitting actually happens, builds a multi-image output, filters images first, and tests failure handling before you trust it in a batch. Allow about fifteen minutes.
The route
Jump straight to the step you need, or tick off Done means at the end.
You need a shell, pamexec, a Netpbm image stream and the command you want run per image. Examples use Netpbm 11.5.2 from Debian package netpbm 2:11.05.02-1.1build1, and are all unprivileged; do not add sudo unless the input or destination is genuinely protected, and check the command first as the account that will actually run it.
1. Check the installed command
$ command -v pamexec
/usr/bin/pamexec
$ pamexec --version
pamexec: Using libnetpbm from Netpbm Version: Netpbm 11.5.2
pamexec: Built from source dated 2024-03-31 09:09:47
pamexec: Built by Debian
pamexec: Use 'man pamexec' for help.
Its basic shape is pamexec [command] [netpbmfile]. The command is a shell command, not just an executable name, so quote it when it takes arguments. The input file is optional and defaults to standard input; use - explicitly when that reads more clearly in a pipeline.
Checkpoint
If command -v finds a different copy, or the version differs, note that beside your test results. Option details and failure behaviour vary between Netpbm builds.
2. Confirm the input is actually a stream
A Netpbm stream can hold multiple images. Before processing it, check the first one without touching the file:
$ pamfile /path/to/input.pam
/path/to/input.pam: PAM, 640 by 480 pixels, 3 channels, depth 8, RGB, true
$ test -s /path/to/input.pam && echo "input is non-empty"
input is non-empty
Your own wording from pamfile will differ. What matters: the path is readable, the file is non-empty, and the first image has the format and dimensions you expect. A single-image file is still valid input, but it hides whether a later command silently stops after image one.
For a harmless local test, make a two-image plain PGM stream. The redirection only creates or replaces this temporary file:
$ printf 'P2\n1 1\n255\n7\nP2\n1 1\n255\n9\n' > /tmp/pamexec-two-images.pgm
$ pamfile /tmp/pamexec-two-images.pgm
/tmp/pamexec-two-images.pgm: PGM plain, 1 by 1 maxval 255
pamfile only reports the first image, which is normal: splitting the stream is pamexec's job, not pamfile's.
3. Run the command once per image
Use a command that consumes its whole standard input. pamfile works well as a diagnostic since it reads one image and reports its format:
$ pamexec pamfile /tmp/pamexec-two-images.pgm
stdin: PGM raw, 1 by 1 maxval 255
stdin: PGM raw, 1 by 1 maxval 255
Two lines confirm the command ran twice. The child always receives its image on standard input, so it reports stdin, never the original filename. Its output goes to the parent's standard output, and its diagnostics to standard error as usual.
To use pamexec inside a pipeline, drop the file argument or pass -:
$ cat /tmp/pamexec-two-images.pgm | pamexec - pamfile
stdin: PGM raw, 1 by 1 maxval 255
stdin: PGM raw, 1 by 1 maxval 255
Do not nest a second pipe inside the command unless you are sure which process gets the image: pamexec pamfile | other-command sends pamfile's text output onward, not the original images.
4. Build a multi-image output
The usual goal is transforming every input image while preserving the resulting stream. The Netpbm manual gives this pattern for an animated GIF:
$ pamexec pamtogif myvideo.ppm | gifsicle --multifile > myvideo.gif
pamtogif takes one image at a time, and gifsicle --multifile reads the resulting sequence. Whatever command follows pamexec must accept the format the child produces, so test the first image and the final file before an unattended batch run.
When replacing a valuable destination, avoid truncating it before conversion succeeds: write to a new name, check it, then replace the old file as an explicit final step:
$ pamexec pamtogif /path/to/source.ppm | gifsicle --multifile > /path/to/movie.gif.new
$ test -s /path/to/movie.gif.new && file /path/to/movie.gif.new
$ mv -- /path/to/movie.gif.new /path/to/movie.gif
That last command changes state and can overwrite the existing destination, so keep a backup if the old file matters. If conversion fails, check that the original is still present, then remove the incomplete .new file; do not automate that deletion until the whole workflow is trusted.
5. Filter images before they reach pamexec
pamexec processes every image it receives. If you only want some of them, put pampick in front of it:
$ pampick 0,2 /path/to/input.pam | pamexec pamfile -
stdin: PAM ...
stdin: PAM ...
The exact pampick selection syntax and output depend on the installed version, so check its own manual before trusting an index expression. The pipe is the important boundary: pampick decides which images enter the stream, and pamexec runs the child on whatever remains.
pamsplit is the alternative when the downstream tool cannot read standard input at all: it writes one file per image first, at the cost of temporary files and cleanup. Prefer the stream form whenever the child already accepts Netpbm input.
6. Make sure the child actually reads its input
pamexec assumes the child reads all of its image from standard input. A child that exits early can leave unread bytes sitting in the pipe, so the next invocation starts with the wrong data and produces errors or misleading results.
If the tool only needs a small part of each image, buffer it through a temporary file and make the child read that instead. The manual illustrates the idea with cat and head:
$ pamexec "cat >/tmp/x; head --lines=3 x" /path/to/myvideo.ppm
This example is deliberately limited: every iteration reuses /tmp/x, so it is unsafe for concurrent jobs and exposes the temporary path to other local processes. For a real batch, use a private directory with restrictive permissions, a unique file per iteration, or a wrapper that handles cleanup, and never feed untrusted image-derived names straight into the shell command.
Common traps
Without --check, pamexec can carry on after a child returns non-zero, which makes a batch look successful unless you inspect the child's own output and your logs. Test the actual failure behaviour with the harmless two-image stream:
$ pamexec 'sh -c "exit 7"' /tmp/pamexec-two-images.pgm
$ printf 'pamexec status: %s\n' "$?"
pamexec status: 0
On this Netpbm 11.5.2 build, the default run returns zero even though the child failed, so never treat that status alone as a success signal.
--check is meant to stop when a command returns non-zero:
$ pamexec --check false /tmp/pamexec-two-images.pgm
pamexec: Failed on image 0: Command 'false' terminated abnormally or with nonzero exit status
Warning
There is a version-specific trap here: this installed build prints that diagnostic and then exits with status 139, a segmentation-fault status, rather than a clean ordinary failure. Reproduce it in a disposable test before relying on --check in automation:
$ pamexec --check false /tmp/pamexec-two-images.pgm >/tmp/pamexec.out 2>/tmp/pamexec.err
$ printf 'status: %s\n' "$?"
status: 139
If your system does the same, record it, do not treat status 139 as an ordinary child error, and consider upgrading or reporting the package issue before this path goes into production. For reliable batch control right now, wrap the child so it records per-image results and test that wrapper against real inputs. Never silently discard a failed image.
Done means
- Version confirmed. You know the executable and Netpbm version used by the job.
- Input checked. It is readable, non-empty and in the expected Netpbm format.
- Splitting proven. A two-image test really produced two child invocations.
- Child behaves. It consumes all of standard input and accepts the format it receives.
- Replacement guarded. Any new output is written to a fresh file and checked before it replaces the old one.
- Failure handling tested, including the local
--checkstatus-139 behaviour.