diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..9569d51 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,30 @@ +{ + "root": true, + + "extends": "@ljharb", + + "rules": { + "array-bracket-newline": 0, + "complexity": 0, + "eqeqeq": 1, + "func-style": [2, "declaration"], + "max-depth": 0, + "max-lines-per-function": 0, + "max-statements": 0, + "multiline-comment-style": 0, + "no-negated-condition": 1, + "no-param-reassign": 1, + "no-lonely-if": 1, + "no-shadow": 1, + "no-template-curly-in-string": 0, + }, + + "overrides": [ + { + "files": "example/**", + "rules": { + "no-console": 0, + }, + }, + ], +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..0b6b6b4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [ljharb] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: npm/shell-quote +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/node-aught.yml b/.github/workflows/node-aught.yml new file mode 100644 index 0000000..624596f --- /dev/null +++ b/.github/workflows/node-aught.yml @@ -0,0 +1,21 @@ +name: 'Tests: node.js < 10' + +on: [pull_request, push] + +permissions: + contents: read + +jobs: + tests: + uses: ljharb/actions/.github/workflows/node.yml@main + with: + range: '< 10' + type: minors + command: npm run tests-only + + node: + name: 'node < 10' + needs: [tests] + runs-on: ubuntu-latest + steps: + - run: 'echo tests completed' diff --git a/.github/workflows/node-pretest.yml b/.github/workflows/node-pretest.yml new file mode 100644 index 0000000..88d49f9 --- /dev/null +++ b/.github/workflows/node-pretest.yml @@ -0,0 +1,10 @@ +name: 'Tests: pretest/posttest' + +on: [pull_request, push] + +permissions: + contents: read + +jobs: + tests: + uses: ljharb/actions/.github/workflows/pretest.yml@main diff --git a/.github/workflows/node-tens.yml b/.github/workflows/node-tens.yml new file mode 100644 index 0000000..ce8d325 --- /dev/null +++ b/.github/workflows/node-tens.yml @@ -0,0 +1,21 @@ +name: 'Tests: node.js >= 10' + +on: [pull_request, push] + +permissions: + contents: read + +jobs: + tests: + uses: ljharb/actions/.github/workflows/node.yml@main + with: + range: '>= 10' + type: minors + command: npm run tests-only + + node: + name: 'node >= 10' + needs: [tests] + runs-on: ubuntu-latest + steps: + - run: 'echo tests completed' diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 0000000..f485047 --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,108 @@ +name: Node CI + +on: [push, pull_request] + +jobs: + matrix: + runs-on: ubuntu-latest + outputs: + latest: ${{ steps.set-matrix.outputs.requireds }} + nonlatest: ${{ steps.set-matrix.outputs.optionals }} + steps: + - uses: ljharb/actions/node/matrix@main + id: set-matrix + with: + versionsAsRoot: true + type: majors + preset: ">= 0.8" + + latest: + needs: [matrix] + name: 'latest majors' + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest] + node-version: ${{ fromJson(needs.matrix.outputs.latest) }} + exclude: + - os: windows-latest + node-version: '3' + - os: windows-latest + node-version: '2' + - os: windows-latest + node-version: '1' + - os: windows-latest + node-version: '0.8' + + runs-on: ${{matrix.os}} + + steps: + - uses: actions/checkout@v3 + + - uses: ljharb/actions/node/install@main + name: 'nvm install ${{ matrix.node-version }} && npm install' + with: + node-version: ${{ matrix.node-version }} + skip-ls-check: true + if: matrix.os != 'windows-latest' + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + if: matrix.os == 'windows-latest' + + - run: npm install + env: + NPM_CONFIG_STRICT_SSL: false + if: matrix.os == 'windows-latest' + + - run: ./node_modules/.bin/tape 'test/**/*.js' + - uses: codecov/codecov-action@v3 + + nonlatest: + needs: [matrix, latest] + name: 'non-latest majors' + continue-on-error: true + if: ${{ needs.matrix.outputs.nonlatest != '[]' && (!github.head_ref || !startsWith(github.head_ref, 'renovate')) }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-latest] + node-version: ${{ fromJson(needs.matrix.outputs.nonlatest) }} + exclude: + - os: windows-latest + node-version: '0.8' + + runs-on: ${{matrix.os}} + + steps: + - uses: actions/checkout@v3 + + - uses: ljharb/actions/node/install@main + name: 'nvm install ${{ matrix.node-version }} && npm install' + with: + node-version: ${{ matrix.node-version }} + skip-ls-check: true + if: matrix.os != 'windows-latest' + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + if: matrix.os == 'windows-latest' + + - run: npm install + env: + NPM_CONFIG_STRICT_SSL: false + if: matrix.os == 'windows-latest' + + - run: ./node_modules/.bin/tape 'test/**/*.js' + - uses: codecov/codecov-action@v3 + + node: + name: 'node majors, windows/mac' + needs: [latest, nonlatest] + runs-on: ubuntu-latest + steps: + - run: 'echo tests completed' diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml new file mode 100644 index 0000000..1818191 --- /dev/null +++ b/.github/workflows/rebase.yml @@ -0,0 +1,22 @@ +name: Automatic Rebase + +on: [pull_request_target] + +permissions: + contents: read + +jobs: + _: + permissions: + contents: write # for ljharb/rebase to push code to rebase + pull-requests: read # for ljharb/rebase to get info about PR + + name: "Automatic Rebase" + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: ljharb/rebase@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/require-allow-edits.yml b/.github/workflows/require-allow-edits.yml new file mode 100644 index 0000000..a685b8a --- /dev/null +++ b/.github/workflows/require-allow-edits.yml @@ -0,0 +1,18 @@ +name: Require “Allow Edits” + +on: [pull_request_target] + +permissions: + contents: read + +jobs: + _: + permissions: + pull-requests: read # for ljharb/require-allow-edits to check 'allow edits' on PR + + name: "Require “Allow Edits”" + + runs-on: ubuntu-latest + + steps: + - uses: ljharb/require-allow-edits@main diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cfeaf4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# gitignore + +node_modules + +coverage/ +.nyc_output/ + +# Only apps should have lockfiles +npm-shrinkwrap.json +package-lock.json +yarn.lock + +.npmignore diff --git a/.npmrc b/.npmrc index 43c97e7..eacea13 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,3 @@ package-lock=false +allow-same-version=true +message=v%s diff --git a/.nycrc b/.nycrc new file mode 100644 index 0000000..55c3d29 --- /dev/null +++ b/.nycrc @@ -0,0 +1,14 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "text", "html", "json"], + "lines": 86, + "statements": 85.93, + "functions": 82.43, + "branches": 76.06, + "exclude": [ + "coverage", + "example", + "test" + ] +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 72a6882..0000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: node_js -os: - - linux - - osx - - windows -node_js: - - "0.8" - - "0.10" - - "0.12" - - "iojs" - - "4" - - "5" - - "6" - - "7" - - "8" - - "9" - - "10" - - "11" - - "12" -matrix: - exclude: - - os: windows - node_js: "0.8" - - os: windows - node_js: "iojs" -before_install: - - 'if [ $TRAVIS_NODE_VERSION == 0.8 ]; then nvm install-latest-npm; fi' diff --git a/CHANGELOG.md b/CHANGELOG.md index b045252..324c195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,279 @@ -# acorn-node change log +# Changelog All notable changes to this project will be documented in this file. -This project adheres to [Semantic Versioning](http://semver.org/). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v1.8.1](https://github.com/ljharb/shell-quote/compare/v1.8.0...v1.8.1) - 2023-04-07 + +### Fixed + +- [Fix] `parse`: preserve whitespace in comments [`#6`](https://github.com/ljharb/shell-quote/issues/6) +- [Fix] properly support the `escape` option [`#5`](https://github.com/ljharb/shell-quote/issues/5) + +### Commits + +- [Refactor] `parse`: hoist `getVar` to module level [`b42ac73`](https://github.com/ljharb/shell-quote/commit/b42ac73e39e566cfc355a4addc4bd2df2652556c) +- [Refactor] hoist some vars to module level [`8f0c5c3`](https://github.com/ljharb/shell-quote/commit/8f0c5c3c9df3a10e32f1972636675af6fffef998) +- [Refactor] `parse`: use `slice` over `substr`, cache some values [`fcb2e1a`](https://github.com/ljharb/shell-quote/commit/fcb2e1acd5312a1a1a4e6c66ec688aab383023b5) +- [Refactor] `parse`: a bit of cleanup [`6780ec5`](https://github.com/ljharb/shell-quote/commit/6780ec5194e36e2a696bfbaaf85169682a333321) +- [Refactor] `parse`: tweak the regex to not match nothing [`227d474`](https://github.com/ljharb/shell-quote/commit/227d4742a006e81ec3fde1eee103731a6f7ea920) +- [Tests] increase coverage [`a66de94`](https://github.com/ljharb/shell-quote/commit/a66de943555e49fbb1b657cbe3c5b2c703ae507d) +- [Refactor] `parse`: avoid shadowing a function arg [`1d58679`](https://github.com/ljharb/shell-quote/commit/1d5867907ecbf553556fe6ad790b6d6658aedba3) + +## [v1.8.0](https://github.com/ljharb/shell-quote/compare/v1.7.4...v1.8.0) - 2023-01-30 + +### Commits + +- [New] extract `parse` and `quote` to their own deep imports [`553fdfc`](https://github.com/ljharb/shell-quote/commit/553fdfc32cc41b4c2f77e061b6957703958ca575) +- [Tests] add `nyc` coverage [`fd7ddcd`](https://github.com/ljharb/shell-quote/commit/fd7ddcdd84bfef064c6d9a06b055a95531b26897) +- [New] Add support for here strings (`<<<`) [`9802fb3`](https://github.com/ljharb/shell-quote/commit/9802fb37c7946e18c672b81122520dc296bde271) +- [New] `parse`: Add syntax support for duplicating input file descriptors [`216b198`](https://github.com/ljharb/shell-quote/commit/216b19894f76b14d164c4c5a68f05a51b06336c4) +- [Dev Deps] update `@ljharb/eslint-config`, `aud`, `tape` [`85f8e31`](https://github.com/ljharb/shell-quote/commit/85f8e31dd80e1dde63d58204b653e497a53857e6) +- [Tests] add `evalmd` [`c5549fc`](https://github.com/ljharb/shell-quote/commit/c5549fcd82d70046bdc2b1c34184ae9f9d0191f9) +- [actions] update checkout action [`62e9b49`](https://github.com/ljharb/shell-quote/commit/62e9b4958cfa2f9009b7069076612fe33528c1fb) + +## [v1.7.4](https://github.com/ljharb/shell-quote/compare/1.7.3...v1.7.4) - 2022-10-12 + +### Merged + +- Add node_modules to .gitignore [`#48`](https://github.com/ljharb/shell-quote/pull/48) + +### Commits + +- [eslint] fix indentation and whitespace [`aaa9d1f`](https://github.com/ljharb/shell-quote/commit/aaa9d1f65bf3445e6af1efaa4a8f8c13a21aa593) +- [eslint] additional cleanup [`397cb62`](https://github.com/ljharb/shell-quote/commit/397cb628f3d96e4e47763147c0d6074997a13880) +- [meta] add `auto-changelog` [`497fca5`](https://github.com/ljharb/shell-quote/commit/497fca509af3b7d6daaba459bad1f45ac0af3ff1) +- [actions] add reusable workflows [`4763c36`](https://github.com/ljharb/shell-quote/commit/4763c36274c5881a2d141ce9f2b17b7d1d95e8cd) +- [eslint] add eslint [`6ee1437`](https://github.com/ljharb/shell-quote/commit/6ee1437df1b10a79bdf2aaa04f2bacc9f420dc15) +- [readme] rename, add badges [`7eb5134`](https://github.com/ljharb/shell-quote/commit/7eb513483d931602452ec572ed456714148acd2b) +- [meta] update URLs [`67381b6`](https://github.com/ljharb/shell-quote/commit/67381b61fa95e57819333463f491428747893186) +- [meta] create FUNDING.yml; add `funding` in package.json [`8641572`](https://github.com/ljharb/shell-quote/commit/86415722d875578adf1f95f9e649ba42c805bc32) +- [meta] use `npmignore` to autogenerate an npmignore file [`2e2007a`](https://github.com/ljharb/shell-quote/commit/2e2007a393f90bf079fc556a921120b3508c4fc3) +- Only apps should have lockfiles [`f97411e`](https://github.com/ljharb/shell-quote/commit/f97411ef4d2f183200fc8a28beca9faf9b08a640) +- [Dev Deps] update `tape` [`051f608`](https://github.com/ljharb/shell-quote/commit/051f60857ad5035280208abdc348bf5ba42a6254) +- [meta] add `safe-publish-latest` [`18cadf9`](https://github.com/ljharb/shell-quote/commit/18cadf95357392fcd78ea8619956fd41eed62649) +- [Tests] add `aud` in `posttest` [`dc1cc12`](https://github.com/ljharb/shell-quote/commit/dc1cc12b956ccd93d58aaaad263bee7d50576d27) + + + +## 1.7.3 +* Fix a security issue where the regex for windows drive letters allowed some shell meta-characters +to escape the quoting rules. (CVE-2021-42740) ## 1.7.2 -* Fix a regression introduced in 1.6.3. This reverts the Windows path quoting fix. ([144e1c2](https://github.com/substack/node-shell-quote/commit/144e1c20cd57549a414c827fb3032e60b7b8721c)) +* Fix a regression introduced in 1.6.3. This reverts the Windows path quoting fix. ([144e1c2](https://github.com/ljharb/shell-quote/commit/144e1c20cd57549a414c827fb3032e60b7b8721c)) ## 1.7.1 -* Fix `$` being removed when not part of an environment variable name. ([@Adman](https://github.com/Admin) in [#32](https://github.com/substack/node-shell-quote/pull/32)) +* Fix `$` being removed when not part of an environment variable name. ([@Adman](https://github.com/Admin) in [#32](https://github.com/ljharb/shell-quote/pull/32)) ## 1.7.0 -* Add support for parsing `>>` and `>&` redirection operators. ([@forivall](https://github.com/forivall) in [#16](https://github.com/substack/node-shell-quote/pull/16)) -* Add support for parsing `<(` process substitution operator. ([@cuonglm](https://github.com/cuonglm) in [#15](https://github.com/substack/node-shell-quote/pull/15)) +* Add support for parsing `>>` and `>&` redirection operators. ([@forivall](https://github.com/forivall) in [#16](https://github.com/ljharb/shell-quote/pull/16)) +* Add support for parsing `<(` process substitution operator. ([@cuonglm](https://github.com/cuonglm) in [#15](https://github.com/ljharb/shell-quote/pull/15)) ## 1.6.3 -* Fix Windows path quoting problems. ([@dy](https://github.com/dy) in [#34](https://github.com/substack/node-shell-quote/pull/34)) +* Fix Windows path quoting problems. ([@dy](https://github.com/dy) in [#34](https://github.com/ljharb/shell-quote/pull/34)) + +## [v1.6.2](https://github.com/ljharb/shell-quote/compare/1.6.1...v1.6.2) - 2019-08-13 + +### Merged + +- Use native JSON and Array methods [`#21`](https://github.com/ljharb/shell-quote/pull/21) + +### Commits + +- fix whitespace [`72fb5a8`](https://github.com/ljharb/shell-quote/commit/72fb5a8ce29b4f67f28302af33c217b58f92e260) +- Disable package-lock.json [`d450577`](https://github.com/ljharb/shell-quote/commit/d4505770b2a4251af2da8e177385c5e0456a83b6) + +## [1.6.1](https://github.com/ljharb/shell-quote/compare/1.6.0...1.6.1) - 2016-06-17 + +### Commits + +- Fix some more escaping for .quote() [`ace52f4`](https://github.com/ljharb/shell-quote/commit/ace52f4c8717b370b301a3db3a4727db26e309ad) +- Fix escaping for greater than and less than [`70e9eb2`](https://github.com/ljharb/shell-quote/commit/70e9eb2a854eb56a3dfa255be12610a722bbe080) + +## [1.6.0](https://github.com/ljharb/shell-quote/compare/1.5.0...1.6.0) - 2016-04-23 + +### Commits + +- add comment parsing feature [`b8b5c31`](https://github.com/ljharb/shell-quote/commit/b8b5c31c16a15aa4ab26c8f23d362a24b9fa57c4) + +## [1.5.0](https://github.com/ljharb/shell-quote/compare/1.4.3...1.5.0) - 2016-03-16 + +### Commits + +- add escape option to .parse [`4d400e7`](https://github.com/ljharb/shell-quote/commit/4d400e773be448c320b6dc9b2eb1323d7a3461ca) + +## [1.4.3](https://github.com/ljharb/shell-quote/compare/1.4.2...1.4.3) - 2015-03-07 + +### Commits + +- Fix quote() with special chars [`811b5a0`](https://github.com/ljharb/shell-quote/commit/811b5a0aff79f347db245edcf88750977c111844) + +## [1.4.2](https://github.com/ljharb/shell-quote/compare/1.4.1...1.4.2) - 2014-07-20 + +### Commits + +- Handle non-strings when quoting [`d435827`](https://github.com/ljharb/shell-quote/commit/d43582741c5599807249c28722487aa86bb16f06) +- falseys ok [`22dbd94`](https://github.com/ljharb/shell-quote/commit/22dbd9492c372038d439d6ec08c6288ca5fa3c10) +- all the falseys test [`c99dca5`](https://github.com/ljharb/shell-quote/commit/c99dca59dca64743877a0411d299ce669f0a2d1d) + +## [1.4.1](https://github.com/ljharb/shell-quote/compare/1.4.0...1.4.1) - 2013-12-24 + +### Commits + +- es5 shims [`00dc6ab`](https://github.com/ljharb/shell-quote/commit/00dc6abfdd2f3ff2908616dbe7b6584bbf1b0e24) +- separate shim file to get the coverage up [`e29a216`](https://github.com/ljharb/shell-quote/commit/e29a2167319913af3f26603bc33938bb9be1d74d) +- use array-{filter,map,reduce} [`97a2fc9`](https://github.com/ljharb/shell-quote/commit/97a2fc9c92917343a33662b3705860e4f2044730) +- add testling badge [`44c98b1`](https://github.com/ljharb/shell-quote/commit/44c98b1e341d348ce9b5b4d78bb4d26345e868ea) +- upgrade tape [`3fc22d3`](https://github.com/ljharb/shell-quote/commit/3fc22d3d38592e6fc3b3308cc73a282d641bad34) + +## [1.4.0](https://github.com/ljharb/shell-quote/compare/1.3.3...1.4.0) - 2013-10-17 + +### Merged + +- Add MIT LICENSE file [`#6`](https://github.com/ljharb/shell-quote/pull/6) + +### Commits + +- Rewrite parser as a character based scanner [`c7ca9a2`](https://github.com/ljharb/shell-quote/commit/c7ca9a200350c02fb86c4222b63f15b54a0a0226) +- Add tests for glob patterns [`3418892`](https://github.com/ljharb/shell-quote/commit/3418892031b126197302eb57cc92a729b740fac6) +- Update algo description [`e1442cf`](https://github.com/ljharb/shell-quote/commit/e1442cfe0521497b59a8204eb1e4d6c4202d42b9) +- Fix test case for backslash in double quotes [`89bc550`](https://github.com/ljharb/shell-quote/commit/89bc5500711643e87fe93dd1bde0e8745c34d733) +- Add failing tests for crazy quoting tricks [`58a5e48`](https://github.com/ljharb/shell-quote/commit/58a5e4800a62fdc3e980feae1e6c6b15c812f0cb) + +## [1.3.3](https://github.com/ljharb/shell-quote/compare/1.3.2...1.3.3) - 2013-06-24 + +### Commits + +- failing set test with an env cb [`9fb2096`](https://github.com/ljharb/shell-quote/commit/9fb20968b407c590745a982d2a562960e952142d) +- remove the broken special case [`f9a0ee5`](https://github.com/ljharb/shell-quote/commit/f9a0ee574f9d5e5d5b382f55da960c23eb7d44c5) + +## [1.3.2](https://github.com/ljharb/shell-quote/compare/1.3.1...1.3.2) - 2013-06-24 + +### Commits + +- tests for setting env vars [`f44b039`](https://github.com/ljharb/shell-quote/commit/f44b03906c60598470676edcabea79c4f7488407) +- fixed the parse test, broke the op tests [`74d6686`](https://github.com/ljharb/shell-quote/commit/74d66863615a60fcb222d2279991cff3a89ff015) +- factored out single and double quote regex [`de9e0a5`](https://github.com/ljharb/shell-quote/commit/de9e0a5081156e5483b5df24878c7414f90ec67e) +- updated set env test, already passes [`7d5636b`](https://github.com/ljharb/shell-quote/commit/7d5636bec5e76ff542712e07643b09c597d20b21) +- ops fixed [`2b4e1b1`](https://github.com/ljharb/shell-quote/commit/2b4e1b1fb63519456c7850d365e1ffe5fa5972b2) +- passing all tests [`44177e3`](https://github.com/ljharb/shell-quote/commit/44177e3dcbd96dfa331483eba0abbfb0291c130f) +- backreferences in negated capture groups don't actually work [`e189d9d`](https://github.com/ljharb/shell-quote/commit/e189d9d5910c6ecc7d564309ca9e110062f9589e) +- another crazy ridiculous passing parse test [`d1beb6b`](https://github.com/ljharb/shell-quote/commit/d1beb6b32ec7ad8752b305834a21c800cae74a95) +- failing test for quoted whitespace and nested quotes [`9a4c11c`](https://github.com/ljharb/shell-quote/commit/9a4c11cba0f61762aaa7887591d78fe7e965cf65) +- failing test for quotes embedded inside barewords [`d997384`](https://github.com/ljharb/shell-quote/commit/d997384018ce107ab8e12aa5b8d8359c2f77128b) + +## [1.3.1](https://github.com/ljharb/shell-quote/compare/1.3.0...1.3.1) - 2013-05-13 + +### Commits + +- pass objects through [`f9c0514`](https://github.com/ljharb/shell-quote/commit/f9c0514abbdf8ba16fafb68736863d14b39015ef) + +## [1.3.0](https://github.com/ljharb/shell-quote/compare/1.2.0...1.3.0) - 2013-05-13 + +### Commits + +- hacky tokenizer is much simpler [`7e91b18`](https://github.com/ljharb/shell-quote/commit/7e91b18d1cf3fffd6a9c5f69d785f200c0c81b66) +- nearly passing with a clunky state env parser, array issues [`d6d6416`](https://github.com/ljharb/shell-quote/commit/d6d64160f2fc8a23018410ffe84ab7f1b0c4fa02) +- test for functional env expansion [`666395f`](https://github.com/ljharb/shell-quote/commit/666395f9f195241c6077f242dc4f2851bed95f8d) +- upgrade travis versions, tape [`f6f8bd6`](https://github.com/ljharb/shell-quote/commit/f6f8bd6026375d44d40c7f2e1fead43d006be211) +- 1.3.0, document env() lookups [`041c5da`](https://github.com/ljharb/shell-quote/commit/041c5da88800b4e15f0ed023049050b11b623a23) +- first half of functional env() works [`7a0cf79`](https://github.com/ljharb/shell-quote/commit/7a0cf79987fbdcc00d8f36c6dc164d22db963d23) +- env() objects even work inside quote strings [`16139f5`](https://github.com/ljharb/shell-quote/commit/16139f52bf7a2beb7e1ca9b61b93a9ea598b0f1a) +- another check just to make sure env() works [`914a1a9`](https://github.com/ljharb/shell-quote/commit/914a1a9ec55cd76bedfed4086c35866733128036) + +## [1.2.0](https://github.com/ljharb/shell-quote/compare/1.1.0...1.2.0) - 2013-05-13 + +### Commits + +- failing test for special shell parameter env vars [`728862a`](https://github.com/ljharb/shell-quote/commit/728862a6ff246754083da5cf22322caf914ae990) +- add the special vars to the replace regex but the chunker breaks on them [`d1ff82a`](https://github.com/ljharb/shell-quote/commit/d1ff82a07c44cb53ab909b61833296f38257eabd) +- fixed the env test, everything is fine [`a45897f`](https://github.com/ljharb/shell-quote/commit/a45897f53ba184a77bc762c63777b95590a83962) + +## [1.1.0](https://github.com/ljharb/shell-quote/compare/1.0.0...1.1.0) - 2013-05-13 + +### Commits + +- quote all ops objects [`ac7be63`](https://github.com/ljharb/shell-quote/commit/ac7be63574e1da48bc6f495aee363d31863222c3) +- test for parsed ops objects in quote() [`59fb71b`](https://github.com/ljharb/shell-quote/commit/59fb71b39c53b83306d015bec62fc93667745f75) +- another test for op object quoting [`5819a31`](https://github.com/ljharb/shell-quote/commit/5819a31a19c34967dcb7bd1719250ed2aa480583) + +## [1.0.0](https://github.com/ljharb/shell-quote/compare/0.1.1...1.0.0) - 2013-05-13 + +### Commits + +- document ops, op example [`a6381e6`](https://github.com/ljharb/shell-quote/commit/a6381e612361148a8433c6ec4891aabc4649cb40) +- some more passing double-char op tests [`fbc6e5c`](https://github.com/ljharb/shell-quote/commit/fbc6e5c40858ef4ea4d69651ac8fdf6c0c780eed) +- failing test for | and & ops [`d817736`](https://github.com/ljharb/shell-quote/commit/d81773643cbc2e25576884d606165dc87e8bbfac) +- labeled regex states [`8c008b2`](https://github.com/ljharb/shell-quote/commit/8c008b223e6174d6bec098251527053e5cc1f30c) +- refactored the chunker regex into a string [`0331c7f`](https://github.com/ljharb/shell-quote/commit/0331c7f63077fda116b3c73540b71880538a4391) +- simple failing double-char op test [`e51fa90`](https://github.com/ljharb/shell-quote/commit/e51fa90f854063a408e8c1645b385c1ed42c72c6) +- failing expanded single-op tests for ; and () [`710bb24`](https://github.com/ljharb/shell-quote/commit/710bb243f23d4a55158688b71cb56b67f66ea99f) +- now passing all the single-char op tests [`e3e9ac1`](https://github.com/ljharb/shell-quote/commit/e3e9ac17ef02300bad7f4faefee5c7a993b3bc97) +- using the control ops directly from the docs [`f535987`](https://github.com/ljharb/shell-quote/commit/f53598732ba606c7bca66fd7d55d809544c452cf) +- first part of op parsing works [`e6f9199`](https://github.com/ljharb/shell-quote/commit/e6f91991fe437eae6b7e4f571843b3d48c746aeb) +- failing redirect tests [`cb94c10`](https://github.com/ljharb/shell-quote/commit/cb94c105a4e32fac2d356b956f53aff999ae88e8) +- another double-char op test just to be sure [`5cf1bf2`](https://github.com/ljharb/shell-quote/commit/5cf1bf29e3324a6cc1e40c01c4529b28ca0b47a5) +- 1.0.0 for ops [`17a40ed`](https://github.com/ljharb/shell-quote/commit/17a40edb3cd7a0f1c44be2be5ddd412c8ca2b7ca) +- adding redirect <> ops to CONTROL makes the tests pass [`48b1eb9`](https://github.com/ljharb/shell-quote/commit/48b1eb97cfa306659de66bd29615051a3644b9ce) +- double-char op test now passing [`3998b0f`](https://github.com/ljharb/shell-quote/commit/3998b0f9ecb32883f8eb3be31110a84d276ac764) +- using the meta chars directly from the docs [`b009ef6`](https://github.com/ljharb/shell-quote/commit/b009ef6d04eb1cc57d66cf3670d24e03fa0fc6bd) +- the spec says tabs are also allowed [`2adb373`](https://github.com/ljharb/shell-quote/commit/2adb37366bdfae198ce61e4658e513d3e0bc98fa) +- op test completely passing [`20a0147`](https://github.com/ljharb/shell-quote/commit/20a01475741d9fba801bbd2b0c1a5f215dc9cec4) + +## [0.1.1](https://github.com/ljharb/shell-quote/compare/0.1.0...0.1.1) - 2013-04-17 + +### Commits + +- Return empty list when parsing an empty (or whitespace-only) string [`1475717`](https://github.com/ljharb/shell-quote/commit/14757177ead209f5ae3c9d4a3020fba9f522725f) + +## [0.1.0](https://github.com/ljharb/shell-quote/compare/0.0.1...0.1.0) - 2013-04-14 + +### Commits + +- externalize the regex declaration [`37d6058`](https://github.com/ljharb/shell-quote/commit/37d60580a4a4656ff836c4a2ecdd7282705ffd27) +- modernize the readme [`24106f5`](https://github.com/ljharb/shell-quote/commit/24106f5c81ab83bddb2bf735cad60e99e1494dcf) +- factor out interpolation [`1b21b01`](https://github.com/ljharb/shell-quote/commit/1b21b018e01392d2c74e53136f8fa0ca838d7643) +- half the env tests are working with basic interpolation [`5891471`](https://github.com/ljharb/shell-quote/commit/589147176be93834bad7fcee83bd255e35c14adc) +- env parse example [`5757c42`](https://github.com/ljharb/shell-quote/commit/5757c4256a4cbaeaabd3d8cd91c7e19109329067) +- failing tests for unimplemented env interpolation [`590534a`](https://github.com/ljharb/shell-quote/commit/590534ae8eced91974a4be8f2b6bc7dcb53e3211) +- denormalize the interpolate logic to make room for special cases [`c669d2e`](https://github.com/ljharb/shell-quote/commit/c669d2e8b84e0eb0dde1798eae7c37a21dc5b7a1) +- cleaner implementation recursing on the double quote case [`adae66f`](https://github.com/ljharb/shell-quote/commit/adae66f47cf11bb7a686c98059436f496d881955) +- one test was wrong, checking for pre escapes [`42b5f83`](https://github.com/ljharb/shell-quote/commit/42b5f8355196d5f2a2c40178576a5d37167e8cb2) +- finally passing all the tests [`efa4084`](https://github.com/ljharb/shell-quote/commit/efa408481db33993fce2a1dd3c15ffac0203fe4c) +- one more test passing with quote recursion [`e9537b9`](https://github.com/ljharb/shell-quote/commit/e9537b943d89535b38c0777c210b5aa9780349b2) +- use tape everywhere [`ed0c1c6`](https://github.com/ljharb/shell-quote/commit/ed0c1c6ae383998874002ae9aa452505c266d630) +- some extra metacharacter tests just to be sure [`a6782ae`](https://github.com/ljharb/shell-quote/commit/a6782aeb931221a459f9cebc371c2311ad680992) +- minor fix to an env test [`601b340`](https://github.com/ljharb/shell-quote/commit/601b3406e7012da97771b0ed538288ddb12d9af8) +- document parse env [`cc0efba`](https://github.com/ljharb/shell-quote/commit/cc0efba0bce75aaab1ead72e81470154a71ec525) +- better parse recursion to capture the containing quotes [`8467961`](https://github.com/ljharb/shell-quote/commit/84679611fd5843777d3d94f157a7b6efe11097ca) +- now just 2 tests failing with a subtle regex reordering [`5448a02`](https://github.com/ljharb/shell-quote/commit/5448a02d356722ec8ef57db2e075e10566e2dcdb) +- pass another test by using "" as the undefined [`46e6cf4`](https://github.com/ljharb/shell-quote/commit/46e6cf4b974e1cec0601e81a0dc2820dc849f775) +- fixed a failing env test [`17d1fda`](https://github.com/ljharb/shell-quote/commit/17d1fdac759e7a94ffc6edc26af27769d30e53bb) +- actually the test was wrong, module works fine [`9d7b727`](https://github.com/ljharb/shell-quote/commit/9d7b727f2911692cffe03e7792b5defad3dd75d2) +- another test to be even more sure [`5afd47b`](https://github.com/ljharb/shell-quote/commit/5afd47ba563d4c1bb7966695e100f63da4b65915) +- failing test for: echo "foo = \"foo\"" [`8dbb280`](https://github.com/ljharb/shell-quote/commit/8dbb2803136a2f7643e7e66b2d6b95f9adfbfd41) + +## [0.0.1](https://github.com/ljharb/shell-quote/compare/0.0.0...0.0.1) - 2012-05-18 + +### Commits + +- fixed unescaped metachars and bump [`5ce339f`](https://github.com/ljharb/shell-quote/commit/5ce339feeaf971a5172fe58faa3ac5f90bdfe8b5) +- failing test for unescaped metachars [`a315125`](https://github.com/ljharb/shell-quote/commit/a315125a0742b01799c89469efd20821540694f6) +- fix for escaped spaces [`669b616`](https://github.com/ljharb/shell-quote/commit/669b61610aad3e6f47e8c043e1635dcc4b5ce375) +- failing test for escaped space [`c6ff3dc`](https://github.com/ljharb/shell-quote/commit/c6ff3dc6811816a667e55a69ec09bffa52f5ee0a) + +## 0.0.0 - 2012-05-18 + +### Commits -## 1.6.2 -* Remove dependencies in favour of native methods. ([@zertosh](https://github.com/zertosh) in [#21](https://github.com/substack/node-shell-quote/pull/21)) +- readme with examples [`6373c0f`](https://github.com/ljharb/shell-quote/commit/6373c0f56de87702a61063ffae354e2bb989de91) +- package.json [`bc27efa`](https://github.com/ljharb/shell-quote/commit/bc27efa033709ede8483bb6ec0f182dcb2b87061) +- passing the parse test [`69c0f85`](https://github.com/ljharb/shell-quote/commit/69c0f8529825d3fdc700e81d32b75378cef47994) +- crazy initial thing [`d6469c9`](https://github.com/ljharb/shell-quote/commit/d6469c95adf0172adc65c4adab04910486368ee7) +- passing quote tests [`e1d6695`](https://github.com/ljharb/shell-quote/commit/e1d669503f0f159068deb45f14ba3a4bf77e90f0) +- failing parse test [`980aa58`](https://github.com/ljharb/shell-quote/commit/980aa585937d049b152b5e7b08c1e068faaaf378) +- using travis [`1c72261`](https://github.com/ljharb/shell-quote/commit/1c72261f45002744fac3fecec3f0395924d66717) +- expand more escape sequences in parse() [`8b2224c`](https://github.com/ljharb/shell-quote/commit/8b2224c465ef70d2320985c57dc0b8ee1c0a3664) diff --git a/readme.markdown b/README.md similarity index 50% rename from readme.markdown rename to README.md index 85d9439..6b545c5 100644 --- a/readme.markdown +++ b/README.md @@ -1,4 +1,11 @@ -# shell-quote +# shell-quote [![Version Badge][npm-version-svg]][package-url] + +[![github actions][actions-image]][actions-url] +[![coverage][codecov-image]][codecov-url] +[![License][license-image]][license-url] +[![Downloads][downloads-image]][downloads-url] + +[![npm badge][npm-badge-png]][package-url] Parse and quote shell commands. @@ -7,7 +14,7 @@ Parse and quote shell commands. ## quote ``` js -var quote = require('shell-quote').quote; +var quote = require('shell-quote/quote'); var s = quote([ 'a', 'b c d', '$f', '"g"' ]); console.log(s); ``` @@ -21,7 +28,7 @@ a 'b c d' \$f '"g"' ## parse ``` js -var parse = require('shell-quote').parse; +var parse = require('shell-quote/parse'); var xs = parse('a "b c" \\$def \'it\\\'s great\''); console.dir(xs); ``` @@ -35,7 +42,7 @@ output ## parse with an environment variable ``` js -var parse = require('shell-quote').parse; +var parse = require('shell-quote/parse'); var xs = parse('beep --boop="$PWD"', { PWD: '/home/robot' }); console.dir(xs); ``` @@ -46,24 +53,24 @@ output [ 'beep', '--boop=/home/robot' ] ``` -## parse with custom escape charcter +## parse with custom escape character ``` js -var parse = require('shell-quote').parse; -var xs = parse('beep --boop="$PWD"', { PWD: '/home/robot' }, { escape: '^' }); +var parse = require('shell-quote/parse'); +var xs = parse('beep ^--boop="$PWD"', { PWD: '/home/robot' }, { escape: '^' }); console.dir(xs); ``` output ``` -[ 'beep', '--boop=/home/robot' ] +[ 'beep --boop=/home/robot' ] ``` ## parsing shell operators ``` js -var parse = require('shell-quote').parse; +var parse = require('shell-quote/parse'); var xs = parse('beep || boop > /byte'); console.dir(xs); ``` @@ -77,7 +84,7 @@ output: ## parsing shell comment ``` js -var parse = require('shell-quote').parse; +var parse = require('shell-quote/parse'); var xs = parse('beep > boop # > kaboom'); console.dir(xs); ``` @@ -91,8 +98,8 @@ output: # methods ``` js -var quote = require('shell-quote').quote; -var parse = require('shell-quote').parse; +var quote = require('shell-quote/quote'); +var parse = require('shell-quote/parse'); ``` ## quote(args) @@ -136,3 +143,19 @@ npm install shell-quote # license MIT + +[package-url]: https://npmjs.org/package/shell-quote +[npm-version-svg]: https://versionbadg.es/ljharb/shell-quote.svg +[deps-svg]: https://david-dm.org/ljharb/shell-quote.svg +[deps-url]: https://david-dm.org/ljharb/shell-quote +[dev-deps-svg]: https://david-dm.org/ljharb/shell-quote/dev-status.svg +[dev-deps-url]: https://david-dm.org/ljharb/shell-quote#info=devDependencies +[npm-badge-png]: https://nodei.co/npm/shell-quote.png?downloads=true&stars=true +[license-image]: https://img.shields.io/npm/l/shell-quote.svg +[license-url]: LICENSE +[downloads-image]: https://img.shields.io/npm/dm/shell-quote.svg +[downloads-url]: https://npm-stat.com/charts.html?package=shell-quote +[codecov-image]: https://codecov.io/gh/ljharb/shell-quote/branch/main/graphs/badge.svg +[codecov-url]: https://app.codecov.io/gh/ljharb/shell-quote/ +[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/shell-quote +[actions-url]: https://github.com/ljharb/shell-quote/actions diff --git a/example/env.js b/example/env.js index 3608a58..1493afc 100644 --- a/example/env.js +++ b/example/env.js @@ -1,3 +1,5 @@ +'use strict'; + var parse = require('../').parse; var xs = parse('beep --boop="$PWD"', { PWD: '/home/robot' }); console.dir(xs); diff --git a/example/op.js b/example/op.js index d8d9064..82ebf65 100644 --- a/example/op.js +++ b/example/op.js @@ -1,3 +1,5 @@ +'use strict'; + var parse = require('../').parse; var xs = parse('beep || boop > /byte'); console.dir(xs); diff --git a/example/parse.js b/example/parse.js index 4b3be5f..36e6758 100644 --- a/example/parse.js +++ b/example/parse.js @@ -1,3 +1,5 @@ +'use strict'; + var parse = require('../').parse; var xs = parse('a "b c" \\$def \'it\\\'s great\''); console.dir(xs); diff --git a/example/quote.js b/example/quote.js index 434bf8a..91c3d20 100644 --- a/example/quote.js +++ b/example/quote.js @@ -1,3 +1,5 @@ +'use strict'; + var quote = require('../').quote; -var s = quote([ 'a', 'b c d', '$f', '"g"' ]); +var s = quote(['a', 'b c d', '$f', '"g"']); console.log(s); diff --git a/index.js b/index.js index fac79be..28fb42d 100644 --- a/index.js +++ b/index.js @@ -1,199 +1,4 @@ -exports.quote = function (xs) { - return xs.map(function (s) { - if (s && typeof s === 'object') { - return s.op.replace(/(.)/g, '\\$1'); - } - else if (/["\s]/.test(s) && !/'/.test(s)) { - return "'" + s.replace(/(['\\])/g, '\\$1') + "'"; - } - else if (/["'\s]/.test(s)) { - return '"' + s.replace(/(["\\$`!])/g, '\\$1') + '"'; - } - else { - return String(s).replace(/([A-z]:)?([#!"$&'()*,:;<=>?@\[\\\]^`{|}])/g, '$1\\$2'); - } - }).join(' '); -}; +'use strict'; -// '<(' is process substitution operator and -// can be parsed the same as control operator -var CONTROL = '(?:' + [ - '\\|\\|', '\\&\\&', ';;', '\\|\\&', '\\<\\(', '>>', '>\\&', '[&;()|<>]' -].join('|') + ')'; -var META = '|&;()<> \\t'; -var BAREWORD = '(\\\\[\'"' + META + ']|[^\\s\'"' + META + '])+'; -var SINGLE_QUOTE = '"((\\\\"|[^"])*?)"'; -var DOUBLE_QUOTE = '\'((\\\\\'|[^\'])*?)\''; - -var TOKEN = ''; -for (var i = 0; i < 4; i++) { - TOKEN += (Math.pow(16,8)*Math.random()).toString(16); -} - -exports.parse = function (s, env, opts) { - var mapped = parse(s, env, opts); - if (typeof env !== 'function') return mapped; - return mapped.reduce(function (acc, s) { - if (typeof s === 'object') return acc.concat(s); - var xs = s.split(RegExp('(' + TOKEN + '.*?' + TOKEN + ')', 'g')); - if (xs.length === 1) return acc.concat(xs[0]); - return acc.concat(xs.filter(Boolean).map(function (x) { - if (RegExp('^' + TOKEN).test(x)) { - return JSON.parse(x.split(TOKEN)[1]); - } - else return x; - })); - }, []); -}; - -function parse (s, env, opts) { - var chunker = new RegExp([ - '(' + CONTROL + ')', // control chars - '(' + BAREWORD + '|' + SINGLE_QUOTE + '|' + DOUBLE_QUOTE + ')*' - ].join('|'), 'g'); - var match = s.match(chunker).filter(Boolean); - var commented = false; - - if (!match) return []; - if (!env) env = {}; - if (!opts) opts = {}; - return match.map(function (s, j) { - if (commented) { - return; - } - if (RegExp('^' + CONTROL + '$').test(s)) { - return { op: s }; - } - - // Hand-written scanner/parser for Bash quoting rules: - // - // 1. inside single quotes, all characters are printed literally. - // 2. inside double quotes, all characters are printed literally - // except variables prefixed by '$' and backslashes followed by - // either a double quote or another backslash. - // 3. outside of any quotes, backslashes are treated as escape - // characters and not printed (unless they are themselves escaped) - // 4. quote context can switch mid-token if there is no whitespace - // between the two quote contexts (e.g. all'one'"token" parses as - // "allonetoken") - var SQ = "'"; - var DQ = '"'; - var DS = '$'; - var BS = opts.escape || '\\'; - var quote = false; - var esc = false; - var out = ''; - var isGlob = false; - - for (var i = 0, len = s.length; i < len; i++) { - var c = s.charAt(i); - isGlob = isGlob || (!quote && (c === '*' || c === '?')); - if (esc) { - out += c; - esc = false; - } - else if (quote) { - if (c === quote) { - quote = false; - } - else if (quote == SQ) { - out += c; - } - else { // Double quote - if (c === BS) { - i += 1; - c = s.charAt(i); - if (c === DQ || c === BS || c === DS) { - out += c; - } else { - out += BS + c; - } - } - else if (c === DS) { - out += parseEnvVar(); - } - else { - out += c; - } - } - } - else if (c === DQ || c === SQ) { - quote = c; - } - else if (RegExp('^' + CONTROL + '$').test(c)) { - return { op: s }; - } - else if (RegExp('^#$').test(c)) { - commented = true; - if (out.length){ - return [out, { comment: s.slice(i+1) + match.slice(j+1).join(' ') }]; - } - return [{ comment: s.slice(i+1) + match.slice(j+1).join(' ') }]; - } - else if (c === BS) { - esc = true; - } - else if (c === DS) { - out += parseEnvVar(); - } - else out += c; - } - - if (isGlob) return {op: 'glob', pattern: out}; - - return out; - - function parseEnvVar() { - i += 1; - var varend, varname; - //debugger - if (s.charAt(i) === '{') { - i += 1; - if (s.charAt(i) === '}') { - throw new Error("Bad substitution: " + s.substr(i - 2, 3)); - } - varend = s.indexOf('}', i); - if (varend < 0) { - throw new Error("Bad substitution: " + s.substr(i)); - } - varname = s.substr(i, varend - i); - i = varend; - } - else if (/[*@#?$!_\-]/.test(s.charAt(i))) { - varname = s.charAt(i); - i += 1; - } - else { - varend = s.substr(i).match(/[^\w\d_]/); - if (!varend) { - varname = s.substr(i); - i = s.length; - } else { - varname = s.substr(i, varend.index); - i += varend.index - 1; - } - } - return getVar(null, '', varname); - } - }) - // finalize parsed aruments - .reduce(function(prev, arg){ - if (arg === undefined){ - return prev; - } - return prev.concat(arg); - },[]); - - function getVar (_, pre, key) { - var r = typeof env === 'function' ? env(key) : env[key]; - if (r === undefined && key != '') - r = ''; - else if (r === undefined) - r = '$'; - - if (typeof r === 'object') { - return pre + TOKEN + JSON.stringify(r) + TOKEN; - } - else return pre + r; - } -} +exports.quote = require('./quote'); +exports.parse = require('./parse'); diff --git a/package.json b/package.json index a8eb0b2..641127e 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,66 @@ { - "name": "shell-quote", - "description": "quote and parse shell commands", - "version": "1.7.2", - "author": { - "name": "James Halliday", - "email": "mail@substack.net", - "url": "http://substack.net" - }, - "bugs": "https://github.com/substack/node-shell-quote/issues", - "devDependencies": { - "tape": "4" - }, - "homepage": "https://github.com/substack/node-shell-quote", - "keywords": [ - "command", - "parse", - "quote", - "shell" - ], - "license": "MIT", - "main": "index.js", - "repository": { - "type": "git", - "url": "http://github.com/substack/node-shell-quote.git" - }, - "scripts": { - "test": "tape test/*.js" - } + "name": "shell-quote", + "description": "quote and parse shell commands", + "version": "1.8.1", + "author": { + "name": "James Halliday", + "email": "mail@substack.net", + "url": "http://substack.net" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "bugs": "https://github.com/ljharb/shell-quote/issues", + "devDependencies": { + "@ljharb/eslint-config": "^21.0.1", + "aud": "^2.0.2", + "auto-changelog": "^2.4.0", + "eslint": "=8.8.0", + "evalmd": "^0.0.19", + "in-publish": "^2.0.1", + "npmignore": "^0.3.0", + "nyc": "^10.3.2", + "safe-publish-latest": "^2.0.0", + "tape": "^5.6.3" + }, + "homepage": "https://github.com/ljharb/shell-quote", + "keywords": [ + "command", + "parse", + "quote", + "shell" + ], + "license": "MIT", + "main": "index.js", + "repository": { + "type": "git", + "url": "http://github.com/ljharb/shell-quote.git" + }, + "scripts": { + "prepack": "npmignore --auto --commentLines=autogenerated", + "prepublish": "not-in-publish || npm run prepublishOnly", + "prepublishOnly": "safe-publish-latest", + "prelint": "evalmd README.md", + "lint": "eslint --ext=js,mjs .", + "pretest": "npm run lint", + "tests-only": "nyc tape 'test/**/*.js'", + "test": "npm run tests-only", + "posttest": "aud --production", + "version": "auto-changelog && git add CHANGELOG.md", + "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" + }, + "auto-changelog": { + "output": "CHANGELOG.md", + "template": "keepachangelog", + "unreleased": false, + "commitLimit": false, + "backfillLimit": false, + "hideCredit": true, + "startingVersion": "1.7.4" + }, + "publishConfig": { + "ignore": [ + ".github/workflows" + ] + } } diff --git a/parse.js b/parse.js new file mode 100644 index 0000000..71d2eea --- /dev/null +++ b/parse.js @@ -0,0 +1,226 @@ +'use strict'; + +// '<(' is process substitution operator and +// can be parsed the same as control operator +var CONTROL = '(?:' + [ + '\\|\\|', + '\\&\\&', + ';;', + '\\|\\&', + '\\<\\(', + '\\<\\<\\<', + '>>', + '>\\&', + '<\\&', + '[&;()|<>]' +].join('|') + ')'; +var controlRE = new RegExp('^' + CONTROL + '$'); +var META = '|&;()<> \\t'; +var SINGLE_QUOTE = '"((\\\\"|[^"])*?)"'; +var DOUBLE_QUOTE = '\'((\\\\\'|[^\'])*?)\''; +var hash = /^#$/; + +var SQ = "'"; +var DQ = '"'; +var DS = '$'; + +var TOKEN = ''; +var mult = 0x100000000; // Math.pow(16, 8); +for (var i = 0; i < 4; i++) { + TOKEN += (mult * Math.random()).toString(16); +} +var startsWithToken = new RegExp('^' + TOKEN); + +function matchAll(s, r) { + var origIndex = r.lastIndex; + + var matches = []; + var matchObj; + + while ((matchObj = r.exec(s))) { + matches.push(matchObj); + if (r.lastIndex === matchObj.index) { + r.lastIndex += 1; + } + } + + r.lastIndex = origIndex; + + return matches; +} + +function getVar(env, pre, key) { + var r = typeof env === 'function' ? env(key) : env[key]; + if (typeof r === 'undefined' && key != '') { + r = ''; + } else if (typeof r === 'undefined') { + r = '$'; + } + + if (typeof r === 'object') { + return pre + TOKEN + JSON.stringify(r) + TOKEN; + } + return pre + r; +} + +function parseInternal(string, env, opts) { + if (!opts) { + opts = {}; + } + var BS = opts.escape || '\\'; + var BAREWORD = '(\\' + BS + '[\'"' + META + ']|[^\\s\'"' + META + '])+'; + + var chunker = new RegExp([ + '(' + CONTROL + ')', // control chars + '(' + BAREWORD + '|' + SINGLE_QUOTE + '|' + DOUBLE_QUOTE + ')+' + ].join('|'), 'g'); + + var matches = matchAll(string, chunker); + + if (matches.length === 0) { + return []; + } + if (!env) { + env = {}; + } + + var commented = false; + + return matches.map(function (match) { + var s = match[0]; + if (!s || commented) { + return void undefined; + } + if (controlRE.test(s)) { + return { op: s }; + } + + // Hand-written scanner/parser for Bash quoting rules: + // + // 1. inside single quotes, all characters are printed literally. + // 2. inside double quotes, all characters are printed literally + // except variables prefixed by '$' and backslashes followed by + // either a double quote or another backslash. + // 3. outside of any quotes, backslashes are treated as escape + // characters and not printed (unless they are themselves escaped) + // 4. quote context can switch mid-token if there is no whitespace + // between the two quote contexts (e.g. all'one'"token" parses as + // "allonetoken") + var quote = false; + var esc = false; + var out = ''; + var isGlob = false; + var i; + + function parseEnvVar() { + i += 1; + var varend; + var varname; + var char = s.charAt(i); + + if (char === '{') { + i += 1; + if (s.charAt(i) === '}') { + throw new Error('Bad substitution: ' + s.slice(i - 2, i + 1)); + } + varend = s.indexOf('}', i); + if (varend < 0) { + throw new Error('Bad substitution: ' + s.slice(i)); + } + varname = s.slice(i, varend); + i = varend; + } else if ((/[*@#?$!_-]/).test(char)) { + varname = char; + i += 1; + } else { + var slicedFromI = s.slice(i); + varend = slicedFromI.match(/[^\w\d_]/); + if (!varend) { + varname = slicedFromI; + i = s.length; + } else { + varname = slicedFromI.slice(0, varend.index); + i += varend.index - 1; + } + } + return getVar(env, '', varname); + } + + for (i = 0; i < s.length; i++) { + var c = s.charAt(i); + isGlob = isGlob || (!quote && (c === '*' || c === '?')); + if (esc) { + out += c; + esc = false; + } else if (quote) { + if (c === quote) { + quote = false; + } else if (quote == SQ) { + out += c; + } else { // Double quote + if (c === BS) { + i += 1; + c = s.charAt(i); + if (c === DQ || c === BS || c === DS) { + out += c; + } else { + out += BS + c; + } + } else if (c === DS) { + out += parseEnvVar(); + } else { + out += c; + } + } + } else if (c === DQ || c === SQ) { + quote = c; + } else if (controlRE.test(c)) { + return { op: s }; + } else if (hash.test(c)) { + commented = true; + var commentObj = { comment: string.slice(match.index + i + 1) }; + if (out.length) { + return [out, commentObj]; + } + return [commentObj]; + } else if (c === BS) { + esc = true; + } else if (c === DS) { + out += parseEnvVar(); + } else { + out += c; + } + } + + if (isGlob) { + return { op: 'glob', pattern: out }; + } + + return out; + }).reduce(function (prev, arg) { // finalize parsed arguments + // TODO: replace this whole reduce with a concat + return typeof arg === 'undefined' ? prev : prev.concat(arg); + }, []); +} + +module.exports = function parse(s, env, opts) { + var mapped = parseInternal(s, env, opts); + if (typeof env !== 'function') { + return mapped; + } + return mapped.reduce(function (acc, s) { + if (typeof s === 'object') { + return acc.concat(s); + } + var xs = s.split(RegExp('(' + TOKEN + '.*?' + TOKEN + ')', 'g')); + if (xs.length === 1) { + return acc.concat(xs[0]); + } + return acc.concat(xs.filter(Boolean).map(function (x) { + if (startsWithToken.test(x)) { + return JSON.parse(x.split(TOKEN)[1]); + } + return x; + })); + }, []); +}; diff --git a/quote.js b/quote.js new file mode 100644 index 0000000..afbccf0 --- /dev/null +++ b/quote.js @@ -0,0 +1,16 @@ +'use strict'; + +module.exports = function quote(xs) { + return xs.map(function (s) { + if (s && typeof s === 'object') { + return s.op.replace(/(.)/g, '\\$1'); + } + if ((/["\s]/).test(s) && !(/'/).test(s)) { + return "'" + s.replace(/(['\\])/g, '\\$1') + "'"; + } + if ((/["'\s]/).test(s)) { + return '"' + s.replace(/(["\\$`!])/g, '\\$1') + '"'; + } + return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, '$1\\$2'); + }).join(' '); +}; diff --git a/security.md b/security.md new file mode 100644 index 0000000..dc86413 --- /dev/null +++ b/security.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +Only the latest major version is supported at any given time. + +## Reporting a Vulnerability + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/test/comment.js b/test/comment.js index bc6fbf2..fb15d5c 100644 --- a/test/comment.js +++ b/test/comment.js @@ -1,14 +1,16 @@ +'use strict'; + var test = require('tape'); var parse = require('../').parse; test('comment', function (t) { - t.same(parse('beep#boop'), [ 'beep', { comment: 'boop' } ]); - t.same(parse('beep #boop'), [ 'beep', { comment: 'boop' } ]); - t.same(parse('beep # boop'), [ 'beep', { comment: 'boop' } ]); - t.same(parse('beep # > boop'), [ 'beep', { comment: '> boop' } ]); - t.same(parse('beep # "> boop"'), [ 'beep', { comment: '"> boop"' } ]); - t.same(parse('beep "#"'), [ 'beep', '#' ]); - t.same(parse('beep #"#"#'), [ 'beep', { comment: '"#"#' } ]); - t.same(parse('beep > boop # > foo'), [ 'beep', {op: '>'}, 'boop', { comment: '> foo' } ]); - t.end(); + t.same(parse('beep#boop'), ['beep', { comment: 'boop' }]); + t.same(parse('beep #boop'), ['beep', { comment: 'boop' }]); + t.same(parse('beep # boop'), ['beep', { comment: ' boop' }]); + t.same(parse('beep # > boop'), ['beep', { comment: ' > boop' }]); + t.same(parse('beep # "> boop"'), ['beep', { comment: ' "> boop"' }]); + t.same(parse('beep "#"'), ['beep', '#']); + t.same(parse('beep #"#"#'), ['beep', { comment: '"#"#' }]); + t.same(parse('beep > boop # > foo'), ['beep', { op: '>' }, 'boop', { comment: ' > foo' }]); + t.end(); }); diff --git a/test/env.js b/test/env.js index b3faeb0..4cc0a51 100644 --- a/test/env.js +++ b/test/env.js @@ -1,41 +1,52 @@ +'use strict'; + var test = require('tape'); var parse = require('../').parse; test('expand environment variables', function (t) { - t.same(parse('a $XYZ c', { XYZ: 'b' }), [ 'a', 'b', 'c' ]); - t.same(parse('a${XYZ}c', { XYZ: 'b' }), [ 'abc' ]); - t.same(parse('a${XYZ}c $XYZ', { XYZ: 'b' }), [ 'abc', 'b' ]); - t.same(parse('"-$X-$Y-"', { X: 'a', Y: 'b' }), [ '-a-b-' ]); - t.same(parse("'-$X-$Y-'", { X: 'a', Y: 'b' }), [ '-$X-$Y-' ]); - t.same(parse('qrs"$zzz"wxy', { zzz: 'tuv' }), [ 'qrstuvwxy' ]); - t.same(parse("qrs'$zzz'wxy", { zzz: 'tuv' }), [ 'qrs$zzzwxy' ]); - t.same(parse("qrs${zzz}wxy"), [ 'qrswxy' ]); - t.same(parse("qrs$wxy $"), [ 'qrs', '$' ]); - t.same(parse('grep "xy$"'), [ 'grep', 'xy$' ]); - t.same(parse("ab$x", { x: 'c' }), [ 'abc' ]); - t.same(parse("ab\\$x", { x: 'c' }), [ 'ab$x' ]); - t.same(parse("ab${x}def", { x: 'c' }), [ 'abcdef' ]); - t.same(parse("ab\\${x}def", { x: 'c' }), [ 'ab${x}def' ]); - t.same(parse('"ab\\${x}def"', { x: 'c' }), [ 'ab${x}def' ]); - - t.end(); + t.same(parse('a $XYZ c', { XYZ: 'b' }), ['a', 'b', 'c']); + t.same(parse('a${XYZ}c', { XYZ: 'b' }), ['abc']); + t.same(parse('a${XYZ}c $XYZ', { XYZ: 'b' }), ['abc', 'b']); + t.same(parse('"-$X-$Y-"', { X: 'a', Y: 'b' }), ['-a-b-']); + t.same(parse("'-$X-$Y-'", { X: 'a', Y: 'b' }), ['-$X-$Y-']); + t.same(parse('qrs"$zzz"wxy', { zzz: 'tuv' }), ['qrstuvwxy']); + t.same(parse("qrs'$zzz'wxy", { zzz: 'tuv' }), ['qrs$zzzwxy']); + t.same(parse('qrs${zzz}wxy'), ['qrswxy']); + t.same(parse('qrs$wxy $'), ['qrs', '$']); + t.same(parse('grep "xy$"'), ['grep', 'xy$']); + t.same(parse('ab$x', { x: 'c' }), ['abc']); + t.same(parse('ab\\$x', { x: 'c' }), ['ab$x']); + t.same(parse('ab${x}def', { x: 'c' }), ['abcdef']); + t.same(parse('ab\\${x}def', { x: 'c' }), ['ab${x}def']); + t.same(parse('"ab\\${x}def"', { x: 'c' }), ['ab${x}def']); + + t.end(); +}); + +test('expand environment variables within here-strings', function (t) { + t.same(parse('a <<< $x', { x: 'Joe' }), ['a', { op: '<<<' }, 'Joe']); + t.same(parse('a <<< ${x}', { x: 'Joe' }), ['a', { op: '<<<' }, 'Joe']); + t.same(parse('a <<< "$x"', { x: 'Joe' }), ['a', { op: '<<<' }, 'Joe']); + t.same(parse('a <<< "${x}"', { x: 'Joe' }), ['a', { op: '<<<' }, 'Joe']); + + t.end(); }); test('environment variables with metacharacters', function (t) { - t.same(parse('a $XYZ c', { XYZ: '"b"' }), [ 'a', '"b"', 'c' ]); - t.same(parse('a $XYZ c', { XYZ: '$X', X: 5 }), [ 'a', '$X', 'c' ]); - t.same(parse('a"$XYZ"c', { XYZ: "'xyz'" }), [ "a'xyz'c" ]); - - t.end(); + t.same(parse('a $XYZ c', { XYZ: '"b"' }), ['a', '"b"', 'c']); + t.same(parse('a $XYZ c', { XYZ: '$X', X: 5 }), ['a', '$X', 'c']); + t.same(parse('a"$XYZ"c', { XYZ: "'xyz'" }), ["a'xyz'c"]); + + t.end(); }); test('special shell parameters', function (t) { - var chars = '*@#?-$!0_'.split(''); - t.plan(chars.length); - - chars.forEach(function (c) { - var env = {}; - env[c] = 'xxx'; - t.same(parse('a $' + c + ' c', env), [ 'a', 'xxx', 'c' ]); - }); + var chars = '*@#?-$!0_'.split(''); + t.plan(chars.length); + + chars.forEach(function (c) { + var env = {}; + env[c] = 'xxx'; + t.same(parse('a $' + c + ' c', env), ['a', 'xxx', 'c']); + }); }); diff --git a/test/env_fn.js b/test/env_fn.js index b9f3c20..968e912 100644 --- a/test/env_fn.js +++ b/test/env_fn.js @@ -1,19 +1,21 @@ +'use strict'; + var test = require('tape'); var parse = require('../').parse; +function getEnv() { + return 'xxx'; +} + +function getEnvObj() { + return { op: '@@' }; +} + test('functional env expansion', function (t) { - t.plan(4); - - t.same(parse('a $XYZ c', getEnv), [ 'a', 'xxx', 'c' ]); - t.same(parse('a $XYZ c', getEnvObj), [ 'a', { op: '@@' }, 'c' ]); - t.same(parse('a${XYZ}c', getEnvObj), [ 'a', { op: '@@' }, 'c' ]); - t.same(parse('"a $XYZ c"', getEnvObj), [ 'a ', { op: '@@' }, ' c' ]); - - function getEnv (key) { - return 'xxx'; - } - - function getEnvObj (key) { - return { op: '@@' }; - } + t.plan(4); + + t.same(parse('a $XYZ c', getEnv), ['a', 'xxx', 'c']); + t.same(parse('a $XYZ c', getEnvObj), ['a', { op: '@@' }, 'c']); + t.same(parse('a${XYZ}c', getEnvObj), ['a', { op: '@@' }, 'c']); + t.same(parse('"a $XYZ c"', getEnvObj), ['a ', { op: '@@' }, ' c']); }); diff --git a/test/op.js b/test/op.js index 7aa9b49..38d3757 100644 --- a/test/op.js +++ b/test/op.js @@ -1,78 +1,102 @@ +'use strict'; + var test = require('tape'); var parse = require('../').parse; test('single operators', function (t) { - t.same(parse('beep | boop'), [ 'beep', { op: '|' }, 'boop' ]); - t.same(parse('beep|boop'), [ 'beep', { op: '|' }, 'boop' ]); - t.same(parse('beep \\| boop'), [ 'beep', '|', 'boop' ]); - t.same(parse('beep "|boop"'), [ 'beep', '|boop' ]); - - t.same(parse('echo zing &'), [ 'echo', 'zing', { op: '&' } ]); - t.same(parse('echo zing&'), [ 'echo', 'zing', { op: '&' } ]); - t.same(parse('echo zing\\&'), [ 'echo', 'zing&' ]); - t.same(parse('echo "zing\\&"'), [ 'echo', 'zing\\&' ]); - - t.same(parse('beep;boop'), [ 'beep', { op: ';' }, 'boop' ]); - t.same(parse('(beep;boop)'), [ - { op: '(' }, 'beep', { op: ';' }, 'boop', { op: ')' } - ]); - - t.same(parse('beep>boop'), [ 'beep', { op: '>' }, 'boop' ]); - t.same(parse('beep 2>boop'), [ 'beep', '2', { op: '>' }, 'boop' ]); - t.same(parse('beepboop'), ['beep', { op: '>' }, 'boop']); + t.same(parse('beep 2>boop'), ['beep', '2', { op: '>' }, 'boop']); + t.same(parse('beep>blip'), - [ 'beep', { op: ';;' }, 'boop', { op: '|&' }, 'byte', { op: '>>' }, 'blip' ] - ); - - t.same(parse('beep 2>&1'), [ 'beep', '2', { op: '>&' }, '1' ]); - - t.same( - parse('beep<(boop)'), - [ 'beep', { op: '<(' }, 'boop', { op: ')' } ] - ); - t.same( - parse('beep<<(boop)'), - [ 'beep', { op: '<' }, { op: '<(' }, 'boop', { op: ')' } ] - ); - - t.end(); + t.same(parse('beep || boop'), ['beep', { op: '||' }, 'boop']); + t.same(parse('beep||boop'), ['beep', { op: '||' }, 'boop']); + t.same(parse('beep ||boop'), ['beep', { op: '||' }, 'boop']); + t.same(parse('beep|| boop'), ['beep', { op: '||' }, 'boop']); + t.same(parse('beep || boop'), ['beep', { op: '||' }, 'boop']); + + t.same(parse('beep && boop'), ['beep', { op: '&&' }, 'boop']); + t.same( + parse('beep && boop || byte'), + ['beep', { op: '&&' }, 'boop', { op: '||' }, 'byte'] + ); + t.same( + parse('beep&&boop||byte'), + ['beep', { op: '&&' }, 'boop', { op: '||' }, 'byte'] + ); + t.same( + parse('beep\\&\\&boop||byte'), + ['beep&&boop', { op: '||' }, 'byte'] + ); + t.same( + parse('beep\\&&boop||byte'), + ['beep&', { op: '&' }, 'boop', { op: '||' }, 'byte'] + ); + t.same( + parse('beep;;boop|&byte>>blip'), + ['beep', { op: ';;' }, 'boop', { op: '|&' }, 'byte', { op: '>>' }, 'blip'] + ); + + t.same(parse('beep 2>&1'), ['beep', '2', { op: '>&' }, '1']); + + t.same( + parse('beep<(boop)'), + ['beep', { op: '<(' }, 'boop', { op: ')' }] + ); + t.same( + parse('beep<<(boop)'), + ['beep', { op: '<' }, { op: '<(' }, 'boop', { op: ')' }] + ); + + t.end(); +}); + +test('duplicating input file descriptors', function (t) { + // duplicating stdout to file descriptor 3 + t.same(parse('beep 3<&1'), ['beep', '3', { op: '<&' }, '1']); + + // duplicating stdout to file descriptor 0, i.e. stdin + t.same(parse('beep <&1'), ['beep', { op: '<&' }, '1']); + + // closes stdin + t.same(parse('beep <&-'), ['beep', { op: '<&' }, '-']); + + t.end(); +}); + +test('here strings', function (t) { + t.same(parse('cat <<< "hello world"'), ['cat', { op: '<<<' }, 'hello world']); + t.same(parse('cat <<< hello'), ['cat', { op: '<<<' }, 'hello']); + t.same(parse('cat<<<;{}']), '\\>\\<\\;\\{\\}'); - t.equal(quote([ 'a', 1, true, false ]), 'a 1 true false'); - t.equal(quote([ 'a', 1, null, undefined ]), 'a 1 null undefined'); - t.equal(quote([ 'a\\x' ]), 'a\\\\x'); - t.end(); + t.equal(quote(['a', 'b', 'c d']), 'a b \'c d\''); + t.equal( + quote(['a', 'b', "it's a \"neat thing\""]), + 'a b "it\'s a \\"neat thing\\""' + ); + t.equal( + quote(['$', '`', '\'']), + '\\$ \\` "\'"' + ); + t.equal(quote([]), ''); + t.equal(quote(['a\nb']), "'a\nb'"); + t.equal(quote([' #(){}*|][!']), "' #(){}*|][!'"); + t.equal(quote(["'#(){}*|][!"]), '"\'#(){}*|][\\!"'); + t.equal(quote(['X#(){}*|][!']), 'X\\#\\(\\)\\{\\}\\*\\|\\]\\[\\!'); + t.equal(quote(['a\n#\nb']), "'a\n#\nb'"); + t.equal(quote(['><;{}']), '\\>\\<\\;\\{\\}'); + t.equal(quote(['a', 1, true, false]), 'a 1 true false'); + t.equal(quote(['a', 1, null, undefined]), 'a 1 null undefined'); + t.equal(quote(['a\\x']), 'a\\\\x'); + t.end(); }); test('quote ops', function (t) { - t.equal(quote([ 'a', { op: '|' }, 'b' ]), 'a \\| b'); - t.equal( - quote([ 'a', { op: '&&' }, 'b', { op: ';' }, 'c' ]), - 'a \\&\\& b \\; c' - ); - t.end(); + t.equal(quote(['a', { op: '|' }, 'b']), 'a \\| b'); + t.equal( + quote(['a', { op: '&&' }, 'b', { op: ';' }, 'c']), + 'a \\&\\& b \\; c' + ); + t.end(); }); test('quote windows paths', { skip: 'breaking change, disabled until 2.x' }, function (t) { - var path = 'C:\\projects\\node-shell-quote\\index.js' + var path = 'C:\\projects\\node-shell-quote\\index.js'; + + t.equal(quote([path, 'b', 'c d']), 'C:\\projects\\node-shell-quote\\index.js b \'c d\''); - t.equal(quote([path, 'b', 'c d']), 'C:\\projects\\node-shell-quote\\index.js b \'c d\'') + t.end(); +}); - t.end() -}) +test("chars for windows paths don't break out", function (t) { + var x = '`:\\a\\b'; + t.equal(quote([x]), '\\`\\:\\\\a\\\\b'); + t.end(); +}); diff --git a/test/set.js b/test/set.js index ac45cf1..9694538 100644 --- a/test/set.js +++ b/test/set.js @@ -1,29 +1,31 @@ +'use strict'; + var test = require('tape'); var parse = require('../').parse; test('set env vars', function (t) { - t.same( - parse('ABC=444 x y z'), - [ 'ABC=444', 'x', 'y', 'z' ] - ); - t.same( - parse('ABC=3\\ 4\\ 5 x y z'), - [ 'ABC=3 4 5', 'x', 'y', 'z' ] - ); - t.same( - parse('X="7 8 9" printx'), - [ 'X=7 8 9', 'printx' ] - ); - t.same( - parse('X="7 8 9"; printx'), - [ 'X=7 8 9', { op: ';' }, 'printx' ] - ); - t.same( - parse('X="7 8 9"; printx', function (key) { - t.fail('should not have matched any keys'); - }), - [ 'X=7 8 9', { op: ';' }, 'printx' ] - ); - - t.end(); + t.same( + parse('ABC=444 x y z'), + ['ABC=444', 'x', 'y', 'z'] + ); + t.same( + parse('ABC=3\\ 4\\ 5 x y z'), + ['ABC=3 4 5', 'x', 'y', 'z'] + ); + t.same( + parse('X="7 8 9" printx'), + ['X=7 8 9', 'printx'] + ); + t.same( + parse('X="7 8 9"; printx'), + ['X=7 8 9', { op: ';' }, 'printx'] + ); + t.same( + parse('X="7 8 9"; printx', function () { + t.fail('should not have matched any keys'); + }), + ['X=7 8 9', { op: ';' }, 'printx'] + ); + + t.end(); });