{"id":31474600,"url":"https://github.com/fetchte/cli-reap","last_synced_at":"2026-01-20T17:30:37.755Z","repository":{"id":316961555,"uuid":"1065481675","full_name":"fetchTe/cli-reap","owner":"fetchTe","description":"CLI and ENV parser; indifferent to argument order or runtime","archived":false,"fork":false,"pushed_at":"2025-09-27T20:32:28.000Z","size":94,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-09-27T22:17:38.419Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fetchTe.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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":"2025-09-27T20:19:21.000Z","updated_at":"2025-09-27T20:32:08.000Z","dependencies_parsed_at":"2025-09-27T22:27:42.577Z","dependency_job_id":null,"html_url":"https://github.com/fetchTe/cli-reap","commit_stats":null,"previous_names":["fetchte/cli-reap"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/fetchTe/cli-reap","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fetchTe%2Fcli-reap","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fetchTe%2Fcli-reap/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fetchTe%2Fcli-reap/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fetchTe%2Fcli-reap/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fetchTe","download_url":"https://codeload.github.com/fetchTe/cli-reap/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fetchTe%2Fcli-reap/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278254466,"owners_count":25956604,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-04T02:00:05.491Z","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":[],"created_at":"2025-10-01T22:28:37.154Z","updated_at":"2025-10-04T02:15:08.462Z","avatar_url":"https://github.com/fetchTe.png","language":"TypeScript","readme":"\u003ch1\u003e\ncli-reap\n\u003ca href=\"https://mibecode.com\"\u003e\n  \u003cimg align=\"right\" title=\"\u0026#8805;95% Human Code\" alt=\"\u0026#8805;95% Human Code\" src=\"https://mibecode.com/badge.svg\" /\u003e\n\u003c/a\u003e\n\u003cimg align=\"right\" alt=\"empty space\" src=\"https://mibecode.com/4px.svg\" /\u003e\n\u003cimg align=\"right\" alt=\"NPM Version\" src=\"https://img.shields.io/npm/v/cli-reap?color=white\" /\u003e\n\u003c/h1\u003e\n\n\nYour friendly neighborhood CLI reaper (parser), indifferent to argument order or runtime\n\n\u003cimg align=\"right\" width=\"130\" height=\"auto\" alt=\"logo\" src=\"https://raw.githubusercontent.com/fetchTe/cli-reap/master/docs/cli-reap-logo.png\" /\u003e\n\n\u003e ╸ **Supports**: [flags](#-flag), [options](#-opt), [positionals](#-pos), [duplicates](#duplicates), [double-dash](#-end), and [ENV](#-env)/[`globalThis`](#-env)\u003cbr /\u003e\n\u003e ╸ **Runtimes**: [Node.js](https://nodejs.org/), [Deno](https://deno.com/), [Bun](https://bun.sh/), [QuickJS](https://bellard.org/quickjs/), and others with [`argv`](https://nodejs.org/api/process.html#processargv) \u0026 [`env`](https://nodejs.org/api/process.html#processenv) handling \u003cbr /\u003e\n\u003e ╸ **Tests**: [25,757](https://github.com/fetchTe/cli-reap/blob/master/src/index.test.ts) of them\u003cbr /\u003e\n\n\u003cbr /\u003e\n\n### ▎THE GIST\n\nFlags and/or options are removed as they are parsed, which allows operands to be defined anywhere:\n\n```ts\nimport cliReap from 'cli-reap';\nimport assert  from 'node:assert/strict';\n\n// all possible ARGV (out, verbose, input) combinations -\u003e all parsed (reaped) the same\nconst ARGVS = [\n  './order  --out=file.txt --verbose input.txt', // 'input.txt' is the operand\n  './does   --out file.txt input.txt --verbose',\n  './not    --verbose --out=file.txt input.txt',\n  './matter --verbose input.txt --out file.txt',\n  './to-the input.txt --out file.txt --verbose',\n  './reaper input.txt --verbose --out=file.txt',\n];\n\nconst EXPECT = `\nverbose: true\noutput: file.txt\ninput: input.txt`.repeat(ARGVS.length); // repeats the string six time\n\nconst OUTPUT = ARGVS.map(arg =\u003e {\n  const reap = cliReap(arg.split(' ')); // cliReap(\u003cargv\u003e?, \u003cenv\u003e?, \u003cstrict\u003e?)\n  return `\nverbose: ${reap.flag('verbose')}\noutput: ${reap.opt('out')}\ninput: ${reap.pos().pop()}`; // same string format as EXPECT\n}).join('');\n\nassert.strictEqual(EXPECT, OUTPUT); // a-ok - EXPECT equals OUTPUT\n```\n\n\u003c!-- \n  // supports multiple runtimes\n  'node ./cli-reapin.js --verbose --out=file.txt input.txt',\n  'bun run ./reaping.ts input.txt --out file.txt --verbose',\n  'deno run ./reapin.ts --verbose input.txt --out=file.txt',\n--\u003e\n\n### ▎INSTALL\n\n```sh\n# pick your poison\nnpm install cli-reap\nbun  add cli-reap\npnpm add cli-reap\nyarn add cli-reap\n```\n\u003cbr /\u003e\n\n\n\n## API\n\n```ts\nimport cliReap, {\n  cliReapStrict, // convenience wrap with strict enabled (case/hyphen/underscore sensitive)\n  ARGV,          // command-line arguments (Node.js, Deno, Bun, QuickJS)\n  ENV,           // process environment object (Node.js, Deno, Bun, QuickJS)\n} from 'cli-reap';\n\ntype CliReap = (\n  argv?: string[],           // command-line arguments array\n  env?: NodeJS.ProcessEnv,   // process environment variables\n  gthis?: typeof globalThis, // global object for runtime-set/fallback values\n  strict?: boolean,          // enable strict matching (case/hyphen/underscore sensitive)\n) =\u003e Readonly\u003c{\n  /** finds any value, in order: argv \u003e environment \u003e globalThis \u003e default; removes from 'cur' argv if present */\n  any: \u003cR = string\u003e(keys: string | string[], defaultValue?: R)=\u003e R extends undefined ? string | true | null : string | true | R;\n  /** command portion of argv (executable and script name) */\n  cmd: ()=\u003e string[];\n  /** current un-consumed argv */\n  cur: ()=\u003e string[];\n  /** if end-of-options/double-dash (--) delimiter is present in argv */\n  end: ()=\u003e boolean;\n  /** value from environment variables or globalThis; does not mutate */\n  env: (keys: string | string[])=\u003e string | null;\n  /** checks for flag presence and removes it from 'cur' argv */\n  flag: (keys: string | string[])=\u003e true | null;\n  /** retrieves operand value and removes it from 'cur' argv  */\n  opt: \u003cR extends NonEmptyString\u003e(key: string | string[])=\u003e R | null;\n  /** remaining positional arguments (typically called last)  */\n  pos: ()=\u003e string[];\n}\u003e;\n```\n\u003e ╸ **Note**: All args after terminator (`--`) are positionals (even flags) see [here](#-end) \u003cbr /\u003e\n\u003e ╸ **Loose**: matching by default unless [`strict`](#-clireapstrict) \u003cbr /\u003e\n\u003e ━╸ **Case-Insensitive**: `Flag`, `flag`, `FlAg`, `FLAG`\u003cbr /\u003e\n\u003e ━╸ **Hyphen/Underscore Swapping**: `my-key`, `my_key`, `mY-keY`, `My_Key`\u003cbr /\u003e\n\n\n\u003cbr /\u003e\n\n\n### ▎ `any`\nFinds any value in order: `argv \u003e environment \u003e globalThis \u003e default`\n\n```ts\ntype Any = \u003cR = string\u003e(\n  keys: string | string[],   // key(s) to search for; first match returns\n  defaultValue?: R           // default value if key not found; else null\n) =\u003e R extends undefined\n  ? string | true | null     // string value (option), true (flag), or null\n  : string | true | R;       // string value (option), true (flag), or defaultValue\n\nconst reap = cliReap(['node', 'app.js', '--flag', '--out=out.txt', '-v', '-i', 'in.txt']);\n\nreap.any(['f', 'flag'])    === true;      // found --flag\nreap.any(['h', 'help'])    === null;      // no '-h', '--help', or default -\u003e null\nreap.any(['luck'], 7)      === 7;         // no '--luck', but default provided\nreap.any(['v', 'verbose']) === true;      // found -v\nreap.any(['o', 'out'])     === 'out.txt'; // found --out=out.txt\nreap.any(['i', 'in'])      === 'in.txt';  // found -i in.txt\n```\n\u003e ╸ **Removes**: matching arguments from [`cur`](#-cur) `argv` array\u003cbr /\u003e\n\n\u003cbr /\u003e\n\n\n### ▎ `cmd`\nReturns command portion of `argv`: executable and script name\n\n```ts\ntype Cmd = () =\u003e string[];   // command parts array [executable, script, ...]\n\n// node.js execution\ncliReap(['node', 'script.js', '--flag']).cmd() === ['node', 'script.js']\n// bun execution\ncliReap(['bun', 'run', 'script.ts', '--flag']).cmd() === ['bun', 'run', 'script.ts']\n// direct executable\ncliReap(['./my-cli', '--flag', 'value']).cmd() === ['./my-cli']\n// flags at start (no executable detected)\ncliReap(['--flag', 'value']).cmd() === []\n```\n\u003cbr /\u003e\n\n\n### ▎ `cur`\nReturns current un-consumed `argv` array for progressive parsing and/or debugging\n\n```ts\ntype Cur = () =\u003e string[]; // current un-consumed argv array\n\nconst reap = cliReap(['./my-cli', '--yolo', 'value', 'pos1']);\nreap.cur()         === ['--yolo', 'value', 'pos1'] // initially all args\nreap.flag('yolo'); === true // consumes --yolo\nreap.cur()         === ['value', 'pos1'] // after flag consumption\n```\n\n\u003cbr /\u003e\n\n\n### ▎ `env`\nRetrieves values from environment variables or `globalThis`\n\n```ts\ntype Env = (\n  keys: string | string[]    // environment variable key(s) to search for\n) =\u003e string | null;          // environment value or null if not found\n\n// With process.env.NODE_ENV = 'development'\n// and globalThis.DEBUG = 'true'\nconst reap = cliReap();\n\nreap.env('NODE_ENV')         === 'development'; // from process.env\nreap.env('DEBUG')            === 'true';        // from globalThis (fallback)\nreap.env(['TEST', 'DEBUG'])  === 'true';        // first match wins\nreap.env('MISSING')          === null;          // not found anywhere\n```\n\u003e ╸ **Read-only**: does not modify [`cur`](#-cur) `argv` array or modify global object\u003cbr /\u003e\n\u003e ╸ **Reference**: [`environment`](https://nodejs.org/api/process.html#processenv) and [`globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) variables \u003cbr /\u003e\n\u003cbr /\u003e\n\n\n### ▎ `end`\nChecks if `--` (double-dash/end-of-options delimiter) is present in `argv`\n\n```ts\ntype Eod = () =\u003e boolean; // if -- is present in argv\n\nconst reap = cliReap(['./exe', '--flag', '--', '-v', '--in', 'in.txt']);\nreap.end() === true;\n\nreap.pos() === ['-v', '--in', 'in.txt'];\nconst reReap = cliReap(reap.pos());\nreReap.opt('in') === 'in.txt';\n\n```\n\u003e ╸ **Read-only**: does not modify [`cur`](#-cur) `argv` array\u003cbr /\u003e\n\u003e ╸ **Note**: Per [POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02) standard, arguments following `--` are treated as positionals (operands)\u003cbr /\u003e\n\n\u003cbr /\u003e\n\n\n### ▎ `flag`\nChecks for flag presence\n\n```ts\ntype Flag = (\n  key: string | string[]     // flag key(s) to search for\n) =\u003e true | null;            // true if flag exists, null otherwise\n\nconst reap = cliReap(['node', 'app.js', '--verbose', '-d', '--force', 'file.txt']);\n\nreap.flag('verbose')      === true; // --verbose found\nreap.flag(['d', 'debug']) === true; // -d found (matches 'd')\nreap.flag('quiet')        === null; // --quiet not found\nreap.flag('force')        === true; // --force found\n```\n\u003e ╸ **Removes**: matching arguments from [`cur`](#-cur) `argv` array\u003cbr /\u003e\n\n\u003cbr /\u003e\n\n\n### ▎ `opt`\nRetrieves command-line option value and removes it from [`cur`](#-cur) `argv` array\n\n```ts\ntype Opt = \u003cR extends NonEmptyString\u003e(\n  key: string | string[]     // option key(s) to search for\n) =\u003e R | null;               // option value or null if not found\n\nconst reap = cliReap(['node', 'app.js', '--find=file.txt', '--data', 'dog.txt', '-v']);\n\nreap.opt('find')        === 'file.txt'; // --find=file.txt (equals syntax)\nreap.opt(['d', 'data']) === 'dog.txt';  // --data dog.txt (space syntax)\nreap.opt('v')           === null;       // -v is a flag, not an option\n```\n\u003e ╸ **Removes**: matching arguments from [`cur`](#-cur) `argv` array\u003cbr /\u003e\n\n\u003cbr /\u003e\n\n\n### ▎ `pos`\nReturns remaining positional/operand arguments; this should happen after  after parsing [options](#-opt) and [flags](#-flag)\n\n```ts\ntype Pos = () =\u003e string[];   // array of remaining positional arguments\n\nconst reap = cliReap(['node', 'app.js', '-v', 'input.txt', '-f', 'output.txt']);\n\nreap.pos()      === ['input.txt', 'output.txt']; // before reaping '-f' opt\nreap.opt('f');  === 'output.txt'   // removes/reaps 'output.txt'\nreap.pos()      === ['input.txt']; // only 'input.txt' is left, as opt('-f') reaps 'output.txt'\n```\n\u003e ╸ **Read-only**: does not modify [`cur`](#-cur) `argv` array\u003cbr /\u003e\n\n\u003cbr /\u003e\n\n\n\u003cbr /\u003e\n\n\n### ▎ `cliReapStrict`\nExact matching, unlike `cliReap`, `cliReapStrict` is case, hyphen, and underscore sensitive\n```ts\nimport { cliReap, cliReapStrict } from 'cli-reap';\n\n// case sensitivity\ncliReap(['-I', 'test']).opt('i')       === 'test';  // case-insensitive\ncliReapStrict(['-I', 'test']).opt('i') === null;    // strict: no match\n\n// hyphen/underscore swapping\ncliReap(['--swap_in', 'loose']).opt('swap-in')       === 'loose'; // swaps _ \u003c-\u003e -\ncliReapStrict(['--swap_in', 'loose']).opt('swap-in') === null;    // strict: no match\n\n// both case + swapping\ncliReap(['--My_Key', 'value']).opt('my-key')       === 'value'; // case + swap\ncliReapStrict(['--My_Key', 'value']).opt('my-key') === null;    // strict: no match\n```\n\u003cbr /\u003e\n\n\n\n## Duplicates\n\nEach call consumes the first matching option\n\n```ts\nconst reap = cliReap(['./my-cli', '--out', 'first', '--out', 'second', '--out=third']);\nreap.opt('out') === 'first';  // --out first (consumed)\nreap.opt('out') === 'second'; // --out second (consumed)\nreap.opt('out') === 'third';  // --out=third (consumed)\nreap.opt('out') === null;     // no more --out options\n```\n\nNaturally, you can leverage this behavior in your CLI api:\n\n```ts\n// multiple output files\nconst reap = cliReap(['./build', '--out', 'dist/', '--out', 'build/', '--out', 'public/']);\nconst outputs = [];\nlet output;\nwhile ((output = reap.opt('out')) !== null) { outputs.push(output); }\noutputs === ['dist/', 'build/', 'public/'];\n\n// verbose level counting\nconst reap2 = cliReap(['./app', '-v', '-v', '-v']);\nlet verboseLevel = 0;\nwhile (reap2.flag('v') !== null) { verboseLevel++; }\nverboseLevel === 3;\n```\n\u003cbr /\u003e\n\n\n\n## Development/Contributing\n\u003e Required build dependencies: [Bun](https://bun.sh) and [Make](https://www.gnu.org/software/make/manual/make.html) \u003cbr /\u003e\n\n\n### ▎PULL REQUEST STEPS\n\n1. Clone repository\n2. Create and switch to a new branch for your work\n3. Make and commit changes\n4. Run `make release` to clean, setup, build, lint, and test\n5. If everything checks out, push branch to repository and submit pull request\n\u003cbr /\u003e\n\n### ▎MAKEFILE REFERENCE\n\n```\n# USAGE\n   make [flags...] \u003ctarget\u003e\n\n# TARGET\n  -------------------\n   run                   executes entry-point (./src/index.ts) via 'bun run'\n   release               clean, setup, build, lint, test, aok (everything but the kitchen sink)\n  -------------------\n   build                 builds the .{js,d.ts} (skips: lint, test, and .min.* build)\n   build_cjs             builds the .cjs export\n   build_esm             builds the .js (esm) export\n   build_declarations    builds typescript .d.{ts,mts,cts} declarations\n  -------------------\n   install               installs dependencies via bun\n   update                updates dependencies\n   update_dry            lists dependencies that would be updated via 'make update'\n  -------------------\n   lint                  lints via tsc \u0026 eslint\n   lint_eslint           lints via eslint\n   lint_eslint_fix       lints and auto-fixes via eslint --fix\n   lint_tsc              lints via tsc\n   lint_watch            lints via eslint \u0026 tsc with fs.watch to continuously lint on change\n  -------------------\n   test                  runs bun test(s)\n   test_watch            runs bun test(s) in watch mode\n   test_update           runs bun test --update-snapshots\n  -------------------\n   help                  displays (this) help screen\n\n# FLAGS\n  -------------------\n   BUN                   [? ] bun build flag(s) (e.g: make BUN=\"--banner='// bake until golden brown'\")\n  -------------------\n   CJS                   [?1] builds the cjs (common js) target on 'make release'\n   EXE                   [?js|mjs] default esm build extension\n   TAR                   [?0] build target env (-1=bun, 0=node, 1=dom, 2=dom+iife, 3=dom+iife+userscript)\n   MIN                   [?1] builds minified (*.min.{mjs,cjs,js}) targets on 'make release'\n  -------------------\n   BAIL                  [?1] fail fast (bail) on the first test or lint error\n   ENV                   [?DEV|PROD|TEST] sets the 'ENV' \u0026 'IS_*' static build variables (else auto-set)\n   TEST                  [?0] sets the 'IS_TEST' static build variable (always 1 if test target)\n   WATCH                 [?0] sets the '--watch' flag for bun/tsc (e.g: WATCH=1 make test)\n  -------------------\n   DEBUG                 [?0] enables verbose logging and sets the 'IS_DEBUG' static build variable\n   QUIET                 [?0] disables pretty-printed/log target (INIT/DONE) info\n   NO_COLOR              [?0] disables color logging/ANSI codes\n```\n\n\u003cbr /\u003e\n\n\n\n## License\n\n```\nMIT License\n\nCopyright (c) 2025 te \u003clegal@fetchTe.com\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffetchte%2Fcli-reap","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffetchte%2Fcli-reap","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffetchte%2Fcli-reap/lists"}