A lightweight Python CLI for queueing and executing shell commands in parallel. Inspired by GNU Parallel, parallelcmd provides:
- Command generation from argument combinations
- Concurrent execution with live output and progress tracking
- Job management: inspect, reset, delete, and update queued jobs
- Flexible workflows: resume, and scale workers on demand
- Python 3.8+
- Standard library only (no external Python dependencies)
From this directory:
python3 parallelcmd.py --helpCreate a job database:
python3 parallelcmd.py init "echo {}" ::: a b cRun queued jobs with 4 workers:
python3 parallelcmd.py exec -j 4 --progressOr do both in one command:
python3 parallelcmd.py run -j 4 --progress "echo {}" ::: a b cIf you omit the subcommand entirely, parallelcmd.py defaults to run:
python3 parallelcmd.py -j 4 --progress "echo {}" ::: a b cCheck status:
python3 parallelcmd.py checkinit builds commands and stores them in pardb.sqlite by default. Use --db <name> to target <name>.sqlite, or set the PARDB environment variable.
:::starts an inline argument list.::::starts an argument list loaded from a file (one value per line; empty lines and#comments are ignored).:::: -reads the argument list from stdin.- Multiple lists are combined with Cartesian product.
- If the command has no
{}placeholders, placeholders are appended automatically. - If no
:::or::::separator is given and stdin is a pipe, stdin lines are used as the argument list automatically.
Example:
python3 parallelcmd.py init "python train.py --lr {} --seed {}" ::: 1e-3 1e-4 ::: 1 2 3This creates 6 jobs.
Initialize/append job queue.
python3 parallelcmd.py init [options] <command ...> [ ::: <args ...> ]* [ :::: <argfile ...> ]*Options:
-a, --appendappend to existing table instead of recreating-f, --forcedrop the existingparjobtable and recreate it--check_dupskip commands that already exist-v, --verbose
Execute queued jobs in parallel.
python3 parallelcmd.py exec [options]Options:
-j, --nworkersnumber of workers (default:4)--id <id ...>run only these specific job IDs--progressshow aggregate progress line--dashboardcompact live dashboard mode--dryrunprint commands without running-v, --verbose--timeskip <sec>throttle displayed output updates--randomorderfetch pending jobs in random order--prefix <cmd>prefix each command (example:srun -N1 -n1)--max_jobs <n>max jobs per worker--check_timeleft SECONDSstop taking new jobs when SLURM time left is below threshold--wait SECONDSwhen no job is available, wait this many seconds and retry instead of exiting (useful when another process is still adding jobs)--timeout SECONDSkill a task and move to the next if it runs longer than this many seconds; timed-out jobs are recorded with exit code-124
Initialize and execute in one step (init + exec).
python3 parallelcmd.py run [options] <command ...> [ ::: <args ...> ]* [ :::: <argfile ...> ]*Common options include:
- init side:
--append,-f/--force,--check_dup - exec side:
-j/--nworkers,--id,--progress,--dashboard,--dryrun,--randomorder,--prefix,--max_jobs,--check_timeleft,--wait,--timeout
Inspect queue summary or list all rows.
python3 parallelcmd.py check [options]Options:
-l, --listlist all matching rows instead of the summary--nonzerofilter to only jobs with non-zero exit value--where <sql>arbitrary SQLWHEREclause--like <pattern>filter byCommand LIKE <pattern>--id <id ...>filter by specific job IDs
Reset selected jobs to pending (Starttime, JobRuntime, Exitval set to NULL).
python3 parallelcmd.py reset [--all | --nonzero | --like <pattern> | --id <id ...> | --where <sql>]Options:
-a, --allreset all jobs--nonzeroreset only jobs with non-zero exit value--where <sql>arbitrary SQLWHEREclause--like <pattern>filter byCommand LIKE <pattern>--id <id ...>filter by specific job IDs
Prompts for confirmation before changing rows.
Delete selected jobs.
python3 parallelcmd.py delete [options]Options:
-a, --alldelete all jobs--like <pattern>filter by SQL LIKE pattern on command text--id <id ...>filter by job ID(s)
Prompts for confirmation before deleting rows.
Find/replace command text for selected jobs.
python3 parallelcmd.py update [options]Options:
--replace "old,new"find and replace text pair (comma-separated)--like <pattern>filter by SQL LIKE pattern on command text--id <id ...>filter by job ID(s)
Prompts for confirmation before updating rows.
--db <name>SQLite DB basename; the file on disk is<name>.sqlite--db_retries <n>max retries when SQLite is locked (default:10)--log_level {debug,info}logging level (default:info)
Pipe arguments from stdin (auto-detected when no ::: or :::: is given):
cat cases.txt | python3 parallelcmd.py -j 4 "bash run.sh {}"
seq 10 | python3 parallelcmd.py "echo {}"Pipe stdin explicitly with :::: - (combinable with other arg lists):
cat cases.txt | python3 parallelcmd.py run "bash run.sh {} {}" :::: - ::: seed1 seed2Run scripts from values in a file:
python3 parallelcmd.py init "bash run_case.sh {}" :::: cases.txt
python3 parallelcmd.py exec -j 8 --progressUse a custom DB file:
python3 parallelcmd.py --db jobs init "echo {}" ::: x y z
python3 parallelcmd.py --db jobs exec -j 2Kill tasks that exceed a time limit and continue to the next job:
python3 parallelcmd.py exec -j 4 --timeout 300Timed-out jobs are recorded with exit code -124. Find them with:
python3 parallelcmd.py check -l --where "Exitval = -124"Reset timed-out jobs to retry with a longer timeout:
python3 parallelcmd.py reset --where "Exitval = -124"
python3 parallelcmd.py exec -j 4 --timeout 600Keep workers alive while another process appends jobs later:
python3 parallelcmd.py exec -j 4 --wait 10
python3 parallelcmd.py init -a "echo {}" ::: later1 later2Retry failed jobs only:
python3 parallelcmd.py reset
python3 parallelcmd.py exec -j 4 --progressOverwrite the queue with a new set of jobs (drop and recreate):
python3 parallelcmd.py init -f "echo {}" ::: x y z
python3 parallelcmd.py exec -j 4 --progress- Job output is streamed to stdout while running.
- Queue state is persisted in SQLite, so you can stop and resume workflows.
reset,delete, andupdateare interactive (confirmation required).- With
--wait, workers poll for newly appended jobs instead of exiting as soon as the queue is empty.
-
database is locked- Usually temporary when multiple workers/processes access SQLite.
- Retry the command; avoid running multiple
execsessions against the same DB at once.
-
No jobs are executed
- Check queue state:
python3 parallelcmd.py check --list. - If jobs are already completed or marked in-progress, reset them:
python3 parallelcmd.py reset.
- Check queue state:
-
Workers exit before later jobs are appended
- Start
execwith--wait <seconds>so workers keep polling. - Append work with
init -a ...from another process or terminal.
- Start
-
Unexpected shell behavior / quoting issues
- Commands are executed through
bash -c. - Wrap complex commands in quotes and test one command manually before
init.
- Commands are executed through
-
--check_timeleftfails with missingSLURM_JOB_ID- This option requires a SLURM job environment.
- Run inside a SLURM allocation or omit
--check_timeleft.
-
Some jobs have exit code
-124- These jobs were killed by
--timeout. - Reset and retry them:
python3 parallelcmd.py reset --where "Exitval = -124", then re-runexecwith a larger--timeoutor without it.
- These jobs were killed by
-
update --replacedoes not parse as expected- Use exactly one comma-separated pair:
--replace "old,new". - If your text contains commas, run multiple updates with simpler replacement pairs.
- Use exactly one comma-separated pair:
-
Argument file (
::::) seems ignored- Ensure one argument per line.
- Blank lines and lines starting with
#are intentionally skipped.
-
How do I resume after interruption?
- Just run
python3 parallelcmd.py exec -j 4 --progressagain. - Completed jobs (exit code
0) stay done; pending jobs continue.
- Just run
-
How do I retry only failed jobs?
- Failed jobs are those with non-zero exit values.
- Run
python3 parallelcmd.py reset(default filter resets jobs withExitval <> 0), then runexecagain. - Use
--nonzeroto be explicit:python3 parallelcmd.py reset --nonzero.
-
What does exit code
-124mean?- The job was killed by
--timeout. This matches the GNUtimeoutconvention. - Reset and rerun:
python3 parallelcmd.py reset --where "Exitval = -124", thenexecwith a longer--timeout.
- The job was killed by
-
Can I have multiple queues?
- Yes. Use different database basenames with
--db. - Example:
python3 parallelcmd.py --db exp1 init ...thenexecusing the same--db.
- Yes. Use different database basenames with
-
Is it safe to run two
execcommands on the same DB?- It is not recommended.
- SQLite coordination can work, but contention/locking increases and behavior is harder to reason about.
-
Can I inspect/edit queued commands before running?
- Inspect:
python3 parallelcmd.py check --list - Bulk edit text:
python3 parallelcmd.py update --replace "old,new" --like "%pattern%" - Remove unwanted rows:
python3 parallelcmd.py delete --id 12 13 14
- Inspect: