{"id":50872166,"url":"https://github.com/lostjared/shell-cmd-rs","last_synced_at":"2026-06-15T06:06:09.111Z","repository":{"id":347668033,"uuid":"1194604038","full_name":"lostjared/shell-cmd-rs","owner":"lostjared","description":"Parallel batch processing tool with Bash/Regular Expressions","archived":false,"fork":false,"pushed_at":"2026-03-29T00:48:17.000Z","size":57,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-29T00:57:54.507Z","etag":null,"topics":["batch","process","replacement","rust","shell"],"latest_commit_sha":null,"homepage":"https://lostsidedead.biz/shell-cmd","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lostjared.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-28T15:26:11.000Z","updated_at":"2026-03-29T00:48:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/lostjared/shell-cmd-rs","commit_stats":null,"previous_names":["lostjared/shell-cmd-rs"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/lostjared/shell-cmd-rs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lostjared%2Fshell-cmd-rs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lostjared%2Fshell-cmd-rs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lostjared%2Fshell-cmd-rs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lostjared%2Fshell-cmd-rs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lostjared","download_url":"https://codeload.github.com/lostjared/shell-cmd-rs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lostjared%2Fshell-cmd-rs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34349972,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-15T02:00:07.085Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["batch","process","replacement","rust","shell"],"created_at":"2026-06-15T06:06:07.976Z","updated_at":"2026-06-15T06:06:09.102Z","avatar_url":"https://github.com/lostjared.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# shell-cmd-rs\n\nRecursively find files matching a regex pattern and execute a shell command for each match.\nDrop-in replacement for [shell-cmd](https://github.com/lostjared/shell-cmd/), rewritten in Rust.\n\n## Build\n\nRequires Rust 1.70+ (for `LazyLock` stabilization).\n\n```bash\ncargo build --release\n```\n\nThe compiled binary is at `target/release/shell-cmd-rs`.\n\n### Install via Cargo\n\n```bash\ncargo install --path .\n```\n\n## Usage\n\n```\nshell-cmd-rs [options] path \"command %1 [%2 %3..]\" regex [extra_args..]\n```\n\n### Placeholders\n\n| Placeholder | Description |\n|-------------|-------------|\n| `%0` | Filename only (no path); in `--list-all` mode, all matched paths joined by spaces |\n| `%1` | Full path to matched file |\n| `%2+` | Extra arguments from command line |\n| `%b` | Basename without extension (e.g., `report` from `report.txt`) |\n| `%e` | File extension including dot (e.g., `.txt`) |\n\n### Options\n\n| Short | Long | Description |\n|-------|------|-------------|\n| `-b` | `--glob` | Treat pattern as a glob (`*`, `?`) instead of regex |\n| `-z` | `--regex-match` | Use full-path matching (entire path must match the regex) instead of substring search |\n| `-n` | `--dry-run` | Dry-run — print commands without executing |\n| `-v` | `--verbose` | Verbose — print each command before running |\n| `-a` | `--all` | Include hidden files and directories |\n| `-l` | `--list-all` | Collect all matches and run command once with `%0` = all matched paths |\n| `-d N` | `--depth N` | Max recursion depth (0 = current directory only) |\n| `-s SIZE` | `--size SIZE` | Filter by size: `+10M` (\u003e10 MB), `-1K` (\u003c1 KB), `4096` (exact). Suffixes: K, M, G |\n| `-m DAYS` | `--mtime DAYS` | Filter by modification time: `+7` (older than 7 days), `-1` (within last day) |\n| `-p MODE` | `--perm MODE` | Filter by permissions (octal), e.g. `755` |\n| `-u USER` | `--user USER` | Filter by owner username |\n| `-g GROUP` | `--group GROUP` | Filter by group name |\n| `-t TYPE` | `--type TYPE` | Filter by type: `f` (file), `d` (directory), `l` (symlink) |\n| `-x REGEX` | `--exclude REGEX` | Exclude files/directories matching REGEX |\n| `-i` | `--glob-exclude` | Treat exclude pattern as a glob instead of regex |\n| `-f EXPR` | `--expr EXPR` | Expression filter — compose `glob()`, `regex()`, `regex_match()` with `and`/`or`/`not` (replaces the regex positional argument) |\n| `-e` | `--stop-on-error` | Stop on first command failure |\n| `-c` | `--confirm` | Prompt for confirmation before each command |\n| `-j N` | `--jobs N` | Run N commands in parallel (default: 1) |\n| `-w SHELL` | `--shell SHELL` | Shell to use for execution (default: `/bin/bash`) |\n| `-h` | `--help` | Show help |\n\n---\n\n## Pattern Matching Modes\n\n`shell-cmd-rs` supports three independent switches that control how the search pattern and exclude pattern are interpreted. They can be combined freely.\n\n### Default: Regex Search\n\nBy default, the third positional argument is a **regex** tested as a **substring search** against each file's full path. If the pattern appears anywhere in the path, the file matches.\n\n```bash\n# Matches any path containing \".rs\" — e.g. ./src/main.rs, ./lib/foo.rs\nshell-cmd-rs . \"echo %1\" \"\\.rs\"\n\n# Anchor with $ to match only paths ending in .rs\nshell-cmd-rs . \"echo %1\" \"\\.rs$\"\n\n# Match .c, .cpp, .h, and .hpp files\nshell-cmd-rs . \"echo %1\" \"\\.(c|cpp|h|hpp)$\"\n```\n\nBecause this is a substring search, you do **not** need `.*` at the start of the pattern — `\\.rs$` is enough to match all paths ending in `.rs`.\n\n### `--regex-match` / `-z`: Full-Path Matching\n\nWith `-z`, the regex must match the **entire path** (equivalent to wrapping the pattern in `^...$`). This is useful when you want precise control:\n\n```bash\n# Only matches paths that are entirely \".*\\.rs$\"\nshell-cmd-rs -z . \"echo %1\" \".*\\.rs$\"\n\n# Match files whose full path starts with ./src/ and ends with .rs\nshell-cmd-rs -z . \"echo %1\" \"\\./src/.*\\.rs\"\n```\n\n### `--glob` / `-b`: Glob Mode\n\nWith `-b`, write familiar shell wildcard patterns instead of regex. Glob metacharacters:\n\n| Glob | Meaning | Regex equivalent |\n|------|---------|-----------------|\n| `*` | Match any number of characters | `.*` |\n| `?` | Match exactly one character | `.` |\n| `[abc]` | Match one of the listed characters | `[abc]` |\n| `[!abc]` or `[^abc]` | Match any character not listed | `[^abc]` |\n\nAll other regex-special characters (`.`, `+`, `|`, `(`, `)`, etc.) are automatically escaped, so you never need backslashes.\n\nThe glob pattern is anchored — it must match the **entire** path (internally converted to `^...$`).\n\n```bash\n# Match all .rs files\nshell-cmd-rs --glob . \"echo %1\" \"*.rs\"\n\n# Match all .c and .h files (character class)\nshell-cmd-rs --glob . \"echo %1\" \"*.[ch]\"\n\n# Match .cpp and .hpp files\nshell-cmd-rs --glob . \"echo %1\" \"*.[ch]pp\"\n\n# Match files starting with \"test\" and ending with .py\nshell-cmd-rs --glob . \"echo %1\" \"*test*.py\"\n```\n\n### Combining `--glob` with `--regex-match`\n\nWhen both `-b` and `-z` are active, the glob is converted to regex and then full-path matching is applied:\n\n```bash\nshell-cmd-rs --glob --regex-match . \"echo %1\" \"*cmake*\"\n```\n\n---\n\n## Exclude Patterns\n\nThe `-x` / `--exclude` option skips files and directories whose **filename** (not full path) matches the given pattern. By default, the exclude pattern is a **regex** (substring search):\n\n```bash\n# Exclude any file/directory whose name contains \"build\", \"CMakeFiles\", or \"third_party\"\nshell-cmd-rs -x \"build|CMakeFiles|third_party\" . \"echo %1\" \"\\.rs$\"\n\n# Exclude .git and node_modules\nshell-cmd-rs -x \"node_modules|\\.git\" . \"wc -l %1\" \"\\.ts$\"\n```\n\n### `--glob-exclude` / `-i`: Glob Exclude\n\nAdd `-i` to treat the `-x` pattern as a **glob** instead of regex. The glob is converted to an anchored regex internally, so it must match the entire filename:\n\n```bash\n# Exclude files/dirs whose name matches the glob \"build*\"\nshell-cmd-rs --glob -x \"build*\" --glob-exclude . \"echo %1\" \"*.rs\"\n\n# Exclude object files\nshell-cmd-rs --glob -x \"*.o\" -i . \"echo %1\" \"*.c\"\n```\n\n### Mixing Regex Exclude with Glob Search\n\nThe `-x` pattern and the search pattern are **independent** — you can use `--glob` for the search pattern while keeping `-x` as a regex (the default), or vice versa:\n\n```bash\n# Glob search pattern, regex exclude pattern (no -i needed)\nshell-cmd-rs --glob -x \"build|CMakeFiles|third_party\" . \"rustfmt %1\" \"*.rs\"\n\n# Regex search pattern, glob exclude pattern (use -i)\nshell-cmd-rs -x \"build*\" -i . \"echo %1\" \"\\.rs$\"\n```\n\n---\n\n## Expression Filter (`--expr`)\n\nThe `-f` / `--expr` option lets you compose complex match logic in a single argument, combining `glob()`, `regex()`, and `regex_match()` with boolean operators. When `--expr` is used, the third positional argument (regex) is **not required** — the expression replaces it.\n\n### Grammar\n\nExpressions are built from **functions**, **boolean operators**, and **parentheses**:\n\n| Element | Description |\n|---------|-------------|\n| `glob(\"pattern\")` | Convert the glob to an anchored regex and apply `regex_search` (same as `--glob`) |\n| `regex(\"pattern\")` | Substring regex search (same as default mode) |\n| `regex_search(\"pattern\")` | Alias for `regex()` |\n| `regex_match(\"pattern\")` | Full-path regex match (same as `--regex-match`) |\n| `and` | Both sides must match |\n| `or` | Either side must match |\n| `not` | Negate the following expression |\n| `( … )` | Group sub-expressions to control precedence |\n\nOperator precedence (highest to lowest): `not`, `and`, `or`. Use parentheses to override.\n\n### Examples\n\nMatch Rust and TOML files, exclude target directory:\n\n```bash\nshell-cmd-rs . \"echo %1\" --expr '(glob(\"*.rs\") or glob(\"*.toml\")) and not regex(\"target\")'\n```\n\nSingle function — equivalent to a regex positional argument:\n\n```bash\nshell-cmd-rs . \"wc -l %1\" --expr 'regex(\"\\.py$\")'\n```\n\nNested boolean logic — Python or Rust sources, excluding tests and vendor:\n\n```bash\nshell-cmd-rs . \"echo %1\" --expr '(glob(\"*.py\") or glob(\"*.rs\")) and not glob(\"*test*\") and not regex(\"vendor\")'\n```\n\nFull-path matching inside an expression:\n\n```bash\nshell-cmd-rs . \"echo %1\" --expr 'regex_match(\"\\\\./src/.*\\\\.rs\")'\n```\n\nCombine `--expr` with other options (`-x`, `--size`, `--type`):\n\n```bash\nshell-cmd-rs -x \"node_modules\" --size +1K --type f . \"wc -l %1\" --expr 'glob(\"*.ts\") or glob(\"*.tsx\")'\n```\n\n---\n\n## Examples\n\n### Basic Usage\n\nCount lines in all `.rs` files:\n\n```bash\nshell-cmd-rs . \"wc -l %1\" \"\\.rs$\"\n```\n\nDry-run to preview what would be executed:\n\n```bash\nshell-cmd-rs -n . \"rustfmt %1\" \"\\.rs$\"\n```\n\nCopy matched files to a destination, using filename-only placeholder:\n\n```bash\nshell-cmd-rs . \"cp %1 /tmp/backup/%0\" \"\\.txt$\"\n```\n\n### Depth and Hidden Files\n\nLimit search to current directory (no recursion):\n\n```bash\nshell-cmd-rs -d 0 . \"cat %1\" \"\\.md$\"\n```\n\nInclude hidden files:\n\n```bash\nshell-cmd-rs -a ~ \"echo %1\" \"\\.bashrc\"\n```\n\n### Extra Arguments\n\nUse extra arguments — `%2` is replaced with the value passed after the regex:\n\n```bash\nshell-cmd-rs . \"cp %1 %2/%0\" \"\\.log$\" /tmp/logs\n```\n\nMultiple extra arguments:\n\n```bash\nshell-cmd-rs . \"cp %1 %2/%0 \u0026\u0026 echo 'copied to %3'\" \"\\.conf$\" /backup user@host\n```\n\n### Basename and Extension Placeholders\n\nConvert WAV to MP3, using `%b` for the output filename without extension:\n\n```bash\nshell-cmd-rs ~/music \"ffmpeg -i %1 /tmp/mp3/%b.mp3\" \"\\.wav$\"\n```\n\nOrganize files by extension:\n\n```bash\nshell-cmd-rs -n . \"mkdir -p /tmp/by-ext/%e \u0026\u0026 cp %1 /tmp/by-ext/%e/%0\" \".*\"\n```\n\n### List-All Mode\n\nCollect all matches and pass them as a single argument list:\n\n```bash\nshell-cmd-rs -l . \"cat %0\" \"\\.txt$\"\n```\n\nDry-run list-all mode to preview the combined command:\n\n```bash\nshell-cmd-rs -l -n . \"wc -l %0\" \"\\.rs$\"\n```\n\nIn this mode, `%0` is substituted with a single space-separated string containing every matched path.\n\n### Metadata Filters\n\nFind large files (over 10 MB):\n\n```bash\nshell-cmd-rs . \"ls -lh %1\" \".*\" --size +10M\n```\n\nDelete files older than 30 days, with dry-run:\n\n```bash\nshell-cmd-rs --dry-run /tmp \"rm %1\" \"\\.tmp$\" --mtime +30\n```\n\nFind executable files (permission 755):\n\n```bash\nshell-cmd-rs . \"echo %1\" \".*\" --perm 755 --type f\n```\n\nList files owned by root:\n\n```bash\nshell-cmd-rs /etc \"echo %1\" \"\\.conf$\" --user root\n```\n\nList only directories matching a pattern:\n\n```bash\nshell-cmd-rs . \"echo %1\" \"src\" --type d\n```\n\nCombine filters — large `.log` files modified recently:\n\n```bash\nshell-cmd-rs /var/log \"wc -l %1\" \"\\.log$\" -s +1M -m -7\n```\n\n### Glob Mode Examples\n\nMatch all Rust source files:\n\n```bash\nshell-cmd-rs --glob . \"echo %1\" \"*.rs\"\n```\n\nFormat Rust files, excluding build directories:\n\n```bash\nshell-cmd-rs --glob -x \"build|target\" . \"rustfmt %1\" \"*.rs\"\n```\n\nFormat Rust files, excluding with a glob exclude pattern:\n\n```bash\nshell-cmd-rs --glob -x \"target*\" --glob-exclude . \"rustfmt %1\" \"*.rs\"\n```\n\nMatch files with single-character extensions:\n\n```bash\nshell-cmd-rs --glob . \"echo %1\" \"*.?\"\n```\n\n### Parallel Execution\n\nRun commands in parallel with 4 jobs:\n\n```bash\nshell-cmd-rs -j 4 ./images \"convert %1 -resize 800x600 /tmp/thumbs/%0\" \".*\\.jpg$\"\n```\n\n### Safety Options\n\nConfirm before each destructive command:\n\n```bash\nshell-cmd-rs -c /tmp \"rm %1\" \"\\.bak$\"\n```\n\nStop on first error:\n\n```bash\nshell-cmd-rs -e ./src \"gcc -c %1 -o /tmp/%b.o\" \"\\.c$\"\n```\n\n---\n\n## How It Works\n\nThe program recursively walks the specified directory using Rust's `std::fs`. For each entry:\n\n1. Hidden files (names starting with `.`) are skipped unless `-a` is set.\n2. The **exclude pattern** (`-x`) is tested against the entry's filename. If it matches, the entry (and its subtree, if a directory) is skipped.\n3. The **search regex** is tested against the entry's full path.\n4. All active **metadata filters** (size, mtime, permissions, owner, group, type) are applied.\n5. If everything passes, placeholders in the command template are substituted and the command is executed via `fork`/`execv` through the configured shell.\n\nWhen using `--glob`, the search pattern and/or exclude pattern (with `-i`) are converted to anchored regex (`^...$`) with proper escaping before matching begins. When using `--regex-match`, the search regex is wrapped in `^(?:...)$` for full-path matching.\n\nWhen using `-l` / `--list-all`, `shell-cmd-rs` does not run a command per file; it collects all matched paths, joins them with spaces, and runs the command exactly once with `%0` replaced by the full list string.\n\nCommand execution uses the `nix` crate for POSIX `fork`, `execv`, `waitpid`, and signal management — matching the behavior of the original C++ implementation's custom `System()` function.\n\n## Dependencies\n\n| Crate | Purpose |\n|-------|---------|\n| [`clap`](https://crates.io/crates/clap) | Command-line argument parsing with derive macros |\n| [`regex`](https://crates.io/crates/regex) | Fast regex matching for file path filtering |\n| [`nix`](https://crates.io/crates/nix) | POSIX APIs: `fork`, `execv`, `waitpid`, signal handling |\n| [`libc`](https://crates.io/crates/libc) | Raw FFI for `getpwuid`/`getgrgid` (user/group lookup) |\n\n## shell-cmd-rs vs `find -exec`\n\n| Feature | `shell-cmd-rs` | `find -exec` |\n|---------|----------------|---------------|\n| **Filename placeholder** | `%0` gives the filename without the path | No equivalent — requires `sh -c` + `basename` |\n| **Full path placeholder** | `%1` | `{}` |\n| **Extra arguments** | `%2`, `%3`, … with validation | Not supported — use shell variables |\n| **Pattern matching** | Rust `regex` crate on the full path; `--regex-match` for full-path anchoring; `--glob` for wildcard patterns; `--expr` for composable expressions | Glob (`-name`) or implementation-varying `-regex` |\n| **Exclude patterns** | Built-in `-x` with regex or glob (`-i`) | Requires negation logic or `! -name` |\n| **Expression filters** | Built-in `--expr` — combine `glob()`, `regex()`, `regex_match()` with `and`/`or`/`not` | Boolean `-and`/`-or`/`-not` between find predicates |\n| **Dry-run** | Built-in `-n` flag | No native support |\n| **Verbose mode** | Built-in `-v` flag | No native support |\n| **Filter by metadata** | Size (`-s`), time (`-m`), permissions (`-p`), owner (`-u`), group (`-g`), type (`-t`) | Size, time, permissions, ownership, type, boolean logic |\n| **Parallel execution** | Built-in `-j N` | Requires `xargs -P` or GNU `parallel` |\n| **List-all mode** | Built-in `-l` collects matches into single command | Requires `xargs` or `+` terminator |\n| **Confirm mode** | Built-in `-c` flag | Requires `-ok` (not universally supported) |\n| **Stop on error** | Built-in `-e` flag | No native support |\n| **Summary stats** | Automatic (matched/run/failed counts) | No native support |\n| **Portability** | Requires Rust build | POSIX-standard, available everywhere |\n\nSide-by-side example — copy all `.txt` files to a backup directory, preserving filenames:\n\n```bash\n# shell-cmd-rs\nshell-cmd-rs . \"cp %1 /tmp/backup/%0\" \"\\.txt$\"\n\n# find equivalent\nfind . -regex '.*\\.txt$' -exec sh -c 'cp \"$1\" \"/tmp/backup/$(basename \"$1\")\"' _ {} \\;\n```\n\nIn short, `shell-cmd-rs` offers a more ergonomic command-templating experience with built-in dry-run, parallel execution, confirm mode, stop-on-error, exclude patterns (regex or glob), composable expression filters (`--expr` with `and`/`or`/`not`), and summary statistics.\n\n## Compatibility\n\n`shell-cmd-rs` is a **drop-in replacement** for `shell-cmd`. All command-line flags, positional arguments, placeholder syntax, output format, and exit codes are identical. You can alias it:\n\n```bash\nalias shell-cmd='shell-cmd-rs'\n```\n\n## License\n\nGNU GPL v3\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flostjared%2Fshell-cmd-rs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flostjared%2Fshell-cmd-rs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flostjared%2Fshell-cmd-rs/lists"}