{"id":24307751,"url":"https://github.com/lennartcl/shellsync","last_synced_at":"2025-09-26T13:30:44.407Z","repository":{"id":57144371,"uuid":"138578106","full_name":"lennartcl/shellsync","owner":"lennartcl","description":"Synchronous shell scripting with Node.js and TypeScript","archived":false,"fork":false,"pushed_at":"2023-07-22T12:35:44.000Z","size":215,"stargazers_count":11,"open_issues_count":2,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-01-16T10:43:02.152Z","etag":null,"topics":["bash","batch","command-line","mocking","mocks","nodejs","shell","shellscripting","synchronous","testing","typescript","zsh"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/shellsync","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/lennartcl.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}},"created_at":"2018-06-25T10:12:59.000Z","updated_at":"2024-04-04T17:57:59.000Z","dependencies_parsed_at":"2024-06-19T05:14:56.616Z","dependency_job_id":"4335be6f-227f-45b6-91ab-855aee3a03e7","html_url":"https://github.com/lennartcl/shellsync","commit_stats":{"total_commits":122,"total_committers":3,"mean_commits":"40.666666666666664","dds":0.06557377049180324,"last_synced_commit":"03d1ed44330e3aff2a498b8dc8a56fec4b59e535"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lennartcl%2Fshellsync","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lennartcl%2Fshellsync/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lennartcl%2Fshellsync/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lennartcl%2Fshellsync/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lennartcl","download_url":"https://codeload.github.com/lennartcl/shellsync/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234310443,"owners_count":18812101,"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","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":["bash","batch","command-line","mocking","mocks","nodejs","shell","shellscripting","synchronous","testing","typescript","zsh"],"created_at":"2025-01-17T04:19:27.990Z","updated_at":"2025-09-26T13:30:43.975Z","avatar_url":"https://github.com/lennartcl.png","language":"TypeScript","readme":"# shellsync\n\nSynchronous shell scripting for Node.js.\n\n* **Pragmatic**: automate tasks using synchronous code, using familiar shell commands.\n* **Powerful**: combine the shell world with functions, modules, libraries, try/catch/finally, regular expressions, and so on, from JavaScript or TypeScript.\n* **Safe**: avoid most Bash pitfalls and use automatic, [safe variable escaping](#safe-variable-escaping).\n* **Robust**: use [uninterruptable sections](#uninterruptable-sections) and harden your code with [standard testing frameworks and strong support for mocking](#writing-tests).\n\n## Overview\n\n- [Usage](#usage)\n  - [Safe Variable Escaping](#safe-variable-escaping)\n  - [Writing Tests](#writing-tests)\n  - [Debugging](#debugging)\n  - [Uninterruptable Sections](#uninterruptable-sections)\n- [Examples](#examples)\n- [API](#api)\n- [License](#api)\n- [See Also](#api)\n\n## Usage\n\nUse `sh` to synchronously run shell commands:\n\n```javascript\nconst sh = require(\"shellsync\");\nconst filename = \"file name with spaces.txt\";\nsh`cd /tmp`;\nsh`cat ${filename}`; // read filename\\ with\\ spaces.txt\n```\n\nNote how the above uses ES6 [tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals),\ncalling the `sh` function without parentheses. This makes the invocations slightly shorter and allows shellsync to safely escape any values passed to it.\n\nUse `sh`, `sh.array`, or `sh.json` to capture values:\n\n```javascript\nlet v1 = sh`echo hello`;                 // set v1 to \"hello\"\nlet v2 = sh.array`lsof -t -i :8080`;     // set v2 to all process ids listening on port 8080\nlet v3 = sh.json`echo '{\"foo\": \"bar\"}'`; // set v3 to {\"foo\": \"bar\"}\n```\n\nUse `sh.test` to determine command success (by default, failure throws):\n\n```javascript\nif (!sh.test`which node`) {\n    throw new Error(\"Node is not on the path!\");\n}\n```\n\nThe commands above only output what is written to stderr. Use `sh.out` to also print stdout, or use `shh` completely mute stdout and stderr:\n\n```javascript\nconst {shh} = require(\"shellsync\");\nshh`git init`;             // git init (no output printed)\nsh.out`echo \"SHOUTING!\"`;  // print \"SHOUTING!\" to stdout\n```\n\n### Safe Variable Escaping\n\n\u003e _\"The vast majority of [shell scripting] pitfalls are in some way related to unquoted expansions\"_ – [Bash Pitfalls wiki](https://mywiki.wooledge.org/BashPitfalls)\n\nshellsync safely quotes variables automatically:\n\n```javascript\nlet filename = \"filename with spaces.txt\";\nsh`echo \"hello\" \u003e ${filename}`; // write to filename\\ with\\ spaces.txt\n```\n\nUse `unquoted()` to disable automatic quoting:\n\n```javascript\nimport {unquoted} from \"shellsync\";\nlet command2 = \"sudo apt-get install foo\";\nsh`ls; ${unquoted(command2)}`; // ls; sudo apt-get install foo\n```\n\nIf you write your scripts using TypeScript with [`strictNullChecks`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html), undefined variables in shellsync invocations are reported as an error.\n\n### Writing Tests\n\n\u003e _\"I find that writing unit tests actually increases my programming speed.\"_ – Martin Fowler\n\nTest your shellsync scripts using mocking and standard testing frameworks such as Mocha or Jest.\n\nShell scripts often have many side effects, so it's a good habit to mock out commands\nthat touch the file system, interact with processes, and so on.\n\nUse `sh.mock(pattern, command)` to mock shell command using [glob patterns](https://mywiki.wooledge.org/glob).\nFor example, use the pattern `git log` to mock any calls to `git log`, or use `git *` to mock all calls to\n`git` accross the board. If you have multiple mocks, the longest (most specific) matching pattern wins:\n\n```javascript\n// Script under test\nfunction script() {\n    return sh`git status`;\n}\n\n// Mocha tests\nit(\"mocks git status\", () =\u003e {\n    let mock = sh.mock(\"git status\", `echo mock for git status`); // instead of 'git status', run 'echo ...'\n    assert.equal(script(), \"mock for git status\");\n    assert(mock.called);\n});\n\nit(\"mocks arbitrary git command\", () =\u003e {\n    let mock = sh.mock(\"git *\", `echo git command called: $1`);\n    assert.equal(script(), \"git command called: status\");\n    assert(mock.called);\n});\n```\n\nIt's a good habit to mock out all shell commands that have side effects.\nUse `sh.mockAllCommands()` to ensure a mock exists _all_ shell commands.\nYou can then selectively add mocks or use `sh.unmock(pattern)` to unmock command:\n\n```javascript\n// Script under test\nfunction script() {\n    return sh`git status`;\n}\n\n// Before each Mocha test, mock the world \nbeforeEach(() =\u003e sh.mockAllCommands());\n\n// Mocha tests\nit(\"fails when no mocks are defined\", () =\u003e {\n    program(); // FAILS: no mock was defined for \"git status\"\n});\n\nit(\"runs with git status mocked\", () =\u003e {\n    sh.mock(\"git status\");\n    program(); // passes, returns \"\"\n});\n\nit(\"runs with all git commands mocked\", () =\u003e {\n    sh.unmock(\"git *\");\n    program(); // passes, returns response of git status\n});\n```\n\nFinally, `sh.unmockAllCommands()` restores all mocked commands to the original shell command.\n\n```javascript\n// After each Mocha test, restore all mocked commands\nafterEach(() =\u003e sh.unmockAllCommands());\n```\n\nUnder the hood, shellsync implements mocking by defining shell functions for mocked commands\n(e.g., `git() { ... }`) and using a [`DEBUG` trap](https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html#index-trap)\nto intercept unmocked commands for `sh.mockAllCommands()`. \n\n### Debugging\n\nUse `sh.options.debug` to trace all commands executed by your scripts or your mocks:\n\n```javascript\nsh.options.debug = true;\nsh.mock(\"ls *\", \"echo ls was mocked\");\nsh`cd /`;\nsh`ls -l`;\n\n// Prints:\n// + cd /\n// + ls -l\n// + : mock for ls :\n// + echo ls was mocked\n// ls was mocked\n```\n\n### Uninterruptable Sections\n\n\u003e _\"Please do not interrupt me while I'm ignoring you\"_ – unknown author\n\nUsers can press Control-C in CLI programs, which means they can end scripts\nhalfway _any statement_. That means they can leave a system\nin an undefined state. In Node.js, Control-C even ends a program ignoring any `finally`\nclauses that might be used for cleanup.\n\nUse `sh.handleSignals()` for sections of code where these signals should be temporarily ignored:\n\n```javascript\nsh.handleSignals(); // begin critical section\n\nsh`command1`;\nsh`command2`;\nsh`command3`;\nsh`command4`;\n\nsh.handleSignalsEnd(); // end critical section\n```\n\nNote that `sh.handleSignals()` affects both shell and Node.js code. If you're [concerned your program won't end](https://en.wikipedia.org/wiki/Termination_analysis) until the [heat death of the universe](https://en.wikipedia.org/wiki/Heat_death_of_the_universe) and need to offer Control-C as an early way out, you can also pass a timeout in milliseconds: `sh.handleSignals({timeout: 3000})`.\n\n## Examples\n\nSee [`/examples`](https://github.com/lennartcl/shellsync/tree/master/examples/).\n\n## API\n\n### sh\\`command\\`: void\n\nExecute a command, return stdout.\n\n### sh.test\\`command\\`: boolean\n\nExecute a command, return true in case of success.\n\n### sh.array\\`command\\`: string[]\n\nExecute a command, return stdout split by null characters (if found) or by newline characters.\nUse `sh.options.fieldSeperator` to pick a custom delimiter character.\n\n### sh.json\\`command\\`: any\n\nExecute a command, parse the result as JSON.\n\n### sh.handleSignals({timeout = null}): void\n\nDisable processing of SIGINT/TERM/QUIT signals. Optionally accepts a `timeout` in milliseconds, or `null` for no timeout.\n\nWhen invoked, any signals pending since the last invocation get processed.\n\n### sh.handleSignalsEnd(): void\n\nRe-enable processing of SIGINT/TERM/QUIT signals.\n\nWhen invoked, any signals pending since the last invocation get processed.\n\n### sh.echo`output`\n\nPrint `output` to stdout.\n\n### sh.mock(pattern, [command]): Mock\n\nDefine a mock: instead of `pattern`, run `command`.\nPatterns consist of one or more words and support globbing from the second word, e.g.\n`git`, `git status`, `git s*`. The longest (most specific) pattern is used in case multiple\nmocks are defined.\n\n### sh.mockAllCommands(): void\n\nForce mocks for all shell commands, throwing an error when an unmocked command is used.\nDoes not handle commands in subshells or shell functions.\n\n### sh.unmock(pattern: string): void\n\nRemove a specific mock by pattern. Best used with `mockAllCommands()`.\n\n### sh.unmockAllCommands(): void\n\nRemove all mocks. When `pattern` is specified, remove a single mock.\n\n### sh.quote\\`command\\`: string\n\nSimilar to `sh`, but return the command that would be executed.\n\n### sh.unquoted(...args): UnquotedPart\n\nCreate an unquoted part of a `command` template.\n\n### sh.options: SpawnSyncOptions\n\nSee [the options for child_process](https://nodejs.org/api/child_process.html#child_process_child_process_spawnsync_command_args_options).\n\n### sh.options.debug: boolean\n\nRun in debug mode, printing commands that are executed.\n\n### sh.options.fieldSeperator: string\n\nThe delimiter used for `sh.array`.\n\n### sh.options.preferLocal: boolean\n\nWhether to prefer executables installed in node_modules (using [npm-run-path](https://www.npmjs.com/package/npm-run-path)). Default `true`.\n\n### sh(options): Shell\n\nReturn a shell with specific options assigned. See `sh.options`. Example use:\n\n```javascript\nconst input = \"some text to write to a file\";\nsh({input})`cat \u003e file.txt`;\n```\n\n### shh\\`command\\`: string\n\nSame as `sh`; doesn't print anything to stdout or stderr.\n\n### Mock\n\nA mock object.\n\n#### Mock.called: number\n\nIndicates how often this mock was called.\n\n## License\n\nMIT.\n\n## See Also\n\n* [shell-tag](https://www.npmjs.com/package/shell-tag) - Run shell commands with template strings\n* [shell-escape-tag](https://www.npmjs.com/package/shell-escape-tag) - Run shell commands with template strings and control over escaping\n* [any-shell-escape](https://www.npmjs.com/package/any-shell-escape) - Escape shell commands\n* [shelljs](https://www.npmjs.com/package/shelljs) - Portable implementation of Unix shell commands such as `echo` and `grep`\n* [shunit2](https://github.com/kward/shunit2) – unit testing for Bash\n* [bats](https://github.com/sstephenson/bats) – Bash automated testing system\n* [Wooledge Bash pitfalls](https://mywiki.wooledge.org/BashPitfalls) - Bash Pitfalls wiki page\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flennartcl%2Fshellsync","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flennartcl%2Fshellsync","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flennartcl%2Fshellsync/lists"}