Skip to content

Add batch processing via --out-dir (closes #15)#72

Merged
swperb merged 2 commits into
mainfrom
feat/batch-processing
Jun 16, 2026
Merged

Add batch processing via --out-dir (closes #15)#72
swperb merged 2 commits into
mainfrom
feat/batch-processing

Conversation

@swperb

@swperb swperb commented Jun 16, 2026

Copy link
Copy Markdown
Owner

The most-requested feature for an image CLI: process many files in one call.

imgcli -i "photos/*.jpg" --out-dir thumbs -vf "scale=400:-1:lanczos" -f png

What's new

  • --out-dir DIR — batch mode: each input is processed independently and written to DIR/<basename>.<ext> (ext from -f, else the input's). The dir is created if missing.
  • Glob expansion of -i patterns (POSIX glob, sorted = deterministic). A non-matching pattern surfaces as a clear per-file error. Windows falls back to shell expansion.
  • Per-file errors don't abort the batch (continue), with --fail-fast to opt out. Exit code is non-zero if any file failed.
  • --json emits a results array: {"batch":true,"results":[…],"processed":N,"failed":M}.
  • Guards: rejects stdin/generator inputs in batch mode, and OUTPUT + --out-dir together.

Verification

  • Glob expansion, per-file continue-on-error (a corrupt file fails but the rest process), --fail-fast stops early, extensions preserved without -f, the guards fire.
  • ASan/UBSan clean across all batch paths (glob, per-file ok+fail, overwrite, no-match).
  • make check + the ASan CI battery exercise it; docs updated per CONTRIBUTING (README, AGENTS.md JSON contract, --help).

🤖 Generated with Claude Code

`imgcli -i "*.jpg" --out-dir thumbs -vf "scale=400:-1:lanczos" -f png` processes
each input independently and writes one output per input. The #1 ask for an
image CLI.

- src/main.c: run_batch(). Glob-expands each -i (POSIX glob, GLOB_NOCHECK so a
  non-matching pattern surfaces as a clear per-file error; sorted = deterministic;
  shell-expansion fallback on Windows). Output is out_dir/<basename>.<ext>, ext
  from -f or the input's. Per-file errors are reported but don't abort the batch
  unless --fail-fast; --json emits a results array with processed/failed counts;
  exit is non-zero if any file failed. Creates the out-dir. Rejects stdin/
  generator inputs in batch mode and OUTPUT + --out-dir together.
- Docs (README, AGENTS.md incl. the JSON contract, --help) + a make check batch
  smoke and an ASan CI battery line (ok + per-file failure).

Verified: glob expansion, per-file continue-on-error, --fail-fast, extension
preservation, the guards, and ASan/UBSan clean across all batch paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/main.c
if (slen >= sizeof outpath / 2) slen = sizeof outpath / 2;
snprintf(outpath, sizeof outpath, "%s/%.*s.%s", out_dir, (int)slen, base, ext);

if (overwrite != 1 && access(outpath, F_OK) == 0) {
Comment thread src/main.c Fixed
Comment thread src/main.c
const char *out_dir, int quality, int overwrite, int json, int quiet, int fail_fast) {
char m[1024];
if (mkdir(out_dir, 0755) != 0 && errno != EEXIST) {
snprintf(m, sizeof m, "cannot create --out-dir '%s'", out_dir);
Comment thread src/main.c
#endif

int nok = 0, nfail = 0;
char outpath[4096];
Comment thread src/main.c
const char *dot = strrchr(base, '.');
const char *ext = format ? format : (dot ? dot + 1 : NULL);
if (!ext) { why = "no -f and input has no extension"; goto record; }
size_t slen = dot ? (size_t)(dot - base) : strlen(base);
Comment thread src/main.c
fputs("{\"input\":", stdout); json_str(stdout, in);
if (ok) {
fputs(",\"output\":", stdout); json_str(stdout, outpath);
printf(",\"ok\":true,\"width\":%d,\"height\":%d,\"bytes\":%lld}", W, H, bytes);
Comment thread src/main.c
fputc('}', stdout);
}
} else if (!quiet) {
if (ok) printf("imgcli: %s -> %s (%dx%d)\n", in, outpath, W, H);
Comment thread src/main.c
}
} else if (!quiet) {
if (ok) printf("imgcli: %s -> %s (%dx%d)\n", in, outpath, W, H);
else fprintf(stderr, "imgcli: %s: FAILED — %s\n", in, why ? why : "unknown");
Comment thread src/main.c
if (!ok && fail_fast) break;
}

if (json) printf("],\"processed\":%d,\"failed\":%d}\n", nok + nfail, nfail);
Comment thread src/main.c
}

if (json) printf("],\"processed\":%d,\"failed\":%d}\n", nok + nfail, nfail);
else if (!quiet) fprintf(stderr, "imgcli: batch: %d ok, %d failed\n", nok, nfail);
Comment thread src/main.c Dismissed
mingw's mkdir(const char*) takes one arg; POSIX mkdir takes two. Wrap it in an
MKDIR() macro (_mkdir on Windows) so the cross-compile builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/main.c
* Returns 0 if every file succeeded, else 1. */
static int run_batch(const char **patterns, int npat, const char *graph, const char *format,
const char *out_dir, int quality, int overwrite, int json, int quiet, int fail_fast) {
char m[1024];
@swperb swperb merged commit 5db7a46 into main Jun 16, 2026
11 of 13 checks passed
@swperb swperb deleted the feat/batch-processing branch June 16, 2026 02:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants