Output files
Where outputs go, how names are built, and why your original is never at risk.
The rule
The input is never overwritten unless you pass --in-place. Every write goes through an
atomic guard designed around one scenario: someone runs this over the only copy of a photo they
have.
Default naming
Without --out or --in-place, the output is written next to the input:
photo.jpg → photo-compressed.jpg
The pattern is <sanitized base><suffix>.<ext>:
- The base comes from the input’s filename, run through the same filename sanitizer the
website’s own download uses (one sanitiser, not two). It deletes invisible/bidi-override
characters (the kind that can make
photognp.exerender asphoto exe.pngwhile staying an executable) and a small set of Windows-reserved characters, but otherwise preserves your filename exactly — CJK, Devanagari, Arabic, Cyrillic, accented Latin, emoji, spaces and punctuation all survive.休暇写真.jpgbecomes休暇写真-compressed.jpg. - The suffix is
-compressedby default, or whatever you pass to--suffix <text>. It must be a plain filename fragment — no path separator — or the tool refuses at parse time, before it ever builds a path. - The extension always matches the format that was actually encoded — never echoed back from
the input’s own extension, even when the output format happens to equal the input’s format. A
.jpegin becomes.jpgout (this tool’s own extension for the jpeg format), and a--format webpconversion of a.pnggets a.webpextension, not a renamed.png.
--out <dir>
Writes to <dir> instead of next to the input, using the same naming otherwise:
imagekbreducer compress src/*.jpg --to 80kb --out dist/img
--in-place
The only flag that overwrites the input. Refused outright when the input is a symlink — replacing
a symlink with rename/link (see below) would swap out the link, leaving the file it used to
point to untouched and unreachable under that name, which is exactly the “destroyed the only copy”
failure this tool exists to prevent, just reached through an alias instead of the same path twice.
Pass --out <dir> instead for a symlinked input.
--suffix <text>
Overrides the default -compressed suffix. Combine with --out or use alone:
imagekbreducer compress photo.jpg --to 50kb --suffix -small
# photo-small.jpg
Refusing to clobber an existing output
If the computed output path already exists and isn’t the input itself, the tool refuses (exit 4) rather than silently replacing a file you didn’t ask it to touch:
imagekbreducer: refusing to overwrite existing file 'photo-compressed.jpg' -- pass --force to
replace it, or --out <dir> / --suffix <text> to write somewhere else instead
--force authorizes the overwrite. It only relaxes this check — it never bypasses the
input-overwrite guard above; --in-place is the only way to do that. If the blocking path is a
directory rather than a file, the message doesn’t offer --force (replacing a directory this way
would just fail with a different, more confusing error) — only --out/--suffix.
Refusing to clobber another input in the same run
There is a second exit-4 refusal that --force does not relax: a destination that resolves to
a different file this same command was asked to read.
imagekbreducer: refusing to overwrite 'photo-compressed.jpg' -- it is also an input to this run,
and --force replaces an existing output, not a file this same command was asked to read; pass
--out <dir> or --suffix <text> to write somewhere else instead
ikb compress photo.jpg photo-compressed.jpg --to 5kb --force is the shape: the first file’s
output lands on the second, which processing would then read back from its own replacement and
report as a second success over what was really one source. A glob across a directory that already
holds a previous run’s outputs reaches it just as easily as typing both names. It is refused
rather than warned about — a warning arrives on stderr after the bytes are gone, and --quiet
silences it — and the message names the two flags that express the “write it somewhere else”
intent instead. Paths are compared after resolving symlinks, not by the spelling you typed, and
the input at risk counts whether it appears before or after on the command line.
--force keeps its actual job: it still replaces a stale existing output that isn’t an input.
--in-place, which targets the input deliberately, is exempt.
Atomic writes
A write never appears under its final name until it’s complete. The tool writes to a temporary
sibling file first, then makes that file visible under the destination name in one filesystem
operation (link, which POSIX guarantees fails atomically if the destination already exists — no
window for a second process to race into). If the process dies, the disk fills, or the encoder
produces a truncated buffer partway through, the destination is left exactly as it was — never a
half-written file sitting under a name that looks like a finished result.
Globs
Each input containing *, ?, [, or { is expanded with a real filesystem glob; anything else
is used as a literal path. A literal filename that happens to contain a glob character (a real
screenshot tool can produce Screenshot [1].png) is tried as a literal path first — only when
no file exists under the exact string typed does the tool fall back to interpreting it as glob
syntax. A glob that matches nothing exits 3 (not-found), the same as a literal path that doesn’t
exist.
--dry-run
Does everything except write — no directory created, no temp file, no filesystem mutation at
all — but the reported/JSON byte counts (resultBytes, the percentage saved) are the real
numbers a real run would produce, computed the same way. The --dry-run overwrite prediction uses
the same rules the real write does, including for a dangling symlink at the destination, so a dry
run’s predicted exit code matches what the real run would do.