diff --git a/.documentup.json b/.documentup.json deleted file mode 100644 index 57fe30116..000000000 --- a/.documentup.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "ShellJS", - "twitter": [ - "r2r" - ] -} diff --git a/.editorconfig b/.editorconfig index a35cd0dca..afe97de28 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,5 +10,8 @@ indent_style = space insert_final_newline = true trim_trailing_whitespace = true +[*.js] +quote_type = single + [*.{md,markdown}] trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..65dfd0f09 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +.nyc_output/ +coverage/ +make.js +node_modules/ +test/resources/ +test/tmp/ diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..4f40d77b8 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,51 @@ +{ + "env": { + "node": true + }, + "extends": "airbnb-base", + "rules": { + "comma-dangle": ["error", "always-multiline"], + "consistent-return": "off", + "curly": ["error", "multi-line"], + "func-names": "off", + "function-call-argument-newline": "off", + "function-paren-newline": "off", + "global-require": "off", + "import/no-unresolved": "off", + "indent": "off", + "max-len": "off", + "newline-per-chained-call": "off", + "no-bitwise": "off", + "no-console": "off", + "no-continue": "off", + "no-else-return": "off", + "no-empty": "off", + "no-mixed-operators": "off", + "no-multi-spaces": "off", + "no-multiple-empty-lines": ["error", { "max": 2, "maxBOF": 0, "maxEOF": 0 } ], + "no-param-reassign": "off", + "no-plusplus": "off", + "no-prototype-builtins": "off", + "no-throw-literal": "off", + "no-underscore-dangle": "off", + "no-use-before-define": "off", + "no-var": "off", + "operator-linebreak": "off", + "prefer-arrow-callback": "off", + "prefer-destructuring": "off", + "prefer-numeric-literals": "off", + "prefer-object-spread": "off", + "prefer-rest-params": "off", + "prefer-spread": "off", + "prefer-template": "off", + "quote-props": "off", + "spaced-comment": ["error", "always", { "markers": ["@", "@include"], "exceptions": ["@", "@commands"] }], + "strict": "off", + "vars-on-top": "off", + "new-cap": ["error", { + "capIsNewExceptions": [ + "ShellString" + ]} + ] + } +} diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 4416a00c4..172af387f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,8 @@ -### Node version: + +### Node version (or tell us if you're using electron or some other framework): ### ShellJS version (the most recent version/Github branch you see the bug on): diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 000000000..4c96dd513 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,31 @@ +# ShellJS Security Policy + +Thank you for reaching out regarding the security of the ShellJS module! Please +note that this project is maintained on a best-effort basis, however I still +intend to prioritize reviewing and addressing security issues. + +## Supported Versions + +I generally only support the latest ShellJS release (see +https://www.npmjs.com/package/shelljs). My goal is to release security fixes as +patch releases on top of whatever was most recently shipped. + +If breaking changes have already landed on the main development branch, I may +apply the patch on the relevant release branch (ex. +[`0.8-release`](https://github.com/shelljs/shelljs/commits/0.8-release)) and +create a new release from there. + +## Reporting a Vulnerability + +Please report security vulnerabilities to ntfschr@gmail.com. I should respond +within a few days. Although it's not strictly required, it helps me out if you +can include any proof of concept exploit code, suggested fix, etc. + +**Please do not publicly disclose the suspected vulnerability** until I have a +chance to review your report. I'd like a chance to patch the code before the +issue is known to the public. + +Please **only** use this email for security issues. It's also OK to use the +email if you're legitimately unsure if this is a security issue (better safe +than sorry). But for all other non-security issues, please use the GitHub issue +tracker. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..6c207c2bb --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,36 @@ +name: CI +on: + - push + - pull_request +jobs: + test: + name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + node-version: + - 18 + - 20 + - 22 + - 24 + - 26 + os: + - ubuntu-latest + - macos-latest + - windows-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - name: test with coverage + run: npm run test-with-coverage + - run: npm run lint + - run: npm run check-node-support + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v7 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true diff --git a/.gitignore b/.gitignore index 77f3ac742..28d0f3bf5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ npm-debug.log* node_modules tmp +coverage/ +.nyc_output/ # Linux *~ diff --git a/.jshintignore b/.jshintignore deleted file mode 100644 index b17cb96ba..000000000 --- a/.jshintignore +++ /dev/null @@ -1,2 +0,0 @@ -test/resources/ -node_modules/ diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index d84d5351c..000000000 --- a/.jshintrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "loopfunc": true, - "sub": true, - "undef": true, - "unused": true, - "futurehostile": true, - "latedef": "nofunc", - "nocomma": true, - "nonbsp": true, - "notypeof": true, - "node": true -} diff --git a/.lgtm b/.lgtm deleted file mode 100644 index 6702a0df2..000000000 --- a/.lgtm +++ /dev/null @@ -1,2 +0,0 @@ -approvals = 1 -pattern = "(?i):shipit:|:\\+1:|LGTM|\\+1" diff --git a/.npmignore b/.npmignore deleted file mode 100644 index b2379f590..000000000 --- a/.npmignore +++ /dev/null @@ -1,13 +0,0 @@ -test/ -tmp/ -.github/ -.documentup.json -.gitignore -.jshintrc -.jshintignore -.lgtm -.travis.yml -.gitattributes -appveyor.yml -RELEASE.md -CONTRIBUTING.md diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 811b09cdc..000000000 --- a/.travis.yml +++ /dev/null @@ -1,45 +0,0 @@ -language: c++ -sudo: false -env: - - NODE_VERSION="0.10" - - NODE_VERSION="0.11" - - NODE_VERSION="0.12" - - NODE_VERSION="iojs-v1" - - NODE_VERSION="iojs-v2" - - NODE_VERSION="iojs-v3" - - NODE_VERSION="4" - - NODE_VERSION="5" - - NODE_VERSION="6" - -matrix: - allow_failures: - - NODE_VERSION="0.10" - -# keep this blank to make sure there are no before_install steps -before_install: - -install: - - rm -rf ~/.nvm - - git clone https://github.com/creationix/nvm.git ~/.nvm - - source ~/.nvm/nvm.sh - - nvm install $NODE_VERSION - - node --version - - npm install -os: - - linux - - osx -script: - - npm test - # make sure when the docs are generated nothing changes (a.k.a. the docs have already been generated) - - npm run gendocs - - npm run after-travis "Make sure to generate docs!" - -# Gitter -notifications: - email: false - webhooks: - urls: - - https://webhooks.gitter.im/e/743dc6b83b6dd05111ee - on_success: change # options: [always|never|change] default: always - on_failure: always # options: [always|never|change] default: always - on_start: never # options: [always|never|change] default: always diff --git a/CHANGELOG.md b/CHANGELOG.md index d058acfd8..c30d70896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,452 @@ # Change Log -## [Unreleased](https://github.com/shelljs/shelljs/tree/HEAD) +## [v0.8.5](https://github.com/shelljs/shelljs/tree/v0.8.5) (2022-01-13) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.8.4...v0.8.5) -[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.7.0...HEAD) +This was a small security fix for [\#1058](https://github.com/shelljs/shelljs/issues/1058). + +## [v0.8.4](https://github.com/shelljs/shelljs/tree/v0.8.4) (2020-04-24) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.8.3...v0.8.4) + +Small patch release to fix a circular dependency warning in node v14. See [\#973](https://github.com/shelljs/shelljs/issues/973). + +## [v0.8.3](https://github.com/shelljs/shelljs/tree/v0.8.3) (2018-11-13) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.8.2...v0.8.3) + +**Closed issues:** + +- Shelljs print stderr to console even if exec-only "silent" is true [\#905](https://github.com/shelljs/shelljs/issues/905) +- refactor: remove common.state.tempDir [\#902](https://github.com/shelljs/shelljs/issues/902) +- Can't suppress stdout for echo [\#899](https://github.com/shelljs/shelljs/issues/899) +- exec\(\) doesn't apply the arguments correctly [\#895](https://github.com/shelljs/shelljs/issues/895) +- shell.exec\('npm pack'\) painfully slow [\#885](https://github.com/shelljs/shelljs/issues/885) +- shelljs.exec cannot find app.asar/node\_modules/shelljs/src/exec-child.js [\#881](https://github.com/shelljs/shelljs/issues/881) +- test infra: mocks and skipOnWin conflict [\#862](https://github.com/shelljs/shelljs/issues/862) +- Support for shell function completion on IDE [\#859](https://github.com/shelljs/shelljs/issues/859) +- echo command shows options in stdout [\#855](https://github.com/shelljs/shelljs/issues/855) +- silent does not always work [\#851](https://github.com/shelljs/shelljs/issues/851) +- Appveyor installs the latest npm, instead of the latest compatible npm [\#844](https://github.com/shelljs/shelljs/issues/844) +- Force symbolic link \(ln -sf\) does not overwrite/recreate existing destination [\#830](https://github.com/shelljs/shelljs/issues/830) +- inconsistent result when trying to echo to a file [\#798](https://github.com/shelljs/shelljs/issues/798) +- Prevent require\(\)ing executable-only files [\#789](https://github.com/shelljs/shelljs/issues/789) +- Cannot set property to of \[object String\] which has only a getter [\#752](https://github.com/shelljs/shelljs/issues/752) +- which\(\) should check executability before returning a value [\#657](https://github.com/shelljs/shelljs/issues/657) +- Bad encoding experience [\#456](https://github.com/shelljs/shelljs/issues/456) +- phpcs very slow [\#440](https://github.com/shelljs/shelljs/issues/440) +- Error shown when triggering a sigint during shelljs.exec if process.on sigint is defined [\#254](https://github.com/shelljs/shelljs/issues/254) +- `.to\(file\)` does not mute STDIO output [\#146](https://github.com/shelljs/shelljs/issues/146) +- Escaping shell arguments to exec\(\) [\#143](https://github.com/shelljs/shelljs/issues/143) +- Allow multiple string arguments for exec\(\) [\#103](https://github.com/shelljs/shelljs/issues/103) +- cp does not recursively copy from readonly location [\#98](https://github.com/shelljs/shelljs/issues/98) +- Handling permissions errors on file I/O [\#64](https://github.com/shelljs/shelljs/issues/64) + +**Merged pull requests:** + +- Add test case for sed on empty file [\#904](https://github.com/shelljs/shelljs/pull/904) ([wyardley](https://github.com/wyardley)) +- refactor: don't expose tempdir in common.state [\#903](https://github.com/shelljs/shelljs/pull/903) ([nfischer](https://github.com/nfischer)) +- chore\(ci\): fix codecov on travis [\#897](https://github.com/shelljs/shelljs/pull/897) ([nfischer](https://github.com/nfischer)) +- chore\(npm\): add ci-or-install script [\#896](https://github.com/shelljs/shelljs/pull/896) ([nfischer](https://github.com/nfischer)) +- Fix silent exec [\#892](https://github.com/shelljs/shelljs/pull/892) ([nfischer](https://github.com/nfischer)) +- chore\(appveyor\): run entire test matrix [\#886](https://github.com/shelljs/shelljs/pull/886) ([nfischer](https://github.com/nfischer)) +- docs: remove gitter badge [\#880](https://github.com/shelljs/shelljs/pull/880) ([nfischer](https://github.com/nfischer)) +- grep includes the i flag [\#876](https://github.com/shelljs/shelljs/pull/876) ([ppsleep](https://github.com/ppsleep)) +- Fix\(which\): match only executable files \(\#657\) [\#874](https://github.com/shelljs/shelljs/pull/874) ([termosa](https://github.com/termosa)) +- chore: rename some tests [\#871](https://github.com/shelljs/shelljs/pull/871) ([nfischer](https://github.com/nfischer)) +- Fix cp from readonly source [\#870](https://github.com/shelljs/shelljs/pull/870) ([nfischer](https://github.com/nfischer)) +- chore: bump dev dependencies and add package-lock [\#864](https://github.com/shelljs/shelljs/pull/864) ([nfischer](https://github.com/nfischer)) +- fix\(mocks\): fix conflict between mocks and skip [\#863](https://github.com/shelljs/shelljs/pull/863) ([nfischer](https://github.com/nfischer)) +- chore: output npm version in travis [\#850](https://github.com/shelljs/shelljs/pull/850) ([nfischer](https://github.com/nfischer)) +- Prevent require-ing bin/shjs [\#848](https://github.com/shelljs/shelljs/pull/848) ([freitagbr](https://github.com/freitagbr)) +- chore\(appveyor\): do not use latest npm [\#847](https://github.com/shelljs/shelljs/pull/847) ([nfischer](https://github.com/nfischer)) +- chore: update shelljs-release version [\#846](https://github.com/shelljs/shelljs/pull/846) ([nfischer](https://github.com/nfischer)) + +## [v0.8.2](https://github.com/shelljs/shelljs/tree/v0.8.2) (2018-05-08) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.8.1...v0.8.2) + +**Closed issues:** + +- High severity vulnerability in shelljs 0.8.1 [\#842](https://github.com/shelljs/shelljs/issues/842) +- Add test for ls\(\) on a symlink to a directory [\#795](https://github.com/shelljs/shelljs/issues/795) +- Harden shell.exec by writing the child process in a source file [\#782](https://github.com/shelljs/shelljs/issues/782) +- shell.exec\(\) doesn't respond correctly to config.fatal = true [\#735](https://github.com/shelljs/shelljs/issues/735) +- Merge 'exec: internal error' with ShellJSInternalError [\#734](https://github.com/shelljs/shelljs/issues/734) +- exec returning null from command [\#724](https://github.com/shelljs/shelljs/issues/724) +- Only Get Stderr from Exec [\#371](https://github.com/shelljs/shelljs/issues/371) +- Execute child.stdout.on before child.on\("exit"\) [\#224](https://github.com/shelljs/shelljs/issues/224) + +**Merged pull requests:** + +- Workaround codecov bug of miscalculation of coverage \(\#795\) [\#838](https://github.com/shelljs/shelljs/pull/838) ([dwi2](https://github.com/dwi2)) +- Update doc comments and regenerate README.md. [\#825](https://github.com/shelljs/shelljs/pull/825) ([Zearin](https://github.com/Zearin)) +- chore: update contributing guidelines [\#817](https://github.com/shelljs/shelljs/pull/817) ([nfischer](https://github.com/nfischer)) +- chore\(lint\): don't allow excess trailing newlines [\#816](https://github.com/shelljs/shelljs/pull/816) ([nfischer](https://github.com/nfischer)) +- Remove separate "internal error" from exec [\#802](https://github.com/shelljs/shelljs/pull/802) ([freitagbr](https://github.com/freitagbr)) + +## [v0.8.1](https://github.com/shelljs/shelljs/tree/v0.8.1) (2018-01-20) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.8.0...v0.8.1) + +**Closed issues:** + +- Exec failing with internal error when piping large output [\#818](https://github.com/shelljs/shelljs/issues/818) + +**Merged pull requests:** + +- Revert "refactor\(exec\): remove paramsFile \(\#807\)" [\#819](https://github.com/shelljs/shelljs/pull/819) ([nfischer](https://github.com/nfischer)) + +## [v0.8.0](https://github.com/shelljs/shelljs/tree/v0.8.0) (2018-01-12) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.7.8...v0.8.0) + +**Closed issues:** + +- Snyk vulnerability DB reporting command injection vulnerability in ShellJS [\#810](https://github.com/shelljs/shelljs/issues/810) +- chore: upgrade nyc [\#803](https://github.com/shelljs/shelljs/issues/803) +- Update CI to use Node v9 [\#799](https://github.com/shelljs/shelljs/issues/799) +- Link to FAQ wiki section in our issue template [\#787](https://github.com/shelljs/shelljs/issues/787) +- Is it possible to get a js library\(file\) for ShellJS [\#776](https://github.com/shelljs/shelljs/issues/776) +- 48, [\#774](https://github.com/shelljs/shelljs/issues/774) +- 47 [\#773](https://github.com/shelljs/shelljs/issues/773) +- Exec function calls JSON.stringify on command [\#772](https://github.com/shelljs/shelljs/issues/772) +- getting different result from terminal and with shelljs [\#769](https://github.com/shelljs/shelljs/issues/769) +- test\(\) does not support -w and -x options [\#768](https://github.com/shelljs/shelljs/issues/768) +- Snyk "high severity" issue [\#766](https://github.com/shelljs/shelljs/issues/766) +- Snyk "high security [\#765](https://github.com/shelljs/shelljs/issues/765) +- ShellJS doesn't respect NPM Registry being set outside of it [\#761](https://github.com/shelljs/shelljs/issues/761) +- Run second shell script [\#756](https://github.com/shelljs/shelljs/issues/756) +- shelljs seems NOT compatible with nexe under CentOS 6.5 [\#754](https://github.com/shelljs/shelljs/issues/754) +- Feature request: pushd/popd -q option [\#753](https://github.com/shelljs/shelljs/issues/753) +- cat doesn't support '-n' option [\#750](https://github.com/shelljs/shelljs/issues/750) +- shelljs run xcodebuild error [\#749](https://github.com/shelljs/shelljs/issues/749) +- Add wrappers around fs.statSync and fs.lstatSync [\#745](https://github.com/shelljs/shelljs/issues/745) +- Improve coverage for exec\(\) [\#742](https://github.com/shelljs/shelljs/issues/742) +- Improve coverage for head\(\) [\#741](https://github.com/shelljs/shelljs/issues/741) +- shelljs is no longer used in PDF.js [\#737](https://github.com/shelljs/shelljs/issues/737) +- ls doesn't follow links to directories [\#733](https://github.com/shelljs/shelljs/issues/733) +- Add test for `ls regular-file.txt` [\#732](https://github.com/shelljs/shelljs/issues/732) +- Clean up common tests [\#714](https://github.com/shelljs/shelljs/issues/714) +- Cant get encoding buffer to work on exec [\#675](https://github.com/shelljs/shelljs/issues/675) +- Set up Codecov for the project [\#671](https://github.com/shelljs/shelljs/issues/671) +- ShellJS: internal error Error: EBUSY: resource busy or locked, lstat 'C:\pagefile.sys' [\#514](https://github.com/shelljs/shelljs/issues/514) +- Feature request: provide a way to skip option parsing [\#778](https://github.com/shelljs/shelljs/issues/778) +- Switch to os.homedir\(\) when we move to v4+ [\#683](https://github.com/shelljs/shelljs/issues/683) +- Drop support for v0.12 [\#647](https://github.com/shelljs/shelljs/issues/647) +- feature: echo -n [\#559](https://github.com/shelljs/shelljs/issues/559) +- Don't kill the node process upon unexpected error [\#483](https://github.com/shelljs/shelljs/issues/483) +- Echo doesn't return value ending in a trailing newline [\#476](https://github.com/shelljs/shelljs/issues/476) +- Synchronous exec stalls permenantly when there is an error/w the shell [\#7](https://github.com/shelljs/shelljs/issues/7) + +**Merged pull requests:** + +- docs: announce plugin API [\#812](https://github.com/shelljs/shelljs/pull/812) ([nfischer](https://github.com/nfischer)) +- chore: update CI to Node v9 [\#811](https://github.com/shelljs/shelljs/pull/811) ([nfischer](https://github.com/nfischer)) +- refactor\(exec\): remove paramsFile [\#807](https://github.com/shelljs/shelljs/pull/807) ([nfischer](https://github.com/nfischer)) +- chore: update nyc dependency [\#805](https://github.com/shelljs/shelljs/pull/805) ([nfischer](https://github.com/nfischer)) +- refactor: harden plugins against unknown options [\#804](https://github.com/shelljs/shelljs/pull/804) ([nfischer](https://github.com/nfischer)) +- chore\(eslint\): use words instead of numbers [\#797](https://github.com/shelljs/shelljs/pull/797) ([nfischer](https://github.com/nfischer)) +- Add note to issue template about FAQ [\#794](https://github.com/shelljs/shelljs/pull/794) ([freitagbr](https://github.com/freitagbr)) +- Remove codeFile parameter [\#791](https://github.com/shelljs/shelljs/pull/791) ([nfischer](https://github.com/nfischer)) +- Use execFileSync to launch child process [\#790](https://github.com/shelljs/shelljs/pull/790) ([nfischer](https://github.com/nfischer)) +- refactor\(exec\): move child process to source file [\#786](https://github.com/shelljs/shelljs/pull/786) ([nfischer](https://github.com/nfischer)) +- Remove unnecessary shell.error checks from common tests [\#785](https://github.com/shelljs/shelljs/pull/785) ([freitagbr](https://github.com/freitagbr)) +- Add a test for ls for a single file [\#784](https://github.com/shelljs/shelljs/pull/784) ([freitagbr](https://github.com/freitagbr)) +- Wrap fs.statSync and fs.lstatSync [\#783](https://github.com/shelljs/shelljs/pull/783) ([freitagbr](https://github.com/freitagbr)) +- chore: set AVA options [\#780](https://github.com/shelljs/shelljs/pull/780) ([nfischer](https://github.com/nfischer)) +- chore: clean up refs to unsupported node versions [\#779](https://github.com/shelljs/shelljs/pull/779) ([nfischer](https://github.com/nfischer)) +- Added `-q` \(quiet\) option to `push`, `popd`, `dirs` functions. [\#777](https://github.com/shelljs/shelljs/pull/777) ([alexreg](https://github.com/alexreg)) +- feat\(cat\): number output lines \(\#750\) [\#775](https://github.com/shelljs/shelljs/pull/775) ([gcca](https://github.com/gcca)) +- refactor\(test\): update AVA and refactor tests [\#760](https://github.com/shelljs/shelljs/pull/760) ([nfischer](https://github.com/nfischer)) +- chore: add skipOnWin and skipOnUnix test helpers [\#746](https://github.com/shelljs/shelljs/pull/746) ([nfischer](https://github.com/nfischer)) +- test\(exec\): add tests for coverage [\#744](https://github.com/shelljs/shelljs/pull/744) ([nfischer](https://github.com/nfischer)) +- test\(head\): improve coverage [\#743](https://github.com/shelljs/shelljs/pull/743) ([nfischer](https://github.com/nfischer)) +- Remove PDF.js mention from README.md [\#738](https://github.com/shelljs/shelljs/pull/738) ([voy](https://github.com/voy)) +- Provide an API to pass parameters which resemble options [\#792](https://github.com/shelljs/shelljs/pull/792) ([nfischer](https://github.com/nfischer)) +- Fix ls not following links to directories by default [\#764](https://github.com/shelljs/shelljs/pull/764) ([freitagbr](https://github.com/freitagbr)) +- Add "encoding" option to exec [\#763](https://github.com/shelljs/shelljs/pull/763) ([freitagbr](https://github.com/freitagbr)) +- Merge dev into master [\#731](https://github.com/shelljs/shelljs/pull/731) ([freitagbr](https://github.com/freitagbr)) +- Deprecate common.getUserHome, advise using os.homedir instead [\#725](https://github.com/shelljs/shelljs/pull/725) ([freitagbr](https://github.com/freitagbr)) +- Echo test mocks [\#708](https://github.com/shelljs/shelljs/pull/708) ([freitagbr](https://github.com/freitagbr)) +- Safely exit by throwing an error [\#546](https://github.com/shelljs/shelljs/pull/546) ([freitagbr](https://github.com/freitagbr)) +- chore\(make\): depreciate shelljs/make [\#431](https://github.com/shelljs/shelljs/pull/431) ([ariporad](https://github.com/ariporad)) + +## [v0.7.8](https://github.com/shelljs/shelljs/tree/v0.7.8) (2017-06-07) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.7.7...v0.7.8) + +**Closed issues:** + +- Add node v8 to CI [\#729](https://github.com/shelljs/shelljs/issues/729) +- Exec not working in Electron ! [\#726](https://github.com/shelljs/shelljs/issues/726) +- is rechoir used anywhere? [\#723](https://github.com/shelljs/shelljs/issues/723) +- ShellJS: internal error on shelljs.mkdir\('myFile/myDir'\) [\#720](https://github.com/shelljs/shelljs/issues/720) +- Can't make sed perform global replace [\#719](https://github.com/shelljs/shelljs/issues/719) +- grep: option not recognized: l [\#717](https://github.com/shelljs/shelljs/issues/717) +- Problems getting code, stdout, stderr [\#715](https://github.com/shelljs/shelljs/issues/715) +- Copying hidden files fails on Windows 10 [\#711](https://github.com/shelljs/shelljs/issues/711) +- How am I suppose to handle errors with ShellJS? [\#707](https://github.com/shelljs/shelljs/issues/707) +- use cp\('-r', './src', './dist'\) bug [\#705](https://github.com/shelljs/shelljs/issues/705) +- Way to ignore files in globs. [\#699](https://github.com/shelljs/shelljs/issues/699) +- Buffer constructor is deprecated [\#694](https://github.com/shelljs/shelljs/issues/694) +- source command not working via exec method. [\#693](https://github.com/shelljs/shelljs/issues/693) +- Would you be interested in a PR for `open`? [\#692](https://github.com/shelljs/shelljs/issues/692) +- Get rid of common.platform in favor of process.platform [\#670](https://github.com/shelljs/shelljs/issues/670) +- Passing empty string to cp throws internal error [\#664](https://github.com/shelljs/shelljs/issues/664) +- Why does sed split files into an array, call replace on each line and rejoin? [\#645](https://github.com/shelljs/shelljs/issues/645) +- feat: cp & mv should not overwrite recently created files [\#631](https://github.com/shelljs/shelljs/issues/631) +- Echo tests unnecessarily run tests in own process [\#622](https://github.com/shelljs/shelljs/issues/622) +- rm -rf on a symbolic link to a dir deletes its contents [\#587](https://github.com/shelljs/shelljs/issues/587) +- "Cannot extract package" with node-webkit [\#181](https://github.com/shelljs/shelljs/issues/181) +- EBADF, bad file descriptor [\#180](https://github.com/shelljs/shelljs/issues/180) + +**Merged pull requests:** + +- Add node 8 to CI [\#730](https://github.com/shelljs/shelljs/pull/730) ([freitagbr](https://github.com/freitagbr)) +- fix\(mkdir\): improve error handling around files [\#721](https://github.com/shelljs/shelljs/pull/721) ([nfischer](https://github.com/nfischer)) +- Properly handle directories as arguments [\#713](https://github.com/shelljs/shelljs/pull/713) ([nfischer](https://github.com/nfischer)) +- Add common.buffer [\#710](https://github.com/shelljs/shelljs/pull/710) ([freitagbr](https://github.com/freitagbr)) +- Fix common.expand error [\#709](https://github.com/shelljs/shelljs/pull/709) ([freitagbr](https://github.com/freitagbr)) +- refactor: remove unnecessary common.js imports [\#703](https://github.com/shelljs/shelljs/pull/703) ([nfischer](https://github.com/nfischer)) +- Fix \#631 throw error when overwriting recently created file [\#702](https://github.com/shelljs/shelljs/pull/702) ([uttpal](https://github.com/uttpal)) +- Small clarification of verbose flag [\#691](https://github.com/shelljs/shelljs/pull/691) ([zommerfelds](https://github.com/zommerfelds)) +- fix\(grep, sed, sort, uniq\): Split only on newline characters [\#690](https://github.com/shelljs/shelljs/pull/690) ([freitagbr](https://github.com/freitagbr)) +- Refactor: Use process.platform across codebase [\#689](https://github.com/shelljs/shelljs/pull/689) ([freitagbr](https://github.com/freitagbr)) +- Remove contents of symlink to dir with rm -rf [\#688](https://github.com/shelljs/shelljs/pull/688) ([freitagbr](https://github.com/freitagbr)) +- Echo stdout [\#677](https://github.com/shelljs/shelljs/pull/677) ([nfischer](https://github.com/nfischer)) + +## [v0.7.7](https://github.com/shelljs/shelljs/tree/v0.7.7) (2017-03-09) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.7.6...v0.7.7) + +**Closed issues:** + +- Error output should be consistent across all platforms. [\#681](https://github.com/shelljs/shelljs/issues/681) +- \*CRITICAL data loss\* shell.cp\(\) Content of file is erased when trying to copy it to the folder it already belongs to [\#678](https://github.com/shelljs/shelljs/issues/678) +- Use with webpack broken in 0.7.6 [\#667](https://github.com/shelljs/shelljs/issues/667) +- Difference between bash ls -R and ShellJS ls -R with symlinks [\#666](https://github.com/shelljs/shelljs/issues/666) +- Refactor which\(\) \(too many repeated code blocks\) [\#656](https://github.com/shelljs/shelljs/issues/656) +- find\(\) raises error when unable to find any files matching, expected to return empty array. [\#653](https://github.com/shelljs/shelljs/issues/653) +- Reformat the markdown in RELEASE.md [\#642](https://github.com/shelljs/shelljs/issues/642) +- rm -rf doesn't work if the directory contains an asar archive in Electron [\#618](https://github.com/shelljs/shelljs/issues/618) +- Add support for other file types in rm [\#617](https://github.com/shelljs/shelljs/issues/617) +- Feature request: ls -L option [\#563](https://github.com/shelljs/shelljs/issues/563) +- How to send SIGINT signal to child process launched with exec [\#518](https://github.com/shelljs/shelljs/issues/518) +- feature request: option to add node\_modules to the path for shelljs scripts [\#469](https://github.com/shelljs/shelljs/issues/469) +- high cpu usage during synchronous exec [\#167](https://github.com/shelljs/shelljs/issues/167) + +**Merged pull requests:** + +- Add support for removing fifos [\#687](https://github.com/shelljs/shelljs/pull/687) ([freitagbr](https://github.com/freitagbr)) +- chore: add codecov script to appveyor CI [\#686](https://github.com/shelljs/shelljs/pull/686) ([nfischer](https://github.com/nfischer)) +- Refactor tests to improve readability [\#685](https://github.com/shelljs/shelljs/pull/685) ([nfischer](https://github.com/nfischer)) +- fix: convert error output to be consistent cross-platform [\#684](https://github.com/shelljs/shelljs/pull/684) ([nfischer](https://github.com/nfischer)) +- chore: add codecov [\#682](https://github.com/shelljs/shelljs/pull/682) ([nfischer](https://github.com/nfischer)) +- Fix cp overwriting identical files [\#679](https://github.com/shelljs/shelljs/pull/679) ([freitagbr](https://github.com/freitagbr)) +- Modified glob pattern. Fixes \#666 [\#676](https://github.com/shelljs/shelljs/pull/676) ([frandiox](https://github.com/frandiox)) +- refactor\(parseOptions\): better handle errors [\#674](https://github.com/shelljs/shelljs/pull/674) ([nfischer](https://github.com/nfischer)) +- test: add misc. tests to improve coverage [\#673](https://github.com/shelljs/shelljs/pull/673) ([nfischer](https://github.com/nfischer)) +- test: don't count hard-to-test lines for coverage [\#672](https://github.com/shelljs/shelljs/pull/672) ([nfischer](https://github.com/nfischer)) +- fix: switch commands.json -\> commands.js [\#668](https://github.com/shelljs/shelljs/pull/668) ([nfischer](https://github.com/nfischer)) +- ls -L \(follow symlinks\) [\#665](https://github.com/shelljs/shelljs/pull/665) ([frandiox](https://github.com/frandiox)) +- docs\(chmod\): document `options` argument [\#663](https://github.com/shelljs/shelljs/pull/663) ([gkalpak](https://github.com/gkalpak)) +- docs: clean up RELEASE.md [\#662](https://github.com/shelljs/shelljs/pull/662) ([nfischer](https://github.com/nfischer)) +- docs: miscellaneous README changes [\#661](https://github.com/shelljs/shelljs/pull/661) ([nfischer](https://github.com/nfischer)) +- Fix typo in README [\#660](https://github.com/shelljs/shelljs/pull/660) ([faheel](https://github.com/faheel)) +- refactor: reduce repeated code in which\(\) [\#659](https://github.com/shelljs/shelljs/pull/659) ([nfischer](https://github.com/nfischer)) +- feature: add -a option for which command [\#655](https://github.com/shelljs/shelljs/pull/655) ([termosa](https://github.com/termosa)) +- Fix find ENOENT [\#654](https://github.com/shelljs/shelljs/pull/654) ([freitagbr](https://github.com/freitagbr)) +- Safely exit by throwing an error [\#649](https://github.com/shelljs/shelljs/pull/649) ([freitagbr](https://github.com/freitagbr)) +- Chore drop 0.12 [\#648](https://github.com/shelljs/shelljs/pull/648) ([nfischer](https://github.com/nfischer)) +- chore\(lint\): Enforce a trailing comma for multi-line [\#646](https://github.com/shelljs/shelljs/pull/646) ([nfischer](https://github.com/nfischer)) +- docs\(release\): use bulleted list [\#643](https://github.com/shelljs/shelljs/pull/643) ([freitagbr](https://github.com/freitagbr)) + +## [v0.7.6](https://github.com/shelljs/shelljs/tree/v0.7.6) (2017-01-08) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.7.5...v0.7.6) + +**Closed issues:** + +- unable to execute ionic command with shell js [\#640](https://github.com/shelljs/shelljs/issues/640) +- How to increase ShellJS buffer size? [\#639](https://github.com/shelljs/shelljs/issues/639) +- mkdir fails with non-normalized path [\#634](https://github.com/shelljs/shelljs/issues/634) +- Move execPath into common [\#633](https://github.com/shelljs/shelljs/issues/633) +- QUESTION: Feedback while an operation is running? [\#629](https://github.com/shelljs/shelljs/issues/629) +- Test setup/cleanup is broken [\#621](https://github.com/shelljs/shelljs/issues/621) +- Ignore temp directories when running lint [\#620](https://github.com/shelljs/shelljs/issues/620) +- parseOptions should throw an error if the option string doesn't start with '-' [\#614](https://github.com/shelljs/shelljs/issues/614) +- chore: LGTM.co is gone [\#595](https://github.com/shelljs/shelljs/issues/595) +- refactor: objectAssign should refer to Object.assign if it exists, or the internal polyfill otherwise [\#592](https://github.com/shelljs/shelljs/issues/592) +- parseOptions: allow a way to keep errors silent \(exception only\) [\#591](https://github.com/shelljs/shelljs/issues/591) +- \[Question\] commands with multiple options / arguments? [\#589](https://github.com/shelljs/shelljs/issues/589) +- feature: GNU Parallel [\#585](https://github.com/shelljs/shelljs/issues/585) +- write to file [\#568](https://github.com/shelljs/shelljs/issues/568) +- Cannot figure out how to disable globbing for rm [\#567](https://github.com/shelljs/shelljs/issues/567) +- Switch to the ava test framework [\#560](https://github.com/shelljs/shelljs/issues/560) +- Option not recognized [\#556](https://github.com/shelljs/shelljs/issues/556) +- chore: add @freitagbr to LGTM maintainers [\#552](https://github.com/shelljs/shelljs/issues/552) +- chore: set up dev branch [\#548](https://github.com/shelljs/shelljs/issues/548) +- bug: cp\(\) doesn't always copy everything [\#547](https://github.com/shelljs/shelljs/issues/547) +- User-friendly lint command [\#544](https://github.com/shelljs/shelljs/issues/544) +- Lint warning [\#542](https://github.com/shelljs/shelljs/issues/542) +- chore: add nodejs v7 to CI [\#537](https://github.com/shelljs/shelljs/issues/537) +- error.code is not always available [\#536](https://github.com/shelljs/shelljs/issues/536) +- Add shx as a dependency for testing [\#525](https://github.com/shelljs/shelljs/issues/525) +- Feature request: allow `common.error\(\)` to optionally not insert a prefix and optionally not print to console [\#523](https://github.com/shelljs/shelljs/issues/523) +- Feature request: Add "shelljs.unlink" [\#519](https://github.com/shelljs/shelljs/issues/519) +- Sed should allow a replacement string to contain `\1` for match groups [\#507](https://github.com/shelljs/shelljs/issues/507) +- Usage with neodoc [\#445](https://github.com/shelljs/shelljs/issues/445) +- \[ Feature idea \] synchronous sleep command [\#441](https://github.com/shelljs/shelljs/issues/441) +- Improve test coverage [\#347](https://github.com/shelljs/shelljs/issues/347) +- Add a way to prevent shell-expansion on commands \(this issue is not for exec\) [\#345](https://github.com/shelljs/shelljs/issues/345) +- Chown [\#183](https://github.com/shelljs/shelljs/issues/183) +- spawn EMFILE [\#81](https://github.com/shelljs/shelljs/issues/81) +- Rewrite exec using execsync-ng \(which uses node-ffi\) [\#66](https://github.com/shelljs/shelljs/issues/66) +- `exec` gets stuck on my Debian box [\#51](https://github.com/shelljs/shelljs/issues/51) +- 100% cpu usage when a nodejs script goes side ways executing a command. [\#5](https://github.com/shelljs/shelljs/issues/5) + +**Merged pull requests:** + +- refactor: add config.reset\(\) and .resetForTesting\(\) [\#641](https://github.com/shelljs/shelljs/pull/641) ([nfischer](https://github.com/nfischer)) +- chore: set up test coverage [\#638](https://github.com/shelljs/shelljs/pull/638) ([nfischer](https://github.com/nfischer)) +- refactor: create common.execPath [\#636](https://github.com/shelljs/shelljs/pull/636) ([nfischer](https://github.com/nfischer)) +- fix: allow non-normalized paths as input to mkdir [\#635](https://github.com/shelljs/shelljs/pull/635) ([nfischer](https://github.com/nfischer)) +- Finalize moving to ava [\#630](https://github.com/shelljs/shelljs/pull/630) ([freitagbr](https://github.com/freitagbr)) +- test: refactor pushd tests to AVA [\#627](https://github.com/shelljs/shelljs/pull/627) ([nfischer](https://github.com/nfischer)) +- test: refactor popd tests to AVA [\#626](https://github.com/shelljs/shelljs/pull/626) ([nfischer](https://github.com/nfischer)) +- test: refactor shjs tests to AVA [\#625](https://github.com/shelljs/shelljs/pull/625) ([nfischer](https://github.com/nfischer)) +- test: remove tests for make \(deprecated\) [\#624](https://github.com/shelljs/shelljs/pull/624) ([nfischer](https://github.com/nfischer)) +- Ignore test temp directories during linting [\#623](https://github.com/shelljs/shelljs/pull/623) ([freitagbr](https://github.com/freitagbr)) +- refactor: list all commands in commands.json [\#616](https://github.com/shelljs/shelljs/pull/616) ([nfischer](https://github.com/nfischer)) +- Throw an error if the options string does not start with '-' [\#615](https://github.com/shelljs/shelljs/pull/615) ([freitagbr](https://github.com/freitagbr)) +- chore: switch to files attribute from npmignore [\#613](https://github.com/shelljs/shelljs/pull/613) ([nfischer](https://github.com/nfischer)) +- test: refactor 'test' command tests to AVA [\#612](https://github.com/shelljs/shelljs/pull/612) ([nfischer](https://github.com/nfischer)) +- test: refactor find tests to AVA [\#611](https://github.com/shelljs/shelljs/pull/611) ([nfischer](https://github.com/nfischer)) +- test: refactor ln tests to AVA [\#610](https://github.com/shelljs/shelljs/pull/610) ([nfischer](https://github.com/nfischer)) +- test: refactor ls to use AVA [\#609](https://github.com/shelljs/shelljs/pull/609) ([nfischer](https://github.com/nfischer)) +- test: refactor pipe tests to AVA [\#608](https://github.com/shelljs/shelljs/pull/608) ([nfischer](https://github.com/nfischer)) +- test: refactor sed tests to AVA [\#607](https://github.com/shelljs/shelljs/pull/607) ([nfischer](https://github.com/nfischer)) +- test: refactor grep tests to AVA [\#606](https://github.com/shelljs/shelljs/pull/606) ([nfischer](https://github.com/nfischer)) +- test: refactor global tests to AVA [\#605](https://github.com/shelljs/shelljs/pull/605) ([nfischer](https://github.com/nfischer)) +- test: refactor touch tests to AVA [\#604](https://github.com/shelljs/shelljs/pull/604) ([nfischer](https://github.com/nfischer)) +- test: refactor uniq tests to AVA [\#603](https://github.com/shelljs/shelljs/pull/603) ([nfischer](https://github.com/nfischer)) +- test: refactor sort tests to AVA [\#602](https://github.com/shelljs/shelljs/pull/602) ([nfischer](https://github.com/nfischer)) +- test: refactor tail tests to AVA [\#601](https://github.com/shelljs/shelljs/pull/601) ([nfischer](https://github.com/nfischer)) +- test: refactor head tests to AVA [\#600](https://github.com/shelljs/shelljs/pull/600) ([nfischer](https://github.com/nfischer)) +- test: refactor mkdir tests to AVA [\#599](https://github.com/shelljs/shelljs/pull/599) ([nfischer](https://github.com/nfischer)) +- Fix: rm behavior regarding symlinks [\#598](https://github.com/shelljs/shelljs/pull/598) ([freitagbr](https://github.com/freitagbr)) +- test: refactor mv tests to AVA [\#597](https://github.com/shelljs/shelljs/pull/597) ([nfischer](https://github.com/nfischer)) +- Remove files related to lgtm.co [\#596](https://github.com/shelljs/shelljs/pull/596) ([freitagbr](https://github.com/freitagbr)) +- Add ability to configure error from parseOptions [\#594](https://github.com/shelljs/shelljs/pull/594) ([freitagbr](https://github.com/freitagbr)) +- Use Object.assign if possible [\#593](https://github.com/shelljs/shelljs/pull/593) ([freitagbr](https://github.com/freitagbr)) +- Add "-n" option to echo [\#590](https://github.com/shelljs/shelljs/pull/590) ([freitagbr](https://github.com/freitagbr)) +- test: refactor rm tests to AVA [\#586](https://github.com/shelljs/shelljs/pull/586) ([nfischer](https://github.com/nfischer)) +- test: refactor pwd tests to AVA [\#582](https://github.com/shelljs/shelljs/pull/582) ([nfischer](https://github.com/nfischer)) +- test: refactor tempdir tests to AVA [\#581](https://github.com/shelljs/shelljs/pull/581) ([nfischer](https://github.com/nfischer)) +- test: refactor 'which' tests to AVA [\#580](https://github.com/shelljs/shelljs/pull/580) ([nfischer](https://github.com/nfischer)) +- test: refactor plugin tests to AVA [\#579](https://github.com/shelljs/shelljs/pull/579) ([nfischer](https://github.com/nfischer)) +- test: refactor toEnd tests to AVA [\#578](https://github.com/shelljs/shelljs/pull/578) ([nfischer](https://github.com/nfischer)) +- test: refactor to tests to AVA [\#577](https://github.com/shelljs/shelljs/pull/577) ([nfischer](https://github.com/nfischer)) +- test: refactor 'set' tests to AVA [\#576](https://github.com/shelljs/shelljs/pull/576) ([nfischer](https://github.com/nfischer)) +- test: refactor echo tests to AVA [\#575](https://github.com/shelljs/shelljs/pull/575) ([nfischer](https://github.com/nfischer)) +- test: refactor exec tests to AVA [\#574](https://github.com/shelljs/shelljs/pull/574) ([nfischer](https://github.com/nfischer)) +- test: refactor env tests to AVA [\#573](https://github.com/shelljs/shelljs/pull/573) ([nfischer](https://github.com/nfischer)) +- test: refactor dirs tests to AVA [\#572](https://github.com/shelljs/shelljs/pull/572) ([nfischer](https://github.com/nfischer)) +- test: refactor config tests to AVA [\#571](https://github.com/shelljs/shelljs/pull/571) ([nfischer](https://github.com/nfischer)) +- test: refactor common tests to AVA [\#570](https://github.com/shelljs/shelljs/pull/570) ([nfischer](https://github.com/nfischer)) +- test: refactor chmod tests to AVA [\#569](https://github.com/shelljs/shelljs/pull/569) ([nfischer](https://github.com/nfischer)) +- test: refactor cp tests to ava [\#565](https://github.com/shelljs/shelljs/pull/565) ([nfischer](https://github.com/nfischer)) +- test: refactor cat tests to ava [\#564](https://github.com/shelljs/shelljs/pull/564) ([nfischer](https://github.com/nfischer)) +- test: set up ava and move cd.js [\#561](https://github.com/shelljs/shelljs/pull/561) ([nfischer](https://github.com/nfischer)) +- Update sed documentation regarding capture groups [\#558](https://github.com/shelljs/shelljs/pull/558) ([freitagbr](https://github.com/freitagbr)) +- Add newline to output of echo [\#557](https://github.com/shelljs/shelljs/pull/557) ([freitagbr](https://github.com/freitagbr)) +- fix: handle code-less errors more carefully in exec [\#554](https://github.com/shelljs/shelljs/pull/554) ([nfischer](https://github.com/nfischer)) +- Add Brandon Freitag to maintainers/contributors [\#553](https://github.com/shelljs/shelljs/pull/553) ([freitagbr](https://github.com/freitagbr)) +- Get pipe tests running on Windows. [\#550](https://github.com/shelljs/shelljs/pull/550) ([binki](https://github.com/binki)) +- fix: maxdepth doesn't limit total number of copies [\#549](https://github.com/shelljs/shelljs/pull/549) ([nfischer](https://github.com/nfischer)) +- Fix lint warning [\#543](https://github.com/shelljs/shelljs/pull/543) ([freitagbr](https://github.com/freitagbr)) +- chore: remove v0.10 from Travis CI [\#540](https://github.com/shelljs/shelljs/pull/540) ([nfischer](https://github.com/nfischer)) +- chore: add Node v7 for CI [\#539](https://github.com/shelljs/shelljs/pull/539) ([nfischer](https://github.com/nfischer)) + +## [v0.7.5](https://github.com/shelljs/shelljs/tree/v0.7.5) (2016-10-27) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.7.4...v0.7.5) + +**Closed issues:** + +- Project objectives: there is some higher goal to achieve? [\#533](https://github.com/shelljs/shelljs/issues/533) +- fs.existsSync is un-deprecated [\#531](https://github.com/shelljs/shelljs/issues/531) +- Inadvertent breaking change to shell.test\(\) [\#529](https://github.com/shelljs/shelljs/issues/529) +- Add -u flag support for cp [\#526](https://github.com/shelljs/shelljs/issues/526) +- API request: allow `plugin.error\(\)` to take an options parameter [\#522](https://github.com/shelljs/shelljs/issues/522) +- FS Real Path error thrown when requiring shelljs [\#521](https://github.com/shelljs/shelljs/issues/521) +- Question: passing code via pipe? [\#520](https://github.com/shelljs/shelljs/issues/520) +- The performance in `cp` is different between `0.6.0` and `0.7.4` [\#517](https://github.com/shelljs/shelljs/issues/517) +- ShellJS in Electron package don't find ffmpeg anymore [\#516](https://github.com/shelljs/shelljs/issues/516) +- Exec issues with string option introduced in 0.7.4 [\#515](https://github.com/shelljs/shelljs/issues/515) +- \[ Feature \] SSH command [\#435](https://github.com/shelljs/shelljs/issues/435) + +**Merged pull requests:** + +- feat: plugin.error\(\) takes an options parameter [\#535](https://github.com/shelljs/shelljs/pull/535) ([nfischer](https://github.com/nfischer)) +- Revert "refactor: replace fs.existsSync" fixes\(\#531\) [\#532](https://github.com/shelljs/shelljs/pull/532) ([gyandeeps](https://github.com/gyandeeps)) +- Fix: Remove default glob from shell.test \(fixes \#529\) [\#530](https://github.com/shelljs/shelljs/pull/530) ([gyandeeps](https://github.com/gyandeeps)) +- feat: cp -u option [\#527](https://github.com/shelljs/shelljs/pull/527) ([nfischer](https://github.com/nfischer)) +- chore: add downloads per month on README [\#513](https://github.com/shelljs/shelljs/pull/513) ([nfischer](https://github.com/nfischer)) + +## [v0.7.4](https://github.com/shelljs/shelljs/tree/v0.7.4) (2016-08-26) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.7.3...v0.7.4) + +**Closed issues:** + +- fix: echo -e should not print "-e" [\#510](https://github.com/shelljs/shelljs/issues/510) +- Wrong method signature in doc [\#498](https://github.com/shelljs/shelljs/issues/498) +- readFromPipe should be a function with no arguments [\#485](https://github.com/shelljs/shelljs/issues/485) +- TypeError: Cannot read property 'toString' of undefined [\#471](https://github.com/shelljs/shelljs/issues/471) + +**Merged pull requests:** + +- fix: echo supports -e option properly [\#511](https://github.com/shelljs/shelljs/pull/511) ([nfischer](https://github.com/nfischer)) +- refactor: replace fs.existsSync [\#509](https://github.com/shelljs/shelljs/pull/509) ([nfischer](https://github.com/nfischer)) +- refactor: readFromPipe\(\) requires no arguments [\#506](https://github.com/shelljs/shelljs/pull/506) ([nfischer](https://github.com/nfischer)) +- chore: switch to eslint [\#504](https://github.com/shelljs/shelljs/pull/504) ([nfischer](https://github.com/nfischer)) +- feat: add overWrite option for commands [\#503](https://github.com/shelljs/shelljs/pull/503) ([nfischer](https://github.com/nfischer)) +- chore: update issue template [\#502](https://github.com/shelljs/shelljs/pull/502) ([nfischer](https://github.com/nfischer)) +- fixed head/tail readme [\#499](https://github.com/shelljs/shelljs/pull/499) ([charlesread](https://github.com/charlesread)) + +## [v0.7.3](https://github.com/shelljs/shelljs/tree/v0.7.3) (2016-07-27) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.7.2...v0.7.3) + +**Closed issues:** + +- expose execSync [\#494](https://github.com/shelljs/shelljs/issues/494) +- Add a way to create commands that can receive from a pipe without being standalone commands [\#487](https://github.com/shelljs/shelljs/issues/487) +- cp -r breaks when the directory contains a softlink [\#193](https://github.com/shelljs/shelljs/issues/193) +- Redirect output to file fails [\#60](https://github.com/shelljs/shelljs/issues/60) +- We need sed -n ? [\#38](https://github.com/shelljs/shelljs/issues/38) + +**Merged pull requests:** + +- refactor: allow pipeOnly commands \(methods on ShellStrings\) [\#493](https://github.com/shelljs/shelljs/pull/493) ([nfischer](https://github.com/nfischer)) +- refactor: glob by default for commands [\#492](https://github.com/shelljs/shelljs/pull/492) ([nfischer](https://github.com/nfischer)) +- refactor: switch from notUnix to unix in wrap\(\) [\#491](https://github.com/shelljs/shelljs/pull/491) ([nfischer](https://github.com/nfischer)) +- refactor: switch common.extend\(\) to Object.assign ponyfill [\#490](https://github.com/shelljs/shelljs/pull/490) ([nfischer](https://github.com/nfischer)) +- fix: conflicting options now properly override each other [\#489](https://github.com/shelljs/shelljs/pull/489) ([nfischer](https://github.com/nfischer)) +- refactor: expose plugin utils & add initial tests [\#484](https://github.com/shelljs/shelljs/pull/484) ([nfischer](https://github.com/nfischer)) + +## [v0.7.2](https://github.com/shelljs/shelljs/tree/v0.7.2) (2016-07-25) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.7.1...v0.7.2) + +**Closed issues:** + +- shelljs should not kill process if node call throws exception [\#473](https://github.com/shelljs/shelljs/issues/473) +- `cp` work incorrectly when folder name contains '@' [\#463](https://github.com/shelljs/shelljs/issues/463) +- Something went wrong [\#158](https://github.com/shelljs/shelljs/issues/158) + +**Merged pull requests:** + +- fix: resolve a cylcic-dependency problem [\#482](https://github.com/shelljs/shelljs/pull/482) ([nfischer](https://github.com/nfischer)) +- refactor: add wrapOutput option to auto-ShellString-ify command output [\#481](https://github.com/shelljs/shelljs/pull/481) ([nfischer](https://github.com/nfischer)) +- refactor: move option parsing into common.wrap\(\) [\#479](https://github.com/shelljs/shelljs/pull/479) ([nfischer](https://github.com/nfischer)) +- refactor: hook new uniq\(\) command using new format [\#478](https://github.com/shelljs/shelljs/pull/478) ([nfischer](https://github.com/nfischer)) +- Fix mkdir malformed path [\#477](https://github.com/shelljs/shelljs/pull/477) ([nfischer](https://github.com/nfischer)) +- fix: mkdir for invalid perms does not kill process [\#474](https://github.com/shelljs/shelljs/pull/474) ([nfischer](https://github.com/nfischer)) +- feat\(command\): new command: uniq\(\) [\#453](https://github.com/shelljs/shelljs/pull/453) ([joshi-sh](https://github.com/joshi-sh)) + +## [v0.7.1](https://github.com/shelljs/shelljs/tree/v0.7.1) (2016-07-22) +[Full Changelog](https://github.com/shelljs/shelljs/compare/v0.7.0...v0.7.1) **Closed issues:** @@ -12,7 +456,6 @@ - Stdout is empty on Git log command [\#439](https://github.com/shelljs/shelljs/issues/439) - Cannot read toString of null when using execSync [\#415](https://github.com/shelljs/shelljs/issues/415) - cp -R dir/ target fails to copy hidden files in dir [\#140](https://github.com/shelljs/shelljs/issues/140) -- Adding callback to basic commands [\#102](https://github.com/shelljs/shelljs/issues/102) - \#mv Won't Work Across Disks [\#1](https://github.com/shelljs/shelljs/issues/1) **Merged pull requests:** @@ -34,7 +477,6 @@ - docs: comment code better to help contributors [\#437](https://github.com/shelljs/shelljs/pull/437) ([nfischer](https://github.com/nfischer)) - chore\(CI\): update appveyor [\#436](https://github.com/shelljs/shelljs/pull/436) ([nfischer](https://github.com/nfischer)) - chore: test against node v6 [\#433](https://github.com/shelljs/shelljs/pull/433) ([nfischer](https://github.com/nfischer)) -- chore\(make\): depreciate shelljs/make [\#431](https://github.com/shelljs/shelljs/pull/431) ([ariporad](https://github.com/ariporad)) - docs: warn that README contains newest features [\#410](https://github.com/shelljs/shelljs/pull/410) ([nfischer](https://github.com/nfischer)) ## [v0.7.0](https://github.com/shelljs/shelljs/tree/v0.7.0) (2016-04-25) @@ -43,8 +485,6 @@ **Closed issues:** - exec\('nohup node some.js &'\) [\#426](https://github.com/shelljs/shelljs/issues/426) -- cp copy to symlinked folder [\#414](https://github.com/shelljs/shelljs/issues/414) -- Invalid version number \(0.0.1alpha1\) [\#399](https://github.com/shelljs/shelljs/issues/399) - shelljs Breaks SemVer for Alpha and Pre-Release Versions [\#390](https://github.com/shelljs/shelljs/issues/390) - Copy not accepting source end with wildcards \* when using -r on v0.6.0 [\#389](https://github.com/shelljs/shelljs/issues/389) - Support globbing in `shjs` [\#388](https://github.com/shelljs/shelljs/issues/388) @@ -63,7 +503,6 @@ - "exec" causes LiveScript interpreter \(lsc\) to hang [\#160](https://github.com/shelljs/shelljs/issues/160) - Don't modify string prototype [\#159](https://github.com/shelljs/shelljs/issues/159) - `exec\(...\).to\(file\)` should work [\#154](https://github.com/shelljs/shelljs/issues/154) -- Would like to see more async variants for cp/rm etc [\#144](https://github.com/shelljs/shelljs/issues/144) - Can't install shelljs locally instead of globally [\#136](https://github.com/shelljs/shelljs/issues/136) - shelljs and node 0.10.28 [\#125](https://github.com/shelljs/shelljs/issues/125) - Use case for global installed shelljs [\#123](https://github.com/shelljs/shelljs/issues/123) @@ -161,12 +600,12 @@ - why not support set 'cwd' when invoke execAsync ? [\#250](https://github.com/shelljs/shelljs/issues/250) - Not possible to check the failure of cd? [\#247](https://github.com/shelljs/shelljs/issues/247) - By default shelljs runs command in root [\#246](https://github.com/shelljs/shelljs/issues/246) +- /usr/bin/env: node: No such file or directory [\#243](https://github.com/shelljs/shelljs/issues/243) - "Which" command not working properly on Windows Platform. [\#238](https://github.com/shelljs/shelljs/issues/238) - Arguments [\#237](https://github.com/shelljs/shelljs/issues/237) - sed\(\) should accept multiple file arguments [\#231](https://github.com/shelljs/shelljs/issues/231) - shelljs.exec\('aaa && bbb'\) blocks [\#229](https://github.com/shelljs/shelljs/issues/229) - Consider creating a GitHub Organization with more maintainers [\#223](https://github.com/shelljs/shelljs/issues/223) -- Doesn't work inside Electron [\#220](https://github.com/shelljs/shelljs/issues/220) - \[idea\] Add chmodr function. [\#219](https://github.com/shelljs/shelljs/issues/219) - Execute a file [\#211](https://github.com/shelljs/shelljs/issues/211) - Where is standard error going to? [\#209](https://github.com/shelljs/shelljs/issues/209) @@ -178,7 +617,6 @@ - Cannot recursively list all \*.js files [\#162](https://github.com/shelljs/shelljs/issues/162) - exec\(\) breaks if executed in a deleted directory [\#157](https://github.com/shelljs/shelljs/issues/157) - shjs command always exits with zero code [\#133](https://github.com/shelljs/shelljs/issues/133) -- Windows failing tests [\#127](https://github.com/shelljs/shelljs/issues/127) - touch command [\#122](https://github.com/shelljs/shelljs/issues/122) - Symbolic links are broken! [\#100](https://github.com/shelljs/shelljs/issues/100) - interpret `--` as stdin [\#55](https://github.com/shelljs/shelljs/issues/55) @@ -292,7 +730,7 @@ - Breaking: Allow -- as args separators \(fixes \#188\) [\#207](https://github.com/shelljs/shelljs/pull/207) ([nzakas](https://github.com/nzakas)) - Update .travis.yml [\#190](https://github.com/shelljs/shelljs/pull/190) ([arturadib](https://github.com/arturadib)) -- Use new child\_process.execSync instead of busywaiting [\#189](https://github.com/shelljs/shelljs/pull/189) ([devTristan](https://github.com/devTristan)) +- Use new child\_process.execSync instead of busywaiting [\#189](https://github.com/shelljs/shelljs/pull/189) ([madd512](https://github.com/madd512)) - Update README.md: explains how to access "config" [\#145](https://github.com/shelljs/shelljs/pull/145) ([kerphi](https://github.com/kerphi)) - Fix to set state.error before throw the exception [\#120](https://github.com/shelljs/shelljs/pull/120) ([abdul-martinez](https://github.com/abdul-martinez)) - Add -l and -s support to grep. [\#116](https://github.com/shelljs/shelljs/pull/116) ([idearat](https://github.com/idearat)) @@ -358,8 +796,8 @@ **Merged pull requests:** -- Addition of a toEnd\(\) function modeled after the Unix \>\> pipe. [\#78](https://github.com/shelljs/shelljs/pull/78) ([andreweduffy](https://github.com/andreweduffy)) -- Added appendTo\(\) function to imitate '\>\>' redirect-and-append pipe. [\#75](https://github.com/shelljs/shelljs/pull/75) ([andreweduffy](https://github.com/andreweduffy)) +- Addition of a toEnd\(\) function modeled after the Unix \>\> pipe. [\#78](https://github.com/shelljs/shelljs/pull/78) ([a10y](https://github.com/a10y)) +- Added appendTo\(\) function to imitate '\>\>' redirect-and-append pipe. [\#75](https://github.com/shelljs/shelljs/pull/75) ([a10y](https://github.com/a10y)) - Fix a small typo in README.md [\#71](https://github.com/shelljs/shelljs/pull/71) ([asmblah](https://github.com/asmblah)) - adding an `.npmignore` file [\#70](https://github.com/shelljs/shelljs/pull/70) ([stephenmathieson](https://github.com/stephenmathieson)) - tempdir: use `os.tmpDir` when possible [\#67](https://github.com/shelljs/shelljs/pull/67) ([stephenmathieson](https://github.com/stephenmathieson)) @@ -510,4 +948,4 @@ ## [v0.0.2pre1](https://github.com/shelljs/shelljs/tree/v0.0.2pre1) (2012-03-03) -\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 16257e158..1559b4d77 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,22 +5,21 @@ We love to receive bug reports (we're always trying to make ShellJS more stable). If you've found a bug, please follow these steps: - - Search for any issues that may have been created already. We often receive - duplicates, and cutting down on this is helpful. If someone else has already - reported it, please ping that issue thread. - - Let us know your version of NodeJS (`node -v`), your version of ShellJS (from - `package.json`), your OS, and any other useful information. - - Give an example ShellJS command to reproduce the error. + - Please try to cut down on duplicates. Please search for issues which have + already been reported (remember to search closed issues). + - Please see [`ISSUE_TEMPLATE.md`](.github/ISSUE_TEMPLATE.md) for more + information. ## Pull requests PRs are welcome! However, we ask that you follow a few guidelines: - Please add tests for all changes/new features. - - Make sure your code passes `npm test`. Please check the CI (both Appveyor and - Travis). If you can't figure out why something doesn't work, feel free to ask - for help. - - Make changes to the documentation *within the source files*, not in the - README. Then update the README by running `node scripts/generate-docs.js`. + - Make sure your code passes `npm test`. Please check the CI. If you can't + figure out why something doesn't work, feel free to ask for help. + - Make sure you conform to our style guidelines. You can run `npm run lint` to + check style, and `npm run lint -- --fix` to automatically fix some issues. + - Make documentation changes *within the source files*, not in the README. + Update the README with `npm run gendocs`. - Please keep your PR up to date (either via rebase or by pressing the "update branch" button on Github). diff --git a/LICENSE b/LICENSE index 0f0f119be..40a2bf650 100644 --- a/LICENSE +++ b/LICENSE @@ -1,26 +1,29 @@ +BSD 3-Clause License + Copyright (c) 2012, Artur Adib All rights reserved. -You may use this project under the terms of the New BSD license as follows: - Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Artur Adib nor the - names of the contributors may be used to endorse or promote products - derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL ARTUR ADIB BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MAINTAINERS b/MAINTAINERS deleted file mode 100644 index 3f9476150..000000000 --- a/MAINTAINERS +++ /dev/null @@ -1,3 +0,0 @@ -Ari Porad (@ariporad) -Nate Fischer (@nfischer) -Artur Adib (@arturadib) diff --git a/README.md b/README.md index adf0a0e91..a0de67655 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,32 @@ # ShellJS - Unix shell commands for Node.js -[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square)](https://gitter.im/shelljs/shelljs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Travis](https://img.shields.io/travis/shelljs/shelljs/master.svg?style=flat-square&label=unix)](https://travis-ci.org/shelljs/shelljs) -[![AppVeyor](https://img.shields.io/appveyor/ci/shelljs/shelljs/master.svg?style=flat-square&label=windows)](https://ci.appveyor.com/project/shelljs/shelljs/branch/master) -[![npm (scoped)](https://img.shields.io/npm/v/shelljs.svg?style=flat-square)](https://www.npmjs.com/package/shelljs) +[![GitHub Actions](https://img.shields.io/github/actions/workflow/status/shelljs/shelljs/main.yml?style=flat-square&logo=github)](https://github.com/shelljs/shelljs/actions/workflows/main.yml) +[![Codecov](https://img.shields.io/codecov/c/github/shelljs/shelljs/main.svg?style=flat-square&label=coverage)](https://codecov.io/gh/shelljs/shelljs) +[![npm version](https://img.shields.io/npm/v/shelljs.svg?style=flat-square)](https://www.npmjs.com/package/shelljs) +[![npm downloads](https://img.shields.io/npm/dm/shelljs.svg?style=flat-square)](https://www.npmjs.com/package/shelljs) -ShellJS is a portable **(Windows/Linux/OS X)** implementation of Unix shell commands on top of the -Node.js API. You can use it to eliminate your shell script's dependency on Unix while still keeping -its familiar and powerful commands. You can also install it globally so you can run it from outside -Node projects - say goodbye to those gnarly Bash scripts! +ShellJS is a portable **(Windows/Linux/macOS)** implementation of Unix shell +commands on top of the Node.js API. You can use it to eliminate your shell +script's dependency on Unix while still keeping its familiar and powerful +commands. You can also install it globally so you can run it from outside Node +projects - say goodbye to those gnarly Bash scripts! -ShellJS supports node `v0.11`, `v0.12`, `v4`, `v5`, `v6`, and all releases of iojs. +ShellJS is proudly tested on every LTS node release since `v18`! -The project is [unit-tested](http://travis-ci.org/shelljs/shelljs) and battled-tested in projects like: +The project is unit-tested and battle-tested in projects like: -+ [PDF.js](http://github.com/mozilla/pdf.js) - Firefox's next-gen PDF reader + [Firebug](http://getfirebug.com/) - Firefox's infamous debugger -+ [JSHint](http://jshint.com) - Most popular JavaScript linter ++ [JSHint](http://jshint.com) & [ESLint](http://eslint.org/) - popular JavaScript linters + [Zepto](http://zeptojs.com) - jQuery-compatible JavaScript library for modern browsers + [Yeoman](http://yeoman.io/) - Web application stack and development tool + [Deployd.com](http://deployd.com) - Open source PaaS for quick API backend generation + And [many more](https://npmjs.org/browse/depended/shelljs). -If you have feedback, suggestions, or need help, feel free to post in our [issue tracker](https://github.com/shelljs/shelljs/issues). +If you have feedback, suggestions, or need help, feel free to post in our [issue +tracker](https://github.com/shelljs/shelljs/issues). -Think ShellJS is cool? Check out some related projects (like -[cash](https://github.com/dthree/cash)--a javascript-based POSIX shell) -in our [Wiki page](https://github.com/shelljs/shelljs/wiki)! +Think ShellJS is cool? Check out some related projects in our [Wiki +page](https://github.com/shelljs/shelljs/wiki)! Upgrading from an older version? Check out our [breaking changes](https://github.com/shelljs/shelljs/wiki/Breaking-Changes) page to see @@ -46,13 +46,18 @@ $ shx touch foo/bar.txt $ shx rm -rf foo ``` +## Plugin API + +ShellJS now supports third-party plugins! You can learn more about using plugins +and writing your own ShellJS commands in [the +wiki](https://github.com/shelljs/shelljs/wiki/Using-ShellJS-Plugins). + ## A quick note about the docs For documentation on all the latest features, check out our [README](https://github.com/shelljs/shelljs). To read docs that are consistent with the latest release, check out [the npm -page](https://www.npmjs.com/package/shelljs) or -[shelljs.org](http://documentup.com/shelljs/shelljs). +page](https://www.npmjs.com/package/shelljs). ## Installing @@ -62,85 +67,66 @@ Via npm: $ npm install [-g] shelljs ``` -If the global option `-g` is specified, the binary `shjs` will be installed. This makes it possible to -run ShellJS scripts much like any shell script from the command line, i.e. without requiring a `node_modules` folder: - -```bash -$ shjs my_script -``` - ## Examples -### JavaScript - ```javascript -require('shelljs/global'); +var shell = require('shelljs'); -if (!which('git')) { - echo('Sorry, this script requires git'); - exit(1); +if (!shell.which('git')) { + shell.echo('Sorry, this script requires git'); + shell.exit(1); } // Copy files to release dir -rm('-rf', 'out/Release'); -cp('-R', 'stuff/', 'out/Release'); +shell.rm('-rf', 'out/Release'); +shell.cp('-R', 'stuff/', 'out/Release'); // Replace macros in each .js file -cd('lib'); -ls('*.js').forEach(function(file) { - sed('-i', 'BUILD_VERSION', 'v0.1.2', file); - sed('-i', /^.*REMOVE_THIS_LINE.*$/, '', file); - sed('-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, cat('macro.js'), file); +shell.cd('lib'); +shell.ls('*.js').forEach(function (file) { + shell.sed('-i', 'BUILD_VERSION', 'v0.1.2', file); + shell.sed('-i', /^.*REMOVE_THIS_LINE.*$/, '', file); + shell.sed('-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, shell.cat('macro.js'), file); }); -cd('..'); +shell.cd('..'); // Run external tool synchronously -if (exec('git commit -am "Auto-commit"').code !== 0) { - echo('Error: Git commit failed'); - exit(1); +if (shell.exec('git commit -am "Auto-commit"').code !== 0) { + shell.echo('Error: Git commit failed'); + shell.exit(1); } ``` -### CoffeeScript - -CoffeeScript is also supported automatically: +## Exclude options -```coffeescript -require 'shelljs/global' +If you need to pass a parameter that looks like an option, you can do so like: -if not which 'git' - echo 'Sorry, this script requires git' - exit 1 +```js +shell.grep('--', '-v', 'path/to/file'); // Search for "-v", no grep options -# Copy files to release dir -rm '-rf', 'out/Release' -cp '-R', 'stuff/', 'out/Release' - -# Replace macros in each .js file -cd 'lib' -for file in ls '*.js' - sed '-i', 'BUILD_VERSION', 'v0.1.2', file - sed '-i', /^.*REMOVE_THIS_LINE.*$/, '', file - sed '-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, cat('macro.js'), file -cd '..' - -# Run external tool synchronously -if (exec 'git commit -am "Auto-commit"').code != 0 - echo 'Error: Git commit failed' - exit 1 +shell.cp('-R', '-dir', 'outdir'); // If already using an option, you're done ``` ## Global vs. Local -The example above uses the convenience script `shelljs/global` to reduce verbosity. If polluting your global namespace is not desirable, simply require `shelljs`. +We no longer recommend using a global-import for ShellJS (i.e. +`require('shelljs/global')`). While still supported for convenience, this +pollutes the global namespace, and should therefore only be used with caution. -Example: +Instead, we recommend a local import (standard for npm packages): ```javascript var shell = require('shelljs'); shell.echo('hello world'); ``` +Alternatively, we also support importing as a module with: + +```javascript +import shell from 'shelljs'; +shell.echo('hello world'); +``` + @@ -149,70 +135,120 @@ shell.echo('hello world'); All commands run synchronously, unless otherwise stated. All commands accept standard bash globbing characters (`*`, `?`, etc.), -compatible with the [node glob module](https://github.com/isaacs/node-glob). +compatible with [`fast-glob`](https://www.npmjs.com/package/fast-glob). For less-commonly used commands and features, please check out our [wiki page](https://github.com/shelljs/shelljs/wiki). +### cat([options,] file [, file ...]) +### cat([options,] file_array) + +Available options: + ++ `-n`: number all output lines + +Examples: + +```javascript +var str = cat('file*.txt'); +var str = cat('file1', 'file2'); +var str = cat(['file1', 'file2']); // same as above +``` + +Returns a [ShellString](#shellstringstr) containing the given file, or a +concatenated string containing the files if more than one file is given (a +new line character is introduced between each file). + + ### cd([dir]) -Changes to directory `dir` for the duration of the script. Changes to home -directory if no argument is supplied. +Changes to directory `dir` for the duration of the script. Changes to home +directory if no argument is supplied. Returns a +[ShellString](#shellstringstr) to indicate success or failure. -### pwd() -Returns the current directory. +### chmod([options,] octal_mode || octal_string, file) +### chmod([options,] symbolic_mode, file) -### ls([options,] [path, ...]) -### ls([options,] path_array) Available options: -+ `-R`: recursive -+ `-A`: all files (include files beginning with `.`, except for `.` and `..`) -+ `-d`: list directories themselves, not their contents -+ `-l`: list objects representing each file, each with fields containing `ls - -l` output fields. See - [fs.Stats](https://nodejs.org/api/fs.html#fs_class_fs_stats) - for more info ++ `-v`: output a diagnostic for every file processed ++ `-c`: like verbose, but report only when a change is made ++ `-R`: change files and directories recursively Examples: ```javascript -ls('projs/*.js'); -ls('-R', '/users/me', '/tmp'); -ls('-R', ['/users/me', '/tmp']); // same as above -ls('-l', 'file.txt'); // { name: 'file.txt', mode: 33188, nlink: 1, ...} +chmod(755, '/Users/brandon'); +chmod('755', '/Users/brandon'); // same as above +chmod('u+x', '/Users/brandon'); +chmod('-R', 'a-w', '/Users/brandon'); ``` -Returns array of files in the given path, or in current directory if no path provided. +Alters the permissions of a file or directory by either specifying the +absolute permissions in octal form or expressing the changes in symbols. +This command tries to mimic the POSIX behavior as much as possible. +Notable exceptions: ++ In symbolic modes, `a-r` and `-r` are identical. No consideration is + given to the `umask`. ++ There is no "quiet" option, since default behavior is to run silent. ++ Windows OS uses a very different permission model than POSIX. `chmod()` + does its best on Windows, but there are limits to how file permissions can + be set. Note that WSL (Windows subsystem for Linux) **does** follow POSIX, + so cross-platform compatibility should not be a concern there. + +Returns a [ShellString](#shellstringstr) indicating success or failure. + + +### cmd(arg1[, arg2, ...] [, options]) + +Available options: + ++ `cwd: directoryPath`: change the current working directory only for this + cmd() invocation. ++ `maxBuffer: num`: Raise or decrease the default buffer size for + stdout/stderr. ++ `timeout`: Change the default timeout. -### find(path [, path ...]) -### find(path_array) Examples: ```javascript -find('src', 'lib'); -find(['src', 'lib']); // same as above -find('.').filter(function(file) { return file.match(/\.js$/); }); +var version = cmd('node', '--version').stdout; +cmd('git', 'commit', '-am', `Add suport for node ${version}`); +console.log(cmd('echo', '1st arg', '2nd arg', '3rd arg').stdout) +console.log(cmd('echo', 'this handles ;, |, &, etc. as literal characters').stdout) ``` -Returns array of all files (however deep) in the given paths. +Executes the given command synchronously. This is intended as an easier +alternative for [exec()](#execcommand--options--callback), with better +security around globbing, comamnd injection, and variable expansion. This is +guaranteed to only run one external command, and won't give special +treatment for any shell characters (ex. this treats `|` as a literal +character, not as a shell pipeline). +This returns a [ShellString](#shellstringstr). -The main difference from `ls('-R', path)` is that the resulting file names -include the base directories, e.g. `lib/resources/file1` instead of just `file1`. +By default, this performs globbing on all platforms, but you can disable +this with `set('-f')`. + +This **does not** support asynchronous mode. If you need asynchronous +command execution, check out [execa](https://www.npmjs.com/package/execa) or +the node builtin `child_process.execFile()` instead. ### cp([options,] source [, source ...], dest) ### cp([options,] source_array, dest) + Available options: + `-f`: force (default behavior) + `-n`: no-clobber ++ `-u`: only copy if `source` is newer than `dest` + `-r`, `-R`: recursive + `-L`: follow symlinks + `-P`: don't follow symlinks ++ `-p`: preserve file mode, ownership, and timestamps Examples: @@ -223,466 +259,583 @@ cp('-Rf', '/tmp/*', '/usr/local/*', '/home/tmp'); cp('-Rf', ['/tmp/*', '/usr/local/*'], '/home/tmp'); // same as above ``` -Copies files. +Copies files. Returns a [ShellString](#shellstringstr) indicating success +or failure. -### rm([options,] file [, file ...]) -### rm([options,] file_array) +### pushd([options,] [dir | '-N' | '+N']) + Available options: -+ `-f`: force -+ `-r, -R`: recursive ++ `-n`: Suppresses the normal change of directory when adding directories to the stack, so that only the stack is manipulated. ++ `-q`: Suppresses output to the console. + +Arguments: + ++ `dir`: Sets the current working directory to the top of the stack, then executes the equivalent of `cd dir`. ++ `+N`: Brings the Nth directory (counting from the left of the list printed by dirs, starting with zero) to the top of the list by rotating the stack. ++ `-N`: Brings the Nth directory (counting from the right of the list printed by dirs, starting with zero) to the top of the list by rotating the stack. Examples: ```javascript -rm('-rf', '/tmp/*'); -rm('some_file.txt', 'another_file.txt'); -rm(['some_file.txt', 'another_file.txt']); // same as above +// process.cwd() === '/usr' +pushd('/etc'); // Returns /etc /usr +pushd('+1'); // Returns /usr /etc ``` -Removes files. +Save the current directory on the top of the directory stack and then `cd` to `dir`. With no arguments, `pushd` exchanges the top two directories. Returns an array of paths in the stack. -### mv([options ,] source [, source ...], dest') -### mv([options ,] source_array, dest') +### popd([options,] ['-N' | '+N']) + Available options: -+ `-f`: force (default behavior) -+ `-n`: no-clobber ++ `-n`: Suppress the normal directory change when removing directories from the stack, so that only the stack is manipulated. ++ `-q`: Suppresses output to the console. + +Arguments: + ++ `+N`: Removes the Nth directory (counting from the left of the list printed by dirs), starting with zero. ++ `-N`: Removes the Nth directory (counting from the right of the list printed by dirs), starting with zero. Examples: ```javascript -mv('-n', 'file', 'dir/'); -mv('file1', 'file2', 'dir/'); -mv(['file1', 'file2'], 'dir/'); // same as above +echo(process.cwd()); // '/usr' +pushd('/etc'); // '/etc /usr' +echo(process.cwd()); // '/etc' +popd(); // '/usr' +echo(process.cwd()); // '/usr' ``` -Moves files. +When no arguments are given, `popd` removes the top directory from the stack and performs a `cd` to the new top directory. The elements are numbered from 0, starting at the first directory listed with dirs (i.e., `popd` is equivalent to `popd +0`). Returns an array of paths in the stack. -### mkdir([options,] dir [, dir ...]) -### mkdir([options,] dir_array) +### dirs([options | '+N' | '-N']) + Available options: -+ `-p`: full path (will create intermediate dirs if necessary) ++ `-c`: Clears the directory stack by deleting all of the elements. ++ `-q`: Suppresses output to the console. -Examples: +Arguments: -```javascript -mkdir('-p', '/tmp/a/b/c/d', '/tmp/e/f/g'); -mkdir('-p', ['/tmp/a/b/c/d', '/tmp/e/f/g']); // same as above -``` ++ `+N`: Displays the Nth directory (counting from the left of the list printed by dirs when invoked without options), starting with zero. ++ `-N`: Displays the Nth directory (counting from the right of the list printed by dirs when invoked without options), starting with zero. -Creates directories. +Display the list of currently remembered directories. Returns an array of paths in the stack, or a single path if `+N` or `-N` was specified. +See also: `pushd`, `popd` -### test(expression) -Available expression primaries: -+ `'-b', 'path'`: true if path is a block device -+ `'-c', 'path'`: true if path is a character device -+ `'-d', 'path'`: true if path is a directory -+ `'-e', 'path'`: true if path exists -+ `'-f', 'path'`: true if path is a regular file -+ `'-L', 'path'`: true if path is a symbolic link -+ `'-p', 'path'`: true if path is a pipe (FIFO) -+ `'-S', 'path'`: true if path is a socket +### echo([options,] string [, string ...]) + +Available options: + ++ `-e`: interpret backslash escapes (default) ++ `-n`: remove trailing newline from output Examples: ```javascript -if (test('-d', path)) { /* do something with dir */ }; -if (!test('-f', path)) continue; // skip if it's a regular file +echo('hello world'); +var str = echo('hello world'); +echo('-n', 'no newline at end'); ``` -Evaluates expression using the available primaries and returns corresponding value. +Prints `string` to stdout, and returns a [ShellString](#shellstringstr). -### cat(file [, file ...]) -### cat(file_array) +### exec(command [, options] [, callback]) + +Available options: + ++ `async`: Asynchronous execution. If a callback is provided, it will be set to + `true`, regardless of the passed value (default: `false`). ++ `fatal`: Exit upon error (default: `false`). ++ `silent`: Do not echo program output to console (default: `false`). ++ `encoding`: Character encoding to use. Affects the values returned to stdout and stderr, and + what is written to stdout and stderr when not in silent mode (default: `'utf8'`). ++ and any option available to Node.js's + [`child_process.exec()`](https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback) Examples: ```javascript -var str = cat('file*.txt'); -var str = cat('file1', 'file2'); -var str = cat(['file1', 'file2']); // same as above +var version = exec('node --version', {silent:true}).stdout; + +var child = exec('some_long_running_process', {async:true}); +child.stdout.on('data', function(data) { + /* ... do something with data ... */ +}); + +exec('some_long_running_process', function(code, stdout, stderr) { + console.log('Exit code:', code); + console.log('Program output:', stdout); + console.log('Program stderr:', stderr); +}); ``` -Returns a string containing the given file, or a concatenated string -containing the files if more than one file is given (a new line character is -introduced between each file). +Executes the given `command` _synchronously_, unless otherwise specified. +When in synchronous mode, this returns a [ShellString](#shellstringstr). +Otherwise, this returns the child process object, and the `callback` +receives the arguments `(code, stdout, stderr)`. + +Not seeing the behavior you want? `exec()` runs everything through `sh` +by default (or `cmd.exe` on Windows), which differs from `bash`. If you +need bash-specific behavior, try out the `{shell: 'path/to/bash'}` option. +**Security note:** as `shell.exec()` executes an arbitrary string in the +system shell, it is **critical** to properly sanitize user input to avoid +**command injection**. For more context, consult the [Security +Guidelines](https://github.com/shelljs/shelljs/wiki/Security-guidelines). -### head([{'-n', \},] file [, file ...]) -### head([{'-n', \},] file_array) + +### find(path [, path ...]) +### find(path_array) Examples: ```javascript -var str = head({'-n', 1}, 'file*.txt'); -var str = head('file1', 'file2'); -var str = head(['file1', 'file2']); // same as above +find('src', 'lib'); +find(['src', 'lib']); // same as above +find('.').filter(function(file) { return file.match(/\.js$/); }); ``` -Output the first 10 lines of a file (or the first `` if `-n` is -specified) +Returns a [ShellString](#shellstringstr) (with array-like properties) of all +files (however deep) in the given paths. + +The main difference from `ls('-R', path)` is that the resulting file names +include the base directories (e.g., `lib/resources/file1` instead of just `file1`). + +### grep([options,] regex_filter, file [, file ...]) +### grep([options,] regex_filter, file_array) -### tail([{'-n', \},] file [, file ...]) -### tail([{'-n', \},] file_array) +Available options: + ++ `-v`: Invert `regex_filter` (only print non-matching lines). ++ `-l`: Print only filenames of matching files. ++ `-i`: Ignore case. ++ `-n`: Print line numbers. ++ `-B `: Show `` lines before each result. ++ `-A `: Show `` lines after each result. ++ `-C `: Show `` lines before and after each result. -B and -A override this option. Examples: ```javascript -var str = tail({'-n', 1}, 'file*.txt'); -var str = tail('file1', 'file2'); -var str = tail(['file1', 'file2']); // same as above +grep('-v', 'GLOBAL_VARIABLE', '*.js'); +grep('GLOBAL_VARIABLE', '*.js'); +grep('-B', 3, 'GLOBAL_VARIABLE', '*.js'); +grep({ '-B': 3 }, 'GLOBAL_VARIABLE', '*.js'); +grep({ '-B': 3, '-C': 2 }, 'GLOBAL_VARIABLE', '*.js'); ``` -Output the last 10 lines of a file (or the last `` if `-n` is -specified) +Reads input string from given files and returns a +[ShellString](#shellstringstr) containing all lines of the @ file that match +the given `regex_filter`. -### ShellString.prototype.to(file) +### head([{'-n': \},] file [, file ...]) +### head([{'-n': \},] file_array) + +Available options: + ++ `-n `: Show the first `` lines of the files Examples: ```javascript -cat('input.txt').to('output.txt'); +var str = head({'-n': 1}, 'file*.txt'); +var str = head('file1', 'file2'); +var str = head(['file1', 'file2']); // same as above ``` -Analogous to the redirection operator `>` in Unix, but works with -ShellStrings (such as those returned by `cat`, `grep`, etc). _Like Unix -redirections, `to()` will overwrite any existing file!_ +Read the start of a `file`. Returns a [ShellString](#shellstringstr). -### ShellString.prototype.toEnd(file) +### ln([options,] source, dest) + +Available options: + ++ `-s`: symlink ++ `-f`: force Examples: ```javascript -cat('input.txt').toEnd('output.txt'); +ln('file', 'newlink'); +ln('-sf', 'file', 'existing'); ``` -Analogous to the redirect-and-append operator `>>` in Unix, but works with -ShellStrings (such as those returned by `cat`, `grep`, etc). +Links `source` to `dest`. Use `-f` to force the link, should `dest` already +exist. Returns a [ShellString](#shellstringstr) indicating success or +failure. -### sed([options,] search_regex, replacement, file [, file ...]) -### sed([options,] search_regex, replacement, file_array) +### ls([options,] [path, ...]) +### ls([options,] path_array) + Available options: -+ `-i`: Replace contents of 'file' in-place. _Note that no backups will be created!_ ++ `-R`: recursive ++ `-A`: all files (include files beginning with `.`, except for `.` and `..`) ++ `-L`: follow symlinks ++ `-d`: list directories themselves, not their contents ++ `-l`: provides more details for each file. Specifically, each file is + represented by a structured object with separate fields for file + metadata (see + [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats)). The + return value also overrides `.toString()` to resemble `ls -l`'s + output format for human readability, but programmatic usage should + depend on the stable object format rather than the `.toString()` + representation. Examples: ```javascript -sed('-i', 'PROGRAM_VERSION', 'v0.1.3', 'source.js'); -sed(/.*DELETE_THIS_LINE.*\n/, '', 'source.js'); +ls('projs/*.js'); +ls('projs/**/*.js'); // Find all js files recursively in projs +ls('-R', '/users/me', '/tmp'); +ls('-R', ['/users/me', '/tmp']); // same as above +ls('-l', 'file.txt'); // { name: 'file.txt', mode: 33188, nlink: 1, ...} ``` -Reads an input string from `files` and performs a JavaScript `replace()` on the input -using the given search regex and replacement string or function. Returns the new string after replacement. +Returns a [ShellString](#shellstringstr) (with array-like properties) of all +the files in the given `path`, or files in the current directory if no +`path` is provided. -### sort([options,] file [, file ...]) -### sort([options,] file_array) +### mkdir([options,] dir [, dir ...]) +### mkdir([options,] dir_array) + Available options: -+ `-r`: Reverse the result of comparisons -+ `-n`: Compare according to numerical value ++ `-p`: full path (and create intermediate directories, if necessary) Examples: ```javascript -sort('foo.txt', 'bar.txt'); -sort('-r', 'foo.txt'); +mkdir('-p', '/tmp/a/b/c/d', '/tmp/e/f/g'); +mkdir('-p', ['/tmp/a/b/c/d', '/tmp/e/f/g']); // same as above ``` -Return the contents of the files, sorted line-by-line. Sorting multiple -files mixes their content, just like unix sort does. +Creates directories. Returns a [ShellString](#shellstringstr) indicating +success or failure. -### grep([options,] regex_filter, file [, file ...]) -### grep([options,] regex_filter, file_array) +### mv([options ,] source [, source ...], dest') +### mv([options ,] source_array, dest') + Available options: -+ `-v`: Inverse the sense of the regex and print the lines not matching the criteria. -+ `-l`: Print only filenames of matching files ++ `-f`: force (default behavior) ++ `-n`: no-clobber Examples: ```javascript -grep('-v', 'GLOBAL_VARIABLE', '*.js'); -grep('GLOBAL_VARIABLE', '*.js'); +mv('-n', 'file', 'dir/'); +mv('file1', 'file2', 'dir/'); +mv(['file1', 'file2'], 'dir/'); // same as above ``` -Reads input string from given files and returns a string containing all lines of the -file that match the given `regex_filter`. +Moves `source` file(s) to `dest`. Returns a [ShellString](#shellstringstr) +indicating success or failure. -### which(command) +### pwd() -Examples: +Returns the current directory as a [ShellString](#shellstringstr). -```javascript -var nodeExec = which('node'); -``` -Searches for `command` in the system's PATH. On Windows, this uses the -`PATHEXT` variable to append the extension if it's not already executable. -Returns string containing the absolute path to the command. +### rm([options,] file [, file ...]) +### rm([options,] file_array) +Available options: -### echo(string [, string ...]) ++ `-f`: force ++ `-r, -R`: recursive Examples: ```javascript -echo('hello world'); -var str = echo('hello world'); +rm('-rf', '/tmp/*'); +rm('some_file.txt', 'another_file.txt'); +rm(['some_file.txt', 'another_file.txt']); // same as above ``` -Prints string to stdout, and returns string with additional utility methods -like `.to()`. +Removes files. Returns a [ShellString](#shellstringstr) indicating success +or failure. -### pushd([options,] [dir | '-N' | '+N']) +### sed([options,] search_regex, replacement, file [, file ...]) +### sed([options,] search_regex, replacement, file_array) Available options: -+ `-n`: Suppresses the normal change of directory when adding directories to the stack, so that only the stack is manipulated. - -Arguments: - -+ `dir`: Makes the current working directory be the top of the stack, and then executes the equivalent of `cd dir`. -+ `+N`: Brings the Nth directory (counting from the left of the list printed by dirs, starting with zero) to the top of the list by rotating the stack. -+ `-N`: Brings the Nth directory (counting from the right of the list printed by dirs, starting with zero) to the top of the list by rotating the stack. ++ `-i`: Replace contents of `file` in-place. _Note that no backups will be created!_ Examples: ```javascript -// process.cwd() === '/usr' -pushd('/etc'); // Returns /etc /usr -pushd('+1'); // Returns /usr /etc +sed('-i', 'PROGRAM_VERSION', 'v0.1.3', 'source.js'); ``` -Save the current directory on the top of the directory stack and then cd to `dir`. With no arguments, pushd exchanges the top two directories. Returns an array of paths in the stack. +Reads an input string from `file`s, line by line, and performs a JavaScript `replace()` on +each of the lines from the input string using the given `search_regex` and `replacement` string or +function. Returns the new [ShellString](#shellstringstr) after replacement. -### popd([options,] ['-N' | '+N']) +Note: -Available options: +Like unix `sed`, ShellJS `sed` supports capture groups. Capture groups are specified +using the `$n` syntax: -+ `-n`: Suppresses the normal change of directory when removing directories from the stack, so that only the stack is manipulated. +```javascript +sed(/(\w+)\s(\w+)/, '$2, $1', 'file.txt'); +``` -Arguments: +Also, like unix `sed`, ShellJS `sed` runs replacements on each line from the input file +(split by '\n') separately, so `search_regex`es that span more than one line (or include '\n') +will not match anything and nothing will be replaced. -+ `+N`: Removes the Nth directory (counting from the left of the list printed by dirs), starting with zero. -+ `-N`: Removes the Nth directory (counting from the right of the list printed by dirs), starting with zero. + +### set(options) + +Available options: + ++ `+/-e`: exit upon error (`config.fatal`) ++ `+/-v`: verbose: show all commands (`config.verbose`) ++ `+/-f`: disable filename expansion (globbing) Examples: ```javascript -echo(process.cwd()); // '/usr' -pushd('/etc'); // '/etc /usr' -echo(process.cwd()); // '/etc' -popd(); // '/usr' -echo(process.cwd()); // '/usr' +set('-e'); // exit upon first error +set('+e'); // this undoes a "set('-e')" ``` -When no arguments are given, popd removes the top directory from the stack and performs a cd to the new top directory. The elements are numbered from 0 starting at the first directory listed with dirs; i.e., popd is equivalent to popd +0. Returns an array of paths in the stack. +Sets global configuration variables. -### dirs([options | '+N' | '-N']) + +### sort([options,] file [, file ...]) +### sort([options,] file_array) Available options: -+ `-c`: Clears the directory stack by deleting all of the elements. ++ `-r`: Reverse the results ++ `-n`: Compare according to numerical value -Arguments: +Examples: -+ `+N`: Displays the Nth directory (counting from the left of the list printed by dirs when invoked without options), starting with zero. -+ `-N`: Displays the Nth directory (counting from the right of the list printed by dirs when invoked without options), starting with zero. +```javascript +sort('foo.txt', 'bar.txt'); +sort('-r', 'foo.txt'); +``` -Display the list of currently remembered directories. Returns an array of paths in the stack, or a single path if +N or -N was specified. +Return the contents of the `file`s, sorted line-by-line as a +[ShellString](#shellstringstr). Sorting multiple files mixes their content +(just as unix `sort` does). -See also: pushd, popd +### tail([{'-n': \},] file [, file ...]) +### tail([{'-n': \},] file_array) -### ln([options,] source, dest) Available options: -+ `-s`: symlink -+ `-f`: force ++ `-n `: Show the last `` lines of `file`s Examples: ```javascript -ln('file', 'newlink'); -ln('-sf', 'file', 'existing'); +var str = tail({'-n': 1}, 'file*.txt'); +var str = tail('file1', 'file2'); +var str = tail(['file1', 'file2']); // same as above ``` -Links source to dest. Use -f to force the link, should dest already exist. +Read the end of a `file`. Returns a [ShellString](#shellstringstr). -### exit(code) -Exits the current process with the given exit code. +### tempdir() -### env['VAR_NAME'] -Object containing environment variables (both getter and setter). Shortcut to process.env. +Examples: -### exec(command [, options] [, callback]) -Available options (all `false` by default): +```javascript +var tmp = tempdir(); // "/tmp" for most *nix platforms +``` -+ `async`: Asynchronous execution. If a callback is provided, it will be set to - `true`, regardless of the passed value. -+ `silent`: Do not echo program output to console. -+ and any option available to NodeJS's - [child_process.exec()](https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback) +Searches and returns string containing a writeable, platform-dependent temporary directory. +Follows Python's [tempfile algorithm](http://docs.python.org/library/tempfile.html#tempfile.tempdir). -Examples: -```javascript -var version = exec('node --version', {silent:true}).stdout; +### test(expression) -var child = exec('some_long_running_process', {async:true}); -child.stdout.on('data', function(data) { - /* ... do something with data ... */ -}); +Available expression primaries: -exec('some_long_running_process', function(code, stdout, stderr) { - console.log('Exit code:', code); - console.log('Program output:', stdout); - console.log('Program stderr:', stderr); -}); ++ `'-b', 'path'`: true if path is a block device ++ `'-c', 'path'`: true if path is a character device ++ `'-d', 'path'`: true if path is a directory ++ `'-e', 'path'`: true if path exists ++ `'-f', 'path'`: true if path is a regular file ++ `'-L', 'path'`: true if path is a symbolic link ++ `'-p', 'path'`: true if path is a pipe (FIFO) ++ `'-S', 'path'`: true if path is a socket + +Examples: + +```javascript +if (test('-d', path)) { /* do something with dir */ }; +if (!test('-f', path)) continue; // skip if it's not a regular file ``` -Executes the given `command` _synchronously_, unless otherwise specified. When in synchronous -mode, this returns a ShellString (compatible with ShellJS v0.6.x, which returns an object -of the form `{ code:..., stdout:... , stderr:... }`). Otherwise, this returns the child process -object, and the `callback` gets the arguments `(code, stdout, stderr)`. +Evaluates `expression` using the available primaries and returns +corresponding boolean value. -Not seeing the behavior you want? `exec()` runs everything through `sh` -by default (or `cmd.exe` on Windows), which differs from `bash`. If you -need bash-specific behavior, try out the `{shell: 'path/to/bash'}` option. -**Note:** For long-lived processes, it's best to run `exec()` asynchronously as -the current synchronous implementation uses a lot of CPU. This should be getting -fixed soon. +### ShellString.prototype.to(file) +Examples: -### chmod(octal_mode || octal_string, file) -### chmod(symbolic_mode, file) +```javascript +cat('input.txt').to('output.txt'); +``` -Available options: +Analogous to the redirection operator `>` in Unix, but works with +`ShellStrings` (such as those returned by `cat`, `grep`, etc.). _Like Unix +redirections, `to()` will overwrite any existing file!_ Returns the same +[ShellString](#shellstringstr) this operated on, to support chaining. -+ `-v`: output a diagnostic for every file processed -+ `-c`: like verbose but report only when a change is made -+ `-R`: change files and directories recursively + +### ShellString.prototype.toEnd(file) Examples: ```javascript -chmod(755, '/Users/brandon'); -chmod('755', '/Users/brandon'); // same as above -chmod('u+x', '/Users/brandon'); +cat('input.txt').toEnd('output.txt'); ``` -Alters the permissions of a file or directory by either specifying the -absolute permissions in octal form or expressing the changes in symbols. -This command tries to mimic the POSIX behavior as much as possible. -Notable exceptions: - -+ In symbolic modes, 'a-r' and '-r' are identical. No consideration is - given to the umask. -+ There is no "quiet" option since default behavior is to run silent. +Analogous to the redirect-and-append operator `>>` in Unix, but works with +`ShellStrings` (such as those returned by `cat`, `grep`, etc.). Returns the +same [ShellString](#shellstringstr) this operated on, to support chaining. ### touch([options,] file [, file ...]) ### touch([options,] file_array) + Available options: + `-a`: Change only the access time + `-c`: Do not create any files + `-m`: Change only the modification time -+ `-d DATE`: Parse DATE and use it instead of current time -+ `-r FILE`: Use FILE's times instead of current time ++ `{'-d': someDate}`, `{date: someDate}`: Use a `Date` instance (ex. `someDate`) + instead of current time ++ `{'-r': file}`, `{reference: file}`: Use `file`'s times instead of current + time Examples: ```javascript touch('source.js'); -touch('-c', '/path/to/some/dir/source.js'); -touch({ '-r': FILE }, '/path/to/some/dir/source.js'); +touch('-c', 'path/to/file.js'); +touch({ '-r': 'referenceFile.txt' }, 'path/to/file.js'); +touch({ '-d': new Date('December 17, 1995 03:24:00'), '-m': true }, 'path/to/file.js'); +touch({ date: new Date('December 17, 1995 03:24:00') }, 'path/to/file.js'); ``` -Update the access and modification times of each FILE to the current time. -A FILE argument that does not exist is created empty, unless -c is supplied. -This is a partial implementation of *[touch(1)](http://linux.die.net/man/1/touch)*. +Update the access and modification times of each file to the current time. +A file argument that does not exist is created empty, unless `-c` is supplied. +This is a partial implementation of +[`touch(1)`](http://linux.die.net/man/1/touch). Returns a +[ShellString](#shellstringstr) indicating success or failure. -### set(options) +### uniq([options,] [input, [output]]) + Available options: -+ `+/-e`: exit upon error (`config.fatal`) -+ `+/-v`: verbose: show all commands (`config.verbose`) -+ `+/-f`: disable filename expansion (globbing) ++ `-i`: Ignore case while comparing ++ `-c`: Prefix lines by the number of occurrences ++ `-d`: Only print duplicate lines, one for each group of identical lines Examples: ```javascript -set('-e'); // exit upon first error -set('+e'); // this undoes a "set('-e')" +uniq('foo.txt'); +uniq('-i', 'foo.txt'); +uniq('-cd', 'foo.txt', 'bar.txt'); ``` -Sets global configuration variables - - -## Non-Unix commands +Filter adjacent matching lines from `input`. Returns a +[ShellString](#shellstringstr). -### tempdir() +### which(command) Examples: ```javascript -var tmp = tempdir(); // "/tmp" for most *nix platforms +var nodeExec = which('node'); ``` -Searches and returns string containing a writeable, platform-dependent temporary directory. -Follows Python's [tempfile algorithm](http://docs.python.org/library/tempfile.html#tempfile.tempdir). +Searches for `command` in the system's `PATH`. On Windows, this uses the +`PATHEXT` variable to append the extension if it's not already executable. +Returns a [ShellString](#shellstringstr) containing the absolute path to +`command`. + +### exit(code) + +Exits the current process with the given exit `code`. ### error() + Tests if error occurred in the last command. Returns a truthy value if an -error returned and a falsy value otherwise. +error returned, or a falsy value otherwise. **Note**: do not rely on the return value to be an error message. If you need the last error message, use the `.stderr` attribute from the last command's return value instead. +### errorCode() + +Returns the error code from the last command. + + ### ShellString(str) Examples: ```javascript -var foo = ShellString('hello world'); +var foo = new ShellString('hello world'); ``` -Turns a regular string into a string-like object similar to what each -command returns. This has special methods, like `.to()` and `.toEnd()` +This is a dedicated type returned by most ShellJS methods, which wraps a +string (or array) value. This has all the string (or array) methods, but +also exposes extra methods: [`.to()`](#shellstringprototypetofile), +[`.toEnd()`](#shellstringprototypetoendfile), and all the pipe-able methods +(ex. `.cat()`, `.grep()`, etc.). This can be easily converted into a string +by calling `.toString()`. + +This type also exposes the corresponding command's stdout, stderr, and +return status code via the `.stdout` (string), `.stderr` (string), and +`.code` (number) properties respectively. +### env['VAR_NAME'] + +Object containing environment variables (both getter and setter). Shortcut +to `process.env`. + ### Pipes Examples: ```javascript grep('foo', 'file1.txt', 'file2.txt').sed(/o/g, 'a').to('output.txt'); -echo('files with o\'s in the name:\n' + ls().grep('o')); +echo("files with o's in the name:\n" + ls().grep('o')); cat('test.js').exec('node'); // pipe to exec() call ``` @@ -719,9 +872,9 @@ cp('this_file_does_not_exist', '/dev/null'); // throws Error here /* more commands... */ ``` -If `true` the script will throw a Javascript error when any shell.js +If `true`, the script will throw a Javascript error when any shell.js command encounters an error. Default is `false`. This is analogous to -Bash's `set -e` +Bash's `set -e`. ### config.verbose @@ -730,17 +883,26 @@ Example: ```javascript config.verbose = true; // or set('-v'); cd('dir/'); -ls('subdir/'); +rm('-rf', 'foo.txt', 'bar.txt'); +exec('echo hello'); ``` Will print each command as follows: ``` cd dir/ -ls subdir/ +rm -rf foo.txt bar.txt +exec echo hello ``` -### config.globOptions +### config.globOptions (deprecated) + +**Deprecated**: we recommend that you do not edit `config.globOptions`. +Support for this configuration option may be changed or removed in a future +ShellJS release. + +**Breaking change**: ShellJS v0.8.x uses `node-glob`. Starting with ShellJS +v0.9.x, `config.globOptions` is compatible with `fast-glob`. Example: @@ -748,10 +910,40 @@ Example: config.globOptions = {nodir: true}; ``` -Use this value for calls to `glob.sync()` instead of the default options. +`config.globOptions` changes how ShellJS expands glob (wildcard) +expressions. See +[fast-glob](https://github.com/mrmlnc/fast-glob?tab=readme-ov-file#options-3) +for available options. Be aware that modifying `config.globOptions` **may +break ShellJS functionality.** + +### config.reset() + +Example: + +```javascript +var shell = require('shelljs'); +// Make changes to shell.config, and do stuff... +/* ... */ +shell.config.reset(); // reset to original state +// Do more stuff, but with original settings +/* ... */ +``` + +Reset `shell.config` to the defaults: + +```javascript +{ + fatal: false, + globOptions: {}, + maxdepth: 255, + noglob: false, + silent: false, + verbose: false, +} +``` ## Team -| [![Nate Fischer](https://avatars.githubusercontent.com/u/5801521?s=130)](https://github.com/nfischer) | [![Ari Porad](https://avatars1.githubusercontent.com/u/1817508?v=3&s=130)](http://github.com/ariporad) | -|:---:|:---:| -| [Nate Fischer](https://github.com/nfischer) | [Ari Porad](http://github.com/ariporad) | +| [![Nate Fischer](https://avatars.githubusercontent.com/u/5801521?s=130)](https://github.com/nfischer) | +|:---:| +| [Nate Fischer](https://github.com/nfischer) | diff --git a/RELEASE.md b/RELEASE.md index c78d364f4..751cad391 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,15 +1,14 @@ # Release steps -1. Ensure master passes CI tests -2. `npm run gendocs` -3. Bump version, create tags, push, and release: +1. Ensure main branch passes CI tests +2. Bump version, create tags, push, and release: - `$ npm run ` - - >`major` - breaking API changes - >`minor` - backwards-compatible features - >`patch` - backwards-compatible bug fixes -4. Update CHANGELOG.md + - `major` - breaking API changes + - `minor` - backwards-compatible features + - `patch` - backwards-compatible bug fixes +3. Update `CHANGELOG.md` - `$ npm run changelog` + - Manually verify that the changelog makes sense - `$ git push` -5. Generate the documentup website by visiting - [http://documentup.com/shelljs/shelljs/__recompile] in your browser +4. Update https://github.com/shelljs/shelljs/releases to create a new release + from the latest git tag diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 4ca277e03..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,25 +0,0 @@ -environment: - matrix: - - nodejs_version: '6' - - nodejs_version: '5' - - nodejs_version: '4' - -version: '{build}' - -# Install scripts. (runs after repo cloning) -install: - - ps: Install-Product node $env:nodejs_version - - npm -g install npm@latest - - set PATH=%APPDATA%\npm;%PATH% - - node --version - - npm --version - - npm install - -matrix: - fast_finish: true - -# No need for MSBuild on this project -build: off - -test_script: - - npm test diff --git a/bin/shjs b/bin/shjs deleted file mode 100755 index 75ca58b9d..000000000 --- a/bin/shjs +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env node -require('../global'); - -if (process.argv.length < 3) { - console.log('ShellJS: missing argument (script name)'); - console.log(); - process.exit(1); -} - -var args, - scriptName = process.argv[2]; -env['NODE_PATH'] = __dirname + '/../..'; - -if (!scriptName.match(/\.js/) && !scriptName.match(/\.coffee/)) { - if (test('-f', scriptName + '.js')) - scriptName += '.js'; - if (test('-f', scriptName + '.coffee')) - scriptName += '.coffee'; -} - -if (!test('-f', scriptName)) { - console.log('ShellJS: script not found ('+scriptName+')'); - console.log(); - process.exit(1); -} - -args = process.argv.slice(3); - -for (var i = 0, l = args.length; i < l; i++) { - if (args[i][0] !== "-"){ - args[i] = '"' + args[i] + '"'; // fixes arguments with multiple words - } -} - -var path = require('path'); -var extensions = require('interpret').extensions; -var rechoir = require('rechoir'); -rechoir.prepare(extensions, scriptName); -require(require.resolve(path.resolve(process.cwd(), scriptName))); diff --git a/global.js b/global.js index 506cc4a88..e061f5a1e 100644 --- a/global.js +++ b/global.js @@ -1,10 +1,15 @@ -var shell = require('./shell.js'); +/* eslint no-extend-native: 0 */ +var shell = require('./shell'); var common = require('./src/common'); -for (var cmd in shell) + +Object.keys(shell).forEach(function (cmd) { global[cmd] = shell[cmd]; +}); var _to = require('./src/to'); + String.prototype.to = common.wrap('to', _to); var _toEnd = require('./src/toEnd'); + String.prototype.toEnd = common.wrap('toEnd', _toEnd); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..cdaeeed0a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6431 @@ +{ + "name": "shelljs", + "version": "0.10.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "shelljs", + "version": "0.10.0", + "license": "BSD-3-Clause", + "dependencies": { + "execa": "^5.1.1", + "fast-glob": "^3.3.2" + }, + "devDependencies": { + "ava": "^6.2.0", + "chalk": "^4.1.2", + "coffee-script": "^1.12.7", + "eslint": "^8.2.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-plugin-import": "^2.31.0", + "js-yaml": "^4.1.0", + "nyc": "^17.1.0", + "shelljs-changelog": "^0.2.6", + "shelljs-release": "^0.5.3", + "shx": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.0.tgz", + "integrity": "sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==", + "dev": true, + "dependencies": { + "consola": "^3.2.3", + "detect-libc": "^2.0.0", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^2.6.7", + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true + }, + "node_modules/@vercel/nft": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.27.10.tgz", + "integrity": "sha512-zbaF9Wp/NsZtKLE4uVmL3FyfFwlpDyuymQM1kPbeT0mVOHKDQQNjnnfslB3REg3oZprmNFJuh3pkHBk2qAaizg==", + "dev": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^2.0.0-rc.0", + "@rollup/pluginutils": "^5.1.3", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.5", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "node-gyp-build": "^4.2.2", + "picomatch": "^4.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aggregate-error/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrgv": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arrgv/-/arrgv-1.0.2.tgz", + "integrity": "sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/arrify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-3.0.0.tgz", + "integrity": "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-sema": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", + "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", + "dev": true + }, + "node_modules/ava": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ava/-/ava-6.2.0.tgz", + "integrity": "sha512-+GZk5PbyepjiO/68hzCZCUepQOQauKfNnI7sA4JukBTg97jD7E+tDKEA7OhGOGr6EorNNMM9+jqvgHVOTOzG4w==", + "dev": true, + "dependencies": { + "@vercel/nft": "^0.27.5", + "acorn": "^8.13.0", + "acorn-walk": "^8.3.4", + "ansi-styles": "^6.2.1", + "arrgv": "^1.0.2", + "arrify": "^3.0.0", + "callsites": "^4.2.0", + "cbor": "^9.0.2", + "chalk": "^5.3.0", + "chunkd": "^2.0.1", + "ci-info": "^4.0.0", + "ci-parallel-vars": "^1.0.1", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "common-path-prefix": "^3.0.0", + "concordance": "^5.0.4", + "currently-unhandled": "^0.4.1", + "debug": "^4.3.7", + "emittery": "^1.0.3", + "figures": "^6.1.0", + "globby": "^14.0.2", + "ignore-by-default": "^2.1.0", + "indent-string": "^5.0.0", + "is-plain-object": "^5.0.0", + "is-promise": "^4.0.0", + "matcher": "^5.0.0", + "memoize": "^10.0.0", + "ms": "^2.1.3", + "p-map": "^7.0.2", + "package-config": "^5.0.0", + "picomatch": "^4.0.2", + "plur": "^5.1.0", + "pretty-ms": "^9.1.0", + "resolve-cwd": "^3.0.0", + "stack-utils": "^2.0.6", + "strip-ansi": "^7.1.0", + "supertap": "^3.0.1", + "temp-dir": "^3.0.0", + "write-file-atomic": "^6.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "ava": "entrypoints/cli.mjs" + }, + "engines": { + "node": "^18.18 || ^20.8 || ^22 || >=23" + }, + "peerDependencies": { + "@ava/typescript": "*" + }, + "peerDependenciesMeta": { + "@ava/typescript": { + "optional": true + } + } + }, + "node_modules/ava/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/caching-transform/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-4.2.0.tgz", + "integrity": "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/cbor": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", + "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", + "dev": true, + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/chunkd": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/chunkd/-/chunkd-2.0.1.tgz", + "integrity": "sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==", + "dev": true + }, + "node_modules/ci-info": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", + "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/ci-parallel-vars": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ci-parallel-vars/-/ci-parallel-vars-1.0.1.tgz", + "integrity": "sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==", + "dev": true + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "dev": true, + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/coffee-script": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", + "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", + "deprecated": "CoffeeScript on NPM has moved to \"coffeescript\" (no hyphen)", + "dev": true, + "bin": { + "cake": "bin/cake", + "coffee": "bin/coffee" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concordance": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", + "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", + "dev": true, + "dependencies": { + "date-time": "^3.1.0", + "esutils": "^2.0.3", + "fast-diff": "^1.2.0", + "js-string-escape": "^1.0.1", + "lodash": "^4.17.15", + "md5-hex": "^3.0.1", + "semver": "^7.3.2", + "well-known-symbols": "^2.0.0" + }, + "engines": { + "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14" + } + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", + "dev": true, + "dependencies": { + "array-find-index": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", + "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", + "dev": true, + "dependencies": { + "time-zone": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.144", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.144.tgz", + "integrity": "sha512-eJIaMRKeAzxfBSxtjYnoIAw/tdD6VIH6tHBZepZnAbE3Gyqqs5mGN87DvcldPUbVkIljTK8pY0CMcUljP64lfQ==", + "dev": true + }, + "node_modules/emittery": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-1.1.0.tgz", + "integrity": "sha512-rsX7ktqARv/6UQDgMaLfIqUWAEzzbCQiVh7V9rhDXp6c37yoJcks12NVD+XPkgl4AEavmNhVfrhGoqYwIsMYYA==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-2.1.0.tgz", + "integrity": "sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==", + "dev": true, + "engines": { + "node": ">=10 <11 || >=12 <13 || >=14" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-string-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", + "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/load-json-file": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-7.0.1.tgz", + "integrity": "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/matcher": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-5.0.0.tgz", + "integrity": "sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/matcher/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5-hex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", + "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", + "dev": true, + "dependencies": { + "blueimp-md5": "^2.10.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/memoize": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.1.0.tgz", + "integrity": "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/memoize?sponsor=1" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nan": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", + "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "dev": true, + "engines": { + "node": ">=12.19" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz", + "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^3.3.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^6.0.2", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nyc/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-config": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/package-config/-/package-config-5.0.0.tgz", + "integrity": "sha512-GYTTew2slBcYdvRHqjhwaaydVMvn/qrGC323+nKclYioNSLTDUM/lGgtGTgyHVtYcozb+XkE8CNhwcraOmZ9Mg==", + "dev": true, + "dependencies": { + "find-up-simple": "^1.0.0", + "load-json-file": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/plur": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-5.1.0.tgz", + "integrity": "sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==", + "dev": true, + "dependencies": { + "irregular-plurals": "^3.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-ms": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", + "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "dev": true, + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process-on-spawn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shelljs-changelog": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/shelljs-changelog/-/shelljs-changelog-0.2.6.tgz", + "integrity": "sha512-+jghG+Cdksxc4bHyzQzSjWC+ULcUWX9SVzKBE8J6ShIb1yKAS2m10GNrqNquLS9uqSlGrRZPAyIXLxfrmmd+BA==", + "dev": true, + "dependencies": { + "shelljs": "^0.8.5", + "shelljs-plugin-sleep": "^0.2.1" + }, + "bin": { + "shelljs-changelog": "index.js" + } + }, + "node_modules/shelljs-plugin-sleep": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/shelljs-plugin-sleep/-/shelljs-plugin-sleep-0.2.2.tgz", + "integrity": "sha512-w4yozqtrxfqeybp1lrAPmcb4+RfLfkNtHqLMOGvIXuWxNcqDdttREX+DhXy3F8Xfp5ll+/WQLLEgJbH7RCHqWQ==", + "dev": true, + "engines": { + "node": ">=4.0.0" + }, + "optionalDependencies": { + "sleep": "^3.0.1" + }, + "peerDependencies": { + "shelljs": "^0.8.5" + } + }, + "node_modules/shelljs-release": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/shelljs-release/-/shelljs-release-0.5.3.tgz", + "integrity": "sha512-9L15opORTyWZSn7eIm+rmdo4hwgwf99bXqIAPS572KdX9KjqBcdex7FvOrCI8YxaAxosYfS73rVeJ22SkWIxGA==", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "minimist": "^1.2.0", + "shelljs": "^0.8.5" + }, + "bin": { + "shelljs-release": "index.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shelljs-release/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shelljs-release/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shelljs-release/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/shelljs-release/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/shelljs-release/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/shelljs-release/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/shelljs-release/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shx": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.4.0.tgz", + "integrity": "sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.8", + "shelljs": "^0.9.2" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/shx/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/shx/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/shx/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/shx/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shx/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shx/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/shx/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/shx/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shx/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shx/node_modules/shelljs": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.9.2.tgz", + "integrity": "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==", + "dev": true, + "dependencies": { + "execa": "^1.0.0", + "fast-glob": "^3.3.2", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/shx/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sleep": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sleep/-/sleep-3.0.1.tgz", + "integrity": "sha512-dtegFdlKDk05SQzlA29LVj8gzf01bTvtSOHCt+JwYRXEmx3qp85OZ+MpB65IYwxTNylwSmq8mOcsa4MitU8PpQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "nan": ">=2.0.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supertap": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/supertap/-/supertap-3.0.1.tgz", + "integrity": "sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw==", + "dev": true, + "dependencies": { + "indent-string": "^5.0.0", + "js-yaml": "^3.14.1", + "serialize-error": "^7.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/supertap/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/supertap/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "dev": true, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/time-zone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", + "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/well-known-symbols": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", + "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz", + "integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 51ee1695f..243271d9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shelljs", - "version": "0.7.0", + "version": "0.10.0", "description": "Portable Unix shell commands for Node.js", "keywords": [ "shelljs", @@ -13,8 +13,8 @@ "synchronous" ], "contributors": [ - "Ari Porad (http://ariporad.com/)", - "Nate Fischer (https://github.com/nfischer)" + "Nate Fischer (https://github.com/nfischer)", + "Brandon Freitag (https://github.com/freitagbr)" ], "repository": { "type": "git", @@ -23,35 +23,66 @@ "license": "BSD-3-Clause", "homepage": "http://github.com/shelljs/shelljs", "main": "./shell.js", + "exports": { + ".": "./shell.js", + "./global": "./global.js", + "./global.js": "./global.js", + "./make": "./make.js", + "./make.js": "./make.js", + "./package": "./package.json", + "./package.json": "./package.json", + "./plugin": "./plugin.js", + "./plugin.js": "./plugin.js" + }, + "files": [ + "global.js", + "make.js", + "plugin.js", + "shell.js", + "src" + ], "scripts": { + "check-node-support": "node scripts/check-node-support", "posttest": "npm run lint", - "test": "node scripts/run-tests", + "test": "ava", + "test-with-coverage": "nyc --reporter=text --reporter=lcov ava", "gendocs": "node scripts/generate-docs", - "lint": "jshint .", - "after-travis": "travis-check-changes", + "lint": "eslint .", "changelog": "shelljs-changelog", "release:major": "shelljs-release major", "release:minor": "shelljs-release minor", "release:patch": "shelljs-release patch" }, - "bin": { - "shjs": "./bin/shjs" - }, "dependencies": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" + "execa": "^5.1.1", + "fast-glob": "^3.3.2" + }, + "ava": { + "serial": true, + "workerThreads": false, + "powerAssert": false, + "files": [ + "test/*.js" + ], + "helpers": [ + "test/resources/**", + "test/utils/**" + ] }, "devDependencies": { - "coffee-script": "^1.10.0", - "jshint": "^2.9.2", - "shelljs-changelog": "^0.2.0", - "shelljs-release": "^0.2.0", - "travis-check-changes": "^0.2.0" + "ava": "^6.2.0", + "chalk": "^4.1.2", + "coffee-script": "^1.12.7", + "eslint": "^8.2.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-plugin-import": "^2.31.0", + "js-yaml": "^4.1.0", + "nyc": "^17.1.0", + "shelljs-changelog": "^0.2.6", + "shelljs-release": "^0.5.3", + "shx": "^0.4.0" }, - "optionalDependencies": {}, "engines": { - "node": ">=0.11.0", - "iojs": "*" + "node": ">=18" } } diff --git a/plugin.js b/plugin.js new file mode 100644 index 000000000..2e1585092 --- /dev/null +++ b/plugin.js @@ -0,0 +1,16 @@ +// Various utilities exposed to plugins + +require('./shell'); // Create the ShellJS instance (mandatory) + +var common = require('./src/common'); + +var exportedAttributes = [ + 'error', // For signaling errors from within commands + 'parseOptions', // For custom option parsing + 'readFromPipe', // For commands with the .canReceivePipe attribute + 'register', // For registering plugins +]; + +exportedAttributes.forEach(function (attr) { + exports[attr] = common[attr]; +}); diff --git a/scripts/check-node-support.js b/scripts/check-node-support.js new file mode 100755 index 000000000..df2065e17 --- /dev/null +++ b/scripts/check-node-support.js @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +var assert = require('assert'); +var path = require('path'); + +// eslint-disable-next-line import/no-extraneous-dependencies +var yaml = require('js-yaml'); + +var shell = require('..'); + +// This is the authoritative list of supported node versions. +var MIN_NODE_VERSION = 18; +var MAX_NODE_VERSION = 26; + +// Ideally this map should be empty, however we can pin node releases to +// specific versions if necessary to workaround bugs. See +// https://github.com/shelljs/shelljs/issues/1180. +var pinnedNodeVersions = { + // Format: + // majorVersionInt: 'full.node.version', + // Example: + // 22: '22.9.0', +}; + +function checkReadme(minNodeVersion) { + var start = ''; + var stop = ''; + var formattedMinVersion = '`v' + minNodeVersion + '`'; + var expectedReadmeRegex = new RegExp( + start + '\\s*' + formattedMinVersion + '\\s*' + stop, '', + ); + var readme = path.join(__dirname, '..', 'README.md'); + var match = shell.grep(expectedReadmeRegex, readme); + if (!match.toString().trim()) { + var msg = 'Update README to specify the min supported version. Look for "' + + start + '"'; + throw new Error(msg); + } +} + +function checkEngines(minNodeVersion, packageJson) { + var expectedEnginesNode = '>=' + minNodeVersion; + if (packageJson.engines.node !== expectedEnginesNode) { + var msg = 'Update package.json to fix the "engines" attribute'; + throw new Error(msg); + } +} + +function assertDeepEquals(arr1, arr2, msg) { + try { + assert.deepStrictEqual(arr1, arr2); + } catch (e) { + throw new Error(msg + '\n' + e); + } +} + +function range(start, stop) { + var ret = []; + for (var i = start; i <= stop; i += 2) { + if (i % 2 !== 0) { + console.warn('Warning: testing a non-LTS nodejs release: ' + i); + } + ret.push(i); + } + return ret; +} + +function checkGithubActions(minNodeVersion, maxNodeVersion, githubActionsYaml) { + var expectedVersions = range(minNodeVersion, maxNodeVersion); + expectedVersions = expectedVersions.map(function (majorVersion) { + return pinnedNodeVersions[majorVersion] || majorVersion; + }); + var msg = 'Check GitHub Actions node_js versions'; + assertDeepEquals(githubActionsYaml.jobs.test.strategy.matrix['node-version'], + expectedVersions, msg); +} + +try { + checkReadme(MIN_NODE_VERSION); + + var packageJson = require('../package.json'); + checkEngines(MIN_NODE_VERSION, packageJson); + + var githubActionsFileName = path.join(__dirname, '..', '.github', 'workflows', + 'main.yml'); + var githubActionsYaml = yaml.load(shell.cat(githubActionsFileName)); + checkGithubActions(MIN_NODE_VERSION, MAX_NODE_VERSION, githubActionsYaml); + + console.log('All files look good (this project supports LTS releases v' + + MIN_NODE_VERSION + '-v' + MAX_NODE_VERSION + ')!'); +} catch (e) { + console.error('Please check the files which declare our Node version'); + console.error('support, as something is out-of-sync. This script failed'); + console.error('specificaly because:'); + throw e; +} diff --git a/scripts/generate-docs.js b/scripts/generate-docs.js index 98a9a40d0..3942c28b4 100755 --- a/scripts/generate-docs.js +++ b/scripts/generate-docs.js @@ -1,27 +1,76 @@ #!/usr/bin/env node -/* globals cat, cd, echo, grep, sed, ShellString */ +/* globals cat, cd, echo, grep, ls, sed, ShellString */ require('../global'); -echo('Appending docs to README.md'); +var path = require('path'); -cd(__dirname + '/..'); +var COMMAND_PREFIX = '## Command reference'; +var COMMAND_SUFFIX = '\n## Team'; +var COMMAND_PATTERN = /## Command reference\n\n((?:.|\n)*)\n## Team/; +function extractCurrentDocs() { + var m = cat('README.md').match(COMMAND_PATTERN); + if (m) { + var docs = m[1]; + return docs; + } + /* istanbul ignore next */ + throw new Error('Unable to extract current docs'); +} +module.exports.extractCurrentDocs = extractCurrentDocs; -// Extract docs from shell.js -var docs = grep('^//@', 'shell.js'); +function generateNewDocs() { + // Extract docs from shell.js + var docs = grep('^//@', 'shell.js'); -// Now extract docs from the appropriate src/*.js files -docs = docs.replace(/\/\/\@include (.+)/g, function(match, path) { - var file = path.match('.js$') ? path : path+'.js'; - return grep('^//@', file); -}); + // Insert the docs for all the registered commands + var blocklist = [ + './src/common.js', + './src/error.js', + './src/errorCode.js', + ]; + docs = docs.replace(/\/\/@commands\n/g, function () { + return ls('./src/*.js').map(function (file) { + if (blocklist.includes(file)) { + return ''; + } + var commandDoc = grep('^//@', file).toString(); + if (commandDoc !== '') { + commandDoc += '\n'; + } + return commandDoc; + }).join(''); + }); -// Remove '//@' -docs = docs.replace(/\/\/\@ ?/g, ''); + // Now extract docs from the remaining src/*.js files + docs = docs.replace(/\/\/@include (.+)/g, function (match, filename) { + return grep('^//@', filename); + }); -// Wipe out the old docs -ShellString(cat('README.md').replace(/## Command reference(.|\n)*\n## Team/, '## Command reference\n## Team')).to('README.md'); + // Remove '//@' + docs = docs.replace(/\/\/@ ?/g, ''); + return docs; +} +module.exports.generateNewDocs = generateNewDocs; -// Append new docs to README -sed('-i', /## Command reference/, '## Command reference\n\n' + docs, 'README.md'); +/* istanbul ignore next */ +function main() { + echo('Appending docs to README.md'); -echo('All done.'); + cd(path.join(__dirname, '..')); + + var docs = generateNewDocs(); + + // Wipe out the old docs + ShellString(cat('README.md').replace(COMMAND_PATTERN, + COMMAND_PREFIX + COMMAND_SUFFIX)).to('README.md'); + + // Append new docs to README + sed('-i', /## Command reference/, COMMAND_PREFIX + '\n\n' + docs, 'README.md'); + + echo('All done.'); +} + +/* istanbul ignore if */ +if (require.main === module) { + main(); +} diff --git a/scripts/run-tests.js b/scripts/run-tests.js deleted file mode 100755 index 1b361da22..000000000 --- a/scripts/run-tests.js +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env node -/* globals cd, echo, exec, exit, ls */ -require('../global'); - -var failed = false; - -// -// Unit tests -// -cd(__dirname + '/../test'); -ls('*.js').forEach(function(file) { - echo('Running test:', file); - if (exec(JSON.stringify(process.execPath)+' '+file).code !== 123) { // 123 avoids false positives (e.g. premature exit) - failed = true; - echo('*** TEST FAILED! (missing exit code "123")'); - echo(); - } -}); - -echo(); - -if (failed) { - echo('*******************************************************'); - echo('WARNING: Some tests did not pass!'); - echo('*******************************************************'); - exit(1); -} else { - echo('All tests passed.'); -} diff --git a/shell.js b/shell.js index c1449b661..8a3a67d30 100644 --- a/shell.js +++ b/shell.js @@ -3,133 +3,95 @@ // Unix shell commands on top of Node's API // // Copyright (c) 2012 Artur Adib -// http://github.com/arturadib/shelljs +// http://github.com/shelljs/shelljs // var common = require('./src/common'); +module.exports = common.shell; + //@ //@ All commands run synchronously, unless otherwise stated. //@ All commands accept standard bash globbing characters (`*`, `?`, etc.), -//@ compatible with the [node glob module](https://github.com/isaacs/node-glob). +//@ compatible with [`fast-glob`](https://www.npmjs.com/package/fast-glob). //@ //@ For less-commonly used commands and features, please check out our [wiki //@ page](https://github.com/shelljs/shelljs/wiki). //@ -// Boilerplate -// ----------- -// Copy the code block below here & replace variables with appropiate values -// ``` -// //@include ./src/fileName -// var functionName = require('./src/fileName'); -// exports.nameOfCommand = common.wrap(nameOfCommand, functionName, {globStart: firstIndexToExpand}); -// ``` -// -// The //@include includes the docs for that command -// -// firstIndexToExpand should usually be 1 (so, put {globStart: 1}) -// Increase this value if the command takes arguments that shouldn't be expanded -// with wildcards, such as with the regexes for sed & grep +// Include the docs for all the default commands +//@commands -//@include ./src/cd +// Load all default commands. We import these for their side effect of loading +// using the plugin architecture via `common.register()`. +require('./src/cat'); require('./src/cd'); - -//@include ./src/pwd -require('./src/pwd'); - -//@include ./src/ls -require('./src/ls'); - -//@include ./src/find -require('./src/find'); - -//@include ./src/cp +require('./src/chmod'); +require('./src/cmd'); require('./src/cp'); - -//@include ./src/rm -require('./src/rm'); - -//@include ./src/mv -require('./src/mv'); - -//@include ./src/mkdir -require('./src/mkdir'); - -//@include ./src/test -require('./src/test'); - -//@include ./src/cat -require('./src/cat'); - -//@include ./src/head +require('./src/dirs'); +require('./src/echo'); +require('./src/exec'); +require('./src/exec-child'); // A hint to the bundler to keep exec-child.js +require('./src/find'); +require('./src/grep'); require('./src/head'); - -//@include ./src/tail -require('./src/tail'); - -// The below commands have been moved to common.ShellString(), and are only here -// for generating the docs -//@include ./src/to -//@include ./src/toEnd - -//@include ./src/sed +require('./src/ln'); +require('./src/ls'); +require('./src/mkdir'); +require('./src/mv'); +require('./src/popd'); +require('./src/pushd'); +require('./src/pwd'); +require('./src/rm'); require('./src/sed'); - -//@include ./src/sort +require('./src/set'); require('./src/sort'); - -//@include ./src/grep -require('./src/grep'); - -//@include ./src/which +require('./src/tail'); +require('./src/tempdir'); +require('./src/test'); +require('./src/to'); +require('./src/toEnd'); +require('./src/touch'); +require('./src/uniq'); require('./src/which'); -//@include ./src/echo -require('./src/echo'); - -//@include ./src/dirs -require('./src/dirs'); - -//@include ./src/ln -require('./src/ln'); - //@ //@ ### exit(code) -//@ Exits the current process with the given exit code. -exports.exit = process.exit; - //@ -//@ ### env['VAR_NAME'] -//@ Object containing environment variables (both getter and setter). Shortcut to process.env. -exports.env = process.env; - -//@include ./src/exec -require('./src/exec'); - -//@include ./src/chmod -require('./src/chmod'); - -//@include ./src/touch -require('./src/touch'); - -//@include ./src/set -require('./src/set'); - +//@ Exits the current process with the given exit `code`. +module.exports.exit = function exit(code) { + common.state.error = null; + common.state.errorCode = 0; + if (code) { + common.error('exit', { + continue: true, + code, + prefix: '', + silent: true, + fatal: false, + }); + process.exit(code); + } else { + process.exit(); + } +}; + +//@include ./src/error.js +module.exports.error = require('./src/error'); + +//@include ./src/errorCode.js +module.exports.errorCode = require('./src/errorCode'); + +//@include ./src/common.js +module.exports.ShellString = common.ShellString; //@ -//@ ## Non-Unix commands +//@ ### env['VAR_NAME'] //@ - -//@include ./src/tempdir -require('./src/tempdir'); - -//@include ./src/error - -exports.error = require('./src/error'); - -//@include ./src/common -exports.ShellString = common.ShellString; +//@ Object containing environment variables (both getter and setter). Shortcut +//@ to `process.env`. +module.exports.env = process.env; //@ //@ ### Pipes @@ -138,7 +100,7 @@ exports.ShellString = common.ShellString; //@ //@ ```javascript //@ grep('foo', 'file1.txt', 'file2.txt').sed(/o/g, 'a').to('output.txt'); -//@ echo('files with o\'s in the name:\n' + ls().grep('o')); +//@ echo("files with o's in the name:\n" + ls().grep('o')); //@ cat('test.js').exec('node'); // pipe to exec() call //@ ``` //@ @@ -150,7 +112,7 @@ exports.ShellString = common.ShellString; //@ ## Configuration //@ -exports.config = common.config; +module.exports.config = common.config; //@ //@ ### config.silent @@ -180,9 +142,9 @@ exports.config = common.config; //@ /* more commands... */ //@ ``` //@ -//@ If `true` the script will throw a Javascript error when any shell.js +//@ If `true`, the script will throw a Javascript error when any shell.js //@ command encounters an error. Default is `false`. This is analogous to -//@ Bash's `set -e` +//@ Bash's `set -e`. //@ //@ ### config.verbose @@ -192,18 +154,27 @@ exports.config = common.config; //@ ```javascript //@ config.verbose = true; // or set('-v'); //@ cd('dir/'); -//@ ls('subdir/'); +//@ rm('-rf', 'foo.txt', 'bar.txt'); +//@ exec('echo hello'); //@ ``` //@ //@ Will print each command as follows: //@ //@ ``` //@ cd dir/ -//@ ls subdir/ +//@ rm -rf foo.txt bar.txt +//@ exec echo hello //@ ``` //@ -//@ ### config.globOptions +//@ ### config.globOptions (deprecated) +//@ +//@ **Deprecated**: we recommend that you do not edit `config.globOptions`. +//@ Support for this configuration option may be changed or removed in a future +//@ ShellJS release. +//@ +//@ **Breaking change**: ShellJS v0.8.x uses `node-glob`. Starting with ShellJS +//@ v0.9.x, `config.globOptions` is compatible with `fast-glob`. //@ //@ Example: //@ @@ -211,4 +182,35 @@ exports.config = common.config; //@ config.globOptions = {nodir: true}; //@ ``` //@ -//@ Use this value for calls to `glob.sync()` instead of the default options. +//@ `config.globOptions` changes how ShellJS expands glob (wildcard) +//@ expressions. See +//@ [fast-glob](https://github.com/mrmlnc/fast-glob?tab=readme-ov-file#options-3) +//@ for available options. Be aware that modifying `config.globOptions` **may +//@ break ShellJS functionality.** + +//@ +//@ ### config.reset() +//@ +//@ Example: +//@ +//@ ```javascript +//@ var shell = require('shelljs'); +//@ // Make changes to shell.config, and do stuff... +//@ /* ... */ +//@ shell.config.reset(); // reset to original state +//@ // Do more stuff, but with original settings +//@ /* ... */ +//@ ``` +//@ +//@ Reset `shell.config` to the defaults: +//@ +//@ ```javascript +//@ { +//@ fatal: false, +//@ globOptions: {}, +//@ maxdepth: 255, +//@ noglob: false, +//@ silent: false, +//@ verbose: false, +//@ } +//@ ``` diff --git a/src/cat.js b/src/cat.js index 5469cd0a1..ca264a9d1 100644 --- a/src/cat.js +++ b/src/cat.js @@ -1,11 +1,20 @@ -var common = require('./common'); var fs = require('fs'); +var common = require('./common'); -common.register('cat', _cat, {globStart: 1, canReceivePipe: true}); +common.register('cat', _cat, { + canReceivePipe: true, + cmdOptions: { + 'n': 'number', + }, +}); //@ -//@ ### cat(file [, file ...]) -//@ ### cat(file_array) +//@ ### cat([options,] file [, file ...]) +//@ ### cat([options,] file_array) +//@ +//@ Available options: +//@ +//@ + `-n`: number all output lines //@ //@ Examples: //@ @@ -15,24 +24,53 @@ common.register('cat', _cat, {globStart: 1, canReceivePipe: true}); //@ var str = cat(['file1', 'file2']); // same as above //@ ``` //@ -//@ Returns a string containing the given file, or a concatenated string -//@ containing the files if more than one file is given (a new line character is -//@ introduced between each file). +//@ Returns a [ShellString](#shellstringstr) containing the given file, or a +//@ concatenated string containing the files if more than one file is given (a +//@ new line character is introduced between each file). function _cat(options, files) { - var cat = common.readFromPipe(this); + var cat = common.readFromPipe(); - if (!files && !cat) - common.error('no paths given'); + if (!files && !cat) common.error('no paths given'); files = [].slice.call(arguments, 1); - files.forEach(function(file) { - if (!fs.existsSync(file)) + files.forEach(function (file) { + if (!fs.existsSync(file)) { common.error('no such file or directory: ' + file); + } else if (common.statFollowLinks(file).isDirectory()) { + common.error(file + ': Is a directory'); + } cat += fs.readFileSync(file, 'utf8'); }); - return new common.ShellString(cat, common.state.error, common.state.errorCode); + if (options.number) { + cat = addNumbers(cat); + } + + return cat; } module.exports = _cat; + +function addNumbers(cat) { + var lines = cat.split('\n'); + var lastLine = lines.pop(); + + lines = lines.map(function (line, i) { + return numberedLine(i + 1, line); + }); + + if (lastLine.length) { + lastLine = numberedLine(lines.length + 1, lastLine); + } + lines.push(lastLine); + + return lines.join('\n'); +} + +function numberedLine(n, line) { + // GNU cat use six pad start number + tab. See http://lingrok.org/xref/coreutils/src/cat.c#57 + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart + var number = (' ' + n).slice(-6) + '\t'; + return number + line; +} diff --git a/src/cd.js b/src/cd.js index 0b6ef5375..1c6e73fff 100644 --- a/src/cd.js +++ b/src/cd.js @@ -1,21 +1,23 @@ -var fs = require('fs'); +var os = require('os'); var common = require('./common'); -common.register('cd', _cd, {globStart: 1}); +common.register('cd', _cd, {}); //@ //@ ### cd([dir]) +//@ //@ Changes to directory `dir` for the duration of the script. Changes to home -//@ directory if no argument is supplied. +//@ directory if no argument is supplied. Returns a +//@ [ShellString](#shellstringstr) to indicate success or failure. function _cd(options, dir) { - if (!dir) - dir = common.getUserHome(); + if (!dir) dir = os.homedir(); if (dir === '-') { - if (!process.env.OLDPWD) + if (!process.env.OLDPWD) { common.error('could not find previous directory'); - else + } else { dir = process.env.OLDPWD; + } } try { @@ -26,13 +28,13 @@ function _cd(options, dir) { // something went wrong, let's figure out the error var err; try { - fs.statSync(dir); // if this succeeds, it must be some sort of file + common.statFollowLinks(dir); // if this succeeds, it must be some sort of file err = 'not a directory: ' + dir; - } catch (e) { + } catch (e2) { err = 'no such file or directory: ' + dir; } if (err) common.error(err); } - return new common.ShellString('', common.state.error, common.state.errorCode); + return ''; } module.exports = _cd; diff --git a/src/chmod.js b/src/chmod.js index 16588f71d..b930cc764 100644 --- a/src/chmod.js +++ b/src/chmod.js @@ -1,45 +1,45 @@ -var common = require('./common'); var fs = require('fs'); var path = require('path'); +var common = require('./common'); var PERMS = (function (base) { return { - OTHER_EXEC : base.EXEC, - OTHER_WRITE : base.WRITE, - OTHER_READ : base.READ, + OTHER_EXEC: base.EXEC, + OTHER_WRITE: base.WRITE, + OTHER_READ: base.READ, - GROUP_EXEC : base.EXEC << 3, - GROUP_WRITE : base.WRITE << 3, - GROUP_READ : base.READ << 3, + GROUP_EXEC: base.EXEC << 3, + GROUP_WRITE: base.WRITE << 3, + GROUP_READ: base.READ << 3, - OWNER_EXEC : base.EXEC << 6, - OWNER_WRITE : base.WRITE << 6, - OWNER_READ : base.READ << 6, + OWNER_EXEC: base.EXEC << 6, + OWNER_WRITE: base.WRITE << 6, + OWNER_READ: base.READ << 6, - // Literal octal numbers are apparently not allowed in "strict" javascript. Using parseInt is - // the preferred way, else a jshint warning is thrown. - STICKY : parseInt('01000', 8), - SETGID : parseInt('02000', 8), - SETUID : parseInt('04000', 8), + // Literal octal numbers are apparently not allowed in "strict" javascript. + STICKY: parseInt('01000', 8), + SETGID: parseInt('02000', 8), + SETUID: parseInt('04000', 8), - TYPE_MASK : parseInt('0770000', 8) + TYPE_MASK: parseInt('0770000', 8), }; -})({ - EXEC : 1, - WRITE : 2, - READ : 4 -}); +}({ + EXEC: 1, + WRITE: 2, + READ: 4, +})); -common.register('chmod', _chmod, {globStart: 1}); +common.register('chmod', _chmod, { +}); //@ -//@ ### chmod(octal_mode || octal_string, file) -//@ ### chmod(symbolic_mode, file) +//@ ### chmod([options,] octal_mode || octal_string, file) +//@ ### chmod([options,] symbolic_mode, file) //@ //@ Available options: //@ //@ + `-v`: output a diagnostic for every file processed//@ -//@ + `-c`: like verbose but report only when a change is made//@ +//@ + `-c`: like verbose, but report only when a change is made//@ //@ + `-R`: change files and directories recursively//@ //@ //@ Examples: @@ -48,6 +48,7 @@ common.register('chmod', _chmod, {globStart: 1}); //@ chmod(755, '/Users/brandon'); //@ chmod('755', '/Users/brandon'); // same as above //@ chmod('u+x', '/Users/brandon'); +//@ chmod('-R', 'a-w', '/Users/brandon'); //@ ``` //@ //@ Alters the permissions of a file or directory by either specifying the @@ -55,9 +56,15 @@ common.register('chmod', _chmod, {globStart: 1}); //@ This command tries to mimic the POSIX behavior as much as possible. //@ Notable exceptions: //@ -//@ + In symbolic modes, 'a-r' and '-r' are identical. No consideration is -//@ given to the umask. -//@ + There is no "quiet" option since default behavior is to run silent. +//@ + In symbolic modes, `a-r` and `-r` are identical. No consideration is +//@ given to the `umask`. +//@ + There is no "quiet" option, since default behavior is to run silent. +//@ + Windows OS uses a very different permission model than POSIX. `chmod()` +//@ does its best on Windows, but there are limits to how file permissions can +//@ be set. Note that WSL (Windows subsystem for Linux) **does** follow POSIX, +//@ so cross-platform compatibility should not be a concern there. +//@ +//@ Returns a [ShellString](#shellstringstr) indicating success or failure. function _chmod(options, mode, filePattern) { if (!filePattern) { if (options.length > 0 && options.charAt(0) === '-') { @@ -65,8 +72,7 @@ function _chmod(options, mode, filePattern) { // get picked up by the option parser as command flags. // If we are down by one argument and options starts with -, shift everything over. [].unshift.call(arguments, ''); - } - else { + } else { common.error('You must specify a file.'); } } @@ -74,7 +80,7 @@ function _chmod(options, mode, filePattern) { options = common.parseOptions(options, { 'R': 'recursive', 'c': 'changes', - 'v': 'verbose' + 'v': 'verbose', }); filePattern = [].slice.call(arguments, 2); @@ -85,7 +91,7 @@ function _chmod(options, mode, filePattern) { if (options.recursive) { files = []; filePattern.forEach(function addFile(expandedFile) { - var stat = fs.lstatSync(expandedFile); + var stat = common.statNoFollowLinks(expandedFile); if (!stat.isSymbolicLink()) { files.push(expandedFile); @@ -97,8 +103,7 @@ function _chmod(options, mode, filePattern) { } } }); - } - else { + } else { files = filePattern; } @@ -109,22 +114,21 @@ function _chmod(options, mode, filePattern) { } // When recursing, don't follow symlinks. - if (options.recursive && fs.lstatSync(file).isSymbolicLink()) { + if (options.recursive && common.statNoFollowLinks(file).isSymbolicLink()) { return; } - var stat = fs.statSync(file); + var stat = common.statFollowLinks(file); var isDir = stat.isDirectory(); var perms = stat.mode; var type = perms & PERMS.TYPE_MASK; var newPerms = perms; - if (isNaN(parseInt(mode, 8))) { + if (Number.isNaN(parseInt(mode, 8))) { // parse options mode.split(',').forEach(function (symbolicMode) { - /*jshint regexdash:true */ - var pattern = /([ugoa]*)([=\+-])([rwxXst]*)/i; + var pattern = /([ugoa]*)([=+-])([rwxXst]*)/i; var matches = pattern.exec(symbolicMode); if (matches) { @@ -132,19 +136,20 @@ function _chmod(options, mode, filePattern) { var operator = matches[2]; var change = matches[3]; - var changeOwner = applyTo.indexOf('u') != -1 || applyTo === 'a' || applyTo === ''; - var changeGroup = applyTo.indexOf('g') != -1 || applyTo === 'a' || applyTo === ''; - var changeOther = applyTo.indexOf('o') != -1 || applyTo === 'a' || applyTo === ''; + var changeOwner = applyTo.includes('u') || applyTo === 'a' || applyTo === ''; + var changeGroup = applyTo.includes('g') || applyTo === 'a' || applyTo === ''; + var changeOther = applyTo.includes('o') || applyTo === 'a' || applyTo === ''; - var changeRead = change.indexOf('r') != -1; - var changeWrite = change.indexOf('w') != -1; - var changeExec = change.indexOf('x') != -1; - var changeExecDir = change.indexOf('X') != -1; - var changeSticky = change.indexOf('t') != -1; - var changeSetuid = change.indexOf('s') != -1; + var changeRead = change.includes('r'); + var changeWrite = change.includes('w'); + var changeExec = change.includes('x'); + var changeExecDir = change.includes('X'); + var changeSticky = change.includes('t'); + var changeSetuid = change.includes('s'); - if (changeExecDir && isDir) + if (changeExecDir && isDir) { changeExec = true; + } var mask = 0; if (changeOwner) { @@ -174,42 +179,44 @@ function _chmod(options, mode, filePattern) { case '=': newPerms = type + mask; - // According to POSIX, when using = to explicitly set the permissions, setuid and setgid can never be cleared. - if (fs.statSync(file).isDirectory()) { + // According to POSIX, when using = to explicitly set the + // permissions, setuid and setgid can never be cleared. + if (common.statFollowLinks(file).isDirectory()) { newPerms |= (PERMS.SETUID + PERMS.SETGID) & perms; } break; + default: + common.error('Could not recognize operator: `' + operator + '`'); } if (options.verbose) { console.log(file + ' -> ' + newPerms.toString(8)); } - if (perms != newPerms) { + if (perms !== newPerms) { if (!options.verbose && options.changes) { console.log(file + ' -> ' + newPerms.toString(8)); } fs.chmodSync(file, newPerms); perms = newPerms; // for the next round of changes! } - } - else { + } else { common.error('Invalid symbolic mode change: ' + symbolicMode); } }); - } - else { + } else { // they gave us a full number newPerms = type + parseInt(mode, 8); - // POSIX rules are that setuid and setgid can only be added using numeric form, but not cleared. - if (fs.statSync(file).isDirectory()) { + // POSIX rules are that setuid and setgid can only be added using numeric + // form, but not cleared. + if (common.statFollowLinks(file).isDirectory()) { newPerms |= (PERMS.SETUID + PERMS.SETGID) & perms; } fs.chmodSync(file, newPerms); } }); - return new common.ShellString('', common.state.error, common.state.errorCode); + return ''; } module.exports = _chmod; diff --git a/src/cmd.js b/src/cmd.js new file mode 100644 index 000000000..a00d6c497 --- /dev/null +++ b/src/cmd.js @@ -0,0 +1,138 @@ +var execa = require('execa'); +var common = require('./common'); + +var DEFAULT_MAXBUFFER_SIZE = 20 * 1024 * 1024; +var COMMAND_NOT_FOUND_ERROR_CODE = 127; + +common.register('cmd', _cmd, { + cmdOptions: null, + globStart: 1, + canReceivePipe: true, + wrapOutput: true, +}); + +function isCommandNotFound(execaResult) { + if (process.platform === 'win32') { + var str = 'is not recognized as an internal or external command'; + return execaResult.exitCode && execaResult.stderr.includes(str); + } + return execaResult.failed && execaResult.code === 'ENOENT'; +} + +function isExecaInternalError(result) { + if (typeof result.stdout !== 'string') return true; + if (typeof result.stderr !== 'string') return true; + if (typeof result.exitCode !== 'number') return true; + if (result.exitCode === 0 && result.failed) return true; + // Otherwise assume this executed correctly. The command may still have exited + // with non-zero status, but that's not due to anything execa did. + return false; +} + +//@ +//@ ### cmd(arg1[, arg2, ...] [, options]) +//@ +//@ Available options: +//@ +//@ + `cwd: directoryPath`: change the current working directory only for this +//@ cmd() invocation. +//@ + `maxBuffer: num`: Raise or decrease the default buffer size for +//@ stdout/stderr. +//@ + `timeout`: Change the default timeout. +//@ +//@ Examples: +//@ +//@ ```javascript +//@ var version = cmd('node', '--version').stdout; +//@ cmd('git', 'commit', '-am', `Add suport for node ${version}`); +//@ console.log(cmd('echo', '1st arg', '2nd arg', '3rd arg').stdout) +//@ console.log(cmd('echo', 'this handles ;, |, &, etc. as literal characters').stdout) +//@ ``` +//@ +//@ Executes the given command synchronously. This is intended as an easier +//@ alternative for [exec()](#execcommand--options--callback), with better +//@ security around globbing, comamnd injection, and variable expansion. This is +//@ guaranteed to only run one external command, and won't give special +//@ treatment for any shell characters (ex. this treats `|` as a literal +//@ character, not as a shell pipeline). +//@ This returns a [ShellString](#shellstringstr). +//@ +//@ By default, this performs globbing on all platforms, but you can disable +//@ this with `set('-f')`. +//@ +//@ This **does not** support asynchronous mode. If you need asynchronous +//@ command execution, check out [execa](https://www.npmjs.com/package/execa) or +//@ the node builtin `child_process.execFile()` instead. +function _cmd(options, command, commandArgs, userOptions) { + if (!command) { + common.error('Must specify a non-empty string as a command'); + } + + // `options` will usually not have a value: it's added by our commandline flag + // parsing engine. + commandArgs = [].slice.call(arguments, 2); + + // `userOptions` may or may not be provided. We need to check the last + // argument. If it's an object, assume it's meant to be passed as + // userOptions (since ShellStrings are already flattened to strings). + if (commandArgs.length === 0) { + userOptions = {}; + } else { + var lastArg = commandArgs.pop(); + if (common.isObject(lastArg)) { + userOptions = lastArg; + } else { + userOptions = {}; + commandArgs.push(lastArg); + } + } + + var pipe = common.readFromPipe(); + + // Some of our defaults differ from execa's defaults. These can be overridden + // by the user. + var defaultOptions = { + maxBuffer: DEFAULT_MAXBUFFER_SIZE, + stripFinalNewline: false, // Preserve trailing newlines for consistency with unix. + reject: false, // Use ShellJS's error handling system. + }; + + // For other options, we forbid the user from overriding them (either for + // correctness or security). + var requiredOptions = { + input: pipe, + shell: false, + }; + + var execaOptions = + Object.assign(defaultOptions, userOptions, requiredOptions); + + var result = execa.sync(command, commandArgs, execaOptions); + var stdout; + var stderr; + var code; + if (isCommandNotFound(result)) { + // This can happen if `command` is not an executable binary, or possibly + // under other conditions. + stdout = ''; + stderr = "'" + command + "': command not found"; + code = COMMAND_NOT_FOUND_ERROR_CODE; + } else if (isExecaInternalError(result)) { + // Catch-all: execa tried to run `command` but it encountered some error + // (ex. maxBuffer, timeout). + stdout = result.stdout || ''; + stderr = result.stderr || + `'${command}' encountered an error during execution`; + code = result.exitCode !== undefined && result.exitCode > 0 ? result.exitCode : 1; + } else { + // Normal exit: execa was able to execute `command` and get a return value. + stdout = result.stdout.toString(); + stderr = result.stderr.toString(); + code = result.exitCode; + } + + // Pass `continue: true` so we can specify a value for stdout. + if (code) common.error(stderr, code, { silent: true, continue: true }); + return new common.ShellString(stdout, stderr, code); +} +module.exports = _cmd; diff --git a/src/common.js b/src/common.js index 3986d4212..d46e3c9b8 100644 --- a/src/common.js +++ b/src/common.js @@ -1,80 +1,136 @@ -// jshint -W053 -// Ignore warning about 'new String()' +// Ignore warning about 'new String()' and use of the Buffer constructor +/* eslint no-new-wrappers: "off", + no-buffer-constructor: "off" */ + 'use strict'; var os = require('os'); var fs = require('fs'); -var glob = require('glob'); -var shell = require('..'); -var _to = require('./to'); -var _toEnd = require('./toEnd'); +var glob = require('fast-glob'); -var DEFAULT_ERROR_CODE = 1; +var shell = {}; +exports.shell = shell; -// Module globals -var config = { - silent: false, +var shellMethods = Object.create(shell); + +exports.extend = Object.assign; + +// Check if we're running under electron +var isElectron = Boolean(process.versions.electron); + +// Module globals (assume no execPath by default) +var DEFAULT_CONFIG = { fatal: false, - verbose: false, - noglob: false, globOptions: {}, - maxdepth: 255 + maxdepth: 255, + noglob: false, + silent: false, + verbose: false, + execPath: null, + bufLength: 64 * 1024, // 64KB +}; + +var config = { + reset() { + Object.assign(this, DEFAULT_CONFIG); + if (!isElectron) { + this.execPath = process.execPath; + } + }, + resetForTesting() { + this.reset(); + this.silent = true; + }, }; + +config.reset(); exports.config = config; +// Note: commands should generally consider these as read-only values. var state = { error: null, errorCode: 0, currentCmd: 'shell.js', - tempDir: null }; exports.state = state; delete process.env.OLDPWD; // initially, there's no previous directory -var platform = os.type().match(/^Win/) ? 'win' : 'unix'; -exports.platform = platform; - -// This is populated by calls to commonl.wrap() -var pipeMethods = []; +// Reliably test if something is any sort of javascript object +function isObject(a) { + return typeof a === 'object' && a !== null; +} +exports.isObject = isObject; function log() { - if (!config.silent) + /* istanbul ignore next */ + if (!config.silent) { console.error.apply(console, arguments); + } } exports.log = log; -// Shows error message. Throws if config.fatal is true -function error(msg, _code, _continue) { - if (typeof _code === 'boolean') { - _continue = _code; - _code = DEFAULT_ERROR_CODE; +// Converts strings to be equivalent across all platforms. Primarily responsible +// for making sure we use '/' instead of '\' as path separators, but this may be +// expanded in the future if necessary +function convertErrorOutput(msg) { + if (typeof msg !== 'string') { + throw new TypeError('input must be a string'); + } + return msg.replace(/\\/g, '/'); +} +exports.convertErrorOutput = convertErrorOutput; + +// An exception class to help propagate command errors (e.g., non-zero exit +// status) up to the top-level. {@param value} should be a ShellString. +class CommandError extends Error { + constructor(value) { + super(value.toString()); + this.returnValue = value; + } +} +exports.CommandError = CommandError; // visible for testing + +// Shows error message. Throws if fatal is true (defaults to config.fatal, overridable with options.fatal) +function error(msg, _code, options) { + // Validate input + if (typeof msg !== 'string') throw new Error('msg must be a string'); + + var DEFAULT_OPTIONS = { + continue: false, + code: 1, + prefix: state.currentCmd + ': ', + silent: false, + fatal: config.fatal, + }; + + if (typeof _code === 'number' && isObject(options)) { + options.code = _code; + } else if (isObject(_code)) { // no 'code' + options = _code; + } else if (typeof _code === 'number') { // no 'options' + options = { code: _code }; + } else if (typeof _code !== 'number') { // only 'msg' + options = {}; + } + options = Object.assign({}, DEFAULT_OPTIONS, options); + + if (!state.errorCode) state.errorCode = options.code; + + var logEntry = convertErrorOutput(options.prefix + msg); + state.error = state.error ? state.error + '\n' : ''; + state.error += logEntry; + + // Throw an error, or log the entry + if (options.fatal) { + var err = new Error(logEntry); + err.code = options.code; + throw err; } - if (typeof _code !== 'number') - _code = DEFAULT_ERROR_CODE; - - if (state.errorCode === 0) - state.errorCode = _code; - - if (state.error === null) - state.error = ''; - var log_entry = state.currentCmd + ': ' + msg; - if (state.error === '') - state.error = log_entry; - else - state.error += '\n' + log_entry; - - if(config.fatal) - throw new Error(log_entry); - - if (msg.length > 0) - log(log_entry); - - if(!_continue) { - throw { - msg: 'earlyExit', - retValue: (new ShellString('', state.error, state.errorCode)) - }; + if (msg.length > 0 && !options.silent) log(logEntry); + + if (!options.continue) { + throw new CommandError(new ShellString('', state.error, state.errorCode)); } } exports.error = error; @@ -85,11 +141,19 @@ exports.error = error; //@ Examples: //@ //@ ```javascript -//@ var foo = ShellString('hello world'); +//@ var foo = new ShellString('hello world'); //@ ``` //@ -//@ Turns a regular string into a string-like object similar to what each -//@ command returns. This has special methods, like `.to()` and `.toEnd()` +//@ This is a dedicated type returned by most ShellJS methods, which wraps a +//@ string (or array) value. This has all the string (or array) methods, but +//@ also exposes extra methods: [`.to()`](#shellstringprototypetofile), +//@ [`.toEnd()`](#shellstringprototypetoendfile), and all the pipe-able methods +//@ (ex. `.cat()`, `.grep()`, etc.). This can be easily converted into a string +//@ by calling `.toString()`. +//@ +//@ This type also exposes the corresponding command's stdout, stderr, and +//@ return status code via the `.stdout` (string), `.stderr` (string), and +//@ `.code` (number) properties respectively. function ShellString(stdout, stderr, code) { var that; if (stdout instanceof Array) { @@ -102,85 +166,166 @@ function ShellString(stdout, stderr, code) { } that.stderr = stderr; that.code = code; - that.to = function() {wrap('to', _to, {globStart: 1}).apply(that.stdout, arguments); return that;}; - that.toEnd = function() {wrap('toEnd', _toEnd, {globStart: 1}).apply(that.stdout, arguments); return that;}; // A list of all commands that can appear on the right-hand side of a pipe // (populated by calls to common.wrap()) pipeMethods.forEach(function (cmd) { - that[cmd] = function() {return shell[cmd].apply(that.stdout, arguments);}; + that[cmd] = shellMethods[cmd].bind(that); }); return that; } exports.ShellString = ShellString; -// Return the home directory in a platform-agnostic way, with consideration for -// older versions of node -function getUserHome() { - var result; - if (os.homedir) - result = os.homedir(); // node 3+ - else - result = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; - return result; -} -exports.getUserHome = getUserHome; - // Returns {'alice': true, 'bob': false} when passed a string and dictionary as follows: // parseOptions('-a', {'a':'alice', 'b':'bob'}); // Returns {'reference': 'string-value', 'bob': false} when passed two dictionaries of the form: // parseOptions({'-r': 'string-value'}, {'r':'reference', 'b':'bob'}); -function parseOptions(opt, map) { - if (!map) - error('parseOptions() internal error: no map given'); +// Throws an error when passed a string that does not start with '-': +// parseOptions('a', {'a':'alice'}); // throws +function parseOptions(opt, map, errorOptions) { + errorOptions = errorOptions || {}; + // Validate input + if (typeof opt !== 'string' && !isObject(opt)) { + throw new TypeError('options must be strings or key-value pairs'); + } else if (!isObject(map)) { + throw new TypeError('parseOptions() internal error: map must be an object'); + } else if (!isObject(errorOptions)) { + throw new TypeError( + 'parseOptions() internal error: errorOptions must be object', + ); + } + + if (opt === '--') { + // This means there are no options. + return {}; + } // All options are false by default var options = {}; - for (var letter in map) { - if (map[letter][0] !== '!') - options[map[letter]] = false; - } + Object.keys(map).forEach(function (letter) { + var optName = map[letter]; + if (optName[0] !== '!') { + options[optName] = false; + } + }); - if (!opt) - return options; // defaults + if (opt === '') return options; // defaults - var optionName; if (typeof opt === 'string') { - if (opt[0] !== '-') - return options; + if (opt[0] !== '-') { + throw new Error("Options string must start with a '-'"); + } // e.g. chars = ['R', 'f'] var chars = opt.slice(1).split(''); - chars.forEach(function(c) { + chars.forEach(function (c) { if (c in map) { - optionName = map[c]; - if (optionName[0] === '!') - options[optionName.slice(1, optionName.length-1)] = false; - else + var optionName = map[c]; + if (optionName[0] === '!') { + options[optionName.slice(1)] = false; + } else { options[optionName] = true; + } } else { - error('option not recognized: '+c); + error('option not recognized: ' + c, errorOptions); } }); - } else if (typeof opt === 'object') { - for (var key in opt) { - // key is a string of the form '-r', '-d', etc. - var c = key[1]; - if (c in map) { - optionName = map[c]; - options[optionName] = opt[key]; // assign the given value + } else { // opt is an Object + Object.keys(opt).forEach(function (key) { + if (key[0] === '-') { + // key is a string of the form '-r', '-d', etc. + var c = key[1]; + if (c in map) { + var optionName = map[c]; + options[optionName] = opt[key]; // assign the given value + } else { + error('option not recognized: ' + c, errorOptions); + } + } else if (key in options) { + // key is a "long option", so it should be the same + options[key] = opt[key]; } else { - error('option not recognized: '+c); + error('option not recognized: {' + key + ':...}', errorOptions); } - } - } else { - error('options must be strings or key-value pairs'); + }); } return options; } exports.parseOptions = parseOptions; +function globOptions() { + // These options are just to make fast-glob be compatible with POSIX (bash) + // wildcard behavior. + var defaultGlobOptions = { + onlyFiles: false, + followSymbolicLinks: false, + }; + + var newGlobOptions = Object.assign({}, config.globOptions); + var optionRenames = { + // node-glob's 'nodir' is not quote the same as fast-glob's 'onlyFiles'. + // Compatibility for this is implemented at the call site. + mark: 'markDirectories', + matchBase: 'baseNameMatch', + }; + Object.keys(optionRenames).forEach(function (oldKey) { + var newKey = optionRenames[oldKey]; + if (oldKey in config.globOptions) { + newGlobOptions[newKey] = config.globOptions[oldKey]; + } + }); + var invertedOptionRenames = { + nobrace: 'braceExpansion', + noglobstar: 'globstar', + noext: 'extglob', + nocase: 'caseSensitiveMatch', + }; + Object.keys(invertedOptionRenames).forEach(function (oldKey) { + var newKey = invertedOptionRenames[oldKey]; + if (oldKey in config.globOptions) { + newGlobOptions[newKey] = !config.globOptions[oldKey]; + } + }); + return Object.assign({}, defaultGlobOptions, newGlobOptions); +} + + +// Workaround for https://github.com/shelljs/shelljs/issues/1197 +// fast-glob's dependency glob-parent does not recognize the '?' wildcard in +// non-final path segments, causing those patterns to be treated as literal +// directory names. Replace standalone '?' with the equivalent character class +// '[^/]' which glob-parent handles correctly. +function convertQuestionMarkForGlob(pattern) { + if (pattern.indexOf('?') === -1) { + return pattern; + } + var result = ''; + var inBracket = false; + for (var i = 0; i < pattern.length; i++) { + var ch = pattern[i]; + if (ch === '\\' && i + 1 < pattern.length) { + // Escaped character: keep as-is + result += ch + pattern[i + 1]; + i++; + } else if (ch === '[' && !inBracket) { + inBracket = true; + result += ch; + } else if (ch === ']' && inBracket) { + inBracket = false; + result += ch; + } else if (ch === '?' && !inBracket && pattern[i + 1] === '(') { + // Extglob pattern ?(...) keep as-is + result += ch; + } else if (ch === '?' && !inBracket) { + result += '[^/]'; + } else { + result += ch; + } + } + return result; +} + // Expands wildcards with matching (ie. existing) file names. // For example: // expand(['file*.js']) = ['file1.js', 'file2.js', ...] @@ -190,27 +335,52 @@ function expand(list) { throw new TypeError('must be an array'); } var expanded = []; - list.forEach(function(listEl) { + list.forEach(function (listEl) { // Don't expand non-strings if (typeof listEl !== 'string') { expanded.push(listEl); } else { - var ret = glob.sync(listEl, config.globOptions); - // if glob fails, interpret the string literally - expanded = expanded.concat(ret.length > 0 ? ret : [listEl]); + var ret; + var globOpts = globOptions(); + try { + ret = glob.sync(convertQuestionMarkForGlob(listEl), globOpts); + } catch (e) { + // if glob fails, interpret the string literally + ret = [listEl]; + } + // if nothing matched, interpret the string literally + ret = ret.length > 0 ? ret.sort() : [listEl]; + if (globOpts.nodir) { + ret = ret.filter(function (file) { + return !statNoFollowLinks(file).isDirectory(); + }); + } + expanded = expanded.concat(ret); } }); return expanded; } exports.expand = expand; +// Normalizes Buffer creation, using Buffer.alloc if possible. +// Also provides a good default buffer length for most use cases. +var buffer = typeof Buffer.alloc === 'function' ? + function (len) { + return Buffer.alloc(len || config.bufLength); + } : + function (len) { + return new Buffer(len || config.bufLength); + }; +exports.buffer = buffer; + // Normalizes _unlinkSync() across platforms to match Unix behavior, i.e. // file can be unlinked even if it's read-only, see https://github.com/joyent/node/issues/3006 function unlinkSync(file) { try { fs.unlinkSync(file); - } catch(e) { + } catch (e) { // Try to override file permission + /* istanbul ignore next */ if (e.code === 'EPERM') { fs.chmodSync(file, '0666'); fs.unlinkSync(file); @@ -221,45 +391,40 @@ function unlinkSync(file) { } exports.unlinkSync = unlinkSync; +// wrappers around common.statFollowLinks and common.statNoFollowLinks that clarify intent +// and improve readability +function statFollowLinks() { + return fs.statSync.apply(fs, arguments); +} +exports.statFollowLinks = statFollowLinks; + +function statNoFollowLinks() { + return fs.lstatSync.apply(fs, arguments); +} +exports.statNoFollowLinks = statNoFollowLinks; + // e.g. 'shelljs_a5f185d0443ca...' function randomFileName() { function randomHash(count) { - if (count === 1) - return parseInt(16*Math.random(), 10).toString(16); - else { - var hash = ''; - for (var i=0; i= common.config.maxdepth) { - // Max depth has been reached, end copy. - return; - } else { - opts.depth++; - } + // Ensure there is not a run away recursive copy + if (currentDepth >= common.config.maxdepth) return; + currentDepth++; - /* Create the directory where all our junk is moving to; read the mode of the source directory and mirror it */ + var isWindows = process.platform === 'win32'; + + // Create the directory where all our junk is moving to; read the mode/etc. of + // the source directory (we'll set this on the destDir at the end). + var checkDir = common.statFollowLinks(sourceDir); try { - var checkDir = fs.statSync(sourceDir); - fs.mkdirSync(destDir, checkDir.mode); + fs.mkdirSync(destDir); } catch (e) { - //if the directory already exists, that's okay + // if the directory already exists, that's okay if (e.code !== 'EEXIST') throw e; } var files = fs.readdirSync(sourceDir); for (var i = 0; i < files.length; i++) { - var srcFile = sourceDir + "/" + files[i]; - var destFile = destDir + "/" + files[i]; - var srcFileStat = fs.lstatSync(srcFile); + var srcFile = sourceDir + '/' + files[i]; + var destFile = destDir + '/' + files[i]; + var srcFileStat = common.statNoFollowLinks(srcFile); var symlinkFull; if (opts.followsymlink) { @@ -99,46 +132,63 @@ function cpdirSyncRecursive(sourceDir, destDir, opts) { // Cycle link found. console.error('Cycle link found.'); symlinkFull = fs.readlinkSync(srcFile); - fs.symlinkSync(symlinkFull, destFile, os.platform() === "win32" ? "junction" : null); + fs.symlinkSync(symlinkFull, destFile, isWindows ? 'junction' : null); continue; } } if (srcFileStat.isDirectory()) { /* recursion this thing right on back. */ - cpdirSyncRecursive(srcFile, destFile, opts); + cpdirSyncRecursive(srcFile, destFile, currentDepth, opts); } else if (srcFileStat.isSymbolicLink() && !opts.followsymlink) { symlinkFull = fs.readlinkSync(srcFile); try { - fs.lstatSync(destFile); + common.statNoFollowLinks(destFile); common.unlinkSync(destFile); // re-link it } catch (e) { // it doesn't exist, so no work needs to be done } - fs.symlinkSync(symlinkFull, destFile, os.platform() === "win32" ? "junction" : null); + fs.symlinkSync(symlinkFull, destFile, isWindows ? 'junction' : null); } else if (srcFileStat.isSymbolicLink() && opts.followsymlink) { - srcFileStat = fs.statSync(srcFile); + srcFileStat = common.statFollowLinks(srcFile); if (srcFileStat.isDirectory()) { - cpdirSyncRecursive(srcFile, destFile, opts); + cpdirSyncRecursive(srcFile, destFile, currentDepth, opts); } else { copyFileSync(srcFile, destFile, opts); } + } else if (fs.existsSync(destFile) && opts.no_force) { + common.log('skipping existing file: ' + files[i]); } else { - /* At this point, we've hit a file actually worth copying... so copy it on over. */ - if (fs.existsSync(destFile) && opts.no_force) { - common.log('skipping existing file: ' + files[i]); - } else { - copyFileSync(srcFile, destFile, opts); - } + copyFileSync(srcFile, destFile, opts); } - } // for files + + // finally change the mode for the newly created directory (otherwise, we + // couldn't add files to a read-only directory). + // var checkDir = common.statFollowLinks(sourceDir); + if (opts.preserve) { + fs.utimesSync(destDir, checkDir.atime, checkDir.mtime); + } + fs.chmodSync(destDir, checkDir.mode); } // cpdirSyncRecursive +// Checks if cureent file was created recently +function checkRecentCreated(sources, index) { + var lookedSource = sources[index]; + return sources.slice(0, index).some(function (src) { + return path.basename(src) === path.basename(lookedSource); + }); +} + function cpcheckcycle(sourceDir, srcFile) { - var srcFileStat = fs.lstatSync(srcFile); + var srcFileStat = common.statNoFollowLinks(srcFile); if (srcFileStat.isSymbolicLink()) { - // Do cycle check. For example mkdir -p 1/2/3/4 ; cd 1/2/3/4; ln -s ../../3 link ; cd ../../../.. ; cp -RL 1 copy - var cyclecheck = fs.statSync(srcFile); + // Do cycle check. For example: + // $ mkdir -p 1/2/3/4 + // $ cd 1/2/3/4 + // $ ln -s ../../3 link + // $ cd ../../../.. + // $ cp -RL 1 copy + var cyclecheck = common.statFollowLinks(srcFile); if (cyclecheck.isDirectory()) { var sourcerealpath = fs.realpathSync(sourceDir); var symlinkrealpath = fs.realpathSync(srcFile); @@ -154,13 +204,16 @@ function cpcheckcycle(sourceDir, srcFile) { //@ //@ ### cp([options,] source [, source ...], dest) //@ ### cp([options,] source_array, dest) +//@ //@ Available options: //@ //@ + `-f`: force (default behavior) //@ + `-n`: no-clobber +//@ + `-u`: only copy if `source` is newer than `dest` //@ + `-r`, `-R`: recursive //@ + `-L`: follow symlinks //@ + `-P`: don't follow symlinks +//@ + `-p`: preserve file mode, ownership, and timestamps //@ //@ Examples: //@ @@ -171,22 +224,16 @@ function cpcheckcycle(sourceDir, srcFile) { //@ cp('-Rf', ['/tmp/*', '/usr/local/*'], '/home/tmp'); // same as above //@ ``` //@ -//@ Copies files. +//@ Copies files. Returns a [ShellString](#shellstringstr) indicating success +//@ or failure. function _cp(options, sources, dest) { - options = common.parseOptions(options, { - 'f': '!no_force', - 'n': 'no_force', - 'R': 'recursive', - 'r': 'recursive', - 'L': 'followsymlink', - 'P': 'noFollowsymlink', - }); - // If we're missing -R, it actually implies -L (unless -P is explicit) - if (options.followsymlink) + if (options.followsymlink) { options.noFollowsymlink = false; - if (!options.recursive && !options.noFollowsymlink) + } + if (!options.recursive && !options.noFollowsymlink) { options.followsymlink = true; + } // Get sources, dest if (arguments.length < 3) { @@ -196,27 +243,30 @@ function _cp(options, sources, dest) { dest = arguments[arguments.length - 1]; } - var destExists = fs.existsSync(dest), - destStat = destExists && fs.statSync(dest); + var destExists = fs.existsSync(dest); + var destStat = destExists && common.statFollowLinks(dest); // Dest is not existing dir, but multiple sources given - if ((!destExists || !destStat.isDirectory()) && sources.length > 1) + if ((!destExists || !destStat.isDirectory()) && sources.length > 1) { common.error('dest is not a directory (too many sources)'); + } // Dest is an existing file, but -n is given - if (destExists && destStat.isFile() && options.no_force) + if (destExists && destStat.isFile() && options.no_force) { return new common.ShellString('', '', 0); + } - sources.forEach(function(src) { + sources.forEach(function (src, srcIndex) { if (!fs.existsSync(src)) { - common.error('no such file or directory: '+src, true); + if (src === '') src = "''"; // if src was empty string, display empty string + common.error('no such file or directory: ' + src, { continue: true }); return; // skip file } - var srcStat = fs.statSync(src); + var srcStat = common.statFollowLinks(src); if (!options.noFollowsymlink && srcStat.isDirectory()) { if (!options.recursive) { // Non-Recursive - common.error("omitting directory '" + src + "'", true); + common.error("omitting directory '" + src + "'", { continue: true }); } else { // Recursive // 'cp /a/source dest' should create 'source' in 'dest' @@ -224,30 +274,47 @@ function _cp(options, sources, dest) { path.join(dest, path.basename(src)) : dest; + if (path.relative(src, newDest) === '') { + // a directory cannot be copied into itself, but we want to continue copying other sources + common.error("'" + newDest + "' and '" + src + "' are the same file", { continue: true }); + return; + } + try { - fs.statSync(path.dirname(dest)); - cpdirSyncRecursive(src, newDest, {no_force: options.no_force, followsymlink: options.followsymlink}); - } catch(e) { + common.statFollowLinks(path.dirname(dest)); + cpdirSyncRecursive(src, newDest, 0, options); + } catch (e) { + /* istanbul ignore next */ common.error("cannot create directory '" + dest + "': No such file or directory"); } } - return; // done with dir } else { // If here, src is a file // When copying to '/path/dir': // thisDest = '/path/dir/file1' var thisDest = dest; - if (destStat && destStat.isDirectory()) + if (destStat && destStat.isDirectory()) { thisDest = path.normalize(dest + '/' + path.basename(src)); + } + + var thisDestExists = fs.existsSync(thisDest); + if (thisDestExists && checkRecentCreated(sources, srcIndex)) { + // cannot overwrite file created recently in current execution, but we want to continue copying other files + if (!options.no_force) { + common.error("will not overwrite just-created '" + thisDest + "' with '" + src + "'", { continue: true }); + } + return; + } - if (fs.existsSync(thisDest) && options.no_force) { + if (thisDestExists && options.no_force) { return; // skip file } copyFileSync(src, thisDest, options); } }); // forEach(src) + return new common.ShellString('', common.state.error, common.state.errorCode); } module.exports = _cp; diff --git a/src/dirs.js b/src/dirs.js index bb1eccb14..9b7251d0e 100644 --- a/src/dirs.js +++ b/src/dirs.js @@ -1,25 +1,30 @@ +var path = require('path'); var common = require('./common'); var _cd = require('./cd'); -var path = require('path'); -common.register('dirs', _dirs, {globStart: 1}); -common.register('pushd', _pushd, {globStart: 1}); -common.register('popd', _popd, {globStart: 1}); +common.register('dirs', _dirs, { + wrapOutput: false, +}); +common.register('pushd', _pushd, { + wrapOutput: false, +}); +common.register('popd', _popd, { + wrapOutput: false, +}); // Pushd/popd/dirs internals var _dirStack = []; function _isStackIndex(index) { - return (/^[\-+]\d+$/).test(index); + return (/^[-+]\d+$/).test(index); } function _parseStackIndex(index) { if (_isStackIndex(index)) { if (Math.abs(index) < _dirStack.length + 1) { // +1 for pwd return (/^-/).test(index) ? Number(index) - 1 : Number(index); - } else { - common.error(index + ': directory stack index out of range'); } + common.error(index + ': directory stack index out of range'); } else { common.error(index + ': invalid number'); } @@ -35,10 +40,11 @@ function _actualDirStack() { //@ Available options: //@ //@ + `-n`: Suppresses the normal change of directory when adding directories to the stack, so that only the stack is manipulated. +//@ + `-q`: Suppresses output to the console. //@ //@ Arguments: //@ -//@ + `dir`: Makes the current working directory be the top of the stack, and then executes the equivalent of `cd dir`. +//@ + `dir`: Sets the current working directory to the top of the stack, then executes the equivalent of `cd dir`. //@ + `+N`: Brings the Nth directory (counting from the left of the list printed by dirs, starting with zero) to the top of the list by rotating the stack. //@ + `-N`: Brings the Nth directory (counting from the right of the list printed by dirs, starting with zero) to the top of the list by rotating the stack. //@ @@ -50,7 +56,7 @@ function _actualDirStack() { //@ pushd('+1'); // Returns /usr /etc //@ ``` //@ -//@ Save the current directory on the top of the directory stack and then cd to `dir`. With no arguments, pushd exchanges the top two directories. Returns an array of paths in the stack. +//@ Save the current directory on the top of the directory stack and then `cd` to `dir`. With no arguments, `pushd` exchanges the top two directories. Returns an array of paths in the stack. function _pushd(options, dir) { if (_isStackIndex(options)) { dir = options; @@ -58,7 +64,8 @@ function _pushd(options, dir) { } options = common.parseOptions(options, { - 'n' : 'no-cd' + 'n': 'no-cd', + 'q': 'quiet', }); var dirs = _actualDirStack(); @@ -74,12 +81,10 @@ function _pushd(options, dir) { } else if (_isStackIndex(dir)) { var n = _parseStackIndex(dir); dirs = dirs.slice(n).concat(dirs.slice(0, n)); + } else if (options['no-cd']) { + dirs.splice(1, 0, dir); } else { - if (options['no-cd']) { - dirs.splice(1, 0, dir); - } else { - dirs.unshift(dir); - } + dirs.unshift(dir); } if (options['no-cd']) { @@ -90,16 +95,18 @@ function _pushd(options, dir) { } _dirStack = dirs; - return _dirs(''); + return _dirs(options.quiet ? '-q' : ''); } exports.pushd = _pushd; +//@ //@ //@ ### popd([options,] ['-N' | '+N']) //@ //@ Available options: //@ -//@ + `-n`: Suppresses the normal change of directory when removing directories from the stack, so that only the stack is manipulated. +//@ + `-n`: Suppress the normal directory change when removing directories from the stack, so that only the stack is manipulated. +//@ + `-q`: Suppresses output to the console. //@ //@ Arguments: //@ @@ -116,7 +123,7 @@ exports.pushd = _pushd; //@ echo(process.cwd()); // '/usr' //@ ``` //@ -//@ When no arguments are given, popd removes the top directory from the stack and performs a cd to the new top directory. The elements are numbered from 0 starting at the first directory listed with dirs; i.e., popd is equivalent to popd +0. Returns an array of paths in the stack. +//@ When no arguments are given, `popd` removes the top directory from the stack and performs a `cd` to the new top directory. The elements are numbered from 0, starting at the first directory listed with dirs (i.e., `popd` is equivalent to `popd +0`). Returns an array of paths in the stack. function _popd(options, index) { if (_isStackIndex(options)) { index = options; @@ -124,7 +131,8 @@ function _popd(options, index) { } options = common.parseOptions(options, { - 'n' : 'no-cd' + 'n': 'no-cd', + 'q': 'quiet', }); if (!_dirStack.length) { @@ -141,25 +149,27 @@ function _popd(options, index) { _cd('', dir); } - return _dirs(''); + return _dirs(options.quiet ? '-q' : ''); } exports.popd = _popd; +//@ //@ //@ ### dirs([options | '+N' | '-N']) //@ //@ Available options: //@ //@ + `-c`: Clears the directory stack by deleting all of the elements. +//@ + `-q`: Suppresses output to the console. //@ //@ Arguments: //@ //@ + `+N`: Displays the Nth directory (counting from the left of the list printed by dirs when invoked without options), starting with zero. //@ + `-N`: Displays the Nth directory (counting from the right of the list printed by dirs when invoked without options), starting with zero. //@ -//@ Display the list of currently remembered directories. Returns an array of paths in the stack, or a single path if +N or -N was specified. +//@ Display the list of currently remembered directories. Returns an array of paths in the stack, or a single path if `+N` or `-N` was specified. //@ -//@ See also: pushd, popd +//@ See also: `pushd`, `popd` function _dirs(options, index) { if (_isStackIndex(options)) { index = options; @@ -167,10 +177,11 @@ function _dirs(options, index) { } options = common.parseOptions(options, { - 'c' : 'clear' + 'c': 'clear', + 'q': 'quiet', }); - if (options['clear']) { + if (options.clear) { _dirStack = []; return _dirStack; } @@ -184,11 +195,15 @@ function _dirs(options, index) { index = stack.length + index; } - common.log(stack[index]); + if (!options.quiet) { + common.log(stack[index]); + } return stack[index]; } - common.log(stack.join(' ')); + if (!options.quiet) { + common.log(stack.join(' ')); + } return stack; } diff --git a/src/echo.js b/src/echo.js index 0f53ea8f9..da37f438a 100644 --- a/src/echo.js +++ b/src/echo.js @@ -1,23 +1,62 @@ +var format = require('util').format; + var common = require('./common'); -common.register('echo', _echo); +common.register('echo', _echo, { + allowGlobbing: false, +}); //@ -//@ ### echo(string [, string ...]) +//@ ### echo([options,] string [, string ...]) +//@ +//@ Available options: +//@ +//@ + `-e`: interpret backslash escapes (default) +//@ + `-n`: remove trailing newline from output //@ //@ Examples: //@ //@ ```javascript //@ echo('hello world'); //@ var str = echo('hello world'); +//@ echo('-n', 'no newline at end'); //@ ``` //@ -//@ Prints string to stdout, and returns string with additional utility methods -//@ like `.to()`. -function _echo(opts, messages) { +//@ Prints `string` to stdout, and returns a [ShellString](#shellstringstr). +function _echo(opts) { // allow strings starting with '-', see issue #20 - messages = [].slice.call(arguments, opts ? 0 : 1); - console.log.apply(console, messages); - return new common.ShellString(messages.join(' '), '', 0); + var messages = [].slice.call(arguments, opts ? 0 : 1); + var options = {}; + + // If the first argument starts with '-', parse it as options string. + // If parseOptions throws, it wasn't an options string. + try { + options = common.parseOptions(messages[0], { + 'e': 'escapes', + 'n': 'no_newline', + }, { + silent: true, + }); + + // Allow null to be echoed + if (messages[0]) { + messages.shift(); + } + } catch (_) { + // Clear out error if an error occurred + common.state.error = null; + } + + var output = format.apply(null, messages); + + // Add newline if -n is not passed. + if (!options.no_newline) { + output += '\n'; + } + + process.stdout.write(output); + + return output; } + module.exports = _echo; diff --git a/src/error.js b/src/error.js index 507c86ddd..b0ed59e12 100644 --- a/src/error.js +++ b/src/error.js @@ -2,8 +2,9 @@ var common = require('./common'); //@ //@ ### error() +//@ //@ Tests if error occurred in the last command. Returns a truthy value if an -//@ error returned and a falsy value otherwise. +//@ error returned, or a falsy value otherwise. //@ //@ **Note**: do not rely on the //@ return value to be an error message. If you need the last error message, use diff --git a/src/errorCode.js b/src/errorCode.js new file mode 100644 index 000000000..a1c7fd23f --- /dev/null +++ b/src/errorCode.js @@ -0,0 +1,10 @@ +var common = require('./common'); + +//@ +//@ ### errorCode() +//@ +//@ Returns the error code from the last command. +function errorCode() { + return common.state.errorCode; +} +module.exports = errorCode; diff --git a/src/exec-child.js b/src/exec-child.js new file mode 100644 index 000000000..e8446f6f0 --- /dev/null +++ b/src/exec-child.js @@ -0,0 +1,71 @@ +var childProcess = require('child_process'); +var fs = require('fs'); + +function main() { + var paramFilePath = process.argv[2]; + + var serializedParams = fs.readFileSync(paramFilePath, 'utf8'); + var params = JSON.parse(serializedParams); + + var cmd = params.command; + var execOptions = params.execOptions; + var pipe = params.pipe; + var stdoutFile = params.stdoutFile; + var stderrFile = params.stderrFile; + + function isMaxBufferError(err) { + var maxBufferErrorPattern = /^.*\bmaxBuffer\b.*exceeded.*$/; + if (err instanceof Error && err.message && + err.message.match(maxBufferErrorPattern)) { + // < v10 + // Error: stdout maxBuffer exceeded + return true; + } else if (err instanceof RangeError && err.message && + err.message.match(maxBufferErrorPattern)) { + // >= v10 + // RangeError [ERR_CHILD_PROCESS_STDIO_MAXBUFFER]: stdout maxBuffer length + // exceeded + return true; + } + return false; + } + + var stdoutStream = fs.createWriteStream(stdoutFile); + var stderrStream = fs.createWriteStream(stderrFile); + + function appendError(message, code) { + stderrStream.write(message); + process.exitCode = code; + } + + var c = childProcess.exec(cmd, execOptions, function (err) { + if (!err) { + process.exitCode = 0; + } else if (isMaxBufferError(err)) { + appendError('maxBuffer exceeded', 1); + } else if (err.code === undefined && err.message) { + /* istanbul ignore next */ + appendError(err.message, 1); + } else if (err.code === undefined) { + /* istanbul ignore next */ + appendError('Unknown issue', 1); + } else { + process.exitCode = err.code; + } + }); + + c.stdout.pipe(stdoutStream); + c.stderr.pipe(stderrStream); + c.stdout.pipe(process.stdout); + c.stderr.pipe(process.stderr); + + if (pipe) { + c.stdin.end(pipe); + } +} + +// This file should only be executed. This module does not export anything. +/* istanbul ignore else */ +if (require.main === module) { + main(); +} diff --git a/src/exec.js b/src/exec.js index cb7290122..0082f63ed 100644 --- a/src/exec.js +++ b/src/exec.js @@ -1,157 +1,139 @@ -var common = require('./common'); -var _tempDir = require('./tempdir'); -var _pwd = require('./pwd'); var path = require('path'); var fs = require('fs'); var child = require('child_process'); +var common = require('./common'); +var _tempDir = require('./tempdir').tempDir; +var _pwd = require('./pwd'); -var DEFAULT_MAXBUFFER_SIZE = 20*1024*1024; +// Resolve exec-child.js path at module load time using require.resolve() so that +// bundlers (e.g. Next.js/webpack) do not break the path by replacing __dirname +// with the bundled output directory. require.resolve() always returns the real +// filesystem path of the module, regardless of bundler transformations. +var EXEC_CHILD_PATH = require.resolve('./exec-child.js'); -common.register('exec', _exec, {notUnix:true, canReceivePipe: true}); +var DEFAULT_MAXBUFFER_SIZE = 20 * 1024 * 1024; +var DEFAULT_ERROR_CODE = 1; -// Hack to run child_process.exec() synchronously (sync avoids callback hell) -// Uses a custom wait loop that checks for a flag file, created when the child process is done. -// (Can't do a wait loop that checks for internal Node variables/messages as -// Node is single-threaded; callbacks and other internal state changes are done in the -// event loop). +common.register('exec', _exec, { + unix: false, + canReceivePipe: true, + wrapOutput: false, + handlesFatalDynamically: true, +}); + +// We use this function to run `exec` synchronously while also providing realtime +// output. function execSync(cmd, opts, pipe) { + if (!common.config.execPath) { + try { + common.error('Unable to find a path to the node binary. Please manually set config.execPath'); + } catch (e) { + if (opts.fatal) { + throw e; + } + + return; + } + } + var tempDir = _tempDir(); - var stdoutFile = path.resolve(tempDir+'/'+common.randomFileName()), - stderrFile = path.resolve(tempDir+'/'+common.randomFileName()), - codeFile = path.resolve(tempDir+'/'+common.randomFileName()), - scriptFile = path.resolve(tempDir+'/'+common.randomFileName()), - sleepFile = path.resolve(tempDir+'/'+common.randomFileName()); + var paramsFile = path.join(tempDir, common.randomFileName()); + var stderrFile = path.join(tempDir, common.randomFileName()); + var stdoutFile = path.join(tempDir, common.randomFileName()); opts = common.extend({ silent: common.config.silent, + fatal: common.config.fatal, // TODO(nfischer): this and the line above are probably unnecessary cwd: _pwd().toString(), env: process.env, - maxBuffer: DEFAULT_MAXBUFFER_SIZE + maxBuffer: DEFAULT_MAXBUFFER_SIZE, + encoding: 'utf8', }, opts); - var previousStdoutContent = '', - previousStderrContent = ''; - // Echoes stdout and stderr changes from running process, if not silent - function updateStream(streamFile) { - if (opts.silent || !fs.existsSync(streamFile)) - return; - - var previousStreamContent, - proc_stream; - if (streamFile === stdoutFile) { - previousStreamContent = previousStdoutContent; - proc_stream = process.stdout; - } else { // assume stderr - previousStreamContent = previousStderrContent; - proc_stream = process.stderr; - } - - var streamContent = fs.readFileSync(streamFile, 'utf8'); - // No changes since last time? - if (streamContent.length <= previousStreamContent.length) - return; - - proc_stream.write(streamContent.substr(previousStreamContent.length)); - previousStreamContent = streamContent; - } - - if (fs.existsSync(scriptFile)) common.unlinkSync(scriptFile); - if (fs.existsSync(stdoutFile)) common.unlinkSync(stdoutFile); + if (fs.existsSync(paramsFile)) common.unlinkSync(paramsFile); if (fs.existsSync(stderrFile)) common.unlinkSync(stderrFile); - if (fs.existsSync(codeFile)) common.unlinkSync(codeFile); - - var execCommand = JSON.stringify(process.execPath) + ' ' + JSON.stringify(scriptFile); - var script; + if (fs.existsSync(stdoutFile)) common.unlinkSync(stdoutFile); opts.cwd = path.resolve(opts.cwd); - var optString = JSON.stringify(opts); - - if (typeof child.execSync === 'function') { - script = [ - "var child = require('child_process')", - " , fs = require('fs');", - "var childProcess = child.exec("+JSON.stringify(cmd)+", "+optString+", function(err) {", - " fs.writeFileSync("+JSON.stringify(codeFile)+", err ? err.code.toString() : '0');", - "});", - "var stdoutStream = fs.createWriteStream("+JSON.stringify(stdoutFile)+");", - "var stderrStream = fs.createWriteStream("+JSON.stringify(stderrFile)+");", - "childProcess.stdout.pipe(stdoutStream, {end: false});", - "childProcess.stderr.pipe(stderrStream, {end: false});", - "childProcess.stdout.pipe(process.stdout);", - "childProcess.stderr.pipe(process.stderr);" - ].join('\n') + - (pipe ? "\nchildProcess.stdin.end("+JSON.stringify(pipe)+");\n" : '\n') + - [ - "var stdoutEnded = false, stderrEnded = false;", - "function tryClosingStdout(){ if(stdoutEnded){ stdoutStream.end(); } }", - "function tryClosingStderr(){ if(stderrEnded){ stderrStream.end(); } }", - "childProcess.stdout.on('end', function(){ stdoutEnded = true; tryClosingStdout(); });", - "childProcess.stderr.on('end', function(){ stderrEnded = true; tryClosingStderr(); });" - ].join('\n'); - - fs.writeFileSync(scriptFile, script); - - if (opts.silent) { - opts.stdio = 'ignore'; - } else { - opts.stdio = [0, 1, 2]; - } - // Welcome to the future - try { - child.execSync(execCommand, opts); - } catch (e) { - // Clean up immediately if we have an exception - try { common.unlinkSync(scriptFile); } catch(e) {} - try { common.unlinkSync(stdoutFile); } catch(e) {} - try { common.unlinkSync(stderrFile); } catch(e) {} - try { common.unlinkSync(codeFile); } catch(e) {} - throw e; - } + var paramsToSerialize = { + command: cmd, + execOptions: opts, + pipe, + stdoutFile, + stderrFile, + }; + + // Create the files and ensure these are locked down (for read and write) to + // the current user. The main concerns here are: + // + // * If we execute a command which prints sensitive output, then + // stdoutFile/stderrFile must not be readable by other users. + // * paramsFile must not be readable by other users, or else they can read it + // to figure out the path for stdoutFile/stderrFile and create these first + // (locked down to their own access), which will crash exec() when it tries + // to write to the files. + function writeFileLockedDown(filePath, data) { + fs.writeFileSync(filePath, data, { + encoding: 'utf8', + mode: parseInt('600', 8), + }); + } + writeFileLockedDown(stdoutFile, ''); + writeFileLockedDown(stderrFile, ''); + writeFileLockedDown(paramsFile, JSON.stringify(paramsToSerialize)); + + var execArgs = [ + EXEC_CHILD_PATH, + paramsFile, + ]; + + /* istanbul ignore else */ + if (opts.silent) { + opts.stdio = 'ignore'; } else { - cmd += ' > '+stdoutFile+' 2> '+stderrFile; // works on both win/unix - - script = [ - "var child = require('child_process')", - " , fs = require('fs');", - "var childProcess = child.exec("+JSON.stringify(cmd)+", "+optString+", function(err) {", - " fs.writeFileSync("+JSON.stringify(codeFile)+", err ? err.code.toString() : '0');", - "});" - ].join('\n') + - (pipe ? "\nchildProcess.stdin.end("+JSON.stringify(pipe)+");\n" : '\n'); - - fs.writeFileSync(scriptFile, script); - - child.exec(execCommand, opts); - - // The wait loop - // sleepFile is used as a dummy I/O op to mitigate unnecessary CPU usage - // (tried many I/O sync ops, writeFileSync() seems to be only one that is effective in reducing - // CPU usage, though apparently not so much on Windows) - while (!fs.existsSync(codeFile)) { updateStream(stdoutFile); fs.writeFileSync(sleepFile, 'a'); } - while (!fs.existsSync(stdoutFile)) { updateStream(stdoutFile); fs.writeFileSync(sleepFile, 'a'); } - while (!fs.existsSync(stderrFile)) { updateStream(stderrFile); fs.writeFileSync(sleepFile, 'a'); } - try { common.unlinkSync(sleepFile); } catch(e) {} + opts.stdio = [0, 1, 2]; } - // At this point codeFile exists, but it's not necessarily flushed yet. - // Keep reading it until it is. - var code = parseInt('', 10); - while (isNaN(code)) { - code = parseInt(fs.readFileSync(codeFile, 'utf8'), 10); + var code = 0; + + // Welcome to the future + try { + // Bad things if we pass in a `shell` option to child_process.execFileSync, + // so we need to explicitly remove it here. + delete opts.shell; + + child.execFileSync(common.config.execPath, execArgs, opts); + } catch (e) { + // Commands with non-zero exit code raise an exception. + code = e.status || DEFAULT_ERROR_CODE; } - var stdout = fs.readFileSync(stdoutFile, 'utf8'); - var stderr = fs.readFileSync(stderrFile, 'utf8'); + // fs.readFileSync uses buffer encoding by default, so call + // it without the encoding option if the encoding is 'buffer'. + // Also, if the exec timeout is too short for node to start up, + // the files will not be created, so these calls will throw. + var stdout = ''; + var stderr = ''; + if (opts.encoding === 'buffer') { + stdout = fs.readFileSync(stdoutFile); + stderr = fs.readFileSync(stderrFile); + } else { + stdout = fs.readFileSync(stdoutFile, opts.encoding); + stderr = fs.readFileSync(stderrFile, opts.encoding); + } // No biggie if we can't erase the files now -- they're in a temp dir anyway - try { common.unlinkSync(scriptFile); } catch(e) {} - try { common.unlinkSync(stdoutFile); } catch(e) {} - try { common.unlinkSync(stderrFile); } catch(e) {} - try { common.unlinkSync(codeFile); } catch(e) {} - - if (code !== 0) { - common.error('', code, true); + // and we locked down permissions (see the note above). + try { common.unlinkSync(paramsFile); } catch (e) {} + try { common.unlinkSync(stderrFile); } catch (e) {} + try { common.unlinkSync(stdoutFile); } catch (e) {} + + if (code !== 0) { + // Note: `silent` should be unconditionally true to avoid double-printing + // the command's stderr, and to avoid printing any stderr when the user has + // set `shell.config.silent`. + common.error(stderr, code, { continue: true, silent: true, fatal: opts.fatal }); } var obj = common.ShellString(stdout, stderr, code); return obj; @@ -159,48 +141,52 @@ function execSync(cmd, opts, pipe) { // Wrapper around exec() to enable echoing output to console in real time function execAsync(cmd, opts, pipe, callback) { - var stdout = ''; - var stderr = ''; - opts = common.extend({ silent: common.config.silent, + fatal: common.config.fatal, // TODO(nfischer): this and the line above are probably unnecessary cwd: _pwd().toString(), env: process.env, - maxBuffer: DEFAULT_MAXBUFFER_SIZE + maxBuffer: DEFAULT_MAXBUFFER_SIZE, + encoding: 'utf8', }, opts); - var c = child.exec(cmd, opts, function(err) { - if (callback) - callback(err ? err.code : 0, stdout, stderr); + var c = child.exec(cmd, opts, function (err, stdout, stderr) { + if (callback) { + if (!err) { + callback(0, stdout, stderr); + } else if (err.code === undefined) { + // See issue #536 + /* istanbul ignore next */ + callback(1, stdout, stderr); + } else { + callback(err.code, stdout, stderr); + } + } }); - if (pipe) - c.stdin.end(pipe); - - c.stdout.on('data', function(data) { - stdout += data; - if (!opts.silent) - process.stdout.write(data); - }); + if (pipe) c.stdin.end(pipe); - c.stderr.on('data', function(data) { - stderr += data; - if (!opts.silent) - process.stderr.write(data); - }); + if (!opts.silent) { + c.stdout.pipe(process.stdout); + c.stderr.pipe(process.stderr); + } return c; } //@ //@ ### exec(command [, options] [, callback]) -//@ Available options (all `false` by default): +//@ +//@ Available options: //@ //@ + `async`: Asynchronous execution. If a callback is provided, it will be set to -//@ `true`, regardless of the passed value. -//@ + `silent`: Do not echo program output to console. -//@ + and any option available to NodeJS's -//@ [child_process.exec()](https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback) +//@ `true`, regardless of the passed value (default: `false`). +//@ + `fatal`: Exit upon error (default: `false`). +//@ + `silent`: Do not echo program output to console (default: `false`). +//@ + `encoding`: Character encoding to use. Affects the values returned to stdout and stderr, and +//@ what is written to stdout and stderr when not in silent mode (default: `'utf8'`). +//@ + and any option available to Node.js's +//@ [`child_process.exec()`](https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback) //@ //@ Examples: //@ @@ -219,23 +205,23 @@ function execAsync(cmd, opts, pipe, callback) { //@ }); //@ ``` //@ -//@ Executes the given `command` _synchronously_, unless otherwise specified. When in synchronous -//@ mode, this returns a ShellString (compatible with ShellJS v0.6.x, which returns an object -//@ of the form `{ code:..., stdout:... , stderr:... }`). Otherwise, this returns the child process -//@ object, and the `callback` gets the arguments `(code, stdout, stderr)`. +//@ Executes the given `command` _synchronously_, unless otherwise specified. +//@ When in synchronous mode, this returns a [ShellString](#shellstringstr). +//@ Otherwise, this returns the child process object, and the `callback` +//@ receives the arguments `(code, stdout, stderr)`. //@ //@ Not seeing the behavior you want? `exec()` runs everything through `sh` //@ by default (or `cmd.exe` on Windows), which differs from `bash`. If you //@ need bash-specific behavior, try out the `{shell: 'path/to/bash'}` option. //@ -//@ **Note:** For long-lived processes, it's best to run `exec()` asynchronously as -//@ the current synchronous implementation uses a lot of CPU. This should be getting -//@ fixed soon. +//@ **Security note:** as `shell.exec()` executes an arbitrary string in the +//@ system shell, it is **critical** to properly sanitize user input to avoid +//@ **command injection**. For more context, consult the [Security +//@ Guidelines](https://github.com/shelljs/shelljs/wiki/Security-guidelines). function _exec(command, options, callback) { - if (!command) - common.error('must specify command'); + options = options || {}; - var pipe = common.readFromPipe(this); + var pipe = common.readFromPipe(); // Callback is defined instead of options. if (typeof options === 'function') { @@ -250,16 +236,26 @@ function _exec(command, options, callback) { options = common.extend({ silent: common.config.silent, - async: false + fatal: common.config.fatal, + async: false, }, options); - try { - if (options.async) - return execAsync(command, options, pipe, callback); - else - return execSync(command, options, pipe); - } catch (e) { - common.error('internal error'); + if (!command) { + try { + common.error('must specify command'); + } catch (e) { + if (options.fatal) { + throw e; + } + + return; + } + } + + if (options.async) { + return execAsync(command, options, pipe, callback); + } else { + return execSync(command, options, pipe); } } module.exports = _exec; diff --git a/src/find.js b/src/find.js index 4069525b1..80db9935b 100644 --- a/src/find.js +++ b/src/find.js @@ -1,13 +1,17 @@ -var fs = require('fs'); var path = require('path'); var common = require('./common'); var _ls = require('./ls'); -common.register('find', _find, {globStart: 1}); +common.register('find', _find, { + cmdOptions: { + 'L': 'link', + }, +}); //@ //@ ### find(path [, path ...]) //@ ### find(path_array) +//@ //@ Examples: //@ //@ ```javascript @@ -16,38 +20,47 @@ common.register('find', _find, {globStart: 1}); //@ find('.').filter(function(file) { return file.match(/\.js$/); }); //@ ``` //@ -//@ Returns array of all files (however deep) in the given paths. +//@ Returns a [ShellString](#shellstringstr) (with array-like properties) of all +//@ files (however deep) in the given paths. //@ //@ The main difference from `ls('-R', path)` is that the resulting file names -//@ include the base directories, e.g. `lib/resources/file1` instead of just `file1`. +//@ include the base directories (e.g., `lib/resources/file1` instead of just `file1`). function _find(options, paths) { - if (!paths) + if (!paths) { common.error('no path specified'); - else if (typeof paths === 'string') + } else if (typeof paths === 'string') { paths = [].slice.call(arguments, 1); + } var list = []; function pushFile(file) { - if (common.platform === 'win') { + if (process.platform === 'win32') { file = file.replace(/\\/g, '/'); } list.push(file); } - // why not simply do ls('-R', paths)? because the output wouldn't give the base dirs - // to get the base dir in the output, we need instead ls('-R', 'dir/*') for every directory + // why not simply do `ls('-R', paths)`? because the output wouldn't give the base dirs + // to get the base dir in the output, we need instead `ls('-R', 'dir/*')` for every directory + + paths.forEach(function (file) { + var stat; + try { + stat = common.statFollowLinks(file); + } catch (e) { + common.error('no such file or directory: ' + file); + } - paths.forEach(function(file) { pushFile(file); - if (fs.statSync(file).isDirectory()) { - _ls('-RA', file).forEach(function(subfile) { + if (stat.isDirectory()) { + _ls({ recursive: true, all: true, link: options.link }, file).forEach(function (subfile) { pushFile(path.join(file, subfile)); }); } }); - return new common.ShellString(list, common.state.error, common.state.errorCode); + return list; } module.exports = _find; diff --git a/src/grep.js b/src/grep.js index 3a3f5ca08..cfc83e40e 100644 --- a/src/grep.js +++ b/src/grep.js @@ -1,63 +1,198 @@ -var common = require('./common'); var fs = require('fs'); +var common = require('./common'); -common.register('grep', _grep, {globStart: 2, canReceivePipe: true}); // don't glob-expand the regex +common.register('grep', _grep, { + globStart: 2, // don't glob-expand the regex + canReceivePipe: true, + cmdOptions: { + 'v': 'inverse', + 'l': 'nameOnly', + 'i': 'ignoreCase', + 'n': 'lineNumber', + 'B': 'beforeContext', + 'A': 'afterContext', + 'C': 'context', + }, +}); //@ //@ ### grep([options,] regex_filter, file [, file ...]) //@ ### grep([options,] regex_filter, file_array) +//@ //@ Available options: //@ -//@ + `-v`: Inverse the sense of the regex and print the lines not matching the criteria. -//@ + `-l`: Print only filenames of matching files +//@ + `-v`: Invert `regex_filter` (only print non-matching lines). +//@ + `-l`: Print only filenames of matching files. +//@ + `-i`: Ignore case. +//@ + `-n`: Print line numbers. +//@ + `-B `: Show `` lines before each result. +//@ + `-A `: Show `` lines after each result. +//@ + `-C `: Show `` lines before and after each result. -B and -A override this option. //@ //@ Examples: //@ //@ ```javascript //@ grep('-v', 'GLOBAL_VARIABLE', '*.js'); //@ grep('GLOBAL_VARIABLE', '*.js'); +//@ grep('-B', 3, 'GLOBAL_VARIABLE', '*.js'); +//@ grep({ '-B': 3 }, 'GLOBAL_VARIABLE', '*.js'); +//@ grep({ '-B': 3, '-C': 2 }, 'GLOBAL_VARIABLE', '*.js'); //@ ``` //@ -//@ Reads input string from given files and returns a string containing all lines of the -//@ file that match the given `regex_filter`. +//@ Reads input string from given files and returns a +//@ [ShellString](#shellstringstr) containing all lines of the @ file that match +//@ the given `regex_filter`. function _grep(options, regex, files) { - options = common.parseOptions(options, { - 'v': 'inverse', - 'l': 'nameOnly' - }); - // Check if this is coming from a pipe - var pipe = common.readFromPipe(this); + var pipe = common.readFromPipe(); - if (!files && !pipe) - common.error('no paths given', 2); + if (!files && !pipe) common.error('no paths given', 2); - files = [].slice.call(arguments, 2); + var idx = 2; + var contextError = ': invalid context length argument'; + // If the option has been found but not read, copy value from arguments + if (options.beforeContext === true) { + idx = 3; + options.beforeContext = Number(arguments[1]); + if (options.beforeContext < 0) { + common.error(options.beforeContext + contextError, 2); + } + } + if (options.afterContext === true) { + idx = 3; + options.afterContext = Number(arguments[1]); + if (options.afterContext < 0) { + common.error(options.afterContext + contextError, 2); + } + } + if (options.context === true) { + idx = 3; + options.context = Number(arguments[1]); + if (options.context < 0) { + common.error(options.context + contextError, 2); + } + } + // If before or after not given but context is, update values + if (typeof options.context === 'number') { + if (options.beforeContext === false) { + options.beforeContext = options.context; + } + if (options.afterContext === false) { + options.afterContext = options.context; + } + } + regex = arguments[idx - 1]; + files = [].slice.call(arguments, idx); - if (pipe) + if (pipe) { files.unshift('-'); + } var grep = []; - files.forEach(function(file) { + if (options.ignoreCase) { + regex = new RegExp(regex, 'i'); + } + files.forEach(function (file) { if (!fs.existsSync(file) && file !== '-') { - common.error('no such file or directory: ' + file, 2, true); + common.error('no such file or directory: ' + file, 2, { continue: true }); return; } var contents = file === '-' ? pipe : fs.readFileSync(file, 'utf8'); - var lines = contents.split(/\r*\n/); if (options.nameOnly) { - if (contents.match(regex)) + if (contents.match(regex)) { grep.push(file); + } } else { - lines.forEach(function(line) { + var lines = contents.split('\n'); + var matches = []; + + lines.forEach(function (line, index) { var matched = line.match(regex); - if ((options.inverse && !matched) || (!options.inverse && matched)) - grep.push(line); + if ((options.inverse && !matched) || (!options.inverse && matched)) { + var lineNumber = index + 1; + var result = {}; + if (matches.length > 0) { + // If the last result intersects, combine them + var last = matches[matches.length - 1]; + var minimumLineNumber = Math.max( + 1, + lineNumber - options.beforeContext - 1, + ); + if ( + last.hasOwnProperty('' + lineNumber) || + last.hasOwnProperty('' + minimumLineNumber) + ) { + result = last; + } + } + result[lineNumber] = { + line, + match: true, + }; + if (options.beforeContext > 0) { + // Store the lines with their line numbers to check for overlap + lines + .slice(Math.max(index - options.beforeContext, 0), index) + .forEach(function (v, i, a) { + var lineNum = '' + (index - a.length + i + 1); + if (!result.hasOwnProperty(lineNum)) { + result[lineNum] = { line: v, match: false }; + } + }); + } + if (options.afterContext > 0) { + // Store the lines with their line numbers to check for overlap + lines + .slice( + index + 1, + Math.min(index + options.afterContext + 1, lines.length - 1), + ) + .forEach(function (v, i) { + var lineNum = '' + (index + 1 + i + 1); + if (!result.hasOwnProperty(lineNum)) { + result[lineNum] = { line: v, match: false }; + } + }); + } + // Only add the result if it's new + if (!matches.includes(result)) { + matches.push(result); + } + } }); + + // Loop through the matches and add them to the output + Array.prototype.push.apply( + grep, + matches.map(function (result) { + return Object.entries(result) + .map(function (entry) { + var lineNumber = entry[0]; + var line = entry[1].line; + var match = entry[1].match; + return options.lineNumber + ? lineNumber + (match ? ':' : '-') + line + : line; + }) + .join('\n'); + }), + ); } }); - return new common.ShellString(grep.join('\n')+'\n', common.state.error, common.state.errorCode); + if (grep.length === 0 && common.state.errorCode !== 2) { + // We didn't hit the error above, but pattern didn't match + common.error('', { silent: true }); + } + + var separator = '\n'; + if ( + typeof options.beforeContext === 'number' || + typeof options.afterContext === 'number' + ) { + separator = '\n--\n'; + } + return grep.join(separator) + '\n'; } module.exports = _grep; diff --git a/src/head.js b/src/head.js index 305f92788..f3f4f2269 100644 --- a/src/head.js +++ b/src/head.js @@ -1,26 +1,25 @@ -var common = require('./common'); var fs = require('fs'); +var common = require('./common'); -common.register('head', _head, {globStart: 1, canReceivePipe: true}); +common.register('head', _head, { + canReceivePipe: true, + cmdOptions: { + 'n': 'numLines', + }, +}); -// This reads n or more lines, or the entire file, whichever is less. +// Reads |numLines| lines or the entire file, whichever is less. function readSomeLines(file, numLines) { - var BUF_LENGTH = 64*1024, - buf = new Buffer(BUF_LENGTH), - bytesRead = BUF_LENGTH, - pos = 0, - fdr = null; - - try { - fdr = fs.openSync(file, 'r'); - } catch(e) { - common.error('cannot read file: ' + file); - } + var buf = common.buffer(); + var bufLength = buf.length; + var bytesRead = bufLength; + var pos = 0; + var fdr = fs.openSync(file, 'r'); var numLinesRead = 0; var ret = ''; - while (bytesRead === BUF_LENGTH && numLinesRead < numLines) { - bytesRead = fs.readSync(fdr, buf, 0, BUF_LENGTH, pos); + while (bytesRead === bufLength && numLinesRead < numLines) { + bytesRead = fs.readSync(fdr, buf, 0, bufLength, pos); var bufStr = buf.toString('utf8', 0, bytesRead); numLinesRead += bufStr.split('\n').length - 1; ret += bufStr; @@ -30,29 +29,29 @@ function readSomeLines(file, numLines) { fs.closeSync(fdr); return ret; } + +//@ +//@ ### head([{'-n': \},] file [, file ...]) +//@ ### head([{'-n': \},] file_array) //@ -//@ ### head([{'-n', \},] file [, file ...]) -//@ ### head([{'-n', \},] file_array) +//@ Available options: +//@ +//@ + `-n `: Show the first `` lines of the files //@ //@ Examples: //@ //@ ```javascript -//@ var str = head({'-n', 1}, 'file*.txt'); +//@ var str = head({'-n': 1}, 'file*.txt'); //@ var str = head('file1', 'file2'); //@ var str = head(['file1', 'file2']); // same as above //@ ``` //@ -//@ Output the first 10 lines of a file (or the first `` if `-n` is -//@ specified) +//@ Read the start of a `file`. Returns a [ShellString](#shellstringstr). function _head(options, files) { - options = common.parseOptions(options, { - 'n': 'numLines' - }); var head = []; - var pipe = common.readFromPipe(this); + var pipe = common.readFromPipe(); - if (!files && !pipe) - common.error('no paths given'); + if (!files && !pipe) common.error('no paths given'); var idx = 1; if (options.numLines === true) { @@ -63,36 +62,46 @@ function _head(options, files) { } files = [].slice.call(arguments, idx); - if (pipe) + if (pipe) { files.unshift('-'); + } var shouldAppendNewline = false; - files.forEach(function(file) { - if (!fs.existsSync(file) && file !== '-') { - common.error('no such file or directory: ' + file, true); - return; + files.forEach(function (file) { + if (file !== '-') { + if (!fs.existsSync(file)) { + common.error('no such file or directory: ' + file, { continue: true }); + return; + } else if (common.statFollowLinks(file).isDirectory()) { + common.error("error reading '" + file + "': Is a directory", { + continue: true, + }); + return; + } } var contents; - if (file === '-') + if (file === '-') { contents = pipe; - else if (options.numLines < 0) { + } else if (options.numLines < 0) { contents = fs.readFileSync(file, 'utf8'); } else { contents = readSomeLines(file, options.numLines); } var lines = contents.split('\n'); - var hasTrailingNewline = (lines[lines.length-1] === ''); - if (hasTrailingNewline) + var hasTrailingNewline = (lines[lines.length - 1] === ''); + if (hasTrailingNewline) { lines.pop(); + } shouldAppendNewline = (hasTrailingNewline || options.numLines < lines.length); head = head.concat(lines.slice(0, options.numLines)); }); - if (shouldAppendNewline) + if (shouldAppendNewline) { head.push(''); // to add a trailing newline once we join - return new common.ShellString(head.join('\n'), common.state.error, common.state.errorCode); + } + return head.join('\n'); } module.exports = _head; diff --git a/src/ln.js b/src/ln.js index cdf15ca5c..1d3d0e745 100644 --- a/src/ln.js +++ b/src/ln.js @@ -2,10 +2,16 @@ var fs = require('fs'); var path = require('path'); var common = require('./common'); -common.register('ln', _ln, {globStart: 1}); +common.register('ln', _ln, { + cmdOptions: { + 's': 'symlink', + 'f': 'force', + }, +}); //@ //@ ### ln([options,] source, dest) +//@ //@ Available options: //@ //@ + `-s`: symlink @@ -18,13 +24,10 @@ common.register('ln', _ln, {globStart: 1}); //@ ln('-sf', 'file', 'existing'); //@ ``` //@ -//@ Links source to dest. Use -f to force the link, should dest already exist. +//@ Links `source` to `dest`. Use `-f` to force the link, should `dest` already +//@ exist. Returns a [ShellString](#shellstringstr) indicating success or +//@ failure. function _ln(options, source, dest) { - options = common.parseOptions(options, { - 's': 'symlink', - 'f': 'force' - }); - if (!source || !dest) { common.error('Missing and/or '); } @@ -36,30 +39,30 @@ function _ln(options, source, dest) { if (fs.existsSync(dest)) { if (!options.force) { - common.error('Destination file exists', true); + common.error('Destination file exists', { continue: true }); } fs.unlinkSync(dest); } if (options.symlink) { - var isWindows = common.platform === 'win'; + var isWindows = process.platform === 'win32'; var linkType = isWindows ? 'file' : null; var resolvedSourcePath = isAbsolute ? sourcePath : path.resolve(process.cwd(), path.dirname(dest), source); if (!fs.existsSync(resolvedSourcePath)) { - common.error('Source file does not exist', true); - } else if (isWindows && fs.statSync(resolvedSourcePath).isDirectory()) { - linkType = 'junction'; + common.error('Source file does not exist', { continue: true }); + } else if (isWindows && common.statFollowLinks(resolvedSourcePath).isDirectory()) { + linkType = 'junction'; } try { - fs.symlinkSync(linkType === 'junction' ? resolvedSourcePath: source, dest, linkType); + fs.symlinkSync(linkType === 'junction' ? resolvedSourcePath : source, dest, linkType); } catch (err) { common.error(err.message); } } else { if (!fs.existsSync(source)) { - common.error('Source file does not exist', true); + common.error('Source file does not exist', { continue: true }); } try { fs.linkSync(source, dest); @@ -67,6 +70,6 @@ function _ln(options, source, dest) { common.error(err.message); } } - return new common.ShellString('', common.state.error, common.state.errorCode); + return ''; } module.exports = _ln; diff --git a/src/ls.js b/src/ls.js index fc4c767e1..7f32c6e09 100644 --- a/src/ls.js +++ b/src/ls.js @@ -1,44 +1,55 @@ var path = require('path'); var fs = require('fs'); +var glob = require('fast-glob'); var common = require('./common'); -var glob = require('glob'); -var globPatternRecursive = path.sep + '**' + path.sep + '*'; +// glob patterns use the UNIX path seperator +var globPatternRecursive = '/**'; -common.register('ls', _ls, {globStart: 1}); +common.register('ls', _ls, { + cmdOptions: { + 'R': 'recursive', + 'A': 'all', + 'L': 'link', + 'a': 'all_deprecated', + 'd': 'directory', + 'l': 'long', + }, +}); //@ //@ ### ls([options,] [path, ...]) //@ ### ls([options,] path_array) +//@ //@ Available options: //@ //@ + `-R`: recursive //@ + `-A`: all files (include files beginning with `.`, except for `.` and `..`) +//@ + `-L`: follow symlinks //@ + `-d`: list directories themselves, not their contents -//@ + `-l`: list objects representing each file, each with fields containing `ls -//@ -l` output fields. See -//@ [fs.Stats](https://nodejs.org/api/fs.html#fs_class_fs_stats) -//@ for more info +//@ + `-l`: provides more details for each file. Specifically, each file is +//@ represented by a structured object with separate fields for file +//@ metadata (see +//@ [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats)). The +//@ return value also overrides `.toString()` to resemble `ls -l`'s +//@ output format for human readability, but programmatic usage should +//@ depend on the stable object format rather than the `.toString()` +//@ representation. //@ //@ Examples: //@ //@ ```javascript //@ ls('projs/*.js'); +//@ ls('projs/**/*.js'); // Find all js files recursively in projs //@ ls('-R', '/users/me', '/tmp'); //@ ls('-R', ['/users/me', '/tmp']); // same as above //@ ls('-l', 'file.txt'); // { name: 'file.txt', mode: 33188, nlink: 1, ...} //@ ``` //@ -//@ Returns array of files in the given path, or in current directory if no path provided. +//@ Returns a [ShellString](#shellstringstr) (with array-like properties) of all +//@ the files in the given `path`, or files in the current directory if no +//@ `path` is provided. function _ls(options, paths) { - options = common.parseOptions(options, { - 'R': 'recursive', - 'A': 'all', - 'a': 'all_deprecated', - 'd': 'directory', - 'l': 'long' - }); - if (options.all_deprecated) { // We won't support the -a option as it's hard to image why it's useful // (it includes '.' and '..' in addition to '.*' files) @@ -47,18 +58,20 @@ function _ls(options, paths) { options.all = true; } - if (!paths) + if (!paths) { paths = ['.']; - else + } else { paths = [].slice.call(arguments, 1); + } var list = []; function pushFile(abs, relName, stat) { - if (process.platform === 'win32') + if (process.platform === 'win32') { relName = relName.replace(/\\/g, '/'); + } if (options.long) { - stat = stat || fs.lstatSync(abs); + stat = stat || (options.link ? common.statFollowLinks(abs) : common.statNoFollowLinks(abs)); list.push(addLsAttributes(relName, stat)); } else { // list.push(path.relative(rel || '.', file)); @@ -66,13 +79,26 @@ function _ls(options, paths) { } } - paths.forEach(function(p) { + paths.forEach(function (p) { var stat; try { - stat = fs.lstatSync(p); + stat = options.link ? common.statFollowLinks(p) : common.statNoFollowLinks(p); + // follow links to directories by default + if (stat.isSymbolicLink()) { + /* istanbul ignore next */ + // workaround for https://github.com/shelljs/shelljs/issues/795 + // codecov seems to have a bug that miscalculate this block as uncovered. + // but according to nyc report this block does get covered. + try { + var _stat = common.statFollowLinks(p); + if (_stat.isDirectory()) { + stat = _stat; + } + } catch (_) {} // bad symlink, treat it like a file + } } catch (e) { - common.error('no such file or directory: ' + p, 2, true); + common.error('no such file or directory: ' + p, 2, { continue: true }); return; } @@ -80,9 +106,19 @@ function _ls(options, paths) { if (stat.isDirectory() && !options.directory) { if (options.recursive) { // use glob, because it's simple - glob.sync(p + globPatternRecursive, { dot: options.all }) - .forEach(function (item) { - pushFile(item, path.relative(p, item)); + glob.sync(p + globPatternRecursive, { + // These options are just to make fast-glob be compatible with POSIX + // (bash) wildcard behavior. + onlyFiles: false, + + // These options depend on the cmdOptions provided to ls. + dot: options.all, + followSymbolicLinks: options.link, + }).forEach(function (item) { + // Glob pattern returns the directory itself and needs to be filtered out. + if (path.relative(p, item)) { + pushFile(item, path.relative(p, item)); + } }); } else if (options.all) { // use fs.readdirSync, because it's fast @@ -92,8 +128,9 @@ function _ls(options, paths) { } else { // use fs.readdirSync and then filter out secret files fs.readdirSync(p).forEach(function (item) { - if (item[0] !== '.') + if (item[0] !== '.') { pushFile(path.join(p, item), item); + } }); } } else { @@ -102,13 +139,13 @@ function _ls(options, paths) { }); // Add methods, to make this more compatible with ShellStrings - return new common.ShellString(list, common.state.error, common.state.errorCode); + return list; } -function addLsAttributes(path, stats) { +function addLsAttributes(pathName, stats) { // Note: this object will contain more information than .toString() returns - stats.name = path; - stats.toString = function() { + stats.name = pathName; + stats.toString = function () { // Return a string resembling unix's `ls -l` format return [this.mode, this.nlink, this.uid, this.gid, this.size, this.mtime, this.name].join(' '); }; diff --git a/src/mkdir.js b/src/mkdir.js index 7a5036387..021cad9c0 100644 --- a/src/mkdir.js +++ b/src/mkdir.js @@ -1,32 +1,45 @@ -var common = require('./common'); var fs = require('fs'); var path = require('path'); +var common = require('./common'); -common.register('mkdir', _mkdir, {globStart: 1}); +common.register('mkdir', _mkdir, { + cmdOptions: { + 'p': 'fullpath', + }, +}); -// Recursively creates 'dir' +// Recursively creates `dir` function mkdirSyncRecursive(dir) { var baseDir = path.dirname(dir); - // Base dir exists, no recursion necessary - if (fs.existsSync(baseDir)) { - fs.mkdirSync(dir, parseInt('0777', 8)); - return; + // Prevents some potential problems arising from malformed UNCs or + // insufficient permissions. + /* istanbul ignore next */ + if (baseDir === dir) { + common.error('dirname() failed: [' + dir + ']'); } // Base dir does not exist, go recursive - mkdirSyncRecursive(baseDir); + if (!fs.existsSync(baseDir)) { + mkdirSyncRecursive(baseDir); + } - // Base dir created, can create dir - fs.mkdirSync(dir, parseInt('0777', 8)); + try { + // Base dir created, can create dir + fs.mkdirSync(dir, parseInt('0777', 8)); + } catch (e) { + // swallow error if dir already exists + if (e.code !== 'EEXIST' || common.statNoFollowLinks(dir).isFile()) { throw e; } + } } //@ //@ ### mkdir([options,] dir [, dir ...]) //@ ### mkdir([options,] dir_array) +//@ //@ Available options: //@ -//@ + `-p`: full path (will create intermediate dirs if necessary) +//@ + `-p`: full path (and create intermediate directories, if necessary) //@ //@ Examples: //@ @@ -35,23 +48,24 @@ function mkdirSyncRecursive(dir) { //@ mkdir('-p', ['/tmp/a/b/c/d', '/tmp/e/f/g']); // same as above //@ ``` //@ -//@ Creates directories. +//@ Creates directories. Returns a [ShellString](#shellstringstr) indicating +//@ success or failure. function _mkdir(options, dirs) { - options = common.parseOptions(options, { - 'p': 'fullpath' - }); - if (!dirs) - common.error('no paths given'); + if (!dirs) common.error('no paths given'); - if (typeof dirs === 'string') + if (typeof dirs === 'string') { dirs = [].slice.call(arguments, 1); + } // if it's array leave it as it is - dirs.forEach(function(dir) { + dirs.forEach(function (dir) { try { - fs.lstatSync(dir); - if (!options.fullpath) - common.error('path already exists: ' + dir, true); + var stat = common.statNoFollowLinks(dir); + if (!options.fullpath) { + common.error('path already exists: ' + dir, { continue: true }); + } else if (stat.isFile()) { + common.error('cannot create directory ' + dir + ': File exists', { continue: true }); + } return; // skip dir } catch (e) { // do nothing @@ -60,15 +74,29 @@ function _mkdir(options, dirs) { // Base dir does not exist, and no -p option given var baseDir = path.dirname(dir); if (!fs.existsSync(baseDir) && !options.fullpath) { - common.error('no such file or directory: ' + baseDir, true); + common.error('no such file or directory: ' + baseDir, { continue: true }); return; // skip dir } - if (options.fullpath) - mkdirSyncRecursive(dir); - else - fs.mkdirSync(dir, parseInt('0777', 8)); + try { + if (options.fullpath) { + mkdirSyncRecursive(path.resolve(dir)); + } else { + fs.mkdirSync(dir, parseInt('0777', 8)); + } + } catch (e) { + var reason; + if (e.code === 'EACCES') { + reason = 'Permission denied'; + } else if (e.code === 'ENOTDIR' || e.code === 'ENOENT') { + reason = 'Not a directory'; + } else { + /* istanbul ignore next */ + throw e; + } + common.error('cannot create directory ' + dir + ': ' + reason, { continue: true }); + } }); - return new common.ShellString('', common.state.error, common.state.errorCode); -} // mkdir + return ''; +} // man arraykdir module.exports = _mkdir; diff --git a/src/mv.js b/src/mv.js index 6c42a5934..6e89e2fae 100644 --- a/src/mv.js +++ b/src/mv.js @@ -4,11 +4,25 @@ var common = require('./common'); var cp = require('./cp'); var rm = require('./rm'); -common.register('mv', _mv, {globStart: 1}); +common.register('mv', _mv, { + cmdOptions: { + 'f': '!no_force', + 'n': 'no_force', + }, +}); + +// Checks if cureent file was created recently +function checkRecentCreated(sources, index) { + var lookedSource = sources[index]; + return sources.slice(0, index).some(function (src) { + return path.basename(src) === path.basename(lookedSource); + }); +} //@ //@ ### mv([options ,] source [, source ...], dest') //@ ### mv([options ,] source_array, dest') +//@ //@ Available options: //@ //@ + `-f`: force (default behavior) @@ -22,13 +36,9 @@ common.register('mv', _mv, {globStart: 1}); //@ mv(['file1', 'file2'], 'dir/'); // same as above //@ ``` //@ -//@ Moves files. +//@ Moves `source` file(s) to `dest`. Returns a [ShellString](#shellstringstr) +//@ indicating success or failure. function _mv(options, sources, dest) { - options = common.parseOptions(options, { - 'f': '!no_force', - 'n': 'no_force' - }); - // Get sources, dest if (arguments.length < 3) { common.error('missing and/or '); @@ -38,23 +48,26 @@ function _mv(options, sources, dest) { } else if (typeof sources === 'string') { sources = [sources]; } else { + // TODO(nate): figure out if we actually need this line common.error('invalid arguments'); } - var exists = fs.existsSync(dest), - stats = exists && fs.statSync(dest); + var exists = fs.existsSync(dest); + var stats = exists && common.statFollowLinks(dest); // Dest is not existing dir, but multiple sources given - if ((!exists || !stats.isDirectory()) && sources.length > 1) + if ((!exists || !stats.isDirectory()) && sources.length > 1) { common.error('dest is not a directory (too many sources)'); + } // Dest is an existing file, but no -f given - if (exists && stats.isFile() && options.no_force) + if (exists && stats.isFile() && options.no_force) { common.error('dest file already exists: ' + dest); + } - sources.forEach(function(src) { + sources.forEach(function (src, srcIndex) { if (!fs.existsSync(src)) { - common.error('no such file or directory: '+src, true); + common.error('no such file or directory: ' + src, { continue: true }); return; // skip file } @@ -63,30 +76,44 @@ function _mv(options, sources, dest) { // When copying to '/path/dir': // thisDest = '/path/dir/file1' var thisDest = dest; - if (fs.existsSync(dest) && fs.statSync(dest).isDirectory()) + if (fs.existsSync(dest) && common.statFollowLinks(dest).isDirectory()) { thisDest = path.normalize(dest + '/' + path.basename(src)); + } + + var thisDestExists = fs.existsSync(thisDest); + + if (thisDestExists && checkRecentCreated(sources, srcIndex)) { + // cannot overwrite file created recently in current execution, but we want to continue copying other files + if (!options.no_force) { + common.error("will not overwrite just-created '" + thisDest + "' with '" + src + "'", { continue: true }); + } + return; + } if (fs.existsSync(thisDest) && options.no_force) { - common.error('dest file already exists: ' + thisDest, true); + common.error('dest file already exists: ' + thisDest, { continue: true }); return; // skip file } if (path.resolve(src) === path.dirname(path.resolve(thisDest))) { - common.error('cannot move to self: '+src, true); + common.error('cannot move to self: ' + src, { continue: true }); return; // skip file } try { fs.renameSync(src, thisDest); } catch (e) { - if (e.code === 'EXDEV') { // external partition - // if either of these fails, the appropriate error message will bubble - // up to the top level automatically - cp('-r', src, thisDest); - rm('-rf', src); + /* istanbul ignore next */ + if (e.code === 'EXDEV') { + // If we're trying to `mv` to an external partition, we'll actually need + // to perform a copy and then clean up the original file. If either the + // copy or the rm fails with an exception, we should allow this + // exception to pass up to the top level. + cp({ recursive: true }, src, thisDest); + rm({ recursive: true, force: true }, src); } } }); // forEach(src) - return new common.ShellString('', common.state.error, common.state.errorCode); + return ''; } // mv module.exports = _mv; diff --git a/src/popd.js b/src/popd.js index 11ea24fa4..d9eac3f56 100644 --- a/src/popd.js +++ b/src/popd.js @@ -1 +1 @@ -// see dirs.js \ No newline at end of file +// see dirs.js diff --git a/src/pushd.js b/src/pushd.js index 11ea24fa4..d9eac3f56 100644 --- a/src/pushd.js +++ b/src/pushd.js @@ -1 +1 @@ -// see dirs.js \ No newline at end of file +// see dirs.js diff --git a/src/pwd.js b/src/pwd.js index f94373b12..8527d8b38 100644 --- a/src/pwd.js +++ b/src/pwd.js @@ -1,13 +1,16 @@ var path = require('path'); var common = require('./common'); -common.register('pwd', _pwd); +common.register('pwd', _pwd, { + allowGlobbing: false, +}); //@ //@ ### pwd() -//@ Returns the current directory. +//@ +//@ Returns the current directory as a [ShellString](#shellstringstr). function _pwd() { var pwd = path.resolve(process.cwd()); - return new common.ShellString(pwd, '', common.state.errorCode); + return pwd; } module.exports = _pwd; diff --git a/src/rm.js b/src/rm.js index ff43e9982..6bb57558b 100644 --- a/src/rm.js +++ b/src/rm.js @@ -1,7 +1,13 @@ -var common = require('./common'); var fs = require('fs'); +var common = require('./common'); -common.register('rm', _rm, {globStart: 1}); +common.register('rm', _rm, { + cmdOptions: { + 'f': 'force', + 'r': 'recursive', + 'R': 'recursive', + }, +}); // Recursively removes 'dir' // Adapted from https://github.com/ryanmcgrath/wrench-js @@ -11,29 +17,35 @@ common.register('rm', _rm, {globStart: 1}); // // Licensed under the MIT License // http://www.opensource.org/licenses/mit-license.php -function rmdirSyncRecursive(dir, force) { +function rmdirSyncRecursive(dir, force, fromSymlink) { var files; files = fs.readdirSync(dir); // Loop through and delete everything in the sub-tree after checking it - for(var i = 0; i < files.length; i++) { - var file = dir + "/" + files[i], - currFile = fs.lstatSync(file); + for (var i = 0; i < files.length; i++) { + var file = dir + '/' + files[i]; + var currFile = common.statNoFollowLinks(file); - if(currFile.isDirectory()) { // Recursive function back to the beginning + if (currFile.isDirectory()) { // Recursive function back to the beginning rmdirSyncRecursive(file, force); - } else { // Assume it's a file - perhaps a try/catch belongs here? - if (force || isWriteable(file)) { - try { - common.unlinkSync(file); - } catch (e) { - common.error('could not remove file (code '+e.code+'): ' + file, true); - } + } else if (force || isWriteable(file)) { + // Assume it's a file - perhaps a try/catch belongs here? + try { + common.unlinkSync(file); + } catch (e) { + /* istanbul ignore next */ + common.error('could not remove file (code ' + e.code + '): ' + file, { + continue: true, + }); } } } + // if was directory was referenced through a symbolic link, + // the contents should be removed, but not the directory itself + if (fromSymlink) return; + // Now that we know everything in the sub-tree has been deleted, we can delete the main directory. // Huzzah for the shopkeep. @@ -41,16 +53,19 @@ function rmdirSyncRecursive(dir, force) { try { // Retry on windows, sometimes it takes a little time before all the files in the directory are gone var start = Date.now(); - while (true) { + + // TODO: replace this with a finite loop + for (;;) { try { result = fs.rmdirSync(dir); - if (fs.existsSync(dir)) throw { code: "EAGAIN" }; + if (fs.existsSync(dir)) throw { code: 'EAGAIN' }; break; - } catch(er) { + } catch (er) { + /* istanbul ignore next */ // In addition to error codes, also check if the directory still exists and loop again if true - if (process.platform === "win32" && (er.code === "ENOTEMPTY" || er.code === "EBUSY" || er.code === "EPERM" || er.code === "EAGAIN")) { + if (process.platform === 'win32' && (er.code === 'ENOTEMPTY' || er.code === 'EBUSY' || er.code === 'EPERM' || er.code === 'EAGAIN')) { if (Date.now() - start > 1000) throw er; - } else if (er.code === "ENOENT") { + } else if (er.code === 'ENOENT') { // Directory did not exist, deletion was successful break; } else { @@ -58,8 +73,8 @@ function rmdirSyncRecursive(dir, force) { } } } - } catch(e) { - common.error('could not remove directory (code '+e.code+'): ' + dir, true); + } catch (e) { + common.error('could not remove directory (code ' + e.code + '): ' + dir, { continue: true }); } return result; @@ -72,16 +87,68 @@ function isWriteable(file) { try { var __fd = fs.openSync(file, 'a'); fs.closeSync(__fd); - } catch(e) { + } catch (e) { writePermission = false; } return writePermission; } +function handleFile(file, options) { + if (options.force || isWriteable(file)) { + // -f was passed, or file is writable, so it can be removed + common.unlinkSync(file); + } else { + common.error('permission denied: ' + file, { continue: true }); + } +} + +function handleDirectory(file, options) { + if (options.recursive) { + // -r was passed, so directory can be removed + rmdirSyncRecursive(file, options.force); + } else { + common.error('path is a directory', { continue: true }); + } +} + +function handleSymbolicLink(file, options) { + var stats; + try { + stats = common.statFollowLinks(file); + } catch (e) { + // symlink is broken, so remove the symlink itself + common.unlinkSync(file); + return; + } + + if (stats.isFile()) { + common.unlinkSync(file); + } else if (stats.isDirectory()) { + if (file[file.length - 1] === '/') { + // trailing separator, so remove the contents, not the link + if (options.recursive) { + // -r was passed, so directory can be removed + var fromSymlink = true; + rmdirSyncRecursive(file, options.force, fromSymlink); + } else { + common.error('path is a directory', { continue: true }); + } + } else { + // no trailing separator, so remove the link + common.unlinkSync(file); + } + } +} + +function handleFIFO(file) { + common.unlinkSync(file); +} + //@ //@ ### rm([options,] file [, file ...]) //@ ### rm([options,] file_array) +//@ //@ Available options: //@ //@ + `-f`: force @@ -95,58 +162,40 @@ function isWriteable(file) { //@ rm(['some_file.txt', 'another_file.txt']); // same as above //@ ``` //@ -//@ Removes files. +//@ Removes files. Returns a [ShellString](#shellstringstr) indicating success +//@ or failure. function _rm(options, files) { - options = common.parseOptions(options, { - 'f': 'force', - 'r': 'recursive', - 'R': 'recursive' - }); - if (!files) - common.error('no paths given'); + if (!files) common.error('no paths given'); // Convert to array files = [].slice.call(arguments, 1); - files.forEach(function(file) { - var stats; + files.forEach(function (file) { + var lstats; try { - stats = fs.lstatSync(file); // test for existence + var filepath = (file[file.length - 1] === '/') + ? file.slice(0, -1) // remove the '/' so lstatSync can detect symlinks + : file; + lstats = common.statNoFollowLinks(filepath); // test for existence } catch (e) { // Path does not exist, no force flag given - if (!options.force) - common.error('no such file or directory: '+file, true); + if (!options.force) { + common.error('no such file or directory: ' + file, { continue: true }); + } return; // skip file } // If here, path exists - if (stats.isFile() || stats.isSymbolicLink()) { - - // Do not check for file writing permissions - if (options.force) { - common.unlinkSync(file); - return; - } - - if (isWriteable(file)) - common.unlinkSync(file); - else - common.error('permission denied: '+file, true); - - return; - } // simple file - - // Path is an existing directory, but no -r flag given - if (stats.isDirectory() && !options.recursive) { - common.error('path is a directory', true); - return; // skip path - } - - // Recursively remove existing directory - if (stats.isDirectory() && options.recursive) { - rmdirSyncRecursive(file, options.force); + if (lstats.isFile()) { + handleFile(file, options); + } else if (lstats.isDirectory()) { + handleDirectory(file, options); + } else if (lstats.isSymbolicLink()) { + handleSymbolicLink(file, options); + } else if (lstats.isFIFO()) { + handleFIFO(file); } }); // forEach(file) - return new common.ShellString('', common.state.error, common.state.errorCode); + return ''; } // rm module.exports = _rm; diff --git a/src/sed.js b/src/sed.js index 5a12217ee..693652362 100644 --- a/src/sed.js +++ b/src/sed.js @@ -1,70 +1,95 @@ -var common = require('./common'); var fs = require('fs'); +var common = require('./common'); -common.register('sed', _sed, {globStart: 3, canReceivePipe: true}); // don't glob-expand regexes +common.register('sed', _sed, { + globStart: 3, // don't glob-expand regexes + canReceivePipe: true, + cmdOptions: { + 'i': 'inplace', + }, +}); //@ //@ ### sed([options,] search_regex, replacement, file [, file ...]) //@ ### sed([options,] search_regex, replacement, file_array) +//@ //@ Available options: //@ -//@ + `-i`: Replace contents of 'file' in-place. _Note that no backups will be created!_ +//@ + `-i`: Replace contents of `file` in-place. _Note that no backups will be created!_ //@ //@ Examples: //@ //@ ```javascript //@ sed('-i', 'PROGRAM_VERSION', 'v0.1.3', 'source.js'); -//@ sed(/.*DELETE_THIS_LINE.*\n/, '', 'source.js'); //@ ``` //@ -//@ Reads an input string from `files` and performs a JavaScript `replace()` on the input -//@ using the given search regex and replacement string or function. Returns the new string after replacement. +//@ Reads an input string from `file`s, line by line, and performs a JavaScript `replace()` on +//@ each of the lines from the input string using the given `search_regex` and `replacement` string or +//@ function. Returns the new [ShellString](#shellstringstr) after replacement. +//@ +//@ Note: +//@ +//@ Like unix `sed`, ShellJS `sed` supports capture groups. Capture groups are specified +//@ using the `$n` syntax: +//@ +//@ ```javascript +//@ sed(/(\w+)\s(\w+)/, '$2, $1', 'file.txt'); +//@ ``` +//@ +//@ Also, like unix `sed`, ShellJS `sed` runs replacements on each line from the input file +//@ (split by '\n') separately, so `search_regex`es that span more than one line (or include '\n') +//@ will not match anything and nothing will be replaced. function _sed(options, regex, replacement, files) { - options = common.parseOptions(options, { - 'i': 'inplace' - }); - // Check if this is coming from a pipe - var pipe = common.readFromPipe(this); + var pipe = common.readFromPipe(); - if (typeof replacement === 'string' || typeof replacement === 'function') - replacement = replacement; // no-op - else if (typeof replacement === 'number') - replacement = replacement.toString(); // fallback - else - common.error('invalid replacement string'); + if (typeof replacement !== 'string' && typeof replacement !== 'function') { + if (typeof replacement === 'number') { + replacement = replacement.toString(); // fallback + } else { + common.error('invalid replacement string'); + } + } // Convert all search strings to RegExp - if (typeof regex === 'string') + if (typeof regex === 'string') { regex = RegExp(regex); + } - if (!files && !pipe) + if (!files && !pipe) { common.error('no files given'); + } files = [].slice.call(arguments, 3); - if (pipe) + if (pipe) { files.unshift('-'); + } var sed = []; - files.forEach(function(file) { + files.forEach(function (file) { if (!fs.existsSync(file) && file !== '-') { - common.error('no such file or directory: ' + file, 2, true); + common.error('no such file or directory: ' + file, 2, { continue: true }); return; } var contents = file === '-' ? pipe : fs.readFileSync(file, 'utf8'); - var lines = contents.split(/\r*\n/); + var lines = contents.split('\n'); var result = lines.map(function (line) { return line.replace(regex, replacement); }).join('\n'); sed.push(result); - if (options.inplace) + if (options.inplace) { fs.writeFileSync(file, result, 'utf8'); + } }); - return new common.ShellString(sed.join('\n'), common.state.error, common.state.errorCode); + if (options.inplace) { + return ''; + } else { + return sed.join('\n'); + } } module.exports = _sed; diff --git a/src/set.js b/src/set.js index b6faa0974..6f37bc9a9 100644 --- a/src/set.js +++ b/src/set.js @@ -1,9 +1,13 @@ var common = require('./common'); -common.register('set', _set); +common.register('set', _set, { + allowGlobbing: false, + wrapOutput: false, +}); //@ //@ ### set(options) +//@ //@ Available options: //@ //@ + `+/-e`: exit upon error (`config.fatal`) @@ -17,12 +21,11 @@ common.register('set', _set); //@ set('+e'); // this undoes a "set('-e')" //@ ``` //@ -//@ Sets global configuration variables +//@ Sets global configuration variables. function _set(options) { if (!options) { var args = [].slice.call(arguments, 0); - if (args.length < 2) - common.error('must provide an argument'); + if (args.length < 2) common.error('must provide an argument'); options = args[1]; } var negate = (options[0] === '+'); @@ -32,22 +35,21 @@ function _set(options) { options = common.parseOptions(options, { 'e': 'fatal', 'v': 'verbose', - 'f': 'noglob' + 'f': 'noglob', }); - var key; if (negate) { - for (key in options) + Object.keys(options).forEach(function (key) { options[key] = !options[key]; + }); } - for (key in options) { + Object.keys(options).forEach(function (key) { // Only change the global config if `negate` is false and the option is true // or if `negate` is true and the option is false (aka negate !== option) if (negate !== options[key]) { common.config[key] = options[key]; } - } - return; + }); } module.exports = _set; diff --git a/src/sort.js b/src/sort.js index 2b111c40d..66b042ce4 100644 --- a/src/sort.js +++ b/src/sort.js @@ -1,12 +1,18 @@ -var common = require('./common'); var fs = require('fs'); +var common = require('./common'); -common.register('sort', _sort, {globStart: 1, canReceivePipe: true}); +common.register('sort', _sort, { + canReceivePipe: true, + cmdOptions: { + 'r': 'reverse', + 'n': 'numerical', + }, +}); // parse out the number prefix of a line -function parseNumber (str) { +function parseNumber(str) { var match = str.match(/^\s*(\d*)\s*(.*)$/); - return {num: Number(match[1]), value: match[2]}; + return { num: Number(match[1]), value: match[2] }; } // compare two strings case-insensitively, but examine case for strings that are @@ -35,9 +41,10 @@ function numericalCmp(a, b) { //@ //@ ### sort([options,] file [, file ...]) //@ ### sort([options,] file_array) +//@ //@ Available options: //@ -//@ + `-r`: Reverse the result of comparisons +//@ + `-r`: Reverse the results //@ + `-n`: Compare according to numerical value //@ //@ Examples: @@ -47,43 +54,45 @@ function numericalCmp(a, b) { //@ sort('-r', 'foo.txt'); //@ ``` //@ -//@ Return the contents of the files, sorted line-by-line. Sorting multiple -//@ files mixes their content, just like unix sort does. +//@ Return the contents of the `file`s, sorted line-by-line as a +//@ [ShellString](#shellstringstr). Sorting multiple files mixes their content +//@ (just as unix `sort` does). function _sort(options, files) { - options = common.parseOptions(options, { - 'r': 'reverse', - 'n': 'numerical' - }); - // Check if this is coming from a pipe - var pipe = common.readFromPipe(this); + var pipe = common.readFromPipe(); - if (!files && !pipe) - common.error('no files given'); + if (!files && !pipe) common.error('no files given'); files = [].slice.call(arguments, 1); - if (pipe) + if (pipe) { files.unshift('-'); + } - var lines = []; - files.forEach(function(file) { - if (!fs.existsSync(file) && file !== '-') { - // exit upon any sort of error - common.error('no such file or directory: ' + file); + var lines = files.reduce(function (accum, file) { + if (file !== '-') { + if (!fs.existsSync(file)) { + common.error('no such file or directory: ' + file, { continue: true }); + return accum; + } else if (common.statFollowLinks(file).isDirectory()) { + common.error('read failed: ' + file + ': Is a directory', { + continue: true, + }); + return accum; + } } var contents = file === '-' ? pipe : fs.readFileSync(file, 'utf8'); - lines = lines.concat(contents.trimRight().split(/\r*\n/)); - }); + return accum.concat(contents.trimRight().split('\n')); + }, []); - var sorted; - sorted = lines.sort(options.numerical ? numericalCmp : unixCmp); + var sorted = lines.sort(options.numerical ? numericalCmp : unixCmp); - if (options.reverse) + if (options.reverse) { sorted = sorted.reverse(); + } - return new common.ShellString(sorted.join('\n')+'\n', common.state.error, common.state.errorCode); + return sorted.join('\n') + '\n'; } module.exports = _sort; diff --git a/src/tail.js b/src/tail.js index d0fd8976e..eee75c570 100644 --- a/src/tail.js +++ b/src/tail.js @@ -1,67 +1,90 @@ -var common = require('./common'); var fs = require('fs'); +var common = require('./common'); -common.register('tail', _tail, {globStart: 1, canReceivePipe: true}); +common.register('tail', _tail, { + canReceivePipe: true, + cmdOptions: { + 'n': 'numLines', + }, +}); //@ -//@ ### tail([{'-n', \},] file [, file ...]) -//@ ### tail([{'-n', \},] file_array) +//@ ### tail([{'-n': \},] file [, file ...]) +//@ ### tail([{'-n': \},] file_array) +//@ +//@ Available options: +//@ +//@ + `-n `: Show the last `` lines of `file`s //@ //@ Examples: //@ //@ ```javascript -//@ var str = tail({'-n', 1}, 'file*.txt'); +//@ var str = tail({'-n': 1}, 'file*.txt'); //@ var str = tail('file1', 'file2'); //@ var str = tail(['file1', 'file2']); // same as above //@ ``` //@ -//@ Output the last 10 lines of a file (or the last `` if `-n` is -//@ specified) +//@ Read the end of a `file`. Returns a [ShellString](#shellstringstr). function _tail(options, files) { - options = common.parseOptions(options, { - 'n': 'numLines' - }); var tail = []; - var pipe = common.readFromPipe(this); + var pipe = common.readFromPipe(); - if (!files && !pipe) - common.error('no paths given'); + if (!files && !pipe) common.error('no paths given'); var idx = 1; + var plusOption = false; if (options.numLines === true) { idx = 2; + if (arguments[1][0] === '+') { + plusOption = true; + } options.numLines = Number(arguments[1]); } else if (options.numLines === false) { options.numLines = 10; } + // arguments[0] is a json object + if (arguments[0].numLines[0] === '+') { + plusOption = true; + } options.numLines = -1 * Math.abs(options.numLines); files = [].slice.call(arguments, idx); - if (pipe) + if (pipe) { files.unshift('-'); + } var shouldAppendNewline = false; - files.forEach(function(file) { - if (!fs.existsSync(file) && file !== '-') { - common.error('no such file or directory: ' + file, true); - return; + files.forEach(function (file) { + if (file !== '-') { + if (!fs.existsSync(file)) { + common.error('no such file or directory: ' + file, { continue: true }); + return; + } else if (common.statFollowLinks(file).isDirectory()) { + common.error("error reading '" + file + "': Is a directory", { + continue: true, + }); + return; + } } var contents = file === '-' ? pipe : fs.readFileSync(file, 'utf8'); var lines = contents.split('\n'); - if (lines[lines.length-1] === '') { + if (lines[lines.length - 1] === '') { lines.pop(); shouldAppendNewline = true; } else { shouldAppendNewline = false; } - tail = tail.concat(lines.slice(options.numLines)); + tail = tail.concat(plusOption ? lines.slice(-options.numLines - 1) : lines.slice(options.numLines)); }); - if (shouldAppendNewline) + if (shouldAppendNewline) { tail.push(''); // to add a trailing newline once we join - return new common.ShellString(tail.join('\n'), common.state.error, common.state.errorCode); + } + + return tail.join('\n'); } + module.exports = _tail; diff --git a/src/tempdir.js b/src/tempdir.js index f797b8050..b6f7796ee 100644 --- a/src/tempdir.js +++ b/src/tempdir.js @@ -1,27 +1,31 @@ -var common = require('./common'); var os = require('os'); var fs = require('fs'); +var common = require('./common'); -common.register('tempdir', _tempDir); +common.register('tempdir', _tempDir, { + allowGlobbing: false, + wrapOutput: false, +}); // Returns false if 'dir' is not a writeable directory, 'dir' otherwise function writeableDir(dir) { - if (!dir || !fs.existsSync(dir)) - return false; + if (!dir || !fs.existsSync(dir)) return false; - if (!fs.statSync(dir).isDirectory()) - return false; + if (!common.statFollowLinks(dir).isDirectory()) return false; - var testFile = dir+'/'+common.randomFileName(); + var testFile = dir + '/' + common.randomFileName(); try { fs.writeFileSync(testFile, ' '); common.unlinkSync(testFile); return dir; } catch (e) { + /* istanbul ignore next */ return false; } } +// Variable to cache the tempdir value for successive lookups. +var cachedTempDir; //@ //@ ### tempdir() @@ -35,16 +39,13 @@ function writeableDir(dir) { //@ Searches and returns string containing a writeable, platform-dependent temporary directory. //@ Follows Python's [tempfile algorithm](http://docs.python.org/library/tempfile.html#tempfile.tempdir). function _tempDir() { - var state = common.state; - if (state.tempDir) - return state.tempDir; // from cache - - state.tempDir = writeableDir(os.tmpdir && os.tmpdir()) || // node 0.10+ - writeableDir(os.tmpDir && os.tmpDir()) || // node 0.8+ - writeableDir(process.env['TMPDIR']) || - writeableDir(process.env['TEMP']) || - writeableDir(process.env['TMP']) || - writeableDir(process.env['Wimp$ScrapDir']) || // RiscOS + if (cachedTempDir) return cachedTempDir; + + cachedTempDir = writeableDir(os.tmpdir()) || + writeableDir(process.env.TMPDIR) || + writeableDir(process.env.TEMP) || + writeableDir(process.env.TMP) || + writeableDir(process.env.Wimp$ScrapDir) || // RiscOS writeableDir('C:\\TEMP') || // Windows writeableDir('C:\\TMP') || // Windows writeableDir('\\TEMP') || // Windows @@ -54,6 +55,21 @@ function _tempDir() { writeableDir('/usr/tmp') || writeableDir('.'); // last resort - return state.tempDir; + return cachedTempDir; } -module.exports = _tempDir; + +// Indicates if the tempdir value is currently cached. This is exposed for tests +// only. The return value should only be tested for truthiness. +function isCached() { + return cachedTempDir; +} + +// Clears the cached tempDir value, if one is cached. This is exposed for tests +// only. +function clearCache() { + cachedTempDir = undefined; +} + +module.exports.tempDir = _tempDir; +module.exports.isCached = isCached; +module.exports.clearCache = clearCache; diff --git a/src/test.js b/src/test.js index d4d84280f..7e769083d 100644 --- a/src/test.js +++ b/src/test.js @@ -1,10 +1,25 @@ -var common = require('./common'); var fs = require('fs'); +var common = require('./common'); + +common.register('test', _test, { + cmdOptions: { + 'b': 'block', + 'c': 'character', + 'd': 'directory', + 'e': 'exists', + 'f': 'file', + 'L': 'link', + 'p': 'pipe', + 'S': 'socket', + }, + wrapOutput: false, + allowGlobbing: false, +}); -common.register('test', _test); //@ //@ ### test(expression) +//@ //@ Available expression primaries: //@ //@ + `'-b', 'path'`: true if path is a block device @@ -20,68 +35,52 @@ common.register('test', _test); //@ //@ ```javascript //@ if (test('-d', path)) { /* do something with dir */ }; -//@ if (!test('-f', path)) continue; // skip if it's a regular file +//@ if (!test('-f', path)) continue; // skip if it's not a regular file //@ ``` //@ -//@ Evaluates expression using the available primaries and returns corresponding value. +//@ Evaluates `expression` using the available primaries and returns +//@ corresponding boolean value. function _test(options, path) { - if (!path) - common.error('no path given'); - - // hack - only works with unary primaries - options = common.parseOptions(options, { - 'b': 'block', - 'c': 'character', - 'd': 'directory', - 'e': 'exists', - 'f': 'file', - 'L': 'link', - 'p': 'pipe', - 'S': 'socket' - }); + if (!path) common.error('no path given'); var canInterpret = false; - for (var key in options) + Object.keys(options).forEach(function (key) { if (options[key] === true) { canInterpret = true; - break; } + }); - if (!canInterpret) - common.error('could not interpret expression'); + if (!canInterpret) common.error('could not interpret expression'); if (options.link) { try { - return fs.lstatSync(path).isSymbolicLink(); - } catch(e) { + return common.statNoFollowLinks(path).isSymbolicLink(); + } catch (e) { return false; } } - if (!fs.existsSync(path)) - return false; + if (!fs.existsSync(path)) return false; + + if (options.exists) return true; - if (options.exists) - return true; + var stats = common.statFollowLinks(path); - var stats = fs.statSync(path); + if (options.block) return stats.isBlockDevice(); - if (options.block) - return stats.isBlockDevice(); + if (options.character) return stats.isCharacterDevice(); - if (options.character) - return stats.isCharacterDevice(); + if (options.directory) return stats.isDirectory(); - if (options.directory) - return stats.isDirectory(); + if (options.file) return stats.isFile(); - if (options.file) - return stats.isFile(); + /* istanbul ignore next */ + if (options.pipe) return stats.isFIFO(); - if (options.pipe) - return stats.isFIFO(); + /* istanbul ignore next */ + if (options.socket) return stats.isSocket(); - if (options.socket) - return stats.isSocket(); + /* istanbul ignore next */ + return false; // fallback } // test module.exports = _test; diff --git a/src/to.js b/src/to.js index 80d15cbe9..e4b064f49 100644 --- a/src/to.js +++ b/src/to.js @@ -1,6 +1,11 @@ -var common = require('./common'); var fs = require('fs'); var path = require('path'); +var common = require('./common'); + +common.register('to', _to, { + pipeOnly: true, + wrapOutput: false, +}); //@ //@ ### ShellString.prototype.to(file) @@ -12,20 +17,22 @@ var path = require('path'); //@ ``` //@ //@ Analogous to the redirection operator `>` in Unix, but works with -//@ ShellStrings (such as those returned by `cat`, `grep`, etc). _Like Unix -//@ redirections, `to()` will overwrite any existing file!_ +//@ `ShellStrings` (such as those returned by `cat`, `grep`, etc.). _Like Unix +//@ redirections, `to()` will overwrite any existing file!_ Returns the same +//@ [ShellString](#shellstringstr) this operated on, to support chaining. function _to(options, file) { - if (!file) - common.error('wrong arguments'); + if (!file) common.error('wrong arguments'); - if (!fs.existsSync( path.dirname(file) )) - common.error('no such file or directory: ' + path.dirname(file)); + if (!fs.existsSync(path.dirname(file))) { + common.error('no such file or directory: ' + path.dirname(file)); + } try { - fs.writeFileSync(file, this.toString(), 'utf8'); + fs.writeFileSync(file, this.stdout || this.toString(), 'utf8'); return this; - } catch(e) { - common.error('could not write to file (code '+e.code+'): '+file, true); + } catch (e) { + /* istanbul ignore next */ + common.error('could not write to file (code ' + e.code + '): ' + file, { continue: true }); } } module.exports = _to; diff --git a/src/toEnd.js b/src/toEnd.js index 01058ab9b..dc30e6264 100644 --- a/src/toEnd.js +++ b/src/toEnd.js @@ -1,6 +1,11 @@ -var common = require('./common'); var fs = require('fs'); var path = require('path'); +var common = require('./common'); + +common.register('toEnd', _toEnd, { + pipeOnly: true, + wrapOutput: false, +}); //@ //@ ### ShellString.prototype.toEnd(file) @@ -12,19 +17,21 @@ var path = require('path'); //@ ``` //@ //@ Analogous to the redirect-and-append operator `>>` in Unix, but works with -//@ ShellStrings (such as those returned by `cat`, `grep`, etc). +//@ `ShellStrings` (such as those returned by `cat`, `grep`, etc.). Returns the +//@ same [ShellString](#shellstringstr) this operated on, to support chaining. function _toEnd(options, file) { - if (!file) - common.error('wrong arguments'); + if (!file) common.error('wrong arguments'); - if (!fs.existsSync( path.dirname(file) )) - common.error('no such file or directory: ' + path.dirname(file)); + if (!fs.existsSync(path.dirname(file))) { + common.error('no such file or directory: ' + path.dirname(file)); + } try { - fs.appendFileSync(file, this.toString(), 'utf8'); + fs.appendFileSync(file, this.stdout || this.toString(), 'utf8'); return this; - } catch(e) { - common.error('could not append to file (code '+e.code+'): '+file, true); + } catch (e) { + /* istanbul ignore next */ + common.error('could not append to file (code ' + e.code + '): ' + file, { continue: true }); } } module.exports = _toEnd; diff --git a/src/touch.js b/src/touch.js index a84a5a8e7..a26858648 100644 --- a/src/touch.js +++ b/src/touch.js @@ -1,50 +1,58 @@ -var common = require('./common'); var fs = require('fs'); +var common = require('./common'); -common.register('touch', _touch, {globStart: 1}); +common.register('touch', _touch, { + cmdOptions: { + 'a': 'atime_only', + 'c': 'no_create', + 'd': 'date', + 'm': 'mtime_only', + 'r': 'reference', + }, +}); //@ //@ ### touch([options,] file [, file ...]) //@ ### touch([options,] file_array) +//@ //@ Available options: //@ //@ + `-a`: Change only the access time //@ + `-c`: Do not create any files //@ + `-m`: Change only the modification time -//@ + `-d DATE`: Parse DATE and use it instead of current time -//@ + `-r FILE`: Use FILE's times instead of current time +//@ + `{'-d': someDate}`, `{date: someDate}`: Use a `Date` instance (ex. `someDate`) +//@ instead of current time +//@ + `{'-r': file}`, `{reference: file}`: Use `file`'s times instead of current +//@ time //@ //@ Examples: //@ //@ ```javascript //@ touch('source.js'); -//@ touch('-c', '/path/to/some/dir/source.js'); -//@ touch({ '-r': FILE }, '/path/to/some/dir/source.js'); +//@ touch('-c', 'path/to/file.js'); +//@ touch({ '-r': 'referenceFile.txt' }, 'path/to/file.js'); +//@ touch({ '-d': new Date('December 17, 1995 03:24:00'), '-m': true }, 'path/to/file.js'); +//@ touch({ date: new Date('December 17, 1995 03:24:00') }, 'path/to/file.js'); //@ ``` //@ -//@ Update the access and modification times of each FILE to the current time. -//@ A FILE argument that does not exist is created empty, unless -c is supplied. -//@ This is a partial implementation of *[touch(1)](http://linux.die.net/man/1/touch)*. +//@ Update the access and modification times of each file to the current time. +//@ A file argument that does not exist is created empty, unless `-c` is supplied. +//@ This is a partial implementation of +//@ [`touch(1)`](http://linux.die.net/man/1/touch). Returns a +//@ [ShellString](#shellstringstr) indicating success or failure. function _touch(opts, files) { - opts = common.parseOptions(opts, { - 'a': 'atime_only', - 'c': 'no_create', - 'd': 'date', - 'm': 'mtime_only', - 'r': 'reference', - }); - - if (!files) + if (!files) { common.error('no files given'); - else if (typeof files === 'string') + } else if (typeof files === 'string') { files = [].slice.call(arguments, 1); - else + } else { common.error('file arg should be a string file path or an Array of string file paths'); + } - files.forEach(function(f) { + files.forEach(function (f) { touchFile(opts, f); }); - return new common.ShellString('', common.state.error, common.state.errorCode); + return ''; } function touchFile(opts, file) { @@ -102,7 +110,7 @@ module.exports = _touch; function tryStatFile(filePath) { try { - return fs.statSync(filePath); + return common.statFollowLinks(filePath); } catch (e) { return null; } diff --git a/src/uniq.js b/src/uniq.js new file mode 100644 index 000000000..580270651 --- /dev/null +++ b/src/uniq.js @@ -0,0 +1,93 @@ +var fs = require('fs'); +var common = require('./common'); + +// add c spaces to the left of str +function lpad(c, str) { + var res = '' + str; + if (res.length < c) { + res = Array((c - res.length) + 1).join(' ') + res; + } + return res; +} + +common.register('uniq', _uniq, { + canReceivePipe: true, + cmdOptions: { + 'i': 'ignoreCase', + 'c': 'count', + 'd': 'duplicates', + }, +}); + +//@ +//@ ### uniq([options,] [input, [output]]) +//@ +//@ Available options: +//@ +//@ + `-i`: Ignore case while comparing +//@ + `-c`: Prefix lines by the number of occurrences +//@ + `-d`: Only print duplicate lines, one for each group of identical lines +//@ +//@ Examples: +//@ +//@ ```javascript +//@ uniq('foo.txt'); +//@ uniq('-i', 'foo.txt'); +//@ uniq('-cd', 'foo.txt', 'bar.txt'); +//@ ``` +//@ +//@ Filter adjacent matching lines from `input`. Returns a +//@ [ShellString](#shellstringstr). +function _uniq(options, input, output) { + // Check if this is coming from a pipe + var pipe = common.readFromPipe(); + + if (!pipe) { + if (!input) common.error('no input given'); + + if (!fs.existsSync(input)) { + common.error(input + ': No such file or directory'); + } else if (common.statFollowLinks(input).isDirectory()) { + common.error("error reading '" + input + "'"); + } + } + if (output && fs.existsSync(output) && common.statFollowLinks(output).isDirectory()) { + common.error(output + ': Is a directory'); + } + + var lines = (input ? fs.readFileSync(input, 'utf8') : pipe) + .trimRight() + .split('\n'); + + var compare = function (a, b) { + return options.ignoreCase ? + a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()) : + a.localeCompare(b); + }; + var uniqed = lines.reduceRight(function (res, e) { + // Perform uniq -c on the input + if (res.length === 0) { + return [{ count: 1, ln: e }]; + } else if (compare(res[0].ln, e) === 0) { + return [{ count: res[0].count + 1, ln: e }].concat(res.slice(1)); + } else { + return [{ count: 1, ln: e }].concat(res); + } + }, []).filter(function (obj) { + // Do we want only duplicated objects? + return options.duplicates ? obj.count > 1 : true; + }).map(function (obj) { + // Are we tracking the counts of each line? + return (options.count ? (lpad(7, obj.count) + ' ') : '') + obj.ln; + }).join('\n') + '\n'; + + if (output) { + (new common.ShellString(uniqed)).to(output); + // if uniq writes to output, nothing is passed to the next command in the pipeline (if any) + return ''; + } else { + return uniqed; + } +} + +module.exports = _uniq; diff --git a/src/which.js b/src/which.js index e7234e3b1..8ac7b77e5 100644 --- a/src/which.js +++ b/src/which.js @@ -1,26 +1,45 @@ -var common = require('./common'); var fs = require('fs'); var path = require('path'); +var common = require('./common'); -common.register('which', _which); +common.register('which', _which, { + allowGlobbing: false, + cmdOptions: { + 'a': 'all', + }, +}); -// XP's system default value for PATHEXT system variable, just in case it's not +// XP's system default value for `PATHEXT` system variable, just in case it's not // set on Windows. var XP_DEFAULT_PATHEXT = '.com;.exe;.bat;.cmd;.vbs;.vbe;.js;.jse;.wsf;.wsh'; -// Cross-platform method for splitting environment PATH variables +// For earlier versions of NodeJS that doesn't have a list of constants (< v6) +var FILE_EXECUTABLE_MODE = 1; + +function isWindowsPlatform() { + return process.platform === 'win32'; +} + +// Cross-platform method for splitting environment `PATH` variables function splitPath(p) { - if (!p) - return []; + return p ? p.split(path.delimiter) : []; +} - if (common.platform === 'win') - return p.split(';'); - else - return p.split(':'); +// Tests are running all cases for this func but it stays uncovered by codecov due to unknown reason +/* istanbul ignore next */ +function isExecutable(pathName) { + try { + // TODO(node-support): replace with fs.constants.X_OK once remove support for node < v6 + fs.accessSync(pathName, FILE_EXECUTABLE_MODE); + } catch (err) { + return false; + } + return true; } -function checkPath(path) { - return fs.existsSync(path) && !fs.statSync(path).isDirectory(); +function checkPath(pathName) { + return fs.existsSync(pathName) && !common.statFollowLinks(pathName).isDirectory() + && (isWindowsPlatform() || isExecutable(pathName)); } //@ @@ -32,69 +51,69 @@ function checkPath(path) { //@ var nodeExec = which('node'); //@ ``` //@ -//@ Searches for `command` in the system's PATH. On Windows, this uses the +//@ Searches for `command` in the system's `PATH`. On Windows, this uses the //@ `PATHEXT` variable to append the extension if it's not already executable. -//@ Returns string containing the absolute path to the command. +//@ Returns a [ShellString](#shellstringstr) containing the absolute path to +//@ `command`. function _which(options, cmd) { - if (!cmd) - common.error('must specify command'); + if (!cmd) common.error('must specify command'); + + var isWindows = isWindowsPlatform(); + var pathArray = splitPath(process.env.PATH); - var pathEnv = process.env.path || process.env.Path || process.env.PATH, - pathArray = splitPath(pathEnv), - where = null; + var queryMatches = []; // No relative/absolute paths provided? - if (cmd.search(/\//) === -1) { + if (!cmd.includes('/')) { + // Assume that there are no extensions to append to queries (this is the + // case for unix) + var pathExtArray = ['']; + if (isWindows) { + // In case the PATHEXT variable is somehow not set (e.g. + // child_process.spawn with an empty environment), use the XP default. + var pathExtEnv = process.env.PATHEXT || XP_DEFAULT_PATHEXT; + pathExtArray = splitPath(pathExtEnv.toUpperCase()); + } + // Search for command in PATH - pathArray.forEach(function(dir) { - if (where) - return; // already found it + for (var k = 0; k < pathArray.length; k++) { + // already found it + if (queryMatches.length > 0 && !options.all) break; - var attempt = path.resolve(dir, cmd); + var attempt = path.resolve(pathArray[k], cmd); - if (common.platform === 'win') { + if (isWindows) { attempt = attempt.toUpperCase(); + } - // In case the PATHEXT variable is somehow not set (e.g. - // child_process.spawn with an empty environment), use the XP default. - var pathExtEnv = process.env.PATHEXT || XP_DEFAULT_PATHEXT; - var pathExtArray = splitPath(pathExtEnv.toUpperCase()); - var i; - - // If the extension is already in PATHEXT, just return that. - for (i = 0; i < pathExtArray.length; i++) { - var ext = pathExtArray[i]; - if (attempt.slice(-ext.length) === ext && checkPath(attempt)) { - where = attempt; - return; - } + var match = attempt.match(/\.[^<>:"/|?*.]+$/); + if (match && pathExtArray.includes(match[0])) { // this is Windows-only + // The user typed a query with the file extension, like + // `which('node.exe')` + if (checkPath(attempt)) { + queryMatches.push(attempt); + break; } - - // Cycle through the PATHEXT variable - var baseAttempt = attempt; - for (i = 0; i < pathExtArray.length; i++) { - attempt = baseAttempt + pathExtArray[i]; - if (checkPath(attempt)) { - where = attempt; - return; + } else { // All-platforms + // Cycle through the PATHEXT array, and check each extension + // Note: the array is always [''] on Unix + for (var i = 0; i < pathExtArray.length; i++) { + var ext = pathExtArray[i]; + var newAttempt = attempt + ext; + if (checkPath(newAttempt)) { + queryMatches.push(newAttempt); + break; } } - } else { - // Assume it's Unix-like - if (checkPath(attempt)) { - where = attempt; - return; - } } - }); + } + } else if (checkPath(cmd)) { // a valid absolute or relative path + queryMatches.push(path.resolve(cmd)); } - // Command not found anywhere? - if (!checkPath(cmd) && !where) - return null; - - where = where || path.resolve(cmd); - - return new common.ShellString(where, '', common.state.errorCode); + if (queryMatches.length > 0) { + return options.all ? queryMatches : queryMatches[0]; + } + return options.all ? [] : null; } module.exports = _which; diff --git a/test/.eslintrc.json b/test/.eslintrc.json new file mode 100644 index 000000000..1e28f5360 --- /dev/null +++ b/test/.eslintrc.json @@ -0,0 +1,43 @@ +{ + "env": { + "node": true + }, + "extends": "airbnb-base", + "rules": { + "arrow-parens": "off", + "comma-dangle": "off", + "curly": "off", + "function-call-argument-newline": "off", + "function-paren-newline": "off", + "global-require": "off", + "import/no-dynamic-require": "off", + "import/no-mutable-exports": "off", + "import/no-unresolved": "off", + "indent": "off", + "max-len": "off", + "no-bitwise": "off", + "no-console": "off", + "no-multiple-empty-lines": "off", + "no-param-reassign": "off", + "no-plusplus": "off", + "no-underscore-dangle": "off", + "no-var": "error", + "operator-linebreak": "off", + "prefer-arrow-callback": "off", + "prefer-const": "error", + "prefer-destructuring": "off", + "prefer-numeric-literals": "off", + "prefer-template": "off", + "spaced-comment": ["error", "always", { "markers": ["@", "@include"], "exceptions": ["@"] }], + "vars-on-top": "off", + "new-cap": ["error", { + "capIsNewExceptions": [ + "ShellString" + ] + }], + "quotes": ["error", "single", { + "avoidEscape": true, + "allowTemplateLiterals": true + }] + } +} diff --git a/test/.gitignore b/test/.gitignore deleted file mode 100644 index a1632ab58..000000000 --- a/test/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -tmp/ - diff --git a/test/cat.js b/test/cat.js index 3435beb03..e077b9be9 100644 --- a/test/cat.js +++ b/test/cat.js @@ -1,54 +1,119 @@ -var shell = require('..'); +const fs = require('fs'); -var assert = require('assert'), - fs = require('fs'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +shell.config.silent = true; // // Invalids // -var result = shell.cat(); -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'cat: no paths given'); +test('no paths given', t => { + const result = shell.cat(); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'cat: no paths given'); +}); -assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check -result = shell.cat('/asdfasdf'); // file does not exist -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'cat: no such file or directory: /asdfasdf'); +test('nonexistent file', t => { + t.falsy(fs.existsSync('/asdfasdf')); // sanity check + const result = shell.cat('/asdfasdf'); // file does not exist + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'cat: no such file or directory: /asdfasdf'); +}); + +test('directory', t => { + const result = shell.cat('test/resources/cat'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'cat: test/resources/cat: Is a directory'); +}); // // Valids // -// simple -result = shell.cat('resources/cat/file1'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, 'test1\n'); - -// multiple files -result = shell.cat('resources/cat/file2', 'resources/cat/file1'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, 'test2\ntest1\n'); - -// multiple files, array syntax -result = shell.cat(['resources/cat/file2', 'resources/cat/file1']); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, 'test2\ntest1\n'); - -result = shell.cat('resources/file*.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.ok(result.search('test1') > -1); // file order might be random -assert.ok(result.search('test2') > -1); - -shell.exit(123); +test('simple', t => { + const result = shell.cat('test/resources/cat/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'test1\n'); +}); + +test('multiple files', t => { + const result = shell.cat('test/resources/cat/file2', 'test/resources/cat/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'test2\ntest1\n'); +}); + +test('multiple files, array syntax', t => { + const result = shell.cat(['test/resources/cat/file2', 'test/resources/cat/file1']); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'test2\ntest1\n'); +}); + +test('glob', t => { + const result = shell.cat('test/resources/file*.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.search('test1') > -1); // file order might be random + t.truthy(result.search('test2') > -1); +}); + +test('without EOF', t => { + const result = shell.cat('test/resources/cat/file3'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'test3'); +}); + +test('empty', t => { + const result = shell.cat('test/resources/cat/file5'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), ''); +}); + +// +// With numbers +// + +test('simple with numbers', t => { + const result = shell.cat('-n', 'test/resources/cat/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), ' 1\ttest1\n'); +}); + +test('simple twelve lines file with numbers', t => { + const result = shell.cat('-n', 'test/resources/cat/file4'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), ' 1\ttest4-01\n 2\ttest4-02\n 3\ttest4-03\n 4\ttest4-04\n 5\ttest4-05\n 6\ttest4-06\n 7\ttest4-07\n 8\ttest4-08\n 9\ttest4-09\n 10\ttest4-10\n 11\ttest4-11\n 12\ttest4-12\n'); +}); + +test('multiple with numbers', t => { + const result = shell.cat('-n', 'test/resources/cat/file2', 'test/resources/cat/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), ' 1\ttest2\n 2\ttest1\n'); +}); + +test('simple numbers without EOF', t => { + const result = shell.cat('-n', 'test/resources/cat/file3'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), ' 1\ttest3'); +}); + +test('multiple numbers without EOF', t => { + const result = shell.cat('-n', 'test/resources/cat/file3', 'test/resources/cat/file2', 'test/resources/cat/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), ' 1\ttest3test2\n 2\ttest1\n'); +}); diff --git a/test/cd.js b/test/cd.js index 1745ff6e4..53404e249 100644 --- a/test/cd.js +++ b/test/cd.js @@ -1,92 +1,105 @@ -var shell = require('..'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); -var assert = require('assert'), - path = require('path'), - fs = require('fs'), - common = require('../src/common'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const utils = require('./utils/utils'); -// save current dir -var cur = shell.pwd(); +const cur = shell.pwd().toString(); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + process.chdir(cur); + shell.mkdir(t.context.tmp); +}); + +test.afterEach.always(t => { + process.chdir(cur); + shell.rm('-rf', t.context.tmp); +}); // // Invalids // -assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check -var result = shell.cd('/asdfasdf'); // dir does not exist -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'cd: no such file or directory: /asdfasdf'); - -assert.equal(fs.existsSync('resources/file1'), true); // sanity check -result = shell.cd('resources/file1'); // file, not dir -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'cd: not a directory: resources/file1'); - -result = shell.cd('-'); // Haven't changed yet, so there is no previous directory -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'cd: could not find previous directory'); +test('nonexistent directory', t => { + t.falsy(fs.existsSync('/asdfasdf')); + const result = shell.cd('/asdfasdf'); // dir does not exist + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'cd: no such file or directory: /asdfasdf'); +}); + +test('file not dir', t => { + t.truthy(fs.existsSync('test/resources/file1')); // sanity check + const result = shell.cd('test/resources/file1'); // file, not dir + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'cd: not a directory: test/resources/file1'); +}); + +test('no previous dir', t => { + const result = shell.cd('-'); // Haven't changed yet, so there is no previous directory + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'cd: could not find previous directory'); +}); // // Valids // -result = shell.cd(cur); -result = shell.cd('tmp'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(path.basename(process.cwd()), 'tmp'); - -result = shell.cd(cur); -result = shell.cd('/'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(process.cwd(), path.resolve('/')); - -result = shell.cd(cur); -result = shell.cd('/'); -result = shell.cd('-'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(process.cwd(), path.resolve(cur.toString())); - -// cd + other commands - -result = shell.cd(cur); -result = shell.rm('-f', 'tmp/*'); -assert.equal(fs.existsSync('tmp/file1'), false); -result = shell.cd('resources'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -result = shell.cp('file1', '../tmp'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -result = shell.cd('../tmp'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('file1'), true); - -// Test tilde expansion - -result = shell.cd('~'); -assert.equal(process.cwd(), common.getUserHome()); -result = shell.cd('..'); -assert.notEqual(process.cwd(), common.getUserHome()); -result = shell.cd('~'); // Change back to home -assert.equal(process.cwd(), common.getUserHome()); - -// Goes to home directory if no arguments are passed -result = shell.cd(cur); -result = shell.cd(); -assert.ok(!shell.error()); -assert.equal(result.code, 0); -assert.equal(process.cwd(), common.getUserHome()); - -shell.exit(123); +test('relative path', t => { + const result = shell.cd(t.context.tmp); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(path.basename(process.cwd()), t.context.tmp); +}); + +test('absolute path', t => { + const result = shell.cd('/'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(process.cwd(), path.resolve('/')); +}); + +test('previous directory (-)', t => { + shell.cd('/'); + const result = shell.cd('-'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(process.cwd(), path.resolve(cur.toString())); +}); + +test('cd + other commands', t => { + t.falsy(fs.existsSync(`${t.context.tmp}/file1`)); + let result = shell.cd('test/resources'); + t.falsy(shell.error()); + t.is(result.code, 0); + result = shell.cp('file1', `../../${t.context.tmp}`); + t.falsy(shell.error()); + t.is(result.code, 0); + result = shell.cd(`../../${t.context.tmp}`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(fs.existsSync('file1')); +}); + +test('Tilde expansion', t => { + shell.cd('~'); + t.is(process.cwd(), os.homedir()); + shell.cd('..'); + t.not(process.cwd(), os.homedir()); + shell.cd('~'); // Change back to home + t.is(process.cwd(), os.homedir()); +}); + +test('Goes to home directory if no arguments are passed', t => { + const result = shell.cd(); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(process.cwd(), os.homedir()); +}); diff --git a/test/chmod.js b/test/chmod.js index f433f22cb..660972c91 100644 --- a/test/chmod.js +++ b/test/chmod.js @@ -1,200 +1,359 @@ -var shell = require('..'); -var common = require('../src/common'); +const fs = require('fs'); -var assert = require('assert'), - fs = require('fs'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const common = require('../src/common'); +const utils = require('./utils/utils'); + +let TMP; +const BITMASK = parseInt('777', 8); + +test.before(() => { + TMP = utils.getTempDir(); + shell.cp('-r', 'test/resources', TMP); + shell.config.silent = true; +}); + +test.after(() => { + shell.rm('-rf', TMP); +}); // // Invalids // -var result = shell.chmod('blah'); // missing args -assert.ok(shell.error()); -assert.equal(result.code, 1); -result = shell.chmod('893', 'resources/chmod'); // invalid permissions - mode must be in octal -assert.ok(shell.error()); -assert.equal(result.code, 1); +test('invalid permissions', t => { + let result = shell.chmod('blah'); + t.truthy(shell.error()); + t.is(result.code, 1); + result = shell.chmod('893', `${TMP}/chmod`); // invalid permissions - mode must be in octal + t.truthy(shell.error()); + t.is(result.code, 1); +}); -// -// Valids -// +test('Basic usage with octal codes', t => { + utils.skipOnWin(t, () => { + let result = shell.chmod('755', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & BITMASK, + parseInt('755', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & BITMASK, + parseInt('644', 8) + ); + }); +}); + +test('symbolic mode', t => { + utils.skipOnWin(t, () => { + let result = shell.chmod('o+x', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('007', 8), + parseInt('005', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); + }); +}); + +test('symbolic mode, without group', t => { + utils.skipOnWin(t, () => { + let result = shell.chmod('+x', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & BITMASK, + parseInt('755', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); + }); +}); + +test('Test setuid', t => { + utils.skipOnWin(t, () => { + let result = shell.chmod('u+s', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('4000', 8), + parseInt('4000', 8) + ); + result = shell.chmod('u-s', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & BITMASK, + parseInt('644', 8) + ); + + // according to POSIX standards at http://linux.die.net/man/1/chmod, + // setuid is never cleared from a directory unless explicitly asked for. + result = shell.chmod('u+s', `${TMP}/chmod/c`); + + t.is(result.code, 0); + result = shell.chmod('755', `${TMP}/chmod/c`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/c`).mode & parseInt('4000', 8), + parseInt('4000', 8) + ); + result = shell.chmod('u-s', `${TMP}/chmod/c`); + t.is(result.code, 0); + }); +}); + +test('Test setgid', t => { + utils.skipOnWin(t, () => { + let result = shell.chmod('g+s', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('2000', 8), + parseInt('2000', 8) + ); + result = shell.chmod('g-s', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & BITMASK, + parseInt('644', 8) + ); + }); +}); + +test('Test sticky bit', t => { + utils.skipOnWin(t, () => { + let result = shell.chmod('+t', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('1000', 8), + parseInt('1000', 8) + ); + result = shell.chmod('-t', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & BITMASK, + parseInt('644', 8) + ); + t.is(common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('1000', 8), 0); + }); +}); + +test('Test directories', t => { + utils.skipOnWin(t, () => { + let result = shell.chmod('a-w', `${TMP}/chmod/b/a/b`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/b/a/b`).mode & BITMASK, + parseInt('555', 8) + ); + result = shell.chmod('755', `${TMP}/chmod/b/a/b`); + t.is(result.code, 0); + }); +}); + +test('Test recursion', t => { + utils.skipOnWin(t, () => { + let result = shell.chmod('-R', 'a+w', `${TMP}/chmod/b`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/b/a/b`).mode & BITMASK, + BITMASK + ); + result = shell.chmod('-R', '755', `${TMP}/chmod/b`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/b/a/b`).mode & BITMASK, + parseInt('755', 8) + ); + }); +}); + +test('Test symbolic links w/ recursion - WARNING: *nix only', t => { + utils.skipOnWin(t, () => { + fs.symlinkSync(`${TMP}/chmod/b/a`, `${TMP}/chmod/a/b/c/link`, 'dir'); + let result = shell.chmod('-R', 'u-w', `${TMP}/chmod/a/b`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/a/b/c`).mode & parseInt('700', 8), + parseInt('500', 8) + ); + t.is( + common.statFollowLinks(`${TMP}/chmod/b/a`).mode & parseInt('700', 8), + parseInt('700', 8) + ); + result = shell.chmod('-R', 'u+w', `${TMP}/chmod/a/b`); + t.is(result.code, 0); + fs.unlinkSync(`${TMP}/chmod/a/b/c/link`); + }); +}); + +test('Test combinations', t => { + let result = shell.chmod('a-rwx', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('000', 8), + parseInt('000', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); +}); + +test('multiple symbolic modes', t => { + let result = shell.chmod('a-rwx,u+r', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('400', 8), + parseInt('400', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); +}); + +test('multiple symbolic modes #2', t => { + let result = shell.chmod('a-rwx,u+rw', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('600', 8), + parseInt('600', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); +}); + +test('multiple symbolic modes #3', t => { + utils.skipOnWin(t, () => { + let result = shell.chmod('a-rwx,u+rwx', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('700', 8), + parseInt('700', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); + }); +}); + +test('u+rw', t => { + let result = shell.chmod('000', `${TMP}/chmod/file1`); + t.is(result.code, 0); + result = shell.chmod('u+rw', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('600', 8), + parseInt('600', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); +}); + +test('u+wx', t => { + utils.skipOnWin(t, () => { + let result = shell.chmod('000', `${TMP}/chmod/file1`); + t.is(result.code, 0); + result = shell.chmod('u+wx', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('300', 8), + parseInt('300', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); + }); +}); + +test('Multiple symbolic modes at once', t => { + utils.skipOnWin(t, () => { + let result = shell.chmod('000', `${TMP}/chmod/file1`); + t.is(result.code, 0); + result = shell.chmod('u+r,g+w,o+x', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('421', 8), + parseInt('421', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); + }); +}); + +test('u+rw,g+wx', t => { + utils.skipOnWin(t, () => { + let result = shell.chmod('000', `${TMP}/chmod/file1`); + t.is(result.code, 0); + result = shell.chmod('u+rw,g+wx', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('630', 8), + parseInt('630', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); + }); +}); + +test('u-x,g+rw', t => { + let result = shell.chmod('700', `${TMP}/chmod/file1`); + t.is(result.code, 0); + result = shell.chmod('u-x,g+rw', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('660', 8), + parseInt('660', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); +}); + +test('a-rwx,u+rw', t => { + let result = shell.chmod('a-rwx,u+rw', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('600', 8), + parseInt('600', 8) + ); + result = shell.chmod('a-rwx,u+rw', `${TMP}/chmod/file1`); + t.is(result.code, 0); + t.is( + common.statFollowLinks(`${TMP}/chmod/file1`).mode & parseInt('600', 8), + parseInt('600', 8) + ); + result = shell.chmod('644', `${TMP}/chmod/file1`); + t.is(result.code, 0); +}); + +test('Numeric modes', t => { + let result = shell.chmod('744', `${TMP}/chmod/xdir`); + t.is(result.code, 0); + result = shell.chmod('644', `${TMP}/chmod/xdir/file`); + t.is(result.code, 0); + result = shell.chmod('744', `${TMP}/chmod/xdir/deep`); + t.is(result.code, 0); + result = shell.chmod('644', `${TMP}/chmod/xdir/deep/file`); + t.is(result.code, 0); + result = shell.chmod('-R', 'a+X', `${TMP}/chmod/xdir`); + t.is(result.code, 0); +}); -// On Windows, chmod acts VERY differently so skip those tests for now -if (common.platform === 'win') - shell.exit(123); - -// Test files - the bitmasking is to ignore the upper bits. -result = shell.chmod('755', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('777', 8), parseInt('755', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('777', 8), parseInt('644', 8)); - -result = shell.chmod('o+x', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('007', 8), parseInt('005', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); - -result = shell.chmod('+x', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('777', 8), parseInt('755', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); - -// Test setuid -result = shell.chmod('u+s', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('4000', 8), parseInt('4000', 8)); -result = shell.chmod('u-s', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('777', 8), parseInt('644', 8)); - -// according to POSIX standards at http://linux.die.net/man/1/chmod, -// setuid is never cleared from a directory unless explicitly asked for. -result = shell.chmod('u+s', 'resources/chmod/c'); -assert.equal(result.code, 0); -result = shell.chmod('755', 'resources/chmod/c'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/c').mode & parseInt('4000', 8), parseInt('4000', 8)); -result = shell.chmod('u-s', 'resources/chmod/c'); -assert.equal(result.code, 0); - -// Test setgid -result = shell.chmod('g+s', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('2000', 8), parseInt('2000', 8)); -result = shell.chmod('g-s', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('777', 8), parseInt('644', 8)); - -// Test sticky bit -result = shell.chmod('+t', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('1000', 8), parseInt('1000', 8)); -result = shell.chmod('-t', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('777', 8), parseInt('644', 8)); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('1000', 8), 0); - -// Test directories -result = shell.chmod('a-w', 'resources/chmod/b/a/b'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/b/a/b').mode & parseInt('777', 8), parseInt('555', 8)); -result = shell.chmod('755', 'resources/chmod/b/a/b'); -assert.equal(result.code, 0); - -// Test recursion -result = shell.chmod('-R', 'a+w', 'resources/chmod/b'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/b/a/b').mode & parseInt('777', 8), parseInt('777', 8)); -result = shell.chmod('-R', '755', 'resources/chmod/b'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/b/a/b').mode & parseInt('777', 8), parseInt('755', 8)); - -// Test symbolic links w/ recursion - WARNING: *nix only -fs.symlinkSync('resources/chmod/b/a', 'resources/chmod/a/b/c/link', 'dir'); -result = shell.chmod('-R', 'u-w', 'resources/chmod/a/b'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/a/b/c').mode & parseInt('700', 8), parseInt('500', 8)); -assert.equal(fs.statSync('resources/chmod/b/a').mode & parseInt('700', 8), parseInt('700', 8)); -result = shell.chmod('-R', 'u+w', 'resources/chmod/a/b'); -assert.equal(result.code, 0); -fs.unlinkSync('resources/chmod/a/b/c/link'); - -// Test combinations -result = shell.chmod('a-rwx', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('000', 8), parseInt('000', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); - -result = shell.chmod('a-rwx,u+r', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('400', 8), parseInt('400', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); - -result = shell.chmod('a-rwx,u+rw', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('600', 8), parseInt('600', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); - -result = shell.chmod('a-rwx,u+rwx', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('700', 8), parseInt('700', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); - -result = shell.chmod('000', 'resources/chmod/file1'); -assert.equal(result.code, 0); -result = shell.chmod('u+rw', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('600', 8), parseInt('600', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); - -result = shell.chmod('000', 'resources/chmod/file1'); -assert.equal(result.code, 0); -result = shell.chmod('u+wx', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('300', 8), parseInt('300', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); - -result = shell.chmod('000', 'resources/chmod/file1'); -assert.equal(result.code, 0); -result = shell.chmod('u+r,g+w,o+x', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('421', 8), parseInt('421', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); - -result = shell.chmod('000', 'resources/chmod/file1'); -assert.equal(result.code, 0); -result = shell.chmod('u+rw,g+wx', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('630', 8), parseInt('630', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); - -result = shell.chmod('700', 'resources/chmod/file1'); -assert.equal(result.code, 0); -result = shell.chmod('u-x,g+rw', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('660', 8), parseInt('660', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); - -result = shell.chmod('a-rwx,u+rw', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('600', 8), parseInt('600', 8)); -result = shell.chmod('a-rwx,u+rw', 'resources/chmod/file1'); -assert.equal(result.code, 0); -assert.equal(fs.statSync('resources/chmod/file1').mode & parseInt('600', 8), parseInt('600', 8)); -result = shell.chmod('644', 'resources/chmod/file1'); -assert.equal(result.code, 0); - -// Support capital X ("entry" permission aka directory-only execute) - -result = shell.chmod('744', 'resources/chmod/xdir'); -assert.equal(result.code, 0); -result = shell.chmod('644', 'resources/chmod/xdir/file'); -assert.equal(result.code, 0); -result = shell.chmod('744', 'resources/chmod/xdir/deep'); -assert.equal(result.code, 0); -result = shell.chmod('644', 'resources/chmod/xdir/deep/file'); -assert.equal(result.code, 0); -result = shell.chmod('-R', 'a+X', 'resources/chmod/xdir'); -assert.equal(result.code, 0); - -assert.equal(fs.statSync('resources/chmod/xdir').mode & parseInt('755', 8), parseInt('755', 8)); -assert.equal(fs.statSync('resources/chmod/xdir/file').mode & parseInt('644', 8), parseInt('644', 8)); -assert.equal(fs.statSync('resources/chmod/xdir/deep').mode & parseInt('755', 8), parseInt('755', 8)); -assert.equal(fs.statSync('resources/chmod/xdir/deep/file').mode & parseInt('644', 8), parseInt('644', 8)); - -shell.exit(123); +test('Make sure chmod succeeds for a variety of octal codes', t => { + utils.skipOnWin(t, () => { + t.is( + common.statFollowLinks(`${TMP}/chmod/xdir`).mode & parseInt('755', 8), + parseInt('755', 8) + ); + t.is( + common.statFollowLinks(`${TMP}/chmod/xdir/file`).mode & parseInt('644', 8), + parseInt('644', 8) + ); + t.is( + common.statFollowLinks(`${TMP}/chmod/xdir/deep`).mode & parseInt('755', 8), + parseInt('755', 8) + ); + t.is( + common.statFollowLinks(`${TMP}/chmod/xdir/deep/file`).mode & parseInt('644', 8), + parseInt('644', 8) + ); + }); +}); diff --git a/test/cmd.js b/test/cmd.js new file mode 100644 index 000000000..f49c98873 --- /dev/null +++ b/test/cmd.js @@ -0,0 +1,279 @@ +const path = require('path'); + +const test = require('ava'); + +const shell = require('..'); + +const CWD = process.cwd(); + +test.beforeEach(() => { + process.chdir(CWD); + shell.config.resetForTesting(); +}); + +// +// Invalids +// + +test('no args', t => { + shell.cmd(); + t.truthy(shell.error()); +}); + +test('unknown command', t => { + const result = shell.cmd('asdfasdf'); // could not find command + t.truthy(result.code > 0); + t.is(result.code, 127); +}); + +test('config.fatal and unknown command', t => { + shell.config.fatal = true; + t.throws(() => { + shell.cmd('asdfasdf'); // could not find command + }, { message: /.*command not found.*/ }); +}); + +// TODO(nfischer): enable only if we implement realtime output + captured +// output. +test.skip('cmd exits gracefully if we cannot find the execPath', t => { + shell.config.execPath = null; + shell.cmd('shx', 'echo', 'foo'); + t.regex( + shell.error(), + /Unable to find a path to the node binary\. Please manually set config\.execPath/ + ); +}); + +// +// Valids +// + +// +// sync +// + +test('check if stdout goes to output', t => { + const result = shell.cmd('shx', 'echo', 'this is stdout'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, 'this is stdout\n'); +}); + +test('check if stderr goes to output', t => { + const result = shell.cmd(shell.config.execPath, '-e', 'console.error("1234");'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, ''); + t.is(result.stderr, '1234\n'); +}); + +test('check if stdout + stderr go to output', t => { + const result = shell.cmd(shell.config.execPath, '-e', 'console.error(1234); console.log(666);'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, '666\n'); + t.is(result.stderr, '1234\n'); +}); + +test('check exit code', t => { + const result = shell.cmd(shell.config.execPath, '-e', 'process.exit(12);'); + t.truthy(shell.error()); + t.is(result.code, 12); +}); + +test('interaction with cd', t => { + shell.cd('test/resources/external'); + const result = shell.cmd(shell.config.execPath, 'node_script.js'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, 'node_script_1234\n'); +}); + +test('no need to escape quotes', t => { + const result = shell.cmd(shell.config.execPath, '-e', + `console.log("'+'_'+'");`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, "'+'_'+'\n"); +}); + +test('commands can contain newlines', t => { + // GitHub issue #175. This test uses a nodejs script rather than a shell + // command because Windows 'echo' doesn't handle \n the same way as Unix + // 'echo'. This test case proves the newline is passed correctly to the + // underlying program because otherwise node would not parse the two lines as + // separate statements and it would throw a JavaScript syntax error. + const result = shell.cmd(shell.config.execPath, '-e', ` +console.log('line1') +console.log('line2') +`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, 'line1\nline2\n'); +}); + +test('does not expand shell-style variables', t => { + shell.env.FOO = 'Hello world'; + const result = shell.cmd('shx', 'echo', '$FOO'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, '$FOO\n'); +}); + +test('does not expand windows-style variables', t => { + shell.env.FOO = 'Hello world'; + let result = shell.cmd('shx', 'echo', '%FOO%'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, '%FOO%\n'); + result = shell.cmd('shx', 'echo', '!FOO!'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, '!FOO!\n'); +}); + +test('caret character is passed through to the command', t => { + // '^' is a special character on Windows, see issue #1015 + const result = shell.cmd('shx', 'echo', 'shelljs@^0.8.4'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, 'shelljs@^0.8.4\n'); +}); + +test('cannot inject multiple commands', t => { + const injection = '; echo semicolon && echo and || echo or'; + const result = shell.cmd('shx', 'echo', `hi${injection}`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, `hi${injection}\n`); +}); + +test('supports globbing by default', t => { + // `echo` on windows will not glob, so it depends on shell.cmd() to expand the + // glob before spawning the subprocess. + const result = shell.cmd('shx', 'echo', 'test/resources/*.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + const expectedFiles = [ + 'test/resources/a.txt', + 'test/resources/file1.txt', + 'test/resources/file2.txt', + ]; + t.is(result.stdout, `${expectedFiles.join(' ')}\n`); +}); + +test('globbing respects config.noglob', t => { + shell.config.noglob = true; + const result = shell.cmd('shx', 'echo', 'test/resources/*.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, 'test/resources/*.txt\n'); +}); + +test('set cwd', t => { + const result = shell.cmd('shx', 'pwd', { cwd: '..' }); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, path.resolve('..') + '\n'); +}); + +test('command fails silently with non-zero status', t => { + const result = shell.cmd('shx', 'false'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stdout, ''); + t.is(result.stderr, ''); +}); + +test('set maxBuffer (very small)', t => { + let result = shell.cmd('shx', 'echo', '1234567890'); // default maxBuffer is ok + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, '1234567890\n'); + result = shell.cmd('shx', 'echo', '1234567890', { maxBuffer: 6 }); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stdout, '1234567890\n'); +}); + +test('set timeout option', t => { + let result = shell.cmd(shell.config.execPath, 'test/resources/exec/slow.js', '100'); // default timeout is ok + t.falsy(shell.error()); + t.is(result.stdout, 'fast\nslow\n'); + t.is(result.code, 0); + result = shell.cmd(shell.config.execPath, 'test/resources/exec/slow.js', '2000', { timeout: 1000 }); // times out + t.truthy(shell.error()); + t.is(result.stdout, 'fast\n'); + t.truthy(result.stderr); + t.is(result.code, 1); +}); + +test('check process.env works', t => { + shell.env.FOO = 'Hello world'; + // Launch any sub process, and process.env should be propagated through. + const result = + shell.cmd(shell.config.execPath, '-p', 'process.env.FOO'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, 'Hello world\n'); + t.is(result.stderr, ''); +}); + +test('cmd returns a ShellString', t => { + const result = shell.cmd('shx', 'echo', 'foo'); + t.is(typeof result, 'object'); + t.truthy(result instanceof String); + t.is(typeof result.stdout, 'string'); + t.is(result.toString(), result.stdout); +}); + +// +// async +// + +function cmdAsync(...commandArgs) { + return new Promise((resolve) => { + shell.cmd(...commandArgs, (code, stdout, stderr) => { + resolve({ code, stdout, stderr }); + }); + }); +} + +// TODO(nfischer): enable after we implement async. +test.skip('no callback', t => { + const c = shell.cmd(shell.config.execPath, '-e', 'console.log(1234)', { async: true }); + t.falsy(shell.error()); + t.truthy('stdout' in c, 'async exec returns child process object'); +}); + +// TODO(nfischer): enable after we implement async. +test.skip('callback as 2nd argument', async t => { + const result = await cmdAsync(shell.config.execPath, '-e', 'console.log(5678);'); + t.is(result.code, 0); + t.is(result.stdout, '5678\n'); + t.is(result.stderr, ''); +}); + +// TODO(nfischer): enable after we implement async. +test.skip('callback as end argument', async t => { + const result = await cmdAsync(shell.config.execPath, '-e', 'console.log(5566);', { async: true }); + t.is(result.code, 0); + t.is(result.stdout, '5566\n'); + t.is(result.stderr, ''); +}); + +// TODO(nfischer): enable after we implement async. +test.skip('callback as 3rd argument (silent:true)', async t => { + const result = await cmdAsync(shell.config.execPath, '-e', 'console.log(5678);', { silent: true }); + t.is(result.code, 0); + t.is(result.stdout, '5678\n'); + t.is(result.stderr, ''); +}); + +// TODO(nfischer): enable after we implement async. +test.skip('command that fails', async t => { + const result = await cmdAsync('shx', 'cp', 'onlyOneCpArgument.txt', { silent: true }); + t.is(result.code, 1); + t.is(result.stdout, ''); + t.is(result.stderr, 'cp: missing and/or \n'); +}); diff --git a/test/common.js b/test/common.js index 4c6214da6..8fc836590 100644 --- a/test/common.js +++ b/test/common.js @@ -1,111 +1,359 @@ -var shell = require('..'); -var common = require('../src/common'); +const test = require('ava'); -var assert = require('assert'); +const shell = require('..'); +const common = require('../src/common'); +const utils = require('./utils/utils'); -shell.config.silent = true; +test.beforeEach(() => { + shell.config.silent = true; + common.state.error = null; + common.state.errorCode = 0; +}); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +test.afterEach(() => { + common.config.resetForTesting(); +}); // // Invalids // -// too few args -assert.throws(function () { - common.expand(); -}, TypeError); +test('too few args', t => { + t.throws(() => { + common.expand(); + }, { instanceOf: TypeError }); +}); + +test('should be a list', t => { + t.throws(() => { + common.expand('test/resources'); + }, { instanceOf: TypeError }); +}); + +test('parseOptions (invalid option in options object)', t => { + t.throws(() => { + common.parseOptions({ q: 'some string value' }, { + R: 'recursive', + f: 'force', + r: 'reverse', + }); + }, { instanceOf: common.CommandError }); +}); + +test('parseOptions (without a hyphen in the string)', t => { + t.throws(() => { + common.parseOptions('f', { + f: 'force', + }); + }, { instanceOf: Error }); +}); + +test('parseOptions (opt is not a string/object)', t => { + t.throws(() => { + common.parseOptions(1, { + f: 'force', + }); + }, { instanceOf: TypeError }); +}); + +test('parseOptions (map is not an object)', t => { + t.throws(() => { + common.parseOptions('-f', 27); + }, { instanceOf: TypeError }); +}); + +test('parseOptions (errorOptions is not an object)', t => { + t.throws(() => { + common.parseOptions('-f', { + f: 'force', + }, 'not a valid errorOptions'); + }, { instanceOf: TypeError }); +}); + +test('parseOptions (unrecognized string option)', t => { + t.throws(() => { + common.parseOptions('-z', { + f: 'force', + }); + }, { instanceOf: common.CommandError }); +}); + +test('parseOptions (unrecognized option in Object)', t => { + t.throws(() => { + common.parseOptions({ '-c': 7 }, { + f: 'force', + }); + }); +}); + +test('parseOptions (invalid type)', t => { + t.throws(() => { + common.parseOptions(12, { + R: 'recursive', + f: 'force', + r: 'reverse', + }); + }); +}); + +test('convertErrorOutput: no args', t => { + t.throws(() => { + common.convertErrorOutput(); + }, { instanceOf: TypeError }); +}); + +test('convertErrorOutput: input must be a vanilla string', t => { + t.throws(() => { + common.convertErrorOutput(3); + }, { instanceOf: TypeError }); -// should be a list -assert.throws(function () { - common.expand("resources"); -}, TypeError); + t.throws(() => { + common.convertErrorOutput({}); + }, { instanceOf: TypeError }); +}); // // Valids // -var result; - -// single file, array syntax -result = common.expand(['resources/file1.txt']); -assert.equal(shell.error(), null); -assert.deepEqual(result, ['resources/file1.txt']); - -// multiple file, glob syntax, * for file name -result = common.expand(['resources/file*.txt']); -assert.equal(shell.error(), null); -assert.deepEqual(result.sort(), ['resources/file1.txt', 'resources/file2.txt'].sort()); - -// multiple file, glob syntax, * for directory name -result = common.expand(['*/file*.txt']); -assert.equal(shell.error(), null); -assert.deepEqual(result.sort(), ['resources/file1.txt', 'resources/file2.txt'].sort()); - -// multiple file, glob syntax, ** for directory name -result = common.expand(['**/file*.js']); -assert.equal(shell.error(), null); -assert.deepEqual(result.sort(), ["resources/file1.js","resources/file2.js","resources/ls/file1.js","resources/ls/file2.js"].sort()); - -// broken links still expand -result = common.expand(['resources/b*dlink']); -assert.equal(shell.error(), null); -assert.deepEqual(result, ['resources/badlink']); - -// common.parseOptions (normal case) -result = common.parseOptions('-Rf', { - 'R': 'recursive', - 'f': 'force', - 'r': 'reverse' -}); -assert.ok(result.recursive === true); -assert.ok(result.force === true); -assert.ok(result.reverse === false); - -// common.parseOptions (with mutually-negating options) -result = common.parseOptions('-f', { - 'n': 'no_force', - 'f': '!no_force', - 'R': 'recursive' -}); -assert.ok(result.recursive === false); -assert.ok(result.no_force === false); -assert.ok(result.force === undefined); // this key shouldn't exist - -// common.parseOptions (the last of the conflicting options should hold) -result = common.parseOptions('-fn', { - 'n': 'no_force', - 'f': '!no_force', - 'R': 'recursive' -}); -assert.ok(result.recursive === false); -assert.ok(result.no_force === true); -assert.ok(result.force === undefined); // this key shouldn't exist - -// common.parseOptions using an object to hold options -result = common.parseOptions({'-v': 'some text here'}, { - 'v': 'value', - 'f': 'force', - 'r': 'reverse' -}); -assert.ok(result.value === 'some text here'); -assert.ok(result.force === false); -assert.ok(result.reverse === false); - -// Some basic tests on the ShellString type -result = shell.ShellString('foo'); -assert.strictEqual(result.toString(), 'foo'); -assert.equal(result.stdout, 'foo'); -assert.ok(typeof result.stderr === 'undefined'); -assert.ok(result.to); -assert.ok(result.toEnd); - -// Commands that fail will still output error messages to stderr -result = shell.exec(JSON.stringify(process.execPath) + ' -e "require(\'../global\'); ls(\'noexist\'); cd(\'noexist\');"'); -assert.equal(result.stdout, ''); -assert.equal(result.stderr, 'ls: no such file or directory: noexist\ncd: no such file or directory: noexist\n'); - -shell.exit(123); +// +// common.convertErrorOutput() +// +test('convertErrorOutput: nothing to convert', t => { + const input = 'hello world'; + const result = common.convertErrorOutput(input); + t.is(result, input); +}); + +test('convertErrorOutput: does not change forward slash', t => { + const input = 'dir/sub/file.txt'; + const result = common.convertErrorOutput(input); + t.is(result, input); +}); + +test('convertErrorOutput: changes backslashes to forward slashes', t => { + const input = 'dir\\sub\\file.txt'; + const result = common.convertErrorOutput(input); + t.is(result, 'dir/sub/file.txt'); +}); + +// +// common.expand() +// +test('single file, array syntax', t => { + const result = common.expand(['test/resources/file1.txt']); + t.deepEqual(result, ['test/resources/file1.txt']); +}); + +test('multiple file, glob syntax, * for file name', t => { + const result = common.expand(['test/resources/file*.txt']); + t.deepEqual(result.sort(), ['test/resources/file1.txt', 'test/resources/file2.txt'].sort()); +}); + +test('multiple file, glob syntax, * for directory name', t => { + const result = common.expand(['test/r*/file*.txt']); + t.deepEqual(result.sort(), ['test/resources/file1.txt', 'test/resources/file2.txt'].sort()); +}); + +test('multiple file, glob syntax, ** for directory name', t => { + const result = common.expand(['test/resources/**/file*.js']); + t.deepEqual( + result.sort(), + ['test/resources/file1.js', 'test/resources/file2.js', 'test/resources/ls/file1.js', 'test/resources/ls/file2.js'].sort() + ); +}); + +test('broken links still expand', t => { + const result = common.expand(['test/resources/b*dlink']); + t.deepEqual(result, ['test/resources/badlink']); +}); + +test('glob syntax, ? for single char in directory name', t => { + const result = common.expand(['test/r?sources/file1.txt']); + t.deepEqual(result, ['test/resources/file1.txt']); +}); +test('glob syntax, ? for single char in directory name with * in file name', t => { + const result = common.expand(['test/r?sources/file*.txt']); + t.deepEqual(result.sort(), ['test/resources/file1.txt', 'test/resources/file2.txt'].sort()); +}); +test('glob syntax, ? in multiple path segments', t => { + const result = common.expand(['test/r?sources/file?.txt']); + t.deepEqual(result.sort(), ['test/resources/file1.txt', 'test/resources/file2.txt'].sort()); +}); + +test('glob syntax, ? in file name still works', t => { + const result = common.expand(['test/resources/file?.txt']); + t.deepEqual(result.sort(), ['test/resources/file1.txt', 'test/resources/file2.txt'].sort()); +}); + +test('empty array', t => { + const result = common.expand([]); + t.deepEqual(result, []); +}); + +test('empty string', t => { + const result = common.expand(['']); + t.deepEqual(result, ['']); +}); + +test('non-string', t => { + const result = common.expand([5]); + t.deepEqual(result, [5]); +}); + +// +// common.buffer() +// +test('common.buffer returns buffer', t => { + const buf = common.buffer(); + t.truthy(buf instanceof Buffer); + t.is(buf.length, 64 * 1024); +}); + +test('common.buffer with explicit length', t => { + const buf = common.buffer(20); + t.truthy(buf instanceof Buffer); + t.is(buf.length, 20); +}); + +test('common.buffer with different config.bufLength', t => { + common.config.bufLength = 20; + const buf = common.buffer(); + t.truthy(buf instanceof Buffer); + t.is(buf.length, 20); +}); + +test('common.parseOptions (normal case)', t => { + const result = common.parseOptions('-Rf', { + R: 'recursive', + f: 'force', + r: 'reverse', + }); + + t.truthy(result.recursive); + t.truthy(result.force); + t.falsy(result.reverse); +}); + +test('common.parseOptions (with mutually-negating options)', t => { + const result = common.parseOptions('-f', { + n: 'no_force', + f: '!no_force', + R: 'recursive', + }); + + t.falsy(result.recursive); + t.falsy(result.no_force); + t.is(result.force, undefined); // this key shouldn't exist +}); + +test( + 'common.parseOptions (the last of the conflicting options should hold)', + t => { + const options = { + n: 'no_force', + f: '!no_force', + R: 'recursive', + }; + let result = common.parseOptions('-fn', options); + t.false(result.recursive); + t.truthy(result.no_force); + t.is(result.force, undefined); // this key shouldn't exist + result = common.parseOptions('-nf', options); + t.false(result.recursive); + t.false(result.no_force); + t.is(result.force, undefined); // this key shouldn't exist + } +); + +test('common.parseOptions using an object to hold options', t => { + const result = common.parseOptions({ '-v': 'some text here' }, { + v: 'value', + f: 'force', + r: 'reverse', + }); + + t.is(result.value, 'some text here'); + t.false(result.force); + t.false(result.reverse); +}); + +test('common.parseOptions throws when passed a string not starting with "-"', t => { + t.throws(() => { + common.parseOptions('a', { '-a': 'throws' }); + }, { instanceOf: Error }, "Options string must start with a '-'"); +}); + +test('common.parseOptions allows long options', t => { + const result = common.parseOptions({ value: true }, { + v: 'value', + }); + t.truthy(result.value); +}); + +test('common.parseOptions allows long options with values', t => { + const someObject = {}; + const result = common.parseOptions({ value: someObject }, { + v: 'value', + }); + t.is(result.value, someObject); +}); + +test('common.parseOptions throws for unknown long option', t => { + t.throws(() => { + common.parseOptions({ throws: true }, { + v: 'value', + }); + }, { instanceOf: common.CommandError }); +}); + +test('common.parseOptions with -- argument', t => { + const result = common.parseOptions('--', { + R: 'recursive', + f: 'force', + r: 'reverse', + }); + + t.falsy(result.recursive); + t.falsy(result.force); + t.falsy(result.reverse); +}); + +test('Some basic tests on the ShellString type', t => { + const result = shell.ShellString('foo'); + t.is(result.toString(), 'foo'); + t.is(result.stdout, 'foo'); + t.is(result.stderr, undefined); + t.truthy(result.to); + t.truthy(result.toEnd); +}); + +test('Commands that fail will still output error messages to stderr', async t => { + const script = "require('./global'); ls('noexist'); cd('noexist');"; + const result = await utils.runScript(script); + t.is(result.stdout, ''); + t.is( + result.stderr, + 'ls: no such file or directory: noexist\ncd: no such file or directory: noexist\n' + ); +}); + +test('execPath value makes sense', t => { + // TODO(nate): change this test if we add electron support in the unit tests + t.is(shell.config.execPath, process.execPath); + t.is(typeof shell.config.execPath, 'string'); +}); + +test('Changing shell.config.execPath does not modify process', t => { + shell.config.execPath = 'foo'; + t.not(shell.config.execPath, process.execPath); +}); + +test('CommandError is a subclass of Error', t => { + const e = new common.CommandError(new common.ShellString('some value')); + t.truthy(e instanceof common.CommandError); + t.truthy(e instanceof Error); + t.is(e.constructor, common.CommandError); +}); diff --git a/test/config.js b/test/config.js index ea024a470..1e3a337ce 100644 --- a/test/config.js +++ b/test/config.js @@ -1,69 +1,326 @@ -var shell = require('..'); +const path = require('path'); -var assert = require('assert'), - child = require('child_process'); -var common = require('../src/common'); +const test = require('ava'); + +const shell = require('..'); +const common = require('../src/common'); +const utils = require('./utils/utils'); + +// +// Valids +// // // config.silent // -assert.equal(shell.config.silent, false); // default +test('config.silent is false by default', t => { + t.falsy(shell.config.silent); +}); -shell.config.silent = true; -assert.equal(shell.config.silent, true); +test('config.silent can be set to true', t => { + shell.config.silent = true; + t.truthy(shell.config.silent); +}); -shell.config.silent = false; -assert.equal(shell.config.silent, false); +test('config.silent can be set to false', t => { + shell.config.silent = false; + t.falsy(shell.config.silent); +}); // // config.fatal // -assert.equal(shell.config.fatal, false); // default +test('config.fatal = false', async t => { + t.falsy(shell.config.fatal); + const script = `require('./global.js'); config.silent=true; config.fatal=false; cp("this_file_doesnt_exist", "."); echo("got here");`; + const result = await utils.runScript(script); + t.truthy(result.stdout.match('got here')); +}); -// -// config.fatal = false -// -shell.mkdir('-p', 'tmp'); -var file = 'tmp/tempscript'+Math.random()+'.js', - script = 'require(\'../../global.js\'); config.silent=true; config.fatal=false; cp("this_file_doesnt_exist", "."); echo("got here");'; -shell.ShellString(script).to(file); -child.exec(JSON.stringify(process.execPath)+' '+file, function(err, stdout) { - assert.ok(stdout.match('got here')); - - // - // config.fatal = true - // - shell.mkdir('-p', 'tmp'); - var file = 'tmp/tempscript'+Math.random()+'.js', - script = 'require(\'../../global.js\'); config.silent=true; config.fatal=true; cp("this_file_doesnt_exist", "."); echo("got here");'; - shell.ShellString(script).to(file); - child.exec(JSON.stringify(process.execPath)+' '+file, function(err, stdout) { - assert.ok(!stdout.match('got here')); - - shell.exit(123); +test('config.fatal = true', async t => { + const script = `require('./global.js'); config.silent=true; config.fatal=true; cp("this_file_doesnt_exist", "."); echo("got here");`; + await t.throwsAsync(utils.runScript(script), + { message: /this_file_doesnt_exist/ }); +}); + +test('config.fatal = false with an exec() failure returns, does not throw', t => { + const expected = { code: 2 }; + t.notThrows(() => { + const result = shell.exec('exit 2'); + t.is(result.code, expected.code); }); }); +test('config.fatal = true with an exec() failure includes a .code on the Error', t => { + shell.config.fatal = true; + try { + t.throws(() => { + shell.exec('exit 2'); + }, { code: 2 }); + } finally { + shell.config.fatal = false; + } +}); + // // config.globOptions // -// Expands to directories by default -var result = common.expand(['resources/*a*']); -assert.equal(result.length, 5); -assert.ok(result.indexOf('resources/a.txt') > -1); -assert.ok(result.indexOf('resources/badlink') > -1); -assert.ok(result.indexOf('resources/cat') > -1); -assert.ok(result.indexOf('resources/head') > -1); -assert.ok(result.indexOf('resources/external') > -1); - -// Check to make sure options get passed through (nodir is an example) -shell.config.globOptions = {nodir: true}; -result = common.expand(['resources/*a*']); -assert.equal(result.length, 2); -assert.ok(result.indexOf('resources/a.txt') > -1); -assert.ok(result.indexOf('resources/badlink') > -1); -assert.ok(result.indexOf('resources/cat') < 0); -assert.ok(result.indexOf('resources/external') < 0); +test('config.globOptions expands directories by default', t => { + const result = common.expand(['test/resources/*a*']); + const expected = [ + 'test/resources/a.txt', + 'test/resources/badlink', + 'test/resources/cat', + 'test/resources/external', + 'test/resources/head', + ]; + t.deepEqual(result, expected); +}); + +test('config.globOptions handles non-wildcards by default', t => { + const result = common.expand(['test/resources/a.txt']); + const expected = [ + 'test/resources/a.txt', + ]; + t.deepEqual(result, expected); +}); + +test('config.globOptions expands "?" symbol by default', t => { + const result = common.expand(['test/resources/file?.t*']); + const expected = [ + 'test/resources/file1.txt', + 'test/resources/file2.txt', + ]; + t.deepEqual(result, expected); +}); + +test('config.globOptions expands "*" in multiple path segments by default', t => { + const result = common.expand(['test/r*sources/file?.txt']); + const expected = [ + 'test/resources/file1.txt', + 'test/resources/file2.txt', + ]; + t.deepEqual(result, expected); +}); + +// https://github.com/shelljs/shelljs/issues/1197 +test('config.globOptions expands "?" in folder path by default', t => { + const result = common.expand(['test/r?sources/file*.txt']); + const expected = [ + 'test/resources/file1.txt', + 'test/resources/file2.txt', + ]; + t.deepEqual(result, expected); +}); + +test('config.globOptions respects cwd', t => { + // Both node-glob and fast-glob call this option 'cwd'. + shell.config.globOptions = { cwd: 'test' }; + const result = common.expand(['resources/*a*']); + const expected = [ + 'resources/a.txt', + 'resources/badlink', + 'resources/cat', + 'resources/external', + 'resources/head', + ]; + t.deepEqual(result, expected); +}); + +test('config.globOptions respects dot', t => { + // Both node-glob and fast-glob call this option 'dot'. + shell.config.globOptions = { dot: true }; + const result = common.expand(['test/resources/ls/*']); + t.is(result.length, 8); + t.truthy(result.includes('test/resources/ls/.hidden_dir')); + t.truthy(result.includes('test/resources/ls/.hidden_file')); +}); + +test('config.globOptions respects ignore', t => { + // Both node-glob and fast-glob call this option 'ignore'. + shell.config.globOptions = { ignore: ['test/resources/external'] }; + const result = common.expand(['test/resources/*a*']); + const expected = [ + 'test/resources/a.txt', + 'test/resources/badlink', + 'test/resources/cat', + 'test/resources/head', + ]; + t.deepEqual(result, expected); + // Does not include the result that we chose to ignore + t.falsy(result.includes('test/resources/external')); +}); + +test('config.globOptions respects absolute', t => { + // Both node-glob and fast-glob call this option 'absolute'. + shell.config.globOptions = { absolute: true }; + const result = common.expand(['test/resources/*a*']); + function abs(file) { + // Normalize to posix-style path separators on all platforms. + const CWD = process.platform === 'win32' ? + process.cwd().replace(/\\/g, '/') : + process.cwd(); + return path.posix.join(CWD, file); + } + const expected = [ + abs('test/resources/a.txt'), + abs('test/resources/badlink'), + abs('test/resources/cat'), + abs('test/resources/external'), + abs('test/resources/head'), + ]; + t.deepEqual(result, expected); +}); + +test('config.globOptions respects nodir', t => { + shell.config.globOptions = { nodir: true }; + const result = common.expand(['test/resources/*a*']); + // Includes files and symlinks. + const expected = [ + 'test/resources/a.txt', + 'test/resources/badlink', + ]; + t.deepEqual(result, expected); + // Does not include the directories. + t.falsy(result.includes('test/resources/cat')); + t.falsy(result.includes('test/resources/head')); + t.falsy(result.includes('test/resources/external')); +}); + +test('config.globOptions respects mark', t => { + shell.config.globOptions = { mark: true }; + const result = common.expand(['test/resources/*a*']); + // Directories get a '/' character at the end. + const expected = [ + 'test/resources/a.txt', + 'test/resources/badlink', + 'test/resources/cat/', + 'test/resources/external/', + 'test/resources/head/', + ]; + t.deepEqual(result, expected); +}); + +test('config.globOptions respects nobrace', t => { + // Default behavior is to expand "file{1..2}.txt" to ["file1.txt", + // "file2.txt"]. + let result = common.expand(['test/resources/file{1..2}.txt']); + let expected = [ + 'test/resources/file1.txt', + 'test/resources/file2.txt', + ]; + t.deepEqual(result, expected); + + // When 'nobrace' is true, brace expressions will expand as literals. + shell.config.globOptions = { nobrace: true }; + result = common.expand(['test/resources/file{1..2}.txt']); + expected = [ + 'test/resources/file{1..2}.txt', + ]; + t.deepEqual(result, expected); +}); + +test('config.globOptions respects noglobstar', t => { + // Default behavior is to expand "**" to match zero or more directories. + let result = common.expand(['test/**/file1']); + let expected = [ + 'test/resources/cat/file1', + 'test/resources/chmod/file1', + 'test/resources/cp/file1', + 'test/resources/file1', + 'test/resources/ls/file1', + 'test/resources/sort/file1', + 'test/resources/uniq/file1', + ]; + t.deepEqual(result, expected); + + // When 'noglobstar' is true, "**" will behave like a regular "*" and matches + // exactly 1 directory. + shell.config.globOptions = { noglobstar: true }; + result = common.expand(['test/**/file1']); + expected = [ + 'test/resources/file1', + ]; + t.deepEqual(result, expected); +}); + +test('config.globOptions respects noext', t => { + // Default behavior is to support fancy glob patterns (like "file1.+(js|txt)"). + let result = common.expand([ + 'test/resources/file1.+(js|txt)', + 'test/resources/file2.*', + ]); + let expected = [ + 'test/resources/file1.js', + 'test/resources/file1.txt', + 'test/resources/file2.js', + 'test/resources/file2.txt', + ]; + t.deepEqual(result, expected); + + // When 'noext' is true, this only matches regular globs (like "file2.*"). + shell.config.globOptions = { noext: true }; + result = common.expand([ + 'test/resources/file1.+(js|txt)', + 'test/resources/file2.*', + ]); + expected = [ + 'test/resources/file1.+(js|txt)', + 'test/resources/file2.js', + 'test/resources/file2.txt', + ]; + t.deepEqual(result, expected); +}); + +test('config.globOptions respects nocase', t => { + // Default behavior will change depending on macOS, Windows, or Linux. This is + // difficult to test in a cross-platform way. + + // When 'nocase' is true, we should be able to match files even if we use the + // wrong case in the pattern. + shell.config.globOptions = { nocase: true }; + let result = common.expand(['test/resources/FILE*.TXT']); + let expected = [ + 'test/resources/file1.txt', + 'test/resources/file2.txt', + ]; + t.deepEqual(result, expected); + + // When 'nocase' is false, using the wrong case will fail to match any files. + shell.config.globOptions = { nocase: false }; + result = common.expand(['test/resources/FILE*.TXT']); + expected = [ + 'test/resources/FILE*.TXT', + ]; + t.deepEqual(result, expected); +}); + +test('config.globOptions respects matchBase', t => { + // By default, "*" expressions only match inside of the same directory. + shell.config.globOptions = { cwd: 'test/resources' }; + let result = common.expand(['*ile1']); + let expected = [ + 'file1', + ]; + t.deepEqual(result, expected); + + // When 'matchBase' is true (and the pattern contains no slashes), the + // pattern is implicitly treated like "**/*" and will expand to + // subdirectories. + shell.config.globOptions = { cwd: 'test/resources', matchBase: true }; + result = common.expand(['*ile1']); + expected = [ + 'cat/file1', + 'chmod/file1', + 'cp/file1', + 'file1', + 'head/shortfile1', + 'ls/file1', + 'sort/file1', + 'uniq/file1', + ]; + t.deepEqual(result, expected); +}); diff --git a/test/cp.js b/test/cp.js index 17240cfaa..749884ccb 100644 --- a/test/cp.js +++ b/test/cp.js @@ -1,524 +1,995 @@ -var shell = require('..'); -var common = require('../src/common'); +const fs = require('fs'); -var assert = require('assert'), - fs = require('fs'), - numLines = require('./utils/utils').numLines; +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const common = require('../src/common'); +const utils = require('./utils/utils'); -var isWindows = common.platform === 'win'; - -// On Windows, symlinks for files need admin permissions. This helper -// skips certain tests if we are on Windows and got an EPERM error -function skipOnWinForEPERM (action, test) { - action(); - var error = shell.error(); - - if (isWindows && error && /EPERM:/.test(error)) { - console.log("Got EPERM when testing symlinks on Windows. Assuming non-admin environment and skipping test."); - } else { - test(); - } -} +const oldMaxDepth = shell.config.maxdepth; +const CWD = process.cwd(); +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.mkdir(t.context.tmp); +}); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +test.afterEach.always(t => { + process.chdir(CWD); + shell.rm('-rf', t.context.tmp); + shell.config.maxdepth = oldMaxDepth; +}); // // Invalids // -var result = shell.cp(); -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'cp: missing and/or '); - -result = shell.cp('file1'); -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'cp: missing and/or '); - -result = shell.cp('-f'); -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'cp: missing and/or '); - -shell.rm('-rf', 'tmp/*'); -result = shell.cp('-@', 'resources/file1', 'tmp/file1'); // option not supported, files OK -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(fs.existsSync('tmp/file1'), false); -assert.equal(result.stderr, 'cp: option not recognized: @'); - -result = shell.cp('-Z', 'asdfasdf', 'tmp/file2'); // option not supported, files NOT OK -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(fs.existsSync('tmp/file2'), false); -assert.equal(result.stderr, 'cp: option not recognized: Z'); - -result = shell.cp('asdfasdf', 'tmp'); // source does not exist -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(numLines(result.stderr), 1); -assert.equal(fs.existsSync('tmp/asdfasdf'), false); -assert.equal(result.stderr, 'cp: no such file or directory: asdfasdf'); - -result = shell.cp('asdfasdf1', 'asdfasdf2', 'tmp'); // sources do not exist -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(numLines(result.stderr), 2); -assert.equal(fs.existsSync('tmp/asdfasdf1'), false); -assert.equal(fs.existsSync('tmp/asdfasdf2'), false); -assert.equal(result.stderr, 'cp: no such file or directory: asdfasdf1\ncp: no such file or directory: asdfasdf2'); - -result = shell.cp('asdfasdf1', 'asdfasdf2', 'resources/file1'); // too many sources (dest is file) -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'cp: dest is not a directory (too many sources)'); - -result = shell.cp('resources/file1', 'resources/file2', 'tmp/a_file'); // too many sources -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(fs.existsSync('tmp/a_file'), false); -assert.equal(result.stderr, 'cp: dest is not a directory (too many sources)'); +test('no args', t => { + const result = shell.cp(); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'cp: missing and/or '); +}); + +test('no destination', t => { + const result = shell.cp('file1'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'cp: missing and/or '); +}); + +test('only an option', t => { + const result = shell.cp('-f'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'cp: missing and/or '); +}); + +test('invalid option', t => { + const result = shell.cp('-@', 'test/resources/file1', `${t.context.tmp}/file1`); + t.truthy(shell.error()); + t.is(result.code, 1); + t.falsy(fs.existsSync(`${t.context.tmp}/file1`)); + t.is(result.stderr, 'cp: option not recognized: @'); +}); + +test('invalid option #2', t => { + const result = shell.cp('-Z', 'asdfasdf', `${t.context.tmp}/file2`); + t.truthy(shell.error()); + t.is(result.code, 1); + t.falsy(fs.existsSync(`${t.context.tmp}/file2`)); + t.is(result.stderr, 'cp: option not recognized: Z'); +}); + +test('source does not exist', t => { + const result = shell.cp('asdfasdf', t.context.tmp); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(utils.numLines(result.stderr), 1); + t.falsy(fs.existsSync(`${t.context.tmp}/asdfasdf`)); + t.is(result.stderr, 'cp: no such file or directory: asdfasdf'); +}); + +test('multiple sources do not exist', t => { + const result = shell.cp('asdfasdf1', 'asdfasdf2', t.context.tmp); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(utils.numLines(result.stderr), 2); + t.falsy(fs.existsSync(`${t.context.tmp}/asdfasdf1`)); + t.falsy(fs.existsSync(`${t.context.tmp}/asdfasdf2`)); + t.is( + result.stderr, + 'cp: no such file or directory: asdfasdf1\ncp: no such file or directory: asdfasdf2' + ); +}); + +test('too many sources', t => { + const result = shell.cp('asdfasdf1', 'asdfasdf2', 'test/resources/file1'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'cp: dest is not a directory (too many sources)'); +}); + +test('too many sources #2', t => { + const result = shell.cp('test/resources/file1', 'test/resources/file2', `${t.context.tmp}/a_file`); + t.truthy(shell.error()); + t.is(result.code, 1); + t.falsy(fs.existsSync(`${t.context.tmp}/a_file`)); + t.is(result.stderr, 'cp: dest is not a directory (too many sources)'); +}); + +test('empty string source', t => { + const result = shell.cp('', 'dest'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, "cp: no such file or directory: ''"); +}); // // Valids // -var oldContents = shell.cat('resources/file2').toString(); -result = shell.cp('-n', 'resources/file1', 'resources/file2'); // dest already exists -assert.ok(!shell.error()); -assert.equal(result.code, 0); -assert.equal(result.stderr, ''); -assert.equal(shell.cat('resources/file2').toString(), oldContents); - -// -f by default -result = shell.cp('resources/file2', 'resources/copyfile2'); -result = shell.cp('resources/file1', 'resources/file2'); // dest already exists -assert.ok(!shell.error()); -assert.equal(result.code, 0); -assert.ok(!result.stderr); -assert.equal(shell.cat('resources/file1') + '', shell.cat('resources/file2') + ''); // after cp -shell.mv('resources/copyfile2', 'resources/file2'); // restore -assert.ok(!shell.error()); - -// -f (explicitly) -result = shell.cp('resources/file2', 'resources/copyfile2'); -result = shell.cp('-f', 'resources/file1', 'resources/file2'); // dest already exists -assert.ok(!shell.error()); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.equal(shell.cat('resources/file1') + '', shell.cat('resources/file2') + ''); // after cp -shell.mv('resources/copyfile2', 'resources/file2'); // restore -assert.ok(!shell.error()); -assert.equal(result.code, 0); - -// simple - to dir -result = shell.cp('resources/file1', 'tmp'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/file1'), true); - -// simple - to file -result = shell.cp('resources/file2', 'tmp/file2'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/file2'), true); - -// simple - file list -shell.rm('-rf', 'tmp/*'); -result = shell.cp('resources/file1', 'resources/file2', 'tmp'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/file1'), true); -assert.equal(fs.existsSync('tmp/file2'), true); - -// simple - file list, array syntax -shell.rm('-rf', 'tmp/*'); -result = shell.cp(['resources/file1', 'resources/file2'], 'tmp'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/file1'), true); -assert.equal(fs.existsSync('tmp/file2'), true); - -result = shell.cp('resources/file2', 'tmp/file3'); -assert.equal(fs.existsSync('tmp/file3'), true); -result = shell.cp('-f', 'resources/file2', 'tmp/file3'); // file exists, but -f specified -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/file3'), true); - -// glob -shell.rm('-rf', 'tmp/*'); -result = shell.cp('resources/file?', 'tmp'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.ok(fs.existsSync('tmp/file1')); -assert.ok(fs.existsSync('tmp/file2')); -assert.ok(!fs.existsSync('tmp/file1.js')); -assert.ok(!fs.existsSync('tmp/file2.js')); -assert.ok(!fs.existsSync('tmp/file1.txt')); -assert.ok(!fs.existsSync('tmp/file2.txt')); - -// wildcard -shell.rm('tmp/file1', 'tmp/file2'); -result = shell.cp('resources/file*', 'tmp'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.ok(fs.existsSync('tmp/file1')); -assert.ok(fs.existsSync('tmp/file2')); -assert.ok(fs.existsSync('tmp/file1.js')); -assert.ok(fs.existsSync('tmp/file2.js')); -assert.ok(fs.existsSync('tmp/file1.txt')); -assert.ok(fs.existsSync('tmp/file2.txt')); - -// recursive, with regular files -shell.rm('-rf', 'tmp/*'); -result = shell.cp('-R', 'resources/file1', 'resources/file2', 'tmp'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.ok(fs.existsSync('tmp/file1')); -assert.ok(fs.existsSync('tmp/file2')); - -// recursive, nothing exists -shell.rm('-rf', 'tmp/*'); -result = shell.cp('-R', 'resources/cp', 'tmp'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.equal(shell.ls('-R', 'resources/cp') + '', shell.ls('-R', 'tmp/cp') + ''); - -//recursive, nothing exists, source ends in '/' (see Github issue #15) -shell.rm('-rf', 'tmp/*'); -result = shell.cp('-R', 'resources/cp/', 'tmp/'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.equal(shell.ls('-R', 'resources/cp') + '', shell.ls('-R', 'tmp/cp') + ''); - -// recursive, globbing regular files with extension (see Github issue #376) -shell.rm('-rf', 'tmp/*'); -result = shell.cp('-R', 'resources/file*.txt', 'tmp'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.ok(fs.existsSync('tmp/file1.txt')); -assert.ok(fs.existsSync('tmp/file2.txt')); - -// recursive, copying one regular file (also related to Github issue #376) -shell.rm('-rf', 'tmp/*'); -result = shell.cp('-R', 'resources/file1.txt', 'tmp'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.ok(fs.existsSync('tmp/file1.txt')); -assert.ok(!fs.statSync('tmp/file1.txt').isDirectory()); // don't let it be a dir - -//recursive, everything exists, no force flag -shell.rm('-rf', 'tmp/*'); -result = shell.cp('-R', 'resources/cp', 'tmp'); -result = shell.cp('-R', 'resources/cp', 'tmp'); -assert.equal(shell.error(), null); // crash test only -assert.ok(!result.stderr); -assert.equal(result.code, 0); - -if (process.platform !== 'win32') { - // Recursive, everything exists, overwrite a real file with a link (if same name) - // Because -R implies to not follow links! - shell.rm('-rf', 'tmp/*'); - shell.cp('-R', 'resources/cp/*', 'tmp'); - assert.ok(fs.lstatSync('tmp/links/sym.lnk').isSymbolicLink()); // this one is a link - assert.ok(!(fs.lstatSync('tmp/fakeLinks/sym.lnk').isSymbolicLink())); // this one isn't - assert.notEqual(shell.cat('tmp/links/sym.lnk').toString(), shell.cat('tmp/fakeLinks/sym.lnk').toString()); - result = shell.cp('-R', 'tmp/links/*', 'tmp/fakeLinks'); - assert.equal(shell.error(), null); - assert.ok(!result.stderr); - assert.equal(result.code, 0); - assert.ok(fs.lstatSync('tmp/links/sym.lnk').isSymbolicLink()); // this one is a link - assert.ok(fs.lstatSync('tmp/fakeLinks/sym.lnk').isSymbolicLink()); // this one is now a link - assert.equal(shell.cat('tmp/links/sym.lnk').toString(), shell.cat('tmp/fakeLinks/sym.lnk').toString()); - - // Recursive, everything exists, overwrite a real file *by following a link* - // Because missing the -R implies -L. - shell.rm('-rf', 'tmp/*'); - shell.cp('-R', 'resources/cp/*', 'tmp'); - assert.ok(fs.lstatSync('tmp/links/sym.lnk').isSymbolicLink()); // this one is a link - assert.ok(!(fs.lstatSync('tmp/fakeLinks/sym.lnk').isSymbolicLink())); // this one isn't - assert.notEqual(shell.cat('tmp/links/sym.lnk').toString(), shell.cat('tmp/fakeLinks/sym.lnk').toString()); - result = shell.cp('tmp/links/*', 'tmp/fakeLinks'); // don't use -R - assert.equal(shell.error(), null); - assert.ok(!result.stderr); - assert.equal(result.code, 0); - assert.ok(fs.lstatSync('tmp/links/sym.lnk').isSymbolicLink()); // this one is a link - assert.ok(!fs.lstatSync('tmp/fakeLinks/sym.lnk').isSymbolicLink()); // this one is still not a link - // But it still follows the link - assert.equal(shell.cat('tmp/links/sym.lnk').toString(), shell.cat('tmp/fakeLinks/sym.lnk').toString()); -} - -//recursive, everything exists, with force flag -shell.rm('-rf', 'tmp/*'); -result = shell.cp('-R', 'resources/cp', 'tmp'); -shell.ShellString('changing things around').to('tmp/cp/dir_a/z'); -assert.notEqual(shell.cat('resources/cp/dir_a/z') + '', shell.cat('tmp/cp/dir_a/z') + ''); // before cp -result = shell.cp('-Rf', 'resources/cp', 'tmp'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.equal(shell.cat('resources/cp/dir_a/z') + '', shell.cat('tmp/cp/dir_a/z') + ''); // after cp - -//recursive, creates dest dir since it's only one level deep (see Github issue #44) -shell.rm('-rf', 'tmp/*'); -result = shell.cp('-r', 'resources/issue44', 'tmp/dir2'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.equal(shell.ls('-R', 'resources/issue44') + '', shell.ls('-R', 'tmp/dir2') + ''); -assert.equal(shell.cat('resources/issue44/main.js') + '', shell.cat('tmp/dir2/main.js') + ''); - -//recursive, does *not* create dest dir since it's too deep (see Github issue #44) -shell.rm('-rf', 'tmp/*'); -result = shell.cp('-r', 'resources/issue44', 'tmp/dir2/dir3'); -assert.ok(shell.error()); -assert.equal(result.stderr, 'cp: cannot create directory \'tmp/dir2/dir3\': No such file or directory'); -assert.equal(result.code, 1); -assert.equal(fs.existsSync('tmp/dir2'), false); - -//recursive, copies entire directory -shell.rm('-rf', 'tmp/*'); -result = shell.cp('-r', 'resources/cp/dir_a', 'tmp/dest'); -assert.equal(shell.error(), null); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/dest/z'), true); - -//recursive, with trailing slash, does the exact same -shell.rm('-rf', 'tmp/*'); -result = shell.cp('-r', 'resources/cp/dir_a/', 'tmp/dest'); -assert.equal(shell.error(), null); -assert.equal(fs.existsSync('tmp/dest/z'), true); - -// On Windows, permission bits are quite different so skip those tests for now -if (common.platform !== 'win') { - //preserve mode bits - shell.rm('-rf', 'tmp/*'); - var execBit = parseInt('001', 8); - assert.equal(fs.statSync('resources/cp-mode-bits/executable').mode & execBit, execBit); - shell.cp('resources/cp-mode-bits/executable', 'tmp/executable'); - assert.equal(fs.statSync('resources/cp-mode-bits/executable').mode, fs.statSync('tmp/executable').mode); -} - -// Make sure hidden files are copied recursively -shell.rm('-rf', 'tmp/'); -result = shell.cp('-r', 'resources/ls/', 'tmp/'); -assert.ok(!shell.error()); -assert.ok(!result.stderr); -assert.equal(result.code, 0); -assert.ok(fs.existsSync('tmp/.hidden_file')); - -// no-recursive will copy regular files only -shell.rm('-rf', 'tmp/'); -shell.mkdir('tmp/'); -result = shell.cp('resources/file1.txt', 'resources/ls/', 'tmp/'); -assert.ok(shell.error()); -assert.ok(!fs.existsSync('tmp/.hidden_file')); // doesn't copy dir contents -assert.ok(!fs.existsSync('tmp/ls')); // doesn't copy dir itself -assert.ok(fs.existsSync('tmp/file1.txt')); - -// no-recursive will copy regular files only -shell.rm('-rf', 'tmp/'); -shell.mkdir('tmp/'); -result = shell.cp('resources/file1.txt', 'resources/file2.txt', 'resources/cp', - 'resources/ls/', 'tmp/'); -assert.ok(shell.error()); -assert.ok(!fs.existsSync('tmp/.hidden_file')); // doesn't copy dir contents -assert.ok(!fs.existsSync('tmp/ls')); // doesn't copy dir itself -assert.ok(!fs.existsSync('tmp/a')); // doesn't copy dir contents -assert.ok(!fs.existsSync('tmp/cp')); // doesn't copy dir itself -assert.ok(fs.existsSync('tmp/file1.txt')); -assert.ok(fs.existsSync('tmp/file2.txt')); - -if (process.platform !== 'win32') { - // -R implies -P - shell.rm('-rf', 'tmp/*'); - shell.cp('-R', 'resources/cp/links/sym.lnk', 'tmp'); - assert.ok(fs.lstatSync('tmp/sym.lnk').isSymbolicLink()); - - // using -P explicitly works - shell.rm('-rf', 'tmp/*'); - shell.cp('-P', 'resources/cp/links/sym.lnk', 'tmp'); - assert.ok(fs.lstatSync('tmp/sym.lnk').isSymbolicLink()); - - // using -PR on a link to a folder does not follow the link - shell.rm('-rf', 'tmp/*'); - shell.cp('-PR', 'resources/cp/symFolder', 'tmp'); - assert.ok(fs.lstatSync('tmp/symFolder').isSymbolicLink()); - - // -L overrides -P for copying directory - shell.rm('-rf', 'tmp/*'); - shell.cp('-LPR', 'resources/cp/symFolder', 'tmp'); - assert.ok(!fs.lstatSync('tmp/symFolder').isSymbolicLink()); - assert.ok(!fs.lstatSync('tmp/symFolder/sym.lnk').isSymbolicLink()); - - // Recursive, copies entire directory with no symlinks and -L option does not cause change in behavior. - shell.rm('-rf', 'tmp/*'); - result = shell.cp('-rL', 'resources/cp/dir_a', 'tmp/dest'); - assert.equal(shell.error(), null); - assert.ok(!result.stderr); - assert.equal(result.code, 0); - assert.equal(fs.existsSync('tmp/dest/z'), true); -} - -// using -R on a link to a folder *does* follow the link -shell.rm('-rf', 'tmp/*'); -shell.cp('-R', 'resources/cp/symFolder', 'tmp'); -assert.ok(!fs.lstatSync('tmp/symFolder').isSymbolicLink()); - -// Without -R, -L is implied -shell.rm('-rf', 'tmp/*'); -shell.cp('resources/cp/links/sym.lnk', 'tmp'); -assert.ok(!fs.lstatSync('tmp/sym.lnk').isSymbolicLink()); - -// -L explicitly works -shell.rm('-rf', 'tmp/*'); -shell.cp('-L', 'resources/cp/links/sym.lnk', 'tmp'); -assert.ok(!fs.lstatSync('tmp/sym.lnk').isSymbolicLink()); - -// using -LR does not imply -P -shell.rm('-rf', 'tmp/*'); -shell.cp('-LR', 'resources/cp/links/sym.lnk', 'tmp'); -assert.ok(!fs.lstatSync('tmp/sym.lnk').isSymbolicLink()); - -// using -LR also works recursively on directories containing links -shell.rm('-rf', 'tmp/*'); -shell.cp('-LR', 'resources/cp/links', 'tmp'); -assert.ok(!fs.lstatSync('tmp/links/sym.lnk').isSymbolicLink()); - -// -L always overrides a -P -shell.rm('-rf', 'tmp/*'); -shell.cp('-LP', 'resources/cp/links/sym.lnk', 'tmp'); -assert.ok(!fs.lstatSync('tmp/sym.lnk').isSymbolicLink()); -shell.rm('-rf', 'tmp/*'); -shell.cp('-LPR', 'resources/cp/links/sym.lnk', 'tmp'); -assert.ok(!fs.lstatSync('tmp/sym.lnk').isSymbolicLink()); - -// Test max depth. -shell.rm('-rf', 'tmp/'); -shell.mkdir('tmp/'); -shell.config.maxdepth = 32; -var directory = ''; -for (var i = 1; i < 40; i++) { - directory += '/'+i; -} -var directory32deep = ''; -for (var i = 1; i < 32; i++) { - directory32deep += '/'+i; -} -shell.mkdir('-p', 'tmp/0' + directory); -shell.cp('-r', 'tmp/0', 'tmp/copytestdepth'); -// Check full directory exists. -assert.ok(shell.test('-d', 'tmp/0/' + directory)); -// Check full copy of directory does not exist. -assert.ok(!shell.test('-d', 'tmp/copytestdepth'+directory)); -// Check last directory to exist is bellow maxdepth. -assert.ok(shell.test('-d', 'tmp/copytestdepth'+directory32deep)); -assert.ok(!shell.test('-d', 'tmp/copytestdepth'+directory32deep+'/32')); - -// Only complete sym link checks if script has permission to do so. -skipOnWinForEPERM(shell.ln.bind(shell, '-s', 'tmp/0', 'tmp/symlinktest'), function () { - if (!shell.test('-L', 'tmp/symlinktest')) { - return; +test('dest already exists', t => { + const oldContents = shell.cat('test/resources/file2').toString(); + const result = shell.cp('-n', 'test/resources/file1', 'test/resources/file2'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stderr, ''); + t.is(shell.cat('test/resources/file2').toString(), oldContents); +}); + +test('-nR does not overwrite an existing file at the destination', t => { + // Create tmp/new/cp/a + const dest = `${t.context.tmp}/new/cp`; + shell.mkdir('-p', dest); + const oldContents = 'original content'; + shell.ShellString(oldContents).to(`${dest}/a`); + + // Attempt to overwrite /tmp/new/cp/ with test/resources/cp/ + const result = shell.cp('-nR', 'test/resources/cp/', `${t.context.tmp}/new/`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(result.stderr); + t.is(shell.cat(`${dest}/a`).toString(), oldContents); +}); + +test('-n does not overwrite an existing file if the destination is a directory', t => { + const oldContents = 'original content'; + shell.cp('test/resources/file1', `${t.context.tmp}`); + new shell.ShellString(oldContents).to(`${t.context.tmp}/file1`); + const result = shell.cp('-n', 'test/resources/file1', `${t.context.tmp}`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(result.stderr); + t.is(shell.cat(`${t.context.tmp}/file1`).toString(), oldContents); +}); + +test('-f by default', t => { + shell.cp('test/resources/file2', 'test/resources/copyfile2'); + const result = shell.cp('test/resources/file1', 'test/resources/file2'); // dest already exists + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(result.stderr); + t.is(shell.cat('test/resources/file1').toString(), shell.cat('test/resources/file2').toString()); // after cp + shell.mv('test/resources/copyfile2', 'test/resources/file2'); // restore + t.falsy(shell.error()); +}); + +test('-f (explicitly)', t => { + shell.cp('test/resources/file2', 'test/resources/copyfile2'); + const result = shell.cp('-f', 'test/resources/file1', 'test/resources/file2'); // dest already exists + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.is(shell.cat('test/resources/file1').toString(), shell.cat('test/resources/file2').toString()); // after cp + shell.mv('test/resources/copyfile2', 'test/resources/file2'); // restore + t.falsy(shell.error()); + t.is(result.code, 0); +}); + +test('simple - to dir', t => { + const result = shell.cp('test/resources/file1', t.context.tmp); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/file1`)); +}); + +test('simple - to file', t => { + const result = shell.cp('test/resources/file2', `${t.context.tmp}/file2`); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/file2`)); +}); + +test('simple - file list', t => { + const result = shell.cp('test/resources/file1', 'test/resources/file2', t.context.tmp); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/file1`)); + t.truthy(fs.existsSync(`${t.context.tmp}/file2`)); +}); + +test('simple - file list, array syntax', t => { + const result = shell.cp(['test/resources/file1', 'test/resources/file2'], t.context.tmp); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/file1`)); + t.truthy(fs.existsSync(`${t.context.tmp}/file2`)); +}); + +test('-f option', t => { + shell.cp('test/resources/file2', `${t.context.tmp}/file3`); + t.truthy(fs.existsSync(`${t.context.tmp}/file3`)); + const result = shell.cp('-f', 'test/resources/file2', `${t.context.tmp}/file3`); // file exists, but -f specified + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/file3`)); +}); + +test('glob', t => { + const result = shell.cp('test/resources/file?', t.context.tmp); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/file1`)); + t.truthy(fs.existsSync(`${t.context.tmp}/file2`)); + t.falsy(fs.existsSync(`${t.context.tmp}/file1.js`)); + t.falsy(fs.existsSync(`${t.context.tmp}/file2.js`)); + t.falsy(fs.existsSync(`${t.context.tmp}/file1.txt`)); + t.falsy(fs.existsSync(`${t.context.tmp}/file2.txt`)); +}); + +test('wildcard', t => { + shell.rm(`${t.context.tmp}/file1`, `${t.context.tmp}/file2`); + const result = shell.cp('test/resources/file*', t.context.tmp); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/file1`)); + t.truthy(fs.existsSync(`${t.context.tmp}/file2`)); + t.truthy(fs.existsSync(`${t.context.tmp}/file1.js`)); + t.truthy(fs.existsSync(`${t.context.tmp}/file2.js`)); + t.truthy(fs.existsSync(`${t.context.tmp}/file1.txt`)); + t.truthy(fs.existsSync(`${t.context.tmp}/file2.txt`)); +}); + +test('recursive, with regular files', t => { + const result = shell.cp('-R', 'test/resources/file1', 'test/resources/file2', t.context.tmp); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/file1`)); + t.truthy(fs.existsSync(`${t.context.tmp}/file2`)); +}); + +test('omit directory if missing recursive flag', t => { + const result = shell.cp('test/resources/cp', t.context.tmp); + t.is(shell.error(), "cp: omitting directory 'test/resources/cp'"); + t.is(result.stderr, "cp: omitting directory 'test/resources/cp'"); + t.is(result.code, 1); + t.falsy(fs.existsSync(`${t.context.tmp}/file1`)); + t.falsy(fs.existsSync(`${t.context.tmp}/file2`)); +}); + +test('recursive, nothing exists', t => { + const result = shell.cp('-R', 'test/resources/cp', t.context.tmp); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.is(shell.ls('-R', 'test/resources/cp').toString(), shell.ls('-R', `${t.context.tmp}/cp`).toString()); +}); + +test('recursive, nothing exists, source ends in "/"', t => { + // Github issue #15 + const result = shell.cp('-R', 'test/resources/cp/', `${t.context.tmp}/`); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.is(shell.ls('-R', 'test/resources/cp').toString(), shell.ls('-R', `${t.context.tmp}/cp`).toString()); +}); + +test('recursive, globbing regular files with extension', t => { + // Github issue #376 + const result = shell.cp('-R', 'test/resources/file*.txt', t.context.tmp); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/file1.txt`)); + t.truthy(fs.existsSync(`${t.context.tmp}/file2.txt`)); +}); + +test('recursive, copying one regular file', t => { + // Github issue #376 + const result = shell.cp('-R', 'test/resources/file1.txt', t.context.tmp); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/file1.txt`)); + t.falsy(common.statFollowLinks(`${t.context.tmp}/file1.txt`).isDirectory()); // don't let it be a dir +}); + +test('recursive, everything exists, no force flag', t => { + const result = shell.cp('-R', 'test/resources/cp', t.context.tmp); + t.falsy(shell.error()); // crash test only + t.falsy(result.stderr); + t.is(result.code, 0); +}); + +test('-R implies to not follow links', t => { + utils.skipOnWin(t, () => { + shell.cp('-R', 'test/resources/cp/*', t.context.tmp); + t.truthy(common.statNoFollowLinks(`${t.context.tmp}/links/sym.lnk`).isSymbolicLink()); // this one is a link + t.falsy((common.statNoFollowLinks(`${t.context.tmp}/fakeLinks/sym.lnk`).isSymbolicLink())); // this one isn't + t.not( + shell.cat(`${t.context.tmp}/links/sym.lnk`).toString(), + shell.cat(`${t.context.tmp}/fakeLinks/sym.lnk`).toString() + ); + const result = shell.cp('-R', `${t.context.tmp}/links/*`, `${t.context.tmp}/fakeLinks`); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(common.statNoFollowLinks(`${t.context.tmp}/links/sym.lnk`).isSymbolicLink()); // this one is a link + t.truthy(common.statNoFollowLinks(`${t.context.tmp}/fakeLinks/sym.lnk`).isSymbolicLink()); // this one is now a link + t.is( + shell.cat(`${t.context.tmp}/links/sym.lnk`).toString(), + shell.cat(`${t.context.tmp}/fakeLinks/sym.lnk`).toString() + ); + }); +}); + +test('Missing -R implies -L', t => { + utils.skipOnWin(t, () => { + // Recursive, everything exists, overwrite a real file *by following a link* + // Because missing the -R implies -L. + shell.cp('-R', 'test/resources/cp/*', t.context.tmp); + t.truthy(common.statNoFollowLinks(`${t.context.tmp}/links/sym.lnk`).isSymbolicLink()); // this one is a link + t.falsy((common.statNoFollowLinks(`${t.context.tmp}/fakeLinks/sym.lnk`).isSymbolicLink())); // this one isn't + t.not( + shell.cat(`${t.context.tmp}/links/sym.lnk`).toString(), + shell.cat(`${t.context.tmp}/fakeLinks/sym.lnk`).toString() + ); + const result = shell.cp(`${t.context.tmp}/links/*`, `${t.context.tmp}/fakeLinks`); // don't use -R + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(common.statNoFollowLinks(`${t.context.tmp}/links/sym.lnk`).isSymbolicLink()); // this one is a link + t.falsy(common.statNoFollowLinks(`${t.context.tmp}/fakeLinks/sym.lnk`).isSymbolicLink()); // this one is still not a link + // But it still follows the link + t.is( + shell.cat(`${t.context.tmp}/links/sym.lnk`).toString(), + shell.cat(`${t.context.tmp}/fakeLinks/sym.lnk`).toString() + ); + }); +}); + +test('recursive, everything exists, with force flag', t => { + let result = shell.cp('-R', 'test/resources/cp', t.context.tmp); + shell.ShellString('changing things around').to(`${t.context.tmp}/cp/dir_a/z`); + t.not(shell.cat('test/resources/cp/dir_a/z').toString(), shell.cat(`${t.context.tmp}/cp/dir_a/z`).toString()); // before cp + result = shell.cp('-Rf', 'test/resources/cp', t.context.tmp); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.is(shell.cat('test/resources/cp/dir_a/z').toString(), shell.cat(`${t.context.tmp}/cp/dir_a/z`).toString()); // after cp +}); + +test("recursive, creates dest dir since it's only one level deep", t => { + // Github issue #44 + const result = shell.cp('-r', 'test/resources/issue44', `${t.context.tmp}/dir2`); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.is(shell.ls('-R', 'test/resources/issue44').toString(), shell.ls('-R', `${t.context.tmp}/dir2`).toString()); + t.is( + shell.cat('test/resources/issue44/main.js').toString(), + shell.cat(`${t.context.tmp}/dir2/main.js`).toString() + ); +}); + +test("recursive, does *not* create dest dir since it's too deep", t => { + // Github issue #44 + const result = shell.cp('-r', 'test/resources/issue44', `${t.context.tmp}/dir2/dir3`); + t.truthy(shell.error()); + t.is( + result.stderr, + `cp: cannot create directory '${t.context.tmp}/dir2/dir3': No such file or directory` + ); + t.is(result.code, 1); + t.falsy(fs.existsSync(`${t.context.tmp}/dir2`)); +}); + +test('recursive, copies entire directory', t => { + const result = shell.cp('-r', 'test/resources/cp/dir_a', `${t.context.tmp}/dest`); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/dest/z`)); +}); + +test('recursive, with trailing slash, does the exact same', t => { + const result = shell.cp('-r', 'test/resources/cp/dir_a/', `${t.context.tmp}/dest`); + t.is(result.code, 0); + t.falsy(shell.error()); + t.truthy(fs.existsSync(`${t.context.tmp}/dest/z`)); +}); + +test('preserve mode bits by default for file', t => { + utils.skipOnWin(t, () => { + const execBit = parseInt('001', 8); + t.is(common.statFollowLinks('test/resources/cp-mode-bits/executable').mode & execBit, execBit); + shell.cp('test/resources/cp-mode-bits/executable', `${t.context.tmp}/executable`); + t.is( + common.statFollowLinks('test/resources/cp-mode-bits/executable').mode, + common.statFollowLinks(`${t.context.tmp}/executable`).mode + ); + }); +}); + +test('Make sure hidden files are copied recursively', t => { + shell.rm('-rf', t.context.tmp); + const result = shell.cp('-r', 'test/resources/ls/', t.context.tmp); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/.hidden_file`)); +}); + +test('no-recursive will copy regular files only', t => { + const result = shell.cp( + 'test/resources/file1.txt', 'test/resources/file2.txt', 'test/resources/cp', + 'test/resources/ls/', t.context.tmp + ); + + t.is(result.code, 1); + t.truthy(shell.error()); + t.falsy(fs.existsSync(`${t.context.tmp}/.hidden_file`)); // doesn't copy dir contents + t.falsy(fs.existsSync(`${t.context.tmp}/ls`)); // doesn't copy dir itself + t.falsy(fs.existsSync(`${t.context.tmp}/a`)); // doesn't copy dir contents + t.falsy(fs.existsSync(`${t.context.tmp}/cp`)); // doesn't copy dir itself + t.truthy(fs.existsSync(`${t.context.tmp}/file1.txt`)); + t.truthy(fs.existsSync(`${t.context.tmp}/file2.txt`)); +}); + +test('-R implies -P', t => { + utils.skipOnWin(t, () => { + shell.cp('-R', 'test/resources/cp/links/sym.lnk', t.context.tmp); + t.truthy(common.statNoFollowLinks(`${t.context.tmp}/sym.lnk`).isSymbolicLink()); + }); +}); + +test("-Ru respects the -u flag recursively (don't update newer file)", t => { + // Setup code + const dir = `${t.context.tmp}/cp-Ru`; + const sourceDir = `${dir}/original`; + const sourceFile = `${sourceDir}/file`; + const destDir = `${dir}/new`; + const nestedDestDir = `${dir}/new/original`; + const destFile = `${nestedDestDir}/file`; + [sourceDir, destDir, nestedDestDir].forEach(d => shell.mkdir('-p', d)); + shell.ShellString('Source File Contents\n').to(sourceFile); + shell.ShellString('Destination File Contents\n').to(destFile); + const oldModifyTimeMs = 12345000; + const newModifyTimeMs = 67890000; + // End setup + + // Set the source file to be OLDER than the destination file + shell.touch({ '-m': true, '-d': new Date(oldModifyTimeMs) }, sourceFile); + shell.touch({ '-m': true, '-d': new Date(newModifyTimeMs) }, destFile); + shell.cp('-Ru', sourceDir, destDir); + // Check that dest has not been updated + t.is(shell.cat(destFile).stdout, 'Destination File Contents\n'); +}); + +test('-Ru respects the -u flag recursively (update older file)', t => { + // Setup code + const dir = `${t.context.tmp}/cp-Ru`; + const sourceDir = `${dir}/original`; + const sourceFile = `${sourceDir}/file`; + const destDir = `${dir}/new`; + const nestedDestDir = `${dir}/new/original`; + const destFile = `${nestedDestDir}/file`; + [sourceDir, destDir, nestedDestDir].forEach(d => shell.mkdir('-p', d)); + shell.ShellString('Source File Contents\n').to(sourceFile); + shell.ShellString('Destination File Contents\n').to(destFile); + const oldModifyTimeMs = 12345000; + const newModifyTimeMs = 67890000; + // End setup + + // Set the source file to be NEWER than the destination file + shell.touch({ '-m': true, '-d': new Date(newModifyTimeMs) }, sourceFile); + shell.touch({ '-m': true, '-d': new Date(oldModifyTimeMs) }, destFile); + shell.cp('-Ru', sourceDir, destDir); + // Check that dest has been overwritten + t.is(shell.cat(destFile).stdout, 'Source File Contents\n'); +}); + +test('using -P explicitly works', t => { + utils.skipOnWin(t, () => { + shell.cp('-P', 'test/resources/cp/links/sym.lnk', t.context.tmp); + t.truthy(common.statNoFollowLinks(`${t.context.tmp}/sym.lnk`).isSymbolicLink()); + }); +}); + +test('using -PR on a link to a folder does not follow the link', t => { + utils.skipOnWin(t, () => { + shell.cp('-PR', 'test/resources/cp/symFolder', t.context.tmp); + t.truthy(common.statNoFollowLinks(`${t.context.tmp}/symFolder`).isSymbolicLink()); + }); +}); + +test('-L overrides -P for copying directory', t => { + utils.skipOnWin(t, () => { + shell.cp('-LPR', 'test/resources/cp/symFolder', t.context.tmp); + t.falsy(common.statNoFollowLinks(`${t.context.tmp}/symFolder`).isSymbolicLink()); + t.falsy(common.statNoFollowLinks(`${t.context.tmp}/symFolder/sym.lnk`).isSymbolicLink()); + }); +}); + +test('Recursive, copies entire directory with no symlinks and -L option does not cause change in behavior', t => { + utils.skipOnWin(t, () => { + const result = shell.cp('-rL', 'test/resources/cp/dir_a', `${t.context.tmp}/dest`); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/dest/z`)); + }); +}); + +test("-u flag won't overwrite newer files", t => { + shell.touch(`${t.context.tmp}/file1.js`); + shell.cp('-u', 'test/resources/file1.js', t.context.tmp); + t.falsy(shell.error()); + t.not(shell.cat('test/resources/file1.js').toString(), shell.cat(`${t.context.tmp}/file1.js`).toString()); +}); + +test('-u flag does overwrite older files', t => { + shell.touch({ '-d': new Date(10) }, `${t.context.tmp}/file1.js`); // really old file + shell.cp('-u', 'test/resources/file1.js', t.context.tmp); + t.falsy(shell.error()); + t.is(shell.cat('test/resources/file1.js').toString(), shell.cat(`${t.context.tmp}/file1.js`).toString()); +}); + +test("-u flag works even if it's not overwriting a file", t => { + t.falsy(fs.existsSync(`${t.context.tmp}/file1.js`)); + shell.cp('-u', 'test/resources/file1.js', t.context.tmp); + t.falsy(shell.error()); + t.is(shell.cat('test/resources/file1.js').toString(), shell.cat(`${t.context.tmp}/file1.js`).toString()); +}); + +test('-u flag works correctly recursively', t => { + shell.mkdir(`${t.context.tmp}/foo`); + [1, 2, 3].forEach(num => { + new shell.ShellString('old\n').to(`${t.context.tmp}/foo/file${num}`); + shell.touch({ '-d': new Date(10) }, `${t.context.tmp}/foo/file${num}`); + }); + shell.mkdir(`${t.context.tmp}/bar`); + [1, 2, 3].forEach(num => { + new shell.ShellString('new\n').to(`${t.context.tmp}/bar/file${num}`); + shell.touch({ '-d': new Date(1000) }, `${t.context.tmp}/bar/file${num}`); + }); + // put one new one in the foo directory + new shell.ShellString('newest\n').to(`${t.context.tmp}/foo/file3`); + shell.touch({ '-d': new Date(10000) }, `${t.context.tmp}/foo/file3`); + shell.cp('-u', `${t.context.tmp}/foo/*`, `${t.context.tmp}/bar`); + t.falsy(shell.error()); + t.is(shell.cat(`${t.context.tmp}/bar/*`).toString(), 'new\nnew\nnewest\n'); +}); + +test('using -R on a link to a folder *does* follow the link', t => { + shell.cp('-R', 'test/resources/cp/symFolder', t.context.tmp); + t.falsy(common.statNoFollowLinks(`${t.context.tmp}/symFolder`).isSymbolicLink()); +}); + +test('Without -R, -L is implied', t => { + shell.cp('test/resources/cp/links/sym.lnk', t.context.tmp); + t.falsy(common.statNoFollowLinks(`${t.context.tmp}/sym.lnk`).isSymbolicLink()); +}); + +test('-L explicitly works', t => { + shell.cp('-L', 'test/resources/cp/links/sym.lnk', t.context.tmp); + t.falsy(common.statNoFollowLinks(`${t.context.tmp}/sym.lnk`).isSymbolicLink()); +}); + +test('using -LR does not imply -P', t => { + shell.cp('-LR', 'test/resources/cp/links/sym.lnk', t.context.tmp); + t.falsy(common.statNoFollowLinks(`${t.context.tmp}/sym.lnk`).isSymbolicLink()); +}); + +test('using -LR also works recursively on directories containing links', t => { + shell.cp('-LR', 'test/resources/cp/links', t.context.tmp); + t.falsy(common.statNoFollowLinks(`${t.context.tmp}/links/sym.lnk`).isSymbolicLink()); +}); + +test('-L always overrides a -P', t => { + shell.cp('-LP', 'test/resources/cp/links/sym.lnk', t.context.tmp); + t.falsy(common.statNoFollowLinks(`${t.context.tmp}/sym.lnk`).isSymbolicLink()); + shell.cp('-LPR', 'test/resources/cp/links/sym.lnk', t.context.tmp); + t.falsy(common.statNoFollowLinks(`${t.context.tmp}/sym.lnk`).isSymbolicLink()); +}); + +test('Make sure max depth does not limit shallow directory structures', t => { + shell.config.maxdepth = 3; + const TMP = t.context.tmp; + shell.mkdir(`${TMP}/foo`); + for (let k = 0; k < 5; k++) { + shell.mkdir(`${TMP}/foo/dir${k}`); + } + shell.cp('-r', `${TMP}/foo`, `${TMP}/bar`); + t.is(shell.ls(`${TMP}/foo`).stdout, shell.ls(`${TMP}/bar`).stdout); +}); + +test('Test max depth.', t => { + shell.config.maxdepth = 32; + let directory = ''; + for (let i = 1; i < 40; i++) { + directory += '/' + i; + } + let directory32deep = ''; + for (let i = 1; i < 32; i++) { + directory32deep += '/' + i; + } + shell.mkdir('-p', `${t.context.tmp}/0${directory}`); + shell.cp('-r', `${t.context.tmp}/0`, `${t.context.tmp}/copytestdepth`); + // Check full directory exists. + t.truthy(shell.test('-d', `${t.context.tmp}/0/${directory}`)); + // Check full copy of directory does not exist. + t.falsy(shell.test('-d', `${t.context.tmp}/copytestdepth${directory}`)); + // Check last directory to exist is below maxdepth. + t.truthy(shell.test('-d', `${t.context.tmp}/copytestdepth${directory32deep}`)); + t.falsy(shell.test('-d', `${t.context.tmp}/copytestdepth${directory32deep}/32`)); + utils.skipOnWinForEPERM(shell.ln.bind(shell, '-s', `${t.context.tmp}/0`, `${t.context.tmp}/symlinktest`), () => { + if (!shell.test('-L', `${t.context.tmp}/symlinktest`)) { + t.fail(); } - shell.rm('-rf', 'tmp/symlinktest'); - // Create sym links to check for cycle. - shell.cd('tmp/0/1/2/3/4'); + + // Create symlinks to check for cycle. + shell.cd(`${t.context.tmp}/0/1/2/3/4`); + t.falsy(shell.error()); shell.ln('-s', '../../../2', 'link'); + t.falsy(shell.error()); shell.ln('-s', './5/6/7', 'link1'); + t.falsy(shell.error()); shell.cd('../../../../../..'); - assert.ok(shell.test('-d', 'tmp/')); - - shell.rm('-fr', 'tmp/copytestdepth'); - shell.cp('-r', 'tmp/0', 'tmp/copytestdepth'); - assert.ok(shell.test('-d', 'tmp/copytestdepth/1/2/3/4/link/3/4/link/3/4')); - - // Test copying of symlinked files cp -L. - shell.rm('-fr', 'tmp'); - shell.mkdir('-p', 'tmp/sub'); - shell.mkdir('-p', 'tmp/new'); - shell.cp('-f', 'resources/file1.txt', 'tmp/sub/file.txt'); - shell.cd('tmp/sub'); + t.falsy(shell.error()); + t.truthy(shell.test('-d', t.context.tmp)); + + shell.cp('-r', `${t.context.tmp}/0/1`, `${t.context.tmp}/copytestdepth`); + t.falsy(shell.error()); + t.truthy(shell.test('-d', `${t.context.tmp}/copytestdepth/1/2/3/4/link/3/4/link/3/4`)); + }); +}); + +test('cp -L follows symlinks', t => { + utils.skipOnWinForEPERM(shell.ln.bind(shell, '-s', `${t.context.tmp}/0`, `${t.context.tmp}/symlinktest`), () => { + shell.mkdir('-p', `${t.context.tmp}/sub`); + shell.mkdir('-p', `${t.context.tmp}/new`); + shell.cp('-f', 'test/resources/file1.txt', `${t.context.tmp}/sub/file.txt`); + shell.cd(`${t.context.tmp}/sub`); shell.ln('-s', 'file.txt', 'foo.lnk'); shell.ln('-s', 'file.txt', 'sym.lnk'); shell.cd('..'); shell.cp('-L', 'sub/*', 'new/'); - - // Ensure copies are files. shell.cd('new'); - shell.cp('-f', '../../resources/file2.txt', 'file.txt'); - assert.equal(shell.cat('file.txt').toString(), 'test2\n'); - // Ensure other files have not changed. - assert.equal(shell.cat('foo.lnk').toString(), 'test1\n'); - assert.equal(shell.cat('sym.lnk').toString(), 'test1\n'); - // Ensure the links are converted to files. - assert.equal(shell.test('-L', 'foo.lnk'), false); - assert.equal(shell.test('-L', 'sym.lnk'), false); + shell.cp('-f', '../../test/resources/file2.txt', 'file.txt'); + t.is(shell.cat('file.txt').toString(), 'test2\n'); + // Ensure other files have not changed. + t.is(shell.cat('foo.lnk').toString(), 'test1\n'); + t.is(shell.cat('sym.lnk').toString(), 'test1\n'); + t.falsy(shell.test('-L', 'foo.lnk')); + t.falsy(shell.test('-L', 'sym.lnk')); shell.cd('../..'); + }); +}); - // Test with recurisve option and symlinks. - - shell.rm('-fr', 'tmp'); - shell.mkdir('-p', 'tmp/sub/sub1'); - shell.cp('-f', 'resources/file1.txt', 'tmp/sub/file.txt'); - shell.cp('-f', 'resources/file1.txt', 'tmp/sub/sub1/file.txt'); - shell.cd('tmp/sub'); +test('Test with recursive option and symlinks.', t => { + utils.skipOnWinForEPERM(shell.ln.bind(shell, '-s', `${t.context.tmp}/0`, `${t.context.tmp}/symlinktest`), () => { + shell.mkdir('-p', `${t.context.tmp}/sub/sub1`); + shell.cp('-f', 'test/resources/file1.txt', `${t.context.tmp}/sub/file.txt`); + shell.cp('-f', 'test/resources/file1.txt', `${t.context.tmp}/sub/sub1/file.txt`); + shell.cd(`${t.context.tmp}/sub`); shell.ln('-s', 'file.txt', 'foo.lnk'); shell.ln('-s', 'file.txt', 'sym.lnk'); shell.cd('sub1'); shell.ln('-s', '../file.txt', 'foo.lnk'); shell.ln('-s', '../file.txt', 'sym.lnk'); - // Ensure file reads from proper source. - assert.equal(shell.cat('file.txt').toString(), 'test1\n'); - assert.equal(shell.cat('foo.lnk').toString(), 'test1\n'); - assert.equal(shell.cat('sym.lnk').toString(), 'test1\n'); - assert.equal(shell.test('-L', 'foo.lnk'), true); - assert.equal(shell.test('-L', 'sym.lnk'), true); + // Ensure file reads from proper source + t.is(shell.cat('file.txt').toString(), 'test1\n'); + t.is(shell.cat('foo.lnk').toString(), 'test1\n'); + t.is(shell.cat('sym.lnk').toString(), 'test1\n'); + t.truthy(shell.test('-L', 'foo.lnk')); + t.truthy(shell.test('-L', 'sym.lnk')); shell.cd('../..'); shell.cp('-rL', 'sub/', 'new/'); shell.cd('new'); // Ensure copies of files are symlinks by updating file contents. - shell.cp('-f', '../../resources/file2.txt', 'file.txt'); - assert.equal(shell.cat('file.txt').toString(), 'test2\n'); + shell.cp('-f', '../../test/resources/file2.txt', 'file.txt'); + t.is(shell.cat('file.txt').toString(), 'test2\n'); // Ensure other files have not changed. - assert.equal(shell.cat('foo.lnk').toString(), 'test1\n'); - assert.equal(shell.cat('sym.lnk').toString(), 'test1\n'); + t.is(shell.cat('foo.lnk').toString(), 'test1\n'); + t.is(shell.cat('sym.lnk').toString(), 'test1\n'); // Ensure the links are converted to files. - assert.equal(shell.test('-L', 'foo.lnk'), false); - assert.equal(shell.test('-L', 'sym.lnk'), false); + t.falsy(shell.test('-L', 'foo.lnk')); + t.falsy(shell.test('-L', 'sym.lnk')); - shell.cd('sub1'); - shell.cp('-f', '../../../resources/file2.txt', 'file.txt'); - assert.equal(shell.cat('file.txt').toString(), 'test2\n'); // Ensure other files have not changed. - assert.equal(shell.cat('foo.lnk').toString(), 'test1\n'); - assert.equal(shell.cat('sym.lnk').toString(), 'test1\n'); + shell.cd('sub1'); + shell.cp('-f', '../../../test/resources/file2.txt', 'file.txt'); + t.is(shell.cat('file.txt').toString(), 'test2\n'); + t.is(shell.cat('foo.lnk').toString(), 'test1\n'); + t.is(shell.cat('sym.lnk').toString(), 'test1\n'); + + // Ensure the links are converted to files + t.falsy(shell.test('-L', 'foo.lnk')); + t.falsy(shell.test('-L', 'sym.lnk')); + }); +}); - // Ensure the links are converted to files. - assert.equal(shell.test('-L', 'foo.lnk'), false); - assert.equal(shell.test('-L', 'sym.lnk'), false); +test('recursive, with a non-normalized path', t => { + const result = shell.cp('-R', 'test/resources/../resources/./cp', t.context.tmp); + t.falsy(shell.error()); // crash test only + t.falsy(result.stderr); + t.is(result.code, 0); +}); + +test('copy file to same path', t => { + const result = shell.cp('test/resources/file1', 'test/resources/file1'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, "cp: 'test/resources/file1' and 'test/resources/file1' are the same file"); +}); + +test('copy file to same directory', t => { + const result = shell.cp('test/resources/file1', 'test/resources'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, "cp: 'test/resources/file1' and 'test/resources/file1' are the same file"); +}); + +test('copy multiple files to same location', t => { + const result = shell.cp('test/resources/file1', 'test/resources/file2', 'test/resources'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is( + result.stderr, + "cp: 'test/resources/file1' and 'test/resources/file1' are the same file\n" + + "cp: 'test/resources/file2' and 'test/resources/file2' are the same file" + ); +}); + +test('should not overwrite recently created files', t => { + const result = shell.cp('test/resources/file1', 'test/resources/cp/file1', t.context.tmp); + t.truthy(shell.error()); + t.is(result.code, 1); + + // Ensure First file is copied + t.is(shell.cat(`${t.context.tmp}/file1`).toString(), 'test1'); + t.is( + result.stderr, + `cp: will not overwrite just-created '${t.context.tmp}/file1' with 'test/resources/cp/file1'` + ); +}); + + +test('should not overwrite recently created files (in recursive Mode)', t => { + const result = shell.cp('-R', 'test/resources/file1', 'test/resources/cp/file1', t.context.tmp); + t.truthy(shell.error()); + t.is(result.code, 1); + + // Ensure First file is copied + t.is(shell.cat(`${t.context.tmp}/file1`).toString(), 'test1'); + t.is( + result.stderr, + `cp: will not overwrite just-created '${t.context.tmp}/file1' with 'test/resources/cp/file1'` + ); +}); + +test('should not overwrite recently created files (not give error no-force mode)', t => { + const result = shell.cp('-n', 'test/resources/file1', 'test/resources/cp/file1', t.context.tmp); + t.falsy(shell.error()); + t.is(result.code, 0); + + // Ensure First file is copied + t.is(shell.cat(`${t.context.tmp}/file1`).toString(), 'test1'); +}); + +// cp -R should be able to copy a readonly src (issue #98). +// On Windows, chmod acts VERY differently so skip these tests for now +test('cp -R should be able to copy a readonly src. issue #98; (Non window platforms only)', t => { + utils.skipOnWin(t, () => { + shell.cp('-r', 'test/resources/cp', t.context.tmp); + shell.chmod('555', `${t.context.tmp}/cp/`); + shell.chmod('555', `${t.context.tmp}/cp/dir_a`); + shell.chmod('555', `${t.context.tmp}/cp/dir_b`); + shell.chmod('555', `${t.context.tmp}/cp/a`); + + const result = shell.cp('-r', `${t.context.tmp}/cp`, `${t.context.tmp}/cp_cp`); + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); + + t.is(shell.ls('-R', `${t.context.tmp}/cp`) + '', shell.ls('-R', `${t.context.tmp}/cp_cp`) + ''); + t.is(fs.statSync(`${t.context.tmp}/cp_cp`).mode & parseInt('777', 8), parseInt('555', 8)); + t.is(fs.statSync(`${t.context.tmp}/cp_cp/dir_a`).mode & parseInt('777', 8), parseInt('555', 8)); + t.is(fs.statSync(`${t.context.tmp}/cp_cp/a`).mode & parseInt('777', 8), parseInt('555', 8)); + + shell.chmod('-R', '755', t.context.tmp); + }); +}); + +test('cp -p should preserve mode, ownership, and timestamp (regular file)', t => { + // Setup: copy to srcFile and modify mode and timestamp + const srcFile = `${t.context.tmp}/srcFile`; + shell.cp('test/resources/cp/file1', srcFile); + // Make this a round number of seconds, since the underlying system may not + // have millisecond precision. + const newModifyTimeMs = 12345000; + const newAccessTimeMs = 67890000; + shell.touch({ '-d': new Date(newModifyTimeMs), '-m': true }, srcFile); + shell.touch({ '-d': new Date(newAccessTimeMs), '-a': true }, srcFile); + const mode = '444'; + shell.chmod(mode, srcFile); + + // Now re-copy with '-p' and verify metadata. + const result = shell.cp('-p', srcFile, `${t.context.tmp}/preservedFile1`); + const stat = common.statFollowLinks(srcFile); + const statOfResult = common.statFollowLinks(`${t.context.tmp}/preservedFile1`); + + t.is(result.code, 0); + + // Original file should be unchanged: + t.is(stat.mtime.getTime(), newModifyTimeMs); + // cp appears to update the atime, but only of the srcFile + t.is(stat.mode.toString(8), '100' + mode); + + // New file should keep same attributes + t.is(statOfResult.mtime.getTime(), newModifyTimeMs); + t.is(statOfResult.atime.getTime(), newAccessTimeMs); + t.is(statOfResult.mode.toString(8), '100' + mode); + + t.is(stat.uid, statOfResult.uid); + t.is(stat.gid, statOfResult.gid); +}); + +test('cp -p should preserve mode, ownership, and timestamp (directory)', t => { + // Setup: copy to srcFile and modify mode and timestamp + const srcDir = `${t.context.tmp}/srcDir`; + const srcFile = `${srcDir}/srcFile`; + shell.mkdir(srcDir); + shell.cp('test/resources/cp/file1', srcFile); + // Make this a round number of seconds, since the underlying system may not + // have millisecond precision. + const newModifyTimeMs = 12345000; + const newAccessTimeMs = 67890000; + shell.touch({ '-d': new Date(newModifyTimeMs), '-m': true }, srcFile); + shell.touch({ '-d': new Date(newAccessTimeMs), '-a': true }, srcFile); + fs.utimesSync(srcDir, new Date(newAccessTimeMs), new Date(newModifyTimeMs)); + const mode = '444'; + shell.chmod(mode, srcFile); + + // Now re-copy (the whole dir) with '-p' and verify metadata of file contents. + const result = shell.cp('-pr', srcDir, `${t.context.tmp}/preservedDir`); + const stat = common.statFollowLinks(srcFile); + const statDir = common.statFollowLinks(srcDir); + const statOfResult = common.statFollowLinks(`${t.context.tmp}/preservedDir/srcFile`); + const statOfResultDir = common.statFollowLinks(`${t.context.tmp}/preservedDir`); + + t.is(result.code, 0); + + // Both original file and original dir should be unchanged: + t.is(statDir.mtime.getTime(), newModifyTimeMs); + t.is(stat.mtime.getTime(), newModifyTimeMs); + // cp appears to update the atime, but only of the srcFile & srcDir + t.is(stat.mode.toString(8), '100' + mode); + + // Both new file and new dir should keep same attributes + t.is(statOfResultDir.mtime.getTime(), newModifyTimeMs); + utils.skipOnWin(t, () => { + // The resultDir atime may be updated on Windows as a side-effect, e.g. chmod(). + t.is(statOfResultDir.atime.getTime(), newAccessTimeMs); + }); + t.is(statOfResult.mtime.getTime(), newModifyTimeMs); + t.is(statOfResult.atime.getTime(), newAccessTimeMs); + t.is(statOfResult.mode.toString(8), '100' + mode); + + t.is(stat.uid, statOfResult.uid); + t.is(stat.gid, statOfResult.gid); }); -shell.exit(123); +test('cp -p should preserve mode, ownership, and timestamp (symlink)', t => { + // Skip in Windows because symlinks require elevated permissions. + utils.skipOnWin(t, () => { + // Setup: copy to srcFile, create srcLink, and modify mode and timestamp + shell.cp('test/resources/cp/file1', `${t.context.tmp}/srcFile`); + const srcLink = `${t.context.tmp}/srcLink`; + shell.ln('-s', 'srcFile', `${t.context.tmp}/srcLink`); + // Make this a round number of seconds, since the underlying system may not + // have millisecond precision. + const newModifyTimeMs = 12345000; + const newAccessTimeMs = 67890000; + shell.touch({ '-d': new Date(newModifyTimeMs), '-m': true }, srcLink); + shell.touch({ '-d': new Date(newAccessTimeMs), '-a': true }, srcLink); + const mode = '444'; + shell.chmod(mode, srcLink); + + // Now re-copy with '-p' and verify metadata. + const result = shell.cp('-p', srcLink, `${t.context.tmp}/preservedLink`); + const stat = common.statFollowLinks(srcLink); + const statOfResult = common.statFollowLinks(`${t.context.tmp}/preservedLink`); + + t.is(result.code, 0); + + // Original file should be unchanged: + t.is(stat.mtime.getTime(), newModifyTimeMs); + // cp appears to update the atime, but only of the srcFile + t.is(stat.mode.toString(8), '100' + mode); + + // New file should keep same attributes + t.is(statOfResult.mtime.getTime(), newModifyTimeMs); + t.is(statOfResult.atime.getTime(), newAccessTimeMs); + t.is(statOfResult.mode.toString(8), '100' + mode); + + t.is(stat.uid, statOfResult.uid); + t.is(stat.gid, statOfResult.gid); + }); +}); + +test('copy directory to its parent dir (same location) should not erase content', t => { + // Setup: create a directory with a file inside t.context.tmp + shell.mkdir(`${t.context.tmp}/srcdir`); + shell.ShellString('deadbeef').to(`${t.context.tmp}/srcdir/lol`); + + // Verify setup + t.is(shell.cat(`${t.context.tmp}/srcdir/lol`).toString(), 'deadbeef'); + + // This should error, not silently erase the file contents + const result = shell.cp('-R', `${t.context.tmp}/srcdir`, t.context.tmp); + t.truthy(shell.error()); + t.is(result.code, 1); + t.truthy(result.stderr.match(/are the same file/)); + + // Most importantly: file content must be preserved (no data loss) + t.is(shell.cat(`${t.context.tmp}/srcdir/lol`).toString(), 'deadbeef'); +}); + +test('copy directory to same location with trailing slash should not erase content', t => { + // Setup: create a directory with a file inside t.context.tmp + shell.mkdir(`${t.context.tmp}/srcdir`); + shell.ShellString('deadbeef').to(`${t.context.tmp}/srcdir/lol`); + + // Verify setup + t.is(shell.cat(`${t.context.tmp}/srcdir/lol`).toString(), 'deadbeef'); + + // cp -R /path/srcdir /path/ should detect same directory + const result = shell.cp('-R', `${t.context.tmp}/srcdir`, `${t.context.tmp}/`); + t.truthy(shell.error()); + t.is(result.code, 1); + t.truthy(result.stderr.match(/are the same file/)); + + // File content must be preserved + t.is(shell.cat(`${t.context.tmp}/srcdir/lol`).toString(), 'deadbeef'); +}); + +test('copy multiple dirs where one is to same location should error for that one and continue', t => { + // Setup: src1 in tmp root, src2 inside destination + shell.mkdir(`${t.context.tmp}/src1`); + shell.mkdir('-p', `${t.context.tmp}/destination/src2`); + shell.ShellString('content1').to(`${t.context.tmp}/src1/file`); + shell.ShellString('content2').to(`${t.context.tmp}/destination/src2/file`); + + // src1 copy should succeed, src2 copy should fail (but the original src2 is still intact) + const result = shell.cp('-R', `${t.context.tmp}/src1`, `${t.context.tmp}/destination/src2`, `${t.context.tmp}/destination`); + t.truthy(shell.error()); + t.is(result.code, 1); + + // src1 was copied successfully into destination + t.is(shell.cat(`${t.context.tmp}/destination/src1/file`).toString(), 'content1'); + // src2's original content is preserved (not lost due to same-file copy) + t.is(shell.cat(`${t.context.tmp}/destination/src2/file`).toString(), 'content2'); +}); + +test('copy single file to itself should not erase content', t => { + // Setup: create a file in t.context.tmp + shell.ShellString('important data').to(`${t.context.tmp}/myfile.txt`); + + // Verify setup + t.is(shell.cat(`${t.context.tmp}/myfile.txt`).toString(), 'important data'); + + // Copying a file to itself should error but preserve data + const result = shell.cp(`${t.context.tmp}/myfile.txt`, `${t.context.tmp}/myfile.txt`); + t.truthy(shell.error()); + t.is(result.code, 1); + t.truthy(result.stderr.match(/are the same file/)); + + // File content must be preserved (no data loss) + t.is(shell.cat(`${t.context.tmp}/myfile.txt`).toString(), 'important data'); +}); diff --git a/test/dirs.js b/test/dirs.js index a589893b0..44f82486a 100644 --- a/test/dirs.js +++ b/test/dirs.js @@ -1,33 +1,39 @@ -var shell = require('..'); +const path = require('path'); -var assert = require('assert'), - path = require('path'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); -var root = path.resolve(); +test.beforeEach(() => { + shell.config.resetForTesting(); + shell.pushd('test/resources/pushd'); + shell.pushd('a'); +}); -shell.pushd('resources/pushd'); -shell.pushd('a'); +// +// Valids +// -var trail = [ - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd'), - root +const trail = [ + path.resolve(path.resolve(), 'test/resources/pushd/a'), + path.resolve(path.resolve(), 'test/resources/pushd'), + path.resolve(), ]; -assert.deepEqual(shell.dirs(), trail); - -// Single items -assert.equal(shell.dirs('+0'), trail[0]); -assert.equal(shell.dirs('+1'), trail[1]); -assert.equal(shell.dirs('+2'), trail[2]); -assert.equal(shell.dirs('-0'), trail[2]); -assert.equal(shell.dirs('-1'), trail[1]); -assert.equal(shell.dirs('-2'), trail[0]); - -// Clearing items -assert.deepEqual(shell.dirs('-c'), []); -assert(!shell.error()); - -shell.exit(123); \ No newline at end of file +test('no arguments', t => { + t.deepEqual(shell.dirs(), trail); +}); + +test('Single items', t => { + t.is(shell.dirs('+0'), trail[0]); + t.is(shell.dirs('+1'), trail[1]); + t.is(shell.dirs('+2'), trail[2]); + t.is(shell.dirs('-0'), trail[2]); + t.is(shell.dirs('-1'), trail[1]); + t.is(shell.dirs('-2'), trail[0]); +}); + +test('Clearing items', t => { + t.deepEqual(shell.dirs('-c'), []); + t.falsy(shell.error()); +}); diff --git a/test/docs.js b/test/docs.js new file mode 100644 index 000000000..67d503462 --- /dev/null +++ b/test/docs.js @@ -0,0 +1,11 @@ +const test = require('ava'); + +const gendocs = require('../scripts/generate-docs'); + +test('Documentation generation', t => { + if (gendocs.extractCurrentDocs() === gendocs.generateNewDocs()) { + t.pass(); + } else { + t.fail('README documentation is stale. Please run `npm run gendocs`.'); + } +}); diff --git a/test/echo.js b/test/echo.js index 09b5f1a31..c6989ac1e 100644 --- a/test/echo.js +++ b/test/echo.js @@ -1,50 +1,153 @@ -var shell = require('..'); +const test = require('ava'); -var assert = require('assert'), - child = require('child_process'); +const shell = require('..'); +const utils = require('./utils/utils'); +const mocks = require('./utils/mocks'); shell.config.silent = true; -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + mocks.stdout.init(); + mocks.stderr.init(); +}); + +test.afterEach.always(t => { + shell.rm('-rf', t.context.tmp); + mocks.stdout.restore(); + mocks.stderr.restore(); +}); // // Valids // +test('simple test with defaults', t => { + const result = shell.echo('hello', 'world'); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(stdout, 'hello world\n'); + t.is(stderr, ''); +}); + +test('allow arguments to begin with a hyphen', t => { + // Github issue #20 + const result = shell.echo('-asdf', '111'); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(result.code, 1); + t.is(stdout, '-asdf 111\n'); + t.is(stderr, ''); +}); + +test("using null as an explicit argument doesn't crash the function", t => { + const result = shell.echo(null); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(stdout, 'null\n'); + t.is(stderr, ''); +}); + +test('-e option', t => { + const result = shell.echo('-e', '\tmessage'); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(stdout, '\tmessage\n'); + t.is(stderr, ''); +}); -// From here on we use child.exec() to intercept the stdout +test('piping to a file', t => { + // Github issue #476 + shell.mkdir(t.context.tmp); + const tmp = `${t.context.tmp}/echo.txt`; + const resultA = shell.echo('A').toEnd(tmp); + t.falsy(shell.error()); + t.is(resultA.code, 0); + const resultB = shell.echo('B').toEnd(tmp); + t.falsy(shell.error()); + t.is(resultB.code, 0); + const result = shell.cat(tmp); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(stdout, 'A\nB\n'); + t.is(stderr, ''); + t.is(result.toString(), 'A\nB\n'); +}); +test('-n option', t => { + const result = shell.echo('-n', 'message'); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(stdout, 'message'); + t.is(stderr, ''); +}); -// simple test with defaults -shell.mkdir('-p', 'tmp'); -var file = 'tmp/tempscript'+Math.random()+'.js', - script = 'require(\'../../global.js\'); echo("-asdf", "111");'; // test '-' bug (see issue #20) -shell.ShellString(script).to(file); -child.exec(JSON.stringify(process.execPath)+' '+file, function(err, stdout) { - assert.equal(stdout, '-asdf 111\n'); +test('-ne option', t => { + const result = shell.echo('-ne', 'message'); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(stdout, 'message'); + t.is(stderr, ''); +}); - // using null as an explicit argument doesn't crash the function - file = 'tmp/tempscript'+Math.random()+'.js'; - script = 'require(\'../../global.js\'); echo(null);'; - shell.ShellString(script).to(file); - child.exec(JSON.stringify(process.execPath)+' '+file, function(err, stdout, stderr) { - assert.equal(stdout, 'null\n'); - assert.equal(stderr, ''); +test('-en option', t => { + const result = shell.echo('-en', 'message'); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(stdout, 'message'); + t.is(stderr, ''); +}); - // simple test with silent(true) - shell.mkdir('-p', 'tmp'); - var file = 'tmp/tempscript'+Math.random()+'.js', - script = 'require(\'../../global.js\'); config.silent=true; echo(555);'; - shell.ShellString(script).to(file); - child.exec(JSON.stringify(process.execPath)+' '+file, function(err, stdout) { - assert.equal(stdout, '555\n'); +test('-en option with escaped characters', t => { + const result = shell.echo('-en', '\tmessage\n'); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(stdout, '\tmessage\n'); + t.is(stderr, ''); +}); - theEnd(); - }); - }); +test('piping to a file with -n', t => { + // Github issue #476 + shell.mkdir(t.context.tmp); + const tmp = `${t.context.tmp}/echo.txt`; + const resultA = shell.echo('-n', 'A').toEnd(tmp); + t.falsy(shell.error()); + t.is(resultA.code, 0); + const resultB = shell.echo('-n', 'B').toEnd(tmp); + t.falsy(shell.error()); + t.is(resultB.code, 0); + const result = shell.cat(tmp); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(stdout, 'AB'); + t.is(stderr, ''); + t.is(result.toString(), 'AB'); }); -function theEnd() { - shell.exit(123); -} +test('stderr with unrecognized options is empty', t => { + const result = shell.echo('-asdf'); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(result.code, 1); + t.falsy(result.stderr); + t.is(stdout, '-asdf\n'); + t.is(stderr, ''); +}); diff --git a/test/env.js b/test/env.js index 0e041d6ad..a9f2c009e 100644 --- a/test/env.js +++ b/test/env.js @@ -1,19 +1,18 @@ -var shell = require('..'); +const test = require('ava'); -var assert = require('assert'); +const shell = require('..'); shell.config.silent = true; -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); - // // Valids // -assert.equal(shell.env['PATH'], process.env['PATH']); - -shell.env['SHELLJS_TEST'] = 'hello world'; -assert.equal(shell.env['SHELLJS_TEST'], process.env['SHELLJS_TEST']); +test('existing variables', t => { + t.is(shell.env.PATH, process.env.PATH); +}); -shell.exit(123); +test('variables are exported', t => { + shell.env.SHELLJS_TEST = 'hello world'; + t.is(shell.env.SHELLJS_TEST, process.env.SHELLJS_TEST); +}); diff --git a/test/exec.js b/test/exec.js index 8ab6bc6ec..79caca5da 100644 --- a/test/exec.js +++ b/test/exec.js @@ -1,32 +1,74 @@ -var shell = require('..'); +const os = require('os'); +const path = require('path'); +const util = require('util'); -var assert = require('assert'); -var util = require('util'); -var path = require('path'); -var os = require('os'); +const test = require('ava'); +const shell = require('..'); +const utils = require('./utils/utils'); +const mocks = require('./utils/mocks'); + +const CWD = process.cwd(); +const ORIG_EXEC_PATH = shell.config.execPath; shell.config.silent = true; +test.beforeEach(() => { + mocks.stdout.init(); + mocks.stderr.init(); +}); + +test.afterEach.always(() => { + process.chdir(CWD); + shell.config.execPath = ORIG_EXEC_PATH; + mocks.stdout.restore(); + mocks.stderr.restore(); +}); + // // Invalids // -shell.exec(); -assert.ok(shell.error()); +test('no args', t => { + shell.exec(); + t.truthy(shell.error()); +}); -var result = shell.exec('asdfasdf'); // could not find command -assert.ok(result.code > 0); +test('unknown command', t => { + const result = shell.exec('asdfasdf'); // could not find command + t.truthy(result.code > 0); +}); -// Test 'fatal' mode for exec, temporarily overriding process.exit -var old_fatal = shell.config.fatal; +test('config.fatal and unknown command', t => { + const oldFatal = shell.config.fatal; + shell.config.fatal = true; + t.throws(() => { + shell.exec('asdfasdf'); // could not find command + }, { message: /asdfasdf/ }); // name of command should be in error message + shell.config.fatal = oldFatal; +}); -shell.config.fatal = true; +test('options.fatal = true and unknown command', t => { + const oldFatal = shell.config.fatal; + shell.config.fatal = false; + t.throws(() => { + shell.exec('asdfasdf', { fatal: true }); // could not find command + }, { message: /asdfasdf/ }); // name of command should be in error message + shell.config.fatal = oldFatal; // TODO(nfischer): this setting won't get reset if the assertion above fails +}); -assert.throws(function() { - shell.exec('asdfasdf'); // could not find command -}, /exec: internal error/); +test('exec exits gracefully if we cannot find the execPath', t => { + shell.config.execPath = null; + shell.exec('echo foo'); + t.regex( + shell.error(), + /Unable to find a path to the node binary\. Please manually set config\.execPath/ + ); +}); -shell.config.fatal = old_fatal; +test('exec-child.js should not be imported', t => { + const execChild = require('../src/exec-child'); + t.deepEqual([], Object.keys(execChild)); +}); // // Valids @@ -36,142 +78,194 @@ shell.config.fatal = old_fatal; // sync // -// check if stdout goes to output -var result = shell.exec(JSON.stringify(process.execPath)+' -e \"console.log(1234);\"'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.ok(result.stdout === '1234\n' || result.stdout === '1234\nundefined\n'); // 'undefined' for v0.4 - -// check if stderr goes to output -var result = shell.exec(JSON.stringify(process.execPath)+' -e \"console.error(1234);\"'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.ok(result.stdout === '' || result.stdout === 'undefined\n'); // 'undefined' for v0.4 -assert.ok(result.stderr === '1234\n' || result.stderr === '1234\nundefined\n'); // 'undefined' for v0.4 - -// check if stdout + stderr go to output -var result = shell.exec(JSON.stringify(process.execPath)+' -e \"console.error(1234); console.log(666);\"'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.ok(result.stdout === '666\n' || result.stdout === '666\nundefined\n'); // 'undefined' for v0.4 -assert.ok(result.stderr === '1234\n' || result.stderr === '1234\nundefined\n'); // 'undefined' for v0.4 - -// check exit code -var result = shell.exec(JSON.stringify(process.execPath)+' -e \"process.exit(12);\"'); -assert.ok(shell.error()); -assert.equal(result.code, 12); - -// interaction with cd -shell.cd('resources/external'); -var result = shell.exec(JSON.stringify(process.execPath)+' node_script.js'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.stdout, 'node_script_1234\n'); -shell.cd('../..'); - -// check quotes escaping -var result = shell.exec( util.format(JSON.stringify(process.execPath)+' -e "console.log(%s);"', "\\\"\\'+\\'_\\'+\\'\\\"") ); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.stdout, "'+'_'+'\n"); - -// set cwd -var cmdString = process.platform === 'win32' ? 'cd' : 'pwd'; -result = shell.exec(cmdString, {cwd: '..'}); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.stdout, path.resolve('..') + os.EOL); - -// set maxBuffer (very small) -result = shell.exec('echo 1234567890'); // default maxBuffer is ok -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.stdout, '1234567890' + os.EOL); -if (process.version >= 'v0.11') { // this option doesn't work on v0.10 - shell.exec('echo 1234567890', {maxBuffer: 6}); - assert.ok(shell.error()); -} +test('check if stdout goes to output', t => { + const result = shell.exec(`${JSON.stringify(shell.config.execPath)} -e "console.log(1234);"`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, '1234\n'); +}); -// set timeout option -result = shell.exec(JSON.stringify(process.execPath)+' resources/exec/slow.js 100'); // default timeout is ok -assert.ok(!shell.error()); -assert.equal(result.code, 0); -if (process.version >= 'v0.11') { // this option doesn't work on v0.10 - result = shell.exec(JSON.stringify(process.execPath)+' resources/exec/slow.js 100', {timeout: 10}); // times out - assert.ok(shell.error()); -} +test('check if stderr goes to output', t => { + const result = shell.exec(`${JSON.stringify(shell.config.execPath)} -e "console.error(1234);"`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, ''); + t.is(result.stderr, '1234\n'); +}); -// check process.env works -assert.ok(!shell.env.FOO); -shell.env.FOO = 'Hello world'; -result = shell.exec(process.platform !== 'win32' ? 'echo $FOO' : 'echo %FOO%'); -assert.ok(!shell.error()); -assert.equal(result.code, 0); -assert.equal(result.stdout, 'Hello world' + os.EOL); -assert.equal(result.stderr, ''); - -// set shell option (TODO: add tests for Windows) -if (process.platform !== 'win32') { - result = shell.exec('echo $0'); - assert.ok(!shell.error()); - assert.equal(result.code, 0); - assert.equal(result.stdout, '/bin/sh\n'); // sh by default - var bashPath = shell.which('bash').trim(); - // this option doesn't work on v0.10 - if (bashPath && process.version >= 'v0.11') { - result = shell.exec('echo $0', {shell: '/bin/bash'}); - assert.ok(!shell.error()); - assert.equal(result.code, 0); - assert.equal(result.stdout, '/bin/bash\n'); - } -} +test('check if stdout + stderr go to output', t => { + const result = shell.exec(`${JSON.stringify(shell.config.execPath)} -e "console.error(1234); console.log(666);"`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, '666\n'); + t.is(result.stderr, '1234\n'); +}); -// exec returns a ShellString -result = shell.exec('echo foo'); -assert.ok(typeof result === 'object'); -assert.ok(result instanceof String); -assert.ok(typeof result.stdout === 'string'); -assert.strictEqual(result.toString(), result.stdout); +test('check if stdout + stderr should not be printed to console if silent', t => { + shell.exec(`${JSON.stringify(shell.config.execPath)} -e "console.error(1234); console.log(666); process.exit(12);"`, { silent: true }); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.is(stdout, ''); + t.is(stderr, ''); +}); -// -// async -// +test('check exit code', t => { + const result = shell.exec(`${JSON.stringify(shell.config.execPath)} -e "process.exit(12);"`); + t.truthy(shell.error()); + t.is(result.code, 12); +}); + +test('interaction with cd', t => { + shell.cd('test/resources/external'); + const result = shell.exec(`${JSON.stringify(shell.config.execPath)} node_script.js`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, 'node_script_1234\n'); +}); + +test('check quotes escaping', t => { + const result = shell.exec(util.format(JSON.stringify(shell.config.execPath) + ' -e "console.log(%s);"', "\\\"\\'+\\'_\\'+\\'\\\"")); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, "'+'_'+'\n"); +}); + +test('set cwd', t => { + const cmdString = process.platform === 'win32' ? 'cd' : 'pwd'; + const result = shell.exec(cmdString, { cwd: '..' }); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, path.resolve('..') + os.EOL); +}); + +test('set maxBuffer (very small)', t => { + const result = shell.exec('echo 1234567890'); // default maxBuffer is ok + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, '1234567890' + os.EOL); + const result2 = shell.exec('echo 1234567890', { maxBuffer: 6 }); + t.truthy(shell.error()); + t.is(result2.code, 1); + t.is(result2.stdout, '1234567890' + os.EOL); + const maxBufferErrorPattern = /.*\bmaxBuffer\b.*\bexceeded\b.*/; + t.regex(result2.stderr, maxBufferErrorPattern); +}); + +test('set timeout option', t => { + let result = shell.exec(`${JSON.stringify(shell.config.execPath)} test/resources/exec/slow.js 100`); // default timeout is ok + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, 'fast\nslow\n'); + result = shell.exec(`${JSON.stringify(shell.config.execPath)} test/resources/exec/slow.js 2000`, { timeout: 1000 }); // times out + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stdout, 'fast\n'); +}); + +test('check process.env works', t => { + t.falsy(shell.env.FOO); + shell.env.FOO = 'Hello world'; + const result = shell.exec(process.platform !== 'win32' ? 'echo $FOO' : 'echo %FOO%'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, 'Hello world' + os.EOL); + t.is(result.stderr, ''); +}); -// no callback -var c = shell.exec(JSON.stringify(process.execPath)+' -e \"console.log(1234)\"', {async:true}); -assert.equal(shell.error(), null); -assert.ok('stdout' in c, 'async exec returns child process object'); +test('set shell option (TODO: add tests for Windows)', t => { + utils.skipOnWin(t, () => { + let result = shell.exec('echo $0'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, '/bin/sh\n'); // sh by default + const bashPath = shell.which('bash').trim(); + if (bashPath) { + result = shell.exec('echo $0', { shell: bashPath }); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.stdout, `${bashPath}\n`); + } + }); +}); + +test('exec returns a ShellString', t => { + const result = shell.exec('echo foo'); + t.is(typeof result, 'object'); + t.truthy(result instanceof String); + t.is(typeof result.stdout, 'string'); + t.is(result.toString(), result.stdout); +}); + +test('encoding option works', t => { + const result = shell.exec(`${JSON.stringify(shell.config.execPath)} -e "console.log(1234);"`, { encoding: 'buffer' }); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(Buffer.isBuffer(result.stdout)); + t.truthy(Buffer.isBuffer(result.stderr)); + t.is(result.stdout.toString(), '1234\n'); + t.is(result.stderr.toString(), ''); +}); + +test('options.fatal = false and unknown command', t => { + const oldFatal = shell.config.fatal; + shell.config.fatal = true; + const result = shell.exec('asdfasdf', { fatal: false }); // could not find command + shell.config.fatal = oldFatal; + t.truthy(shell.error()); + t.truthy(result.code); +}); // -// callback as 2nd argument +// async // -shell.exec(JSON.stringify(process.execPath)+' -e \"console.log(5678);\"', function(code, stdout, stderr) { - assert.equal(code, 0); - assert.ok(stdout === '5678\n' || stdout === '5678\nundefined\n'); // 'undefined' for v0.4 - assert.ok(stderr === '' || stderr === 'undefined\n'); // 'undefined' for v0.4 - - // - // callback as 3rd argument - // - shell.exec(JSON.stringify(process.execPath)+' -e \"console.log(5566);\"', {async:true}, function(code, stdout, stderr) { - assert.equal(code, 0); - assert.ok(stdout === '5566\n' || stdout === '5566\nundefined\n'); // 'undefined' for v0.4 - assert.ok(stderr === '' || stderr === 'undefined\n'); // 'undefined' for v0.4 - - // - // callback as 3rd argument (slient:true) - // - shell.exec(JSON.stringify(process.execPath)+' -e \"console.log(5678);\"', {silent:true}, function(code, stdout, stderr) { - assert.equal(code, 0); - assert.ok(stdout === '5678\n' || stdout === '5678\nundefined\n'); // 'undefined' for v0.4 - assert.ok(stderr === '' || stderr === 'undefined\n'); // 'undefined' for v0.4 - - shell.exit(123); +function execAsync(...execArgs) { + return new Promise((resolve) => { + shell.exec(...execArgs, (code, stdout, stderr) => { + resolve({ code, stdout, stderr }); }); - }); +} +test('no callback', t => { + const c = shell.exec(`${JSON.stringify(shell.config.execPath)} -e "console.log(1234)"`, { async: true }); + t.falsy(shell.error()); + t.truthy('stdout' in c, 'async exec returns child process object'); }); -assert.equal(shell.error(), null); +test('callback as 2nd argument', async t => { + const result = await execAsync(`${JSON.stringify(shell.config.execPath)} -e "console.log(5678);"`); + t.is(result.code, 0); + t.is(result.stdout, '5678\n'); + t.is(result.stderr, ''); +}); + +test('callback as end argument', async t => { + const result = await execAsync(`${JSON.stringify(shell.config.execPath)} -e "console.log(5566);"`, { async: true }); + t.is(result.code, 0); + t.is(result.stdout, '5566\n'); + t.is(result.stderr, ''); +}); + +test('callback as 3rd argument (silent:true)', async t => { + const result = await execAsync(`${JSON.stringify(shell.config.execPath)} -e "console.log(5678);"`, { silent: true }); + t.is(result.code, 0); + t.is(result.stdout, '5678\n'); + t.is(result.stderr, ''); +}); + +test('command that fails', async t => { + const result = await execAsync('shx cp onlyOneCpArgument.txt', { silent: true }); + t.is(result.code, 1); + t.is(result.stdout, ''); + t.is(result.stderr, 'cp: missing and/or \n'); +}); + +test('encoding option works with async', async t => { + const result = await execAsync(`${JSON.stringify(shell.config.execPath)} -e "console.log(5566);"`, { async: true, encoding: 'buffer' }); + t.is(result.code, 0); + t.truthy(Buffer.isBuffer(result.stdout)); + t.truthy(Buffer.isBuffer(result.stderr)); + t.is(result.stdout.toString(), '5566\n'); + t.is(result.stderr.toString(), ''); +}); diff --git a/test/exit.js b/test/exit.js new file mode 100644 index 000000000..b0add2552 --- /dev/null +++ b/test/exit.js @@ -0,0 +1,57 @@ +const test = require('ava'); + +const shell = require('..'); + +const mocks = require('./utils/mocks'); + +// +// Valids +// + +function runExitInSubprocess(code) { + const script = code !== undefined + ? `var shell = require("."); shell.exit(${code});` + : 'var shell = require("."); shell.exit();'; + const result = shell.exec( + `${JSON.stringify(shell.config.execPath)} -e ${JSON.stringify(script)}` + ); + const actualReturnCode = result.code; + return actualReturnCode; +} + +test('exit with success status code', t => { + t.is(runExitInSubprocess(0), 0); +}); + +test('exit without explicit code should be success', t => { + t.is(runExitInSubprocess(), 0); +}); + +test('exit with failure status code', t => { + t.is(runExitInSubprocess(5), 5); + t.is(runExitInSubprocess(2), 2); + t.is(runExitInSubprocess(25), 25); +}); + +test('exit correctly sets the shell.errorCode()', t => { + try { + mocks.exit.init(); + shell.exit(5); // Safe to call shell.exit() because it's mocked. + t.is(shell.errorCode(), 5); + t.is(mocks.exit.getValue(), 5); + t.truthy(shell.error()); + + shell.exit(0); // Safe to call shell.exit() because it's mocked. + t.is(shell.errorCode(), 0); + t.falsy(mocks.exit.getValue()); + t.falsy(shell.error()); + + // Also try it without an explicit argument. + shell.exit(); // Safe to call shell.exit() because it's mocked. + t.is(shell.errorCode(), 0); + t.falsy(mocks.exit.getValue()); + t.falsy(shell.error()); + } finally { + mocks.exit.restore(); + } +}); diff --git a/test/find.js b/test/find.js index 4f1408f33..f698028df 100644 --- a/test/find.js +++ b/test/find.js @@ -1,56 +1,76 @@ -var shell = require('..'); +const test = require('ava'); -var assert = require('assert'); +const shell = require('..'); -shell.config.silent = true; +const CWD = process.cwd(); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +test.beforeEach(() => { + shell.config.resetForTesting(); + process.chdir(CWD); +}); // // Invalids // -var result = shell.find(); // no paths given -assert.equal(result.code, 1); -assert.ok(shell.error()); +test('no args', t => { + const result = shell.find(); + t.is(result.code, 1); + t.truthy(shell.error()); +}); // // Valids // -// current path -shell.cd('resources/find'); -result = shell.find('.'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('.hidden') > -1, true); -assert.equal(result.indexOf('dir1/dir11/a_dir11') > -1, true); -assert.equal(result.length, 11); -shell.cd('../..'); - -// simple path -result = shell.find('resources/find'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('resources/find/.hidden') > -1, true); -assert.equal(result.indexOf('resources/find/dir1/dir11/a_dir11') > -1, true); -assert.equal(result.length, 11); - -// multiple paths - comma -result = shell.find('resources/find/dir1', 'resources/find/dir2'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('resources/find/dir1/dir11/a_dir11') > -1, true); -assert.equal(result.indexOf('resources/find/dir2/a_dir1') > -1, true); -assert.equal(result.length, 6); - -// multiple paths - array -result = shell.find(['resources/find/dir1', 'resources/find/dir2']); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('resources/find/dir1/dir11/a_dir11') > -1, true); -assert.equal(result.indexOf('resources/find/dir2/a_dir1') > -1, true); -assert.equal(result.length, 6); - -shell.exit(123); +test('current path', t => { + shell.cd('test/resources/find'); + const result = shell.find('.'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('.hidden')); + t.truthy(result.includes('dir1/dir11/a_dir11')); + t.is(result.length, 12); + shell.cd('../..'); +}); + +test('simple path', t => { + const result = shell.find('test/resources/find'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('test/resources/find/.hidden')); + t.truthy(result.includes('test/resources/find/dir1/dir11/a_dir11')); + t.is(result.length, 12); +}); + +test('multiple paths - comma', t => { + const result = shell.find('test/resources/find/dir1', 'test/resources/find/dir2'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('test/resources/find/dir1/dir11/a_dir11')); + t.truthy(result.includes('test/resources/find/dir2/a_dir1')); + t.is(result.length, 6); +}); + +test('multiple paths - array', t => { + const result = shell.find(['test/resources/find/dir1', 'test/resources/find/dir2']); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('test/resources/find/dir1/dir11/a_dir11')); + t.truthy(result.includes('test/resources/find/dir2/a_dir1')); + t.is(result.length, 6); +}); + +test('nonexistent path', t => { + const result = shell.find('test/resources/find/nonexistent'); + t.is(shell.error(), 'find: no such file or directory: test/resources/find/nonexistent'); + t.is(result.code, 1); +}); + +test('-L flag, folder is symlinked', t => { + const result = shell.find('-L', 'test/resources/find'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('test/resources/find/dir2_link/a_dir1')); + t.is(result.length, 13); +}); diff --git a/test/global.js b/test/global.js index e5db65417..81227f00a 100644 --- a/test/global.js +++ b/test/global.js @@ -1,41 +1,49 @@ -/* globals cat, config, cp, env, error, exit, mkdir, rm */ +/* globals cat, config, cp, env, error, mkdir, rm */ +const fs = require('fs'); + +const test = require('ava'); require('../global'); +const utils = require('./utils/utils'); -var assert = require('assert'); -var fs = require('fs'); +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + config.resetForTesting(); + mkdir(t.context.tmp); +}); -config.silent = true; +test.afterEach.always(t => { + rm('-rf', t.context.tmp); +}); -rm('-rf', 'tmp'); -mkdir('tmp'); // // Valids // -assert.equal(process.env, env); - -// cat -var result = cat('resources/cat/file1'); -assert.equal(error(), null); -assert.equal(result.code, 0); -assert.equal(result, 'test1\n'); - -// rm -cp('-f', 'resources/file1', 'tmp/file1'); -assert.equal(fs.existsSync('tmp/file1'), true); -result = rm('tmp/file1'); -assert.equal(error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/file1'), false); - -// String.prototype is modified for global require -'foo'.to('tmp/testfile.txt'); -assert.equal('foo', cat('tmp/testfile.txt')); -'bar'.toEnd('tmp/testfile.txt'); -assert.equal('foobar', cat('tmp/testfile.txt')); - -exit(123); - - +test('env is exported', t => { + t.is(process.env, env); +}); + +test('cat', t => { + const result = cat('test/resources/cat/file1'); + t.falsy(error()); + t.is(result.code, 0); + t.is(result.toString(), 'test1\n'); +}); + +test('rm', t => { + cp('-f', 'test/resources/file1', `${t.context.tmp}/file1`); + t.truthy(fs.existsSync(`${t.context.tmp}/file1`)); + const result = rm(`${t.context.tmp}/file1`); + t.falsy(error()); + t.is(result.code, 0); + t.falsy(fs.existsSync(`${t.context.tmp}/file1`)); +}); + +test('String.prototype is modified for global require', t => { + 'foo'.to(`${t.context.tmp}/testfile.txt`); + t.is('foo', cat(`${t.context.tmp}/testfile.txt`).toString()); + 'bar'.toEnd(`${t.context.tmp}/testfile.txt`); + t.is('foobar', cat(`${t.context.tmp}/testfile.txt`).toString()); +}); diff --git a/test/grep.js b/test/grep.js index 70249cee8..4b64d6e76 100644 --- a/test/grep.js +++ b/test/grep.js @@ -1,109 +1,416 @@ -var shell = require('..'); +const fs = require('fs'); -var assert = require('assert'), - fs = require('fs'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const utils = require('./utils/utils'); + +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.cp('-r', 'test/resources', t.context.tmp); +}); + +test.afterEach.always(t => { + shell.rm('-rf', t.context.tmp); +}); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); // // Invalids // -var result; +test('no args', t => { + const result = shell.grep(); + t.truthy(shell.error()); + t.is(result.code, 2); + t.is(shell.errorCode(), 2); +}); + +test('too few args', t => { + const result = shell.grep(/asdf/g); // too few args + t.truthy(shell.error()); + t.is(result.code, 2); +}); + +test('no such file', t => { + t.falsy(fs.existsSync('/asdfasdf')); // sanity check + const result = shell.grep(/asdf/g, '/asdfasdf'); // no such file + t.truthy(shell.error()); + t.is(result.stderr, 'grep: no such file or directory: /asdfasdf'); + t.is(result.code, 2); +}); + +test('if at least one file is missing, this should be an error', t => { + t.falsy(fs.existsSync('asdfasdf')); // sanity check + t.truthy(fs.existsSync(`${t.context.tmp}/file1`)); // sanity check + const result = shell.grep(/asdf/g, `${t.context.tmp}/file1`, 'asdfasdf'); + t.truthy(shell.error()); + t.is(result.stderr, 'grep: no such file or directory: asdfasdf'); + t.is(result.code, 2); +}); -result = shell.grep(); -assert.ok(shell.error()); -assert.equal(result.code, 2); +test("multiple files, one doesn't exist, one doesn't match", t => { + const result = shell.grep(/oogabooga/, 'test/resources/file1.txt', + 'test/resources/filedoesnotexist.txt'); + t.truthy(shell.error()); + t.is(result.code, 2); +}); -result = shell.grep(/asdf/g); // too few args -assert.ok(shell.error()); -assert.equal(result.code, 2); +test('-A option, negative value', t => { + const result = shell.grep('-A', -2, 'test*', 'test/resources/grep/file3'); + t.truthy(shell.error()); + t.is(result.code, 2); + t.is(result.stderr, 'grep: -2: invalid context length argument'); +}); -assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check -result = shell.grep(/asdf/g, '/asdfasdf'); // no such file -assert.ok(shell.error()); -assert.equal(result.stderr, 'grep: no such file or directory: /asdfasdf'); -assert.equal(result.code, 2); +test('-B option, negative value', t => { + const result = shell.grep('-B', -3, 'test*', 'test/resources/grep/file3'); + t.truthy(shell.error()); + t.is(result.code, 2); + t.is(result.stderr, 'grep: -3: invalid context length argument'); +}); -// if at least one file is missing, this should be an error -shell.cp('-f', 'resources/file1', 'tmp/file1'); -assert.equal(fs.existsSync('asdfasdf'), false); // sanity check -assert.equal(fs.existsSync('tmp/file1'), true); // sanity check -result = shell.grep(/asdf/g, 'tmp/file1', 'asdfasdf'); -assert.ok(shell.error()); -assert.equal(result.stderr, 'grep: no such file or directory: asdfasdf'); -assert.equal(result.code, 2); +test('-C option, negative value', t => { + const result = shell.grep('-C', -1, 'test*', 'test/resources/grep/file3'); + t.truthy(shell.error()); + t.is(result.code, 2); + t.is(result.stderr, 'grep: -1: invalid context length argument'); +}); // // Valids // -result = shell.grep('line', 'resources/a.txt'); -assert.equal(shell.error(), null); -assert.equal(result.split('\n').length - 1, 4); - -result = shell.grep('-v', 'line', 'resources/a.txt'); -assert.equal(shell.error(), null); -assert.equal(result.split('\n').length - 1, 8); - -result = shell.grep('line one', 'resources/a.txt'); -assert.equal(shell.error(), null); -assert.equal(result, 'This is line one\n'); - -// multiple files -result = shell.grep(/test/, 'resources/file1.txt', 'resources/file2.txt'); -assert.equal(shell.error(), null); -assert.equal(result, 'test1\ntest2\n'); - -// multiple files, array syntax -result = shell.grep(/test/, ['resources/file1.txt', 'resources/file2.txt']); -assert.equal(shell.error(), null); -assert.equal(result, 'test1\ntest2\n'); - -// multiple files, glob syntax, * for file name -result = shell.grep(/test/, 'resources/file*.txt'); -assert.equal(shell.error(), null); -assert.ok(result == 'test1\ntest2\n' || result == 'test2\ntest1\n'); - -// multiple files, glob syntax, * for directory name -result = shell.grep(/test/, '*/file*.txt'); -assert.equal(shell.error(), null); -assert.ok(result == 'test1\ntest2\n' || result == 'test2\ntest1\n'); - -// multiple files, glob syntax, ** for directory name -result = shell.grep(/test/, '**/file*.js'); -assert.equal(shell.error(), null); -assert.equal(result, 'test\ntest\ntest\ntest\n'); - -// one file, * in regex -result = shell.grep(/alpha*beta/, 'resources/grep/file'); -assert.equal(shell.error(), null); -assert.equal(result, 'alphaaaaaaabeta\nalphbeta\n'); - -// one file, * in string-regex -result = shell.grep('alpha*beta', 'resources/grep/file'); -assert.equal(shell.error(), null); -assert.equal(result, 'alphaaaaaaabeta\nalphbeta\n'); - -// one file, * in regex, make sure * is not globbed -result = shell.grep(/l*\.js/, 'resources/grep/file'); -assert.equal(shell.error(), null); -assert.equal(result, 'this line ends in.js\nlllllllllllllllll.js\n'); - -// one file, * in string-regex, make sure * is not globbed -result = shell.grep('l*\\.js', 'resources/grep/file'); -assert.equal(shell.error(), null); -assert.equal(result, 'this line ends in.js\nlllllllllllllllll.js\n'); - -// -l option -result = shell.grep('-l', 'test1', 'resources/file1', 'resources/file2', 'resources/file1.txt'); -assert.equal(shell.error(), null); -assert.ok(result.match(/file1(\n|$)/)); -assert.ok(result.match(/file1.txt/)); -assert.ok(!result.match(/file2.txt/)); -assert.equal(result.split('\n').length - 1, 2); - -shell.exit(123); +test('basic', t => { + const result = shell.grep('line', 'test/resources/a.txt'); + t.falsy(shell.error()); + t.is(result.split('\n').length - 1, 4); +}); + +test('-v option', t => { + const result = shell.grep('-v', 'line', 'test/resources/a.txt'); + t.falsy(shell.error()); + t.is(result.split('\n').length - 1, 8); +}); + +test('matches one line', t => { + const result = shell.grep('line one', 'test/resources/a.txt'); + t.falsy(shell.error()); + t.is(result.toString(), 'This is line one\n'); +}); + +test('multiple files', t => { + const result = shell.grep(/test/, 'test/resources/file1.txt', + 'test/resources/file2.txt'); + t.falsy(shell.error()); + t.is(result.toString(), 'test1\ntest2\n'); +}); + +test('multiple files, array syntax', t => { + const result = shell.grep(/test/, ['test/resources/file1.txt', + 'test/resources/file2.txt']); + t.falsy(shell.error()); + t.is(result.toString(), 'test1\ntest2\n'); +}); + +test('multiple files, glob syntax, * for file name', t => { + const result = shell.grep(/test/, 'test/resources/file*.txt'); + t.falsy(shell.error()); + t.truthy(result.toString(), 'test1\ntest2\n'); +}); + +test('multiple files, glob syntax, * for directory name', t => { + const result = shell.grep(/test/, 'test/r*/file*.txt'); + t.falsy(shell.error()); + t.is(result.toString(), 'test1\ntest2\n'); +}); + +test('multiple files, double-star glob', t => { + const result = shell.grep(/test/, 'test/resources/**/file*.js'); + t.falsy(shell.error()); + t.is(result.toString(), 'test\ntest\ntest\ntest\n'); +}); + +test('one file, * in regex', t => { + const result = shell.grep(/alpha*beta/, 'test/resources/grep/file'); + t.falsy(shell.error()); + t.is(result.toString(), 'alphaaaaaaabeta\nalphbeta\n'); +}); + +test('one file, * in string-regex', t => { + const result = shell.grep('alpha*beta', 'test/resources/grep/file'); + t.falsy(shell.error()); + t.is(result.toString(), 'alphaaaaaaabeta\nalphbeta\n'); +}); + +test('one file, * in regex, make sure * is not globbed', t => { + const result = shell.grep(/l*\.js/, 'test/resources/grep/file'); + t.falsy(shell.error()); + t.is(result.toString(), 'this line ends in.js\nlllllllllllllllll.js\n'); +}); + +test('one file, * in string-regex, make sure * is not globbed', t => { + const result = shell.grep('l*\\.js', 'test/resources/grep/file'); + t.falsy(shell.error()); + t.is(result.toString(), 'this line ends in.js\nlllllllllllllllll.js\n'); +}); + +test("one file, pattern doesn't match", t => { + const result = shell.grep('notfoundstring', 'test/resources/grep/file'); + t.truthy(shell.error()); + t.is(result.toString(), ''); + t.is(result.stdout, ''); + // TODO(#900): "grep: " isn't really the correct stderr output, but we need a + // non-empty string so `shell.error()` is truthy. + t.is(result.stderr, 'grep: '); + t.is(result.code, 1); +}); + +test('-l option', t => { + const result = shell.grep('-l', 'test1', 'test/resources/file1', 'test/resources/file2', + 'test/resources/file1.txt'); + t.falsy(shell.error()); + t.truthy(result.match(/file1(\n|$)/)); + t.truthy(result.match(/file1.txt/)); + t.falsy(result.match(/file2.txt/)); + t.is(result.split('\n').length - 1, 2); +}); + +test('-i option', t => { + const result = shell.grep('-i', 'test', 'test/resources/grep/case1', 'test/resources/grep/case1.txt', + 'test/resources/grep/case1.js'); + t.falsy(shell.error()); + t.is(result.split('\n').length - 1, 3); +}); + +test('-n option', t => { + const result = shell.grep('-n', /alpha*beta/, 'test/resources/grep/file'); + t.falsy(shell.error()); + t.is(result.toString(), '1:alphaaaaaaabeta\n3:alphbeta\n'); +}); + +test('the pattern looks like an option', t => { + const result = shell.grep('--', '-v', 'test/resources/grep/file2'); + t.falsy(shell.error()); + t.is(result.toString(), '-v\n-vv\n'); +}); + +// +// Before & after contexts +// +test('-B option', t => { + const result = shell.grep('-B', 3, 'test*', 'test/resources/grep/file3'); + t.falsy(shell.error()); + t.is( + result.toString(), + 'line1\n' + + 'line2 test line\n' + + 'line3 test line\n' + + '--\n' + + 'line7\n' + + 'line8\n' + + 'line9\n' + + 'line10 test line\n' + + '--\n' + + 'line12\n' + + 'line13\n' + + 'line14\n' + + 'line15 test line\n', + ); +}); + +test('-B option, -n option', t => { + const result = shell.grep('-nB', 3, 'test*', 'test/resources/grep/file3'); + t.falsy(shell.error()); + t.is( + result.toString(), + '1-line1\n' + + '2:line2 test line\n' + + '3:line3 test line\n' + + '--\n' + + '7-line7\n' + + '8-line8\n' + + '9-line9\n' + + '10:line10 test line\n' + + '--\n' + + '12-line12\n' + + '13-line13\n' + + '14-line14\n' + + '15:line15 test line\n', + ); +}); + +test('-A option', t => { + const result = shell.grep('-A', 2, 'test*', 'test/resources/grep/file3'); + t.falsy(shell.error()); + t.is( + result.toString(), + 'line2 test line\n' + + 'line3 test line\n' + + 'line4\n' + + 'line5\n' + + '--\n' + + 'line10 test line\n' + + 'line11\n' + + 'line12\n' + + '--\n' + + 'line15 test line\n', + ); +}); + +test('-A option, -B option', t => { + const result = shell.grep( + { '-A': 2, '-B': 3 }, + 'test*', + 'test/resources/grep/file3', + ); + t.falsy(shell.error()); + t.is( + result.toString(), + 'line1\n' + + 'line2 test line\n' + + 'line3 test line\n' + + 'line4\n' + + 'line5\n' + + '--\n' + + 'line7\n' + + 'line8\n' + + 'line9\n' + + 'line10 test line\n' + + 'line11\n' + + 'line12\n' + + 'line13\n' + + 'line14\n' + + 'line15 test line\n', + ); +}); + +test('-A option, -B option, -n option', t => { + const result = shell.grep( + { '-n': true, '-A': 2, '-B': 3 }, + 'test*', + 'test/resources/grep/file3', + ); + t.falsy(shell.error()); + t.is( + result.toString(), + '1-line1\n' + + '2:line2 test line\n' + + '3:line3 test line\n' + + '4-line4\n' + + '5-line5\n' + + '--\n' + + '7-line7\n' + + '8-line8\n' + + '9-line9\n' + + '10:line10 test line\n' + + '11-line11\n' + + '12-line12\n' + + '13-line13\n' + + '14-line14\n' + + '15:line15 test line\n', + ); +}); + +test('-C option', t => { + const result = shell.grep('-C', 3, 'test*', 'test/resources/grep/file3'); + t.falsy(shell.error()); + t.is( + result.toString(), + 'line1\n' + + 'line2 test line\n' + + 'line3 test line\n' + + 'line4\n' + + 'line5\n' + + 'line6\n' + + 'line7\n' + + 'line8\n' + + 'line9\n' + + 'line10 test line\n' + + 'line11\n' + + 'line12\n' + + 'line13\n' + + 'line14\n' + + 'line15 test line\n', + ); +}); + +test('-C option, small value', t => { + const result = shell.grep('-C', 1, 'test*', 'test/resources/grep/file3'); + t.falsy(shell.error()); + t.is( + result.toString(), + 'line1\n' + + 'line2 test line\n' + + 'line3 test line\n' + + 'line4\n' + + '--\n' + + 'line9\n' + + 'line10 test line\n' + + 'line11\n' + + '--\n' + + 'line14\n' + + 'line15 test line\n', + ); +}); + +test('-C option, large value', t => { + const result = shell.grep('-C', 100, 'test*', 'test/resources/grep/file3'); + t.falsy(shell.error()); + t.is( + result.toString(), + 'line1\n' + + 'line2 test line\n' + + 'line3 test line\n' + + 'line4\n' + + 'line5\n' + + 'line6\n' + + 'line7\n' + + 'line8\n' + + 'line9\n' + + 'line10 test line\n' + + 'line11\n' + + 'line12\n' + + 'line13\n' + + 'line14\n' + + 'line15 test line\n', + ); +}); + +test('-C option, add line separators', t => { + const result = shell.grep('-C', 0, 'test*', 'test/resources/grep/file3'); + t.falsy(shell.error()); + t.is( + result.toString(), + 'line2 test line\n' + + 'line3 test line\n' + + '--\n' + + 'line10 test line\n' + + '--\n' + + 'line15 test line\n', + ); +}); + +test('-C option, -n option', t => { + const result = shell.grep('-nC', 3, 'test*', 'test/resources/grep/file3'); + t.falsy(shell.error()); + t.is( + result.toString(), + '1-line1\n' + + '2:line2 test line\n' + + '3:line3 test line\n' + + '4-line4\n' + + '5-line5\n' + + '6-line6\n' + + '7-line7\n' + + '8-line8\n' + + '9-line9\n' + + '10:line10 test line\n' + + '11-line11\n' + + '12-line12\n' + + '13-line13\n' + + '14-line14\n' + + '15:line15 test line\n', + ); +}); diff --git a/test/head.js b/test/head.js index 1465ecf50..8f5f91574 100644 --- a/test/head.js +++ b/test/head.js @@ -1,103 +1,137 @@ -var shell = require('..'); +const fs = require('fs'); -var assert = require('assert'); -var fs = require('fs'); +const test = require('ava'); -shell.config.silent = true; - -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +const shell = require('..'); +const common = require('../src/common'); -var result; +shell.config.silent = true; // // Invalids // -result = shell.head(); -assert.ok(shell.error()); -assert.equal(result.code, 1); - -assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check -result = shell.head('/adsfasdf'); // file does not exist -assert.ok(shell.error()); -assert.equal(result.code, 1); +test('no args', t => { + const result = shell.head(); + t.truthy(shell.error()); + t.is(result.code, 1); +}); + +test('file does not exist', t => { + t.falsy(fs.existsSync('/asdfasdf')); // sanity check + const result = shell.head('/asdfasdf'); // file does not exist + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'head: no such file or directory: /asdfasdf'); +}); + +test('directory', t => { + t.truthy(common.statFollowLinks('test/resources/').isDirectory()); // sanity check + const result = shell.head('test/resources/'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, "head: error reading 'test/resources/': Is a directory"); +}); // // Valids // -var topOfFile1 = ['file1 1', 'file1 2', 'file1 3', 'file1 4', 'file1 5', - 'file1 6', 'file1 7', 'file1 8', 'file1 9', 'file1 10', - 'file1 11', 'file1 12', 'file1 13', 'file1 14', 'file1 15', - 'file1 16', 'file1 17', 'file1 18', 'file1 19', 'file1 20']; -var topOfFile2 = ['file2 1', 'file2 2', 'file2 3', 'file2 4', 'file2 5', - 'file2 6', 'file2 7', 'file2 8', 'file2 9', 'file2 10', - 'file2 11', 'file2 12', 'file2 13', 'file2 14', 'file2 15', - 'file2 16', 'file2 17', 'file2 18', 'file2 19', 'file2 20']; - -// simple -result = shell.head('resources/head/file1.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, topOfFile1.slice(0, 10).join('\n')+'\n'); - -// multiple files -result = shell.head('resources/head/file2.txt', 'resources/head/file1.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, topOfFile2.slice(0, 10).concat( - topOfFile1.slice(0, 10) - ).join('\n')+'\n'); - -// multiple files, array syntax -result = shell.head(['resources/head/file2.txt', 'resources/head/file1.txt']); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, topOfFile2.slice(0, 10).concat( - topOfFile1.slice(0, 10) - ).join('\n')+'\n'); - -// reading more lines than are in the file (no trailing newline) -result = shell.head('resources/file2', 'resources/file1'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, 'test2\ntest1'); // these files only have one line (no \n) - -// reading more lines than are in the file (with trailing newline) -result = shell.head('resources/head/shortfile2', 'resources/head/shortfile1'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, 'short2\nshort1\n'); // these files only have one line (with \n) - -// Globbed file -result = shell.head('resources/head/file?.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, topOfFile1.slice(0, 10).concat( - topOfFile2.slice(0, 10) - ).join('\n')+'\n'); - -// With `'-n' ` option -result = shell.head('-n', 4, 'resources/head/file2.txt', 'resources/head/file1.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, topOfFile2.slice(0, 4).concat( - topOfFile1.slice(0, 4) - ).join('\n')+'\n'); - -// With `{'-n': }` option -result = shell.head({'-n': 4}, 'resources/head/file2.txt', 'resources/head/file1.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, topOfFile2.slice(0, 4).concat( - topOfFile1.slice(0, 4) - ).join('\n')+'\n'); - -// negative values (-num) are the same as (numLines - num) -result = shell.head('-n', -46, 'resources/head/file1.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, 'file1 1\nfile1 2\nfile1 3\nfile1 4\n'); - -shell.exit(123); +const topOfFile1 = ['file1 1', 'file1 2', 'file1 3', 'file1 4', 'file1 5', + 'file1 6', 'file1 7', 'file1 8', 'file1 9', 'file1 10', + 'file1 11', 'file1 12', 'file1 13', 'file1 14', 'file1 15', + 'file1 16', 'file1 17', 'file1 18', 'file1 19', 'file1 20']; +const topOfFile2 = ['file2 1', 'file2 2', 'file2 3', 'file2 4', 'file2 5', + 'file2 6', 'file2 7', 'file2 8', 'file2 9', 'file2 10', + 'file2 11', 'file2 12', 'file2 13', 'file2 14', 'file2 15', + 'file2 16', 'file2 17', 'file2 18', 'file2 19', 'file2 20']; + +test('simple', t => { + const result = shell.head('test/resources/head/file1.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), topOfFile1.slice(0, 10).join('\n') + '\n'); +}); + +test('multiple files', t => { + const result = shell.head('test/resources/head/file2.txt', + 'test/resources/head/file1.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), topOfFile2 + .slice(0, 10) + .concat(topOfFile1.slice(0, 10)) + .join('\n') + '\n'); +}); + +test('multiple files, array syntax', t => { + const result = shell.head(['test/resources/head/file2.txt', + 'test/resources/head/file1.txt']); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), topOfFile2 + .slice(0, 10) + .concat(topOfFile1.slice(0, 10)) + .join('\n') + '\n'); +}); + +test('reading more lines than are in the file (no trailing newline)', t => { + const result = shell.head('test/resources/file2', 'test/resources/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'test2\ntest1'); // these files only have one line (no \n) +}); + +test('reading more lines than are in the file (with trailing newline)', t => { + const result = shell.head('test/resources/head/shortfile2', + 'test/resources/head/shortfile1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'short2\nshort1\n'); // these files only have one line (with \n) +}); + +test('Globbed file', t => { + const result = shell.head('test/resources/head/file?.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), topOfFile1 + .slice(0, 10) + .concat(topOfFile2.slice(0, 10)) + .join('\n') + '\n'); +}); + +test("With '-n ' option", t => { + const result = shell.head('-n', 4, 'test/resources/head/file2.txt', + 'test/resources/head/file1.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), topOfFile2 + .slice(0, 4) + .concat(topOfFile1.slice(0, 4)) + .join('\n') + '\n'); +}); + +test("With '{-n: }' option", t => { + const result = shell.head({ '-n': 4 }, 'test/resources/head/file2.txt', + 'test/resources/head/file1.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), topOfFile2 + .slice(0, 4) + .concat(topOfFile1.slice(0, 4)) + .join('\n') + '\n'); +}); + +test('negative values (-num) are the same as (numLines - num)', t => { + const result = shell.head('-n', -46, 'test/resources/head/file1.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'file1 1\nfile1 2\nfile1 3\nfile1 4\n'); +}); + +test('right-hand side of a pipe', t => { + const result = shell.cat('test/resources/head/file1.txt').head(); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), topOfFile1.slice(0, 10).join('\n') + '\n'); +}); diff --git a/test/ln.js b/test/ln.js index 8a8e0e6c0..1ef01eef4 100644 --- a/test/ln.js +++ b/test/ln.js @@ -1,197 +1,190 @@ -var shell = require('..'); -var common = require('../src/common'); -var isWindows = common.platform === 'win'; +const fs = require('fs'); +const path = require('path'); -var assert = require('assert'), - fs = require('fs'), - path = require('path'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const utils = require('./utils/utils'); -// On Windows, symlinks for files need admin permissions. This helper -// skips certain tests if we are on Windows and got an EPERM error -function skipOnWinForEPERM (action, test) { - action(); - var error = shell.error(); +const CWD = process.cwd(); - if (isWindows && error && /EPERM:/.test(error)) { - console.log("Got EPERM when testing symlinks on Windows. Assuming non-admin environment and skipping test."); - } else { - test(); - } -} +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.cp('-r', 'test/resources', t.context.tmp); + process.chdir(CWD); +}); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +test.afterEach.always(t => { + shell.rm('-rf', t.context.tmp); +}); -// Prepare tmp/ -shell.cp('resources/*', 'tmp'); // // Invalids // -var result = shell.ln(); -assert.ok(shell.error()); -assert.equal(result.code, 1); - -result = shell.ln('file'); -assert.ok(shell.error()); -assert.equal(result.code, 1); +test('no args', t => { + const result = shell.ln(); + t.truthy(shell.error()); + t.is(result.code, 1); +}); -result = shell.ln('-f'); -assert.ok(shell.error()); -assert.equal(result.code, 1); +test('too few args', t => { + const result = shell.ln('file'); + t.truthy(shell.error()); + t.is(result.code, 1); +}); -result = shell.ln('tmp/file1', 'tmp/file2'); -assert.ok(shell.error()); -assert.equal(result.code, 1); +test('only an option', t => { + const result = shell.ln('-f'); + t.truthy(shell.error()); + t.is(result.code, 1); +}); -result = shell.ln('tmp/noexist', 'tmp/linkfile1'); -assert.ok(shell.error()); -assert.equal(result.code, 1); +test('destination already exists', t => { + const result = shell.ln(`${t.context.tmp}/file1`, `${t.context.tmp}/file2`); + t.truthy(shell.error()); + t.is(result.code, 1); +}); -result = shell.ln('-sf', 'no/exist', 'tmp/badlink'); -assert.ok(shell.error()); -assert.equal(result.code, 1); +test('non-existent source', t => { + const result = shell.ln(`${t.context.tmp}/noexist`, `${t.context.tmp}/linkfile1`); + t.truthy(shell.error()); + t.is(result.code, 1); +}); -result = shell.ln('-sf', 'noexist', 'tmp/badlink'); -assert.ok(shell.error()); -assert.equal(result.code, 1); +test('non-existent source (-sf)', t => { + const result = shell.ln('-sf', 'no/exist', `${t.context.tmp}/badlink`); + t.truthy(shell.error()); + t.is(result.code, 1); +}); -result = shell.ln('-f', 'noexist', 'tmp/badlink'); -assert.ok(shell.error()); -assert.equal(result.code, 1); +test('non-existent source (-f)', t => { + const result = shell.ln('-f', 'noexist', `${t.context.tmp}/badlink`); + t.truthy(shell.error()); + t.is(result.code, 1); +}); // // Valids // -result = shell.ln('tmp/file1', 'tmp/linkfile1'); -assert(fs.existsSync('tmp/linkfile1')); -assert.equal( - fs.readFileSync('tmp/file1').toString(), - fs.readFileSync('tmp/linkfile1').toString() -); -fs.writeFileSync('tmp/file1', 'new content 1'); -assert.equal( - fs.readFileSync('tmp/linkfile1').toString(), - 'new content 1' -); -assert.equal(result.code, 0); - -// With glob -shell.rm('tmp/linkfile1'); -result = shell.ln('tmp/fi*1', 'tmp/linkfile1'); -assert(fs.existsSync('tmp/linkfile1')); -assert.equal( - fs.readFileSync('tmp/file1').toString(), - fs.readFileSync('tmp/linkfile1').toString() -); -fs.writeFileSync('tmp/file1', 'new content 1'); -assert.equal( - fs.readFileSync('tmp/linkfile1').toString(), - 'new content 1' -); -assert.equal(result.code, 0); - -skipOnWinForEPERM(shell.ln.bind(shell, '-s', 'file2', 'tmp/linkfile2'), function () { - assert(fs.existsSync('tmp/linkfile2')); - assert.equal( - fs.readFileSync('tmp/file2').toString(), - fs.readFileSync('tmp/linkfile2').toString() - ); - fs.writeFileSync('tmp/file2', 'new content 2'); - assert.equal( - fs.readFileSync('tmp/linkfile2').toString(), - 'new content 2' - ); +test('basic usage', t => { + const result = shell.ln(`${t.context.tmp}/file1`, `${t.context.tmp}/linkfile1`); + t.truthy(fs.existsSync(`${t.context.tmp}/linkfile1`)); + t.is( + fs.readFileSync(`${t.context.tmp}/file1`).toString(), + fs.readFileSync(`${t.context.tmp}/linkfile1`).toString() + ); + fs.writeFileSync(`${t.context.tmp}/file1`, 'new content 1'); + t.is(fs.readFileSync(`${t.context.tmp}/linkfile1`).toString(), 'new content 1'); + t.is(result.code, 0); }); -// Symbolic link directory test -shell.mkdir('tmp/ln'); -shell.touch('tmp/ln/hello'); -result = shell.ln('-s', 'ln', 'tmp/dir1'); -assert(fs.existsSync('tmp/ln/hello')); -assert(fs.existsSync('tmp/dir1/hello')); -assert.equal(result.code, 0); - -// To current directory -shell.cd('tmp'); -result = shell.ln('-s', './', 'dest'); -assert.equal(result.code, 0); -shell.touch('testfile.txt'); -assert(fs.existsSync('testfile.txt')); -assert(fs.existsSync('dest/testfile.txt')); -shell.rm('-f', 'dest'); -shell.mkdir('dir1'); -shell.cd('dir1'); -result = shell.ln('-s', './', '../dest'); -assert.equal(result.code, 0); -shell.touch('insideDir.txt'); -shell.cd('..'); -assert(fs.existsSync('testfile.txt')); -assert(fs.existsSync('dest/testfile.txt')); -assert(fs.existsSync('dir1/insideDir.txt')); -assert(!fs.existsSync('dest/insideDir.txt')); -shell.cd('..'); - -result = shell.ln('-f', 'tmp/file1.js', 'tmp/file2.js'); -assert.equal(result.code, 0); -assert(fs.existsSync('tmp/file2.js')); -assert.equal( - fs.readFileSync('tmp/file1.js').toString(), - fs.readFileSync('tmp/file2.js').toString() -); -fs.writeFileSync('tmp/file1.js', 'new content js'); -assert.equal( - fs.readFileSync('tmp/file2.js').toString(), - 'new content js' -); - -skipOnWinForEPERM(shell.ln.bind(shell, '-sf', 'file1.txt', 'tmp/file2.txt'), function () { - assert(fs.existsSync('tmp/file2.txt')); - assert.equal( - fs.readFileSync('tmp/file1.txt').toString(), - fs.readFileSync('tmp/file2.txt').toString() - ); - fs.writeFileSync('tmp/file1.txt', 'new content txt'); - assert.equal( - fs.readFileSync('tmp/file2.txt').toString(), - 'new content txt' +test('With glob', t => { + shell.rm(`${t.context.tmp}/linkfile1`); + const result = shell.ln(`${t.context.tmp}/fi*1`, `${t.context.tmp}/linkfile1`); + t.truthy(fs.existsSync(`${t.context.tmp}/linkfile1`)); + t.is( + fs.readFileSync(`${t.context.tmp}/file1`).toString(), + fs.readFileSync(`${t.context.tmp}/linkfile1`).toString() + ); + fs.writeFileSync(`${t.context.tmp}/file1`, 'new content 1'); + t.is(fs.readFileSync(`${t.context.tmp}/linkfile1`).toString(), 'new content 1'); + t.is(result.code, 0); +}); + +test('-s option', t => { + utils.skipOnWinForEPERM(shell.ln.bind(shell, '-s', 'file2', `${t.context.tmp}/linkfile2`), () => { + t.truthy(fs.existsSync(`${t.context.tmp}/linkfile2`)); + t.is( + fs.readFileSync(`${t.context.tmp}/file2`).toString(), + fs.readFileSync(`${t.context.tmp}/linkfile2`).toString() ); + fs.writeFileSync(`${t.context.tmp}/file2`, 'new content 2'); + t.is(fs.readFileSync(`${t.context.tmp}/linkfile2`).toString(), 'new content 2'); + }); +}); + +test('Symbolic link directory test', t => { + shell.mkdir(`${t.context.tmp}/ln`); + shell.touch(`${t.context.tmp}/ln/hello`); + const result = shell.ln('-s', 'ln', `${t.context.tmp}/dir1`); + t.truthy(fs.existsSync(`${t.context.tmp}/ln/hello`)); + t.truthy(fs.existsSync(`${t.context.tmp}/dir1/hello`)); + t.is(result.code, 0); +}); + +test('To current directory', t => { + shell.cd(t.context.tmp); + let result = shell.ln('-s', './', 'dest'); + t.is(result.code, 0); + shell.touch('testfile.txt'); + t.truthy(fs.existsSync('testfile.txt')); + t.truthy(fs.existsSync('dest/testfile.txt')); + shell.rm('-f', 'dest'); + shell.mkdir('dir1'); + shell.cd('dir1'); + result = shell.ln('-s', './', '../dest'); + t.is(result.code, 0); + shell.touch('insideDir.txt'); + shell.cd('..'); + t.truthy(fs.existsSync('testfile.txt')); + t.truthy(fs.existsSync('dest/testfile.txt')); + t.truthy(fs.existsSync('dir1/insideDir.txt')); + t.falsy(fs.existsSync('dest/insideDir.txt')); + shell.cd('..'); }); -// Abspath regression -skipOnWinForEPERM(shell.ln.bind(shell, '-sf', 'file1', path.resolve('tmp/abspath')), function () { - assert(fs.existsSync('tmp/abspath')); - assert.equal( - fs.readFileSync('tmp/file1').toString(), - fs.readFileSync('tmp/abspath').toString() +test('-f option', t => { + const result = shell.ln('-f', `${t.context.tmp}/file1.js`, `${t.context.tmp}/file2.js`); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/file2.js`)); + t.is( + fs.readFileSync(`${t.context.tmp}/file1.js`).toString(), + fs.readFileSync(`${t.context.tmp}/file2.js`).toString() + ); + fs.writeFileSync(`${t.context.tmp}/file1.js`, 'new content js'); + t.is(fs.readFileSync(`${t.context.tmp}/file2.js`).toString(), 'new content js'); +}); + +test('-sf option', t => { + utils.skipOnWinForEPERM(shell.ln.bind(shell, '-sf', 'file1.txt', `${t.context.tmp}/file2.txt`), () => { + t.truthy(fs.existsSync(`${t.context.tmp}/file2.txt`)); + t.is( + fs.readFileSync(`${t.context.tmp}/file1.txt`).toString(), + fs.readFileSync(`${t.context.tmp}/file2.txt`).toString() ); - fs.writeFileSync('tmp/file1', 'new content 3'); - assert.equal( - fs.readFileSync('tmp/abspath').toString(), - 'new content 3' + fs.writeFileSync(`${t.context.tmp}/file1.txt`, 'new content txt'); + t.is(fs.readFileSync(`${t.context.tmp}/file2.txt`).toString(), 'new content txt'); + }); +}); + +test('Abspath regression', t => { + utils.skipOnWinForEPERM(shell.ln.bind(shell, '-sf', 'file1', path.resolve(`${t.context.tmp}/abspath`)), () => { + t.truthy(fs.existsSync(`${t.context.tmp}/abspath`)); + t.is( + fs.readFileSync(`${t.context.tmp}/file1`).toString(), + fs.readFileSync(`${t.context.tmp}/abspath`).toString() ); + fs.writeFileSync(`${t.context.tmp}/file1`, 'new content 3'); + t.is(fs.readFileSync(`${t.context.tmp}/abspath`).toString(), 'new content 3'); + }); }); -// Relative regression -skipOnWinForEPERM(shell.ln.bind(shell, '-sf', 'file1.txt', 'tmp/file2.txt'), function () { - shell.mkdir('-p', 'tmp/new'); +test('Relative regression', t => { + utils.skipOnWinForEPERM(shell.ln.bind(shell, '-sf', 'file1.txt', `${t.context.tmp}/file2.txt`), () => { + shell.mkdir('-p', `${t.context.tmp}/new`); // Move the symlink first, as the reverse confuses `mv`. - shell.mv('tmp/file2.txt', 'tmp/new/file2.txt'); - shell.mv('tmp/file1.txt', 'tmp/new/file1.txt'); - assert(fs.existsSync('tmp/new/file2.txt')); - assert.equal( - fs.readFileSync('tmp/new/file1.txt').toString(), - fs.readFileSync('tmp/new/file2.txt').toString() - ); - fs.writeFileSync('tmp/new/file1.txt', 'new content txt'); - assert.equal( - fs.readFileSync('tmp/new/file2.txt').toString(), - 'new content txt' + shell.mv(`${t.context.tmp}/file2.txt`, `${t.context.tmp}/new/file2.txt`); + shell.mv(`${t.context.tmp}/file1.txt`, `${t.context.tmp}/new/file1.txt`); + t.truthy(fs.existsSync(`${t.context.tmp}/new/file2.txt`)); + t.is( + fs.readFileSync(`${t.context.tmp}/new/file1.txt`).toString(), + fs.readFileSync(`${t.context.tmp}/new/file2.txt`).toString() ); + fs.writeFileSync(`${t.context.tmp}/new/file1.txt`, 'new content txt'); + t.is(fs.readFileSync(`${t.context.tmp}/new/file2.txt`).toString(), 'new content txt'); + }); }); - -shell.exit(123); diff --git a/test/ls.js b/test/ls.js index 1666773d5..1c8ef6276 100644 --- a/test/ls.js +++ b/test/ls.js @@ -1,424 +1,535 @@ -var shell = require('..'); +const fs = require('fs'); -var assert = require('assert'), - fs = require('fs'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const common = require('../src/common'); +const utils = require('./utils/utils'); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +const CWD = process.cwd(); + +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.mkdir(t.context.tmp); +}); + +test.afterEach.always(t => { + process.chdir(CWD); + shell.rm('-rf', t.context.tmp); +}); -var idx; // // Invalids // -assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check -var result = shell.ls('/asdfasdf'); // no such file or dir -assert.ok(shell.error()); -assert.equal(result.code, 2); -assert.equal(result.length, 0); +test('no such file or dir', t => { + t.falsy(fs.existsSync('/asdfasdf')); + const result = shell.ls('/asdfasdf'); // no such file or dir + t.truthy(shell.error()); + t.is(result.code, 2); + t.is(result.length, 0); +}); // // Valids // -result = shell.ls(); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); - -result = shell.ls('/'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); - -// no args -shell.cd('resources/ls'); -result = shell.ls(); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('file1') > -1, true); -assert.equal(result.indexOf('file2') > -1, true); -assert.equal(result.indexOf('file1.js') > -1, true); -assert.equal(result.indexOf('file2.js') > -1, true); -assert.equal(result.indexOf('filename(with)[chars$]^that.must+be-escaped') > -1, true); -assert.equal(result.indexOf('a_dir') > -1, true); -assert.equal(result.length, 6); -shell.cd('../..'); - -// simple arg -result = shell.ls('resources/ls'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('file1') > -1, true); -assert.equal(result.indexOf('file2') > -1, true); -assert.equal(result.indexOf('file1.js') > -1, true); -assert.equal(result.indexOf('file2.js') > -1, true); -assert.equal(result.indexOf('filename(with)[chars$]^that.must+be-escaped') > -1, true); -assert.equal(result.indexOf('a_dir') > -1, true); -assert.equal(result.length, 6); - -// simple arg, with a trailing slash -result = shell.ls('resources/ls/'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('file1') > -1, true); -assert.equal(result.indexOf('file2') > -1, true); -assert.equal(result.indexOf('file1.js') > -1, true); -assert.equal(result.indexOf('file2.js') > -1, true); -assert.equal(result.indexOf('filename(with)[chars$]^that.must+be-escaped') > -1, true); -assert.equal(result.indexOf('a_dir') > -1, true); -assert.equal(result.length, 6); - -// no args, 'all' option -shell.cd('resources/ls'); -result = shell.ls('-A'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('file1') > -1, true); -assert.equal(result.indexOf('file2') > -1, true); -assert.equal(result.indexOf('file1.js') > -1, true); -assert.equal(result.indexOf('file2.js') > -1, true); -assert.equal(result.indexOf('filename(with)[chars$]^that.must+be-escaped') > -1, true); -assert.equal(result.indexOf('a_dir') > -1, true); -assert.equal(result.indexOf('.hidden_file') > -1, true); -assert.equal(result.indexOf('.hidden_dir') > -1, true); -assert.equal(result.length, 8); -shell.cd('../..'); - -// no args, 'all' option -shell.cd('resources/ls'); -result = shell.ls('-a'); // (deprecated) backwards compatibility test -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('file1') > -1, true); -assert.equal(result.indexOf('file2') > -1, true); -assert.equal(result.indexOf('file1.js') > -1, true); -assert.equal(result.indexOf('file2.js') > -1, true); -assert.equal(result.indexOf('filename(with)[chars$]^that.must+be-escaped') > -1, true); -assert.equal(result.indexOf('a_dir') > -1, true); -assert.equal(result.indexOf('.hidden_file') > -1, true); -assert.equal(result.indexOf('.hidden_dir') > -1, true); -assert.equal(result.length, 8); -shell.cd('../..'); - -// wildcard, very simple -result = shell.ls('resources/cat/*'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('resources/cat/file1') > -1, true); -assert.equal(result.indexOf('resources/cat/file2') > -1, true); -assert.equal(result.length, 2); - -// wildcard, simple -result = shell.ls('resources/ls/*'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('resources/ls/file1') > -1, true); -assert.equal(result.indexOf('resources/ls/file2') > -1, true); -assert.equal(result.indexOf('resources/ls/file1.js') > -1, true); -assert.equal(result.indexOf('resources/ls/file2.js') > -1, true); -assert.equal(result.indexOf('resources/ls/filename(with)[chars$]^that.must+be-escaped') > -1, true); -assert.ok(result.indexOf('resources/ls/a_dir') === -1); // this shouldn't be there -assert.ok(result.indexOf('nada') > -1); -assert.ok(result.indexOf('b_dir') > -1); -assert.equal(result.length, 7); - -// wildcard, simple, with -d -result = shell.ls('-d', 'resources/ls/*'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('resources/ls/file1') > -1, true); -assert.equal(result.indexOf('resources/ls/file2') > -1, true); -assert.equal(result.indexOf('resources/ls/file1.js') > -1, true); -assert.equal(result.indexOf('resources/ls/file2.js') > -1, true); -assert.equal(result.indexOf('resources/ls/filename(with)[chars$]^that.must+be-escaped') > -1, true); -assert.ok(result.indexOf('resources/ls/a_dir') > -1); -assert.equal(result.length, 6); - -// wildcard, hidden only -result = shell.ls('-d', 'resources/ls/.*'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('resources/ls/.hidden_file') > -1, true); -assert.equal(result.indexOf('resources/ls/.hidden_dir') > -1, true); -assert.equal(result.length, 2); - -// wildcard, mid-file -result = shell.ls('resources/ls/f*le*'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.length, 5); -assert.equal(result.indexOf('resources/ls/file1') > -1, true); -assert.equal(result.indexOf('resources/ls/file2') > -1, true); -assert.equal(result.indexOf('resources/ls/file1.js') > -1, true); -assert.equal(result.indexOf('resources/ls/file2.js') > -1, true); -assert.equal(result.indexOf('resources/ls/filename(with)[chars$]^that.must+be-escaped') > -1, true); - -// wildcard, mid-file with dot (should escape dot for regex) -result = shell.ls('resources/ls/f*le*.js'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.length, 2); -assert.equal(result.indexOf('resources/ls/file1.js') > -1, true); -assert.equal(result.indexOf('resources/ls/file2.js') > -1, true); - -// one file that exists, one that doesn't -result = shell.ls('resources/ls/file1.js', 'resources/ls/thisdoesntexist'); -assert.ok(shell.error()); -assert.equal(result.code, 2); -assert.equal(result.length, 1); -assert.equal(result.indexOf('resources/ls/file1.js') > -1, true); - -// one file that exists, one that doesn't (other order) -result = shell.ls('resources/ls/thisdoesntexist', 'resources/ls/file1.js'); -assert.ok(shell.error()); -assert.equal(result.code, 2); -assert.equal(result.length, 1); -assert.equal(result.indexOf('resources/ls/file1.js') > -1, true); - -// wildcard, should not do partial matches -result = shell.ls('resources/ls/*.j'); // shouldn't get .js -assert.ok(shell.error()); -assert.equal(result.code, 2); -assert.equal(result.length, 0); - -// wildcard, all files with extension -result = shell.ls('resources/ls/*.*'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.length, 3); -assert.equal(result.indexOf('resources/ls/file1.js') > -1, true); -assert.equal(result.indexOf('resources/ls/file2.js') > -1, true); -assert.equal(result.indexOf('resources/ls/filename(with)[chars$]^that.must+be-escaped') > -1, true); - -// wildcard, with additional path -result = shell.ls('resources/ls/f*le*.js', 'resources/ls/a_dir'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.length, 4); -assert.equal(result.indexOf('resources/ls/file1.js') > -1, true); -assert.equal(result.indexOf('resources/ls/file2.js') > -1, true); -assert.equal(result.indexOf('b_dir') > -1, true); // no wildcard == no path prefix -assert.equal(result.indexOf('nada') > -1, true); // no wildcard == no path prefix - -// wildcard for both paths -result = shell.ls('resources/ls/f*le*.js', 'resources/ls/a_dir/*'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.length, 4); -assert.equal(result.indexOf('resources/ls/file1.js') > -1, true); -assert.equal(result.indexOf('resources/ls/file2.js') > -1, true); -assert.equal(result.indexOf('z') > -1, true); -assert.equal(result.indexOf('resources/ls/a_dir/nada') > -1, true); - -// wildcard for both paths, array -result = shell.ls(['resources/ls/f*le*.js', 'resources/ls/a_dir/*']); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.length, 4); -assert.equal(result.indexOf('resources/ls/file1.js') > -1, true); -assert.equal(result.indexOf('resources/ls/file2.js') > -1, true); -assert.equal(result.indexOf('z') > -1, true); -assert.equal(result.indexOf('resources/ls/a_dir/nada') > -1, true); - -// recursive, no path -shell.cd('resources/ls'); -result = shell.ls('-R'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('a_dir') > -1, true); -assert.equal(result.indexOf('a_dir/b_dir') > -1, true); -assert.equal(result.indexOf('a_dir/b_dir/z') > -1, true); -assert.equal(result.length, 9); -shell.cd('../..'); - -// recusive, path given -result = shell.ls('-R', 'resources/ls'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('a_dir') > -1, true); -assert.equal(result.indexOf('a_dir/b_dir') > -1, true); -assert.equal(result.indexOf('a_dir/b_dir/z') > -1, true); -assert.equal(result.length, 9); - -// recusive, path given - 'all' flag -result = shell.ls('-RA', 'resources/ls'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('a_dir') > -1, true); -assert.equal(result.indexOf('a_dir/b_dir') > -1, true); -assert.equal(result.indexOf('a_dir/b_dir/z') > -1, true); -assert.equal(result.indexOf('a_dir/.hidden_dir/nada') > -1, true); -assert.equal(result.length, 14); - -// recursive, wildcard -result = shell.ls('-R', 'resources/ls'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('a_dir') > -1, true); -assert.equal(result.indexOf('a_dir/b_dir') > -1, true); -assert.equal(result.indexOf('a_dir/b_dir/z') > -1, true); -assert.equal(result.length, 9); - -// -Rd works like -d -result = shell.ls('-Rd', 'resources/ls'); -assert.equal(shell.error(), null); -assert.equal(result.length, 1); - -// directory option, single arg -result = shell.ls('-d', 'resources/ls'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.length, 1); - -// directory option, single arg with trailing '/' -result = shell.ls('-d', 'resources/ls/'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.length, 1); - -// directory option, multiple args -result = shell.ls('-d', 'resources/ls/a_dir', 'resources/ls/file1'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.ok(result.indexOf('resources/ls/a_dir') > -1); -assert.ok(result.indexOf('resources/ls/file1') > -1); -assert.equal(result.length, 2); - -// directory option, globbed arg -result = shell.ls('-d', 'resources/ls/*'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.ok(result.indexOf('resources/ls/a_dir') > -1); -assert.ok(result.indexOf('resources/ls/file1') > -1); -assert.ok(result.indexOf('resources/ls/file1.js') > -1); -assert.ok(result.indexOf('resources/ls/file2') > -1); -assert.ok(result.indexOf('resources/ls/file2.js') > -1); -assert.ok(result.indexOf('resources/ls/file2') > -1); -assert.ok(result.indexOf('resources/ls/filename(with)[chars$]^that.must+be-escaped') > -1); -assert.equal(result.length, 6); - -// long option, single file -result = shell.ls('-l', 'resources/ls/file1'); -assert.equal(result.length, 1); -result = result[0]; -assert.equal(shell.error(), null); -assert.ok(result.name, 'file1'); -assert.equal(result.nlink, 1); -assert.equal(result.size, 5); -assert.ok(result.mode); // check that these keys exist -assert.ok(process.platform === 'win32' || result.uid); // only on unix -assert.ok(process.platform === 'win32' || result.gid); // only on unix -assert.ok(result.mtime); // check that these keys exist -assert.ok(result.atime); // check that these keys exist -assert.ok(result.ctime); // check that these keys exist -assert.ok(result.toString().match(/^(\d+ +){5}.*$/)); - -// long option, glob files -result = shell.ls('-l', 'resources/ls/f*le1'); -assert.equal(result.length, 1); -result = result[0]; -assert.equal(shell.error(), null); -assert.ok(result.name, 'file1'); -assert.equal(result.nlink, 1); -assert.equal(result.size, 5); -assert.ok(result.mode); // check that these keys exist -assert.ok(process.platform === 'win32' || result.uid); // only on unix -assert.ok(process.platform === 'win32' || result.gid); // only on unix -assert.ok(result.mtime); // check that these keys exist -assert.ok(result.atime); // check that these keys exist -assert.ok(result.ctime); // check that these keys exist -assert.ok(result.toString().match(/^(\d+ +){5}.*$/)); - -// long option, directory -result = shell.ls('-l', 'resources/ls'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -idx = -1; -for (var k=0; k < result.length; k++) { - if (result[k].name === 'file1') { - idx = k; - break; - } -} -assert.ok(idx >= 0); -assert.equal(result.length, 6); -result = result[idx]; -assert.equal(result.name, 'file1'); -assert.equal(result.nlink, 1); -assert.equal(result.size, 5); -assert.ok(result.mode); // check that these keys exist -assert.ok(process.platform === 'win32' || result.uid); // only on unix -assert.ok(process.platform === 'win32' || result.gid); // only on unix -assert.ok(result.mtime); // check that these keys exist -assert.ok(result.atime); // check that these keys exist -assert.ok(result.ctime); // check that these keys exist -assert.ok(result.toString().match(/^(\d+ +){5}.*$/)); - -// long option, directory, recursive (and windows converts slashes) -result = shell.ls('-lR', 'resources/ls/'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -idx = -1; -for (var k=0; k < result.length; k++) { - if (result[k].name === 'a_dir/b_dir') { - idx = k; - break; - } -} -assert.equal(result.length, 9); -assert.ok(idx >= 0); -result = result[idx]; -assert.equal(result.name, result.name); -assert.ok(fs.statSync('resources/ls/a_dir/b_dir').isDirectory()); -assert.ok(typeof result.nlink === 'number'); // This can vary between the local machine and travis -assert.ok(typeof result.size === 'number'); // This can vary between different file systems -assert.ok(result.mode); // check that these keys exist -assert.ok(process.platform === 'win32' || result.uid); // only on unix -assert.ok(process.platform === 'win32' || result.gid); // only on unix -assert.ok(result.mtime); // check that these keys exist -assert.ok(result.atime); // check that these keys exist -assert.ok(result.ctime); // check that these keys exist -assert.ok(result.toString().match(/^(\d+ +){5}.*$/)); - -// still lists broken links -result = shell.ls('resources/badlink'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.indexOf('resources/badlink') > -1, true); -assert.equal(result.length, 1); - -// Test new ShellString-like attributes -result = shell.ls('resources/ls'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.stdout.indexOf('file1') > -1, true); -assert.equal(result.stdout.indexOf('file2') > -1, true); -assert.equal(result.stdout.indexOf('file1.js') > -1, true); -assert.equal(result.stdout.indexOf('file2.js') > -1, true); -assert.equal(result.stdout.indexOf('filename(with)[chars$]^that.must+be-escaped') > -1, true); -assert.equal(result.stdout.indexOf('a_dir') > -1, true); -assert.strictEqual(typeof result.stdout, 'string'); -assert.ok(result.to); -assert.ok(result.toEnd); -result.to('tmp/testingToOutput.txt'); -assert.equal(shell.cat('tmp/testingToOutput.txt'), result.stdout); -shell.rm('tmp/testingToOutput.txt'); - -// No trailing newline for ls() on empty directories -shell.mkdir('foo'); -assert.ok(!shell.error()); -result = shell.ls('foo'); -assert.ok(!shell.error()); -assert.equal(result.stdout, ''); -shell.rm('-r', 'foo'); -assert.ok(!shell.error()); - -// Check stderr field -assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check -result = shell.ls('resources/ls/file1', '/asdfasdf'); -assert.ok(shell.error()); -assert.equal('ls: no such file or directory: /asdfasdf', result.stderr); - -shell.exit(123); +test("it's ok to use no arguments", t => { + const result = shell.ls(); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(shell.errorCode(), 0); +}); + +test('root directory', t => { + const result = shell.ls('/'); + t.falsy(shell.error()); + t.is(result.code, 0); +}); + +test('no args provides the correct result', t => { + shell.cd('test/resources/ls'); + const result = shell.ls(); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('file1')); + t.truthy(result.includes('file2')); + t.truthy(result.includes('file1.js')); + t.truthy(result.includes('file2.js')); + t.truthy(result.includes('filename(with)[chars$]^that.must+be-escaped')); + t.truthy(result.includes('a_dir')); + t.is(result.length, 6); +}); + +test('simple arg', t => { + const result = shell.ls('test/resources/ls'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('file1')); + t.truthy(result.includes('file2')); + t.truthy(result.includes('file1.js')); + t.truthy(result.includes('file2.js')); + t.truthy(result.includes('filename(with)[chars$]^that.must+be-escaped')); + t.truthy(result.includes('a_dir')); + t.is(result.length, 6); +}); + +test('simple arg, with a trailing slash', t => { + const result = shell.ls('test/resources/ls/'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('file1')); + t.truthy(result.includes('file2')); + t.truthy(result.includes('file1.js')); + t.truthy(result.includes('file2.js')); + t.truthy(result.includes('filename(with)[chars$]^that.must+be-escaped')); + t.truthy(result.includes('a_dir')); + t.is(result.length, 6); +}); + +test('simple arg, a file', t => { + const result = shell.ls('test/resources/ls/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('test/resources/ls/file1')); + t.is(result.length, 1); +}); + +test('no args, -A option', t => { + shell.cd('test/resources/ls'); + const result = shell.ls('-A'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('file1')); + t.truthy(result.includes('file2')); + t.truthy(result.includes('file1.js')); + t.truthy(result.includes('file2.js')); + t.truthy(result.includes('filename(with)[chars$]^that.must+be-escaped')); + t.truthy(result.includes('a_dir')); + t.truthy(result.includes('.hidden_file')); + t.truthy(result.includes('.hidden_dir')); + t.is(result.length, 8); +}); + +test('no args, deprecated -a option', t => { + shell.cd('test/resources/ls'); + const result = shell.ls('-a'); // (deprecated) backwards compatibility test + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('file1')); + t.truthy(result.includes('file2')); + t.truthy(result.includes('file1.js')); + t.truthy(result.includes('file2.js')); + t.truthy(result.includes('filename(with)[chars$]^that.must+be-escaped')); + t.truthy(result.includes('a_dir')); + t.truthy(result.includes('.hidden_file')); + t.truthy(result.includes('.hidden_dir')); + t.is(result.length, 8); +}); + +test('wildcard, very simple', t => { + const result = shell.ls('test/resources/cat/*'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('test/resources/cat/file1')); + t.truthy(result.includes('test/resources/cat/file2')); + t.truthy(result.includes('test/resources/cat/file3')); + t.truthy(result.includes('test/resources/cat/file4')); + t.truthy(result.includes('test/resources/cat/file5')); + t.is(result.length, 5); +}); + +test('wildcard, simple', t => { + const result = shell.ls('test/resources/ls/*'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('test/resources/ls/file1')); + t.truthy(result.includes('test/resources/ls/file2')); + t.truthy(result.includes('test/resources/ls/file1.js')); + t.truthy(result.includes('test/resources/ls/file2.js')); + t.truthy( + result.includes('test/resources/ls/filename(with)[chars$]^that.must+be-escaped') + ); + t.falsy(result.includes('test/resources/ls/a_dir')); // this shouldn't be there + t.truthy(result.includes('nada')); + t.truthy(result.includes('b_dir')); + t.is(result.length, 7); +}); + +test('wildcard, simple, with -d', t => { + const result = shell.ls('-d', 'test/resources/ls/*'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('test/resources/ls/file1')); + t.truthy(result.includes('test/resources/ls/file2')); + t.truthy(result.includes('test/resources/ls/file1.js')); + t.truthy(result.includes('test/resources/ls/file2.js')); + t.truthy( + result.includes('test/resources/ls/filename(with)[chars$]^that.must+be-escaped') + ); + t.truthy(result.includes('test/resources/ls/a_dir')); + t.is(result.length, 6); +}); + +test('wildcard, hidden only', t => { + const result = shell.ls('-d', 'test/resources/ls/.*'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('test/resources/ls/.hidden_file')); + t.truthy(result.includes('test/resources/ls/.hidden_dir')); + t.is(result.length, 2); +}); + +test('wildcard, mid-file', t => { + const result = shell.ls('test/resources/ls/f*le*'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.length, 5); + t.truthy(result.includes('test/resources/ls/file1')); + t.truthy(result.includes('test/resources/ls/file2')); + t.truthy(result.includes('test/resources/ls/file1.js')); + t.truthy(result.includes('test/resources/ls/file2.js')); + t.truthy( + result.includes('test/resources/ls/filename(with)[chars$]^that.must+be-escaped') + ); +}); + +test('wildcard, mid-file with dot (should escape dot for regex)', t => { + const result = shell.ls('test/resources/ls/f*le*.js'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.length, 2); + t.truthy(result.includes('test/resources/ls/file1.js')); + t.truthy(result.includes('test/resources/ls/file2.js')); +}); + +test("one file that exists, one that doesn't", t => { + const result = shell.ls('test/resources/ls/file1.js', 'test/resources/ls/thisdoesntexist'); + t.truthy(shell.error()); + t.is(result.code, 2); + t.is(result.length, 1); + t.truthy(result.includes('test/resources/ls/file1.js')); +}); + +test("one file that exists, one that doesn't (other order)", t => { + const result = shell.ls('test/resources/ls/thisdoesntexist', 'test/resources/ls/file1.js'); + t.truthy(shell.error()); + t.is(result.code, 2); + t.is(result.length, 1); + t.truthy(result.includes('test/resources/ls/file1.js')); +}); + +test('wildcard, should not do partial matches', t => { + const result = shell.ls('test/resources/ls/*.j'); // shouldn't get .js + t.truthy(shell.error()); + t.is(result.code, 2); + t.is(result.length, 0); +}); + +test('wildcard, all files with extension', t => { + const result = shell.ls('test/resources/ls/*.*'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.length, 3); + t.truthy(result.includes('test/resources/ls/file1.js')); + t.truthy(result.includes('test/resources/ls/file2.js')); + t.truthy( + result.includes('test/resources/ls/filename(with)[chars$]^that.must+be-escaped') + ); +}); + +test('wildcard, with additional path', t => { + const result = shell.ls('test/resources/ls/f*le*.js', 'test/resources/ls/a_dir'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.length, 4); + t.truthy(result.includes('test/resources/ls/file1.js')); + t.truthy(result.includes('test/resources/ls/file2.js')); + t.truthy(result.includes('b_dir')); // no wildcard == no path prefix + t.truthy(result.includes('nada')); // no wildcard == no path prefix +}); + +test('wildcard for both paths', t => { + const result = shell.ls('test/resources/ls/f*le*.js', 'test/resources/ls/a_dir/*'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.length, 4); + t.truthy(result.includes('test/resources/ls/file1.js')); + t.truthy(result.includes('test/resources/ls/file2.js')); + t.truthy(result.includes('z')); + t.truthy(result.includes('test/resources/ls/a_dir/nada')); +}); + +test('wildcard for both paths, array', t => { + const result = shell.ls(['test/resources/ls/f*le*.js', 'test/resources/ls/a_dir/*']); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.length, 4); + t.truthy(result.includes('test/resources/ls/file1.js')); + t.truthy(result.includes('test/resources/ls/file2.js')); + t.truthy(result.includes('z')); + t.truthy(result.includes('test/resources/ls/a_dir/nada')); +}); + +test('recursive, no path', t => { + shell.cd('test/resources/ls'); + const result = shell.ls('-R'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('a_dir')); + t.truthy(result.includes('a_dir/b_dir')); + t.truthy(result.includes('a_dir/b_dir/z')); + t.is(result.length, 9); +}); + +test('recursive, path given', t => { + const result = shell.ls('-R', 'test/resources/ls'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('a_dir')); + t.truthy(result.includes('a_dir/b_dir')); + t.truthy(result.includes('a_dir/b_dir/z')); + t.is(result.length, 9); +}); + +test('-RA flag, path given', t => { + const result = shell.ls('-RA', 'test/resources/ls'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('a_dir')); + t.truthy(result.includes('a_dir/b_dir')); + t.truthy(result.includes('a_dir/b_dir/z')); + t.truthy(result.includes('a_dir/.hidden_dir/nada')); + t.is(result.length, 14); +}); + +test('-RA flag, symlinks are not followed', t => { + const result = shell.ls('-RA', 'test/resources/rm'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('a_dir')); + t.truthy(result.includes('a_dir/a_file')); + t.truthy(result.includes('link_to_a_dir')); + t.falsy(result.includes('link_to_a_dir/a_file')); + t.truthy(result.includes('fake.lnk')); + t.is(result.length, 4); +}); + +test('-RAL flag, follows symlinks', t => { + utils.skipOnWin(t, () => { + const result = shell.ls('-RAL', 'test/resources/rm'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('a_dir')); + t.truthy(result.includes('a_dir/a_file')); + t.truthy(result.includes('link_to_a_dir')); + t.truthy(result.includes('link_to_a_dir/a_file')); + t.truthy(result.includes('fake.lnk')); + t.is(result.length, 5); + }); +}); + +test('-L flag, path is symlink', t => { + utils.skipOnWin(t, () => { + const result = shell.ls('-L', 'test/resources/rm/link_to_a_dir'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('a_file')); + t.is(result.length, 1); + }); +}); + +test('follow links to directories by default', t => { + utils.skipOnWin(t, () => { + const result = shell.ls('test/resources/rm/link_to_a_dir'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('a_file')); + t.is(result.length, 1); + }); +}); + +test('-Rd works like -d', t => { + const result = shell.ls('-Rd', 'test/resources/ls'); + t.falsy(shell.error()); + t.is(result.length, 1); +}); + +test('directory option, single arg', t => { + const result = shell.ls('-d', 'test/resources/ls'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.length, 1); +}); + +test("directory option, single arg with trailing '/'", t => { + const result = shell.ls('-d', 'test/resources/ls/'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.length, 1); +}); + +test('directory option, multiple args', t => { + const result = shell.ls('-d', 'test/resources/ls/a_dir', 'test/resources/ls/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('test/resources/ls/a_dir')); + t.truthy(result.includes('test/resources/ls/file1')); + t.is(result.length, 2); +}); + +test('directory option, globbed arg', t => { + const result = shell.ls('-d', 'test/resources/ls/*'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('test/resources/ls/a_dir')); + t.truthy(result.includes('test/resources/ls/file1')); + t.truthy(result.includes('test/resources/ls/file1.js')); + t.truthy(result.includes('test/resources/ls/file2')); + t.truthy(result.includes('test/resources/ls/file2.js')); + t.truthy(result.includes('test/resources/ls/file2')); + t.truthy( + result.includes('test/resources/ls/filename(with)[chars$]^that.must+be-escaped') + ); + t.is(result.length, 6); +}); + +test('long option, single file', t => { + let result = shell.ls('-l', 'test/resources/ls/file1'); + t.is(result.length, 1); + result = result[0]; + t.falsy(shell.error()); + t.truthy(result.name, 'file1'); + t.is(result.nlink, 1); + t.is(result.size, 5); + t.truthy(result.mode); // check that these keys exist + utils.skipOnWin(t, () => { + t.truthy(result.uid); + t.truthy(result.gid); + }); + t.truthy(result.mtime); // check that these keys exist + t.truthy(result.atime); // check that these keys exist + t.truthy(result.ctime); // check that these keys exist + t.truthy(result.toString().match(/^(\d+ +){5}.*$/)); +}); + +test('long option, glob files', t => { + let result = shell.ls('-l', 'test/resources/ls/f*le1'); + t.is(result.length, 1); + result = result[0]; + t.falsy(shell.error()); + t.truthy(result.name, 'file1'); + t.is(result.nlink, 1); + t.is(result.size, 5); + t.truthy(result.mode); // check that these keys exist + utils.skipOnWin(t, () => { + t.truthy(result.uid); + t.truthy(result.gid); + }); + t.truthy(result.mtime); // check that these keys exist + t.truthy(result.atime); // check that these keys exist + t.truthy(result.ctime); // check that these keys exist + t.truthy(result.toString().match(/^(\d+ +){5}.*$/)); +}); + +test('long option, directory', t => { + let result = shell.ls('-l', 'test/resources/ls'); + t.falsy(shell.error()); + t.is(result.code, 0); + const idx = result.map(r => r.name).indexOf('file1'); + t.truthy(idx >= 0); + t.is(result.length, 6); + result = result[idx]; + t.is(result.name, 'file1'); + t.is(result.nlink, 1); + t.is(result.size, 5); + t.truthy(result.mode); // check that these keys exist + utils.skipOnWin(t, () => { + t.truthy(result.uid); + t.truthy(result.gid); + }); + t.truthy(result.mtime); // check that these keys exist + t.truthy(result.atime); // check that these keys exist + t.truthy(result.ctime); // check that these keys exist + t.truthy(result.toString().match(/^(\d+ +){5}.*$/)); +}); + +test('long option, directory, recursive (and windows converts slashes)', t => { + let result = shell.ls('-lR', 'test/resources/ls/'); + t.falsy(shell.error()); + t.is(result.code, 0); + const idx = result.map(r => r.name).indexOf('a_dir/b_dir'); + t.is(result.length, 9); + t.truthy(idx >= 0); + result = result[idx]; + t.is(result.name, result.name); + t.truthy(common.statFollowLinks('test/resources/ls/a_dir/b_dir').isDirectory()); + t.is(typeof result.nlink, 'number'); // This can vary between the local machine and travis + t.is(typeof result.size, 'number'); // This can vary between different file systems + t.truthy(result.mode); // check that these keys exist + utils.skipOnWin(t, () => { + t.truthy(result.uid); + t.truthy(result.gid); + }); + t.truthy(result.mtime); // check that these keys exist + t.truthy(result.atime); // check that these keys exist + t.truthy(result.ctime); // check that these keys exist + t.truthy(result.toString().match(/^(\d+ +){5}.*$/)); +}); + +test('still lists broken links', t => { + const result = shell.ls('test/resources/badlink'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('test/resources/badlink')); + t.is(result.length, 1); +}); + +test('Test new ShellString-like attributes', t => { + const result = shell.ls('test/resources/ls'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.stdout.includes('file1')); + t.truthy(result.stdout.includes('file2')); + t.truthy(result.stdout.includes('file1.js')); + t.truthy(result.stdout.includes('file2.js')); + t.truthy( + result.stdout.includes('filename(with)[chars$]^that.must+be-escaped') + ); + t.truthy(result.stdout.includes('a_dir')); + t.is(typeof result.stdout, 'string'); + t.truthy(result.to); + t.truthy(result.toEnd); + result.to(`${t.context.tmp}/testingToOutput.txt`); + t.is(shell.cat(`${t.context.tmp}/testingToOutput.txt`).toString(), result.stdout); + shell.rm(`${t.context.tmp}/testingToOutput.txt`); +}); + +test('No trailing newline for ls() on empty directories', t => { + shell.mkdir('foo'); + t.falsy(shell.error()); + const result = shell.ls('foo'); + t.falsy(shell.error()); + t.is(result.stdout, ''); + shell.rm('-r', 'foo'); + t.falsy(shell.error()); +}); + +test('Check stderr field', t => { + t.falsy(fs.existsSync('/asdfasdf')); // sanity check + const result = shell.ls('test/resources/ls/file1', '/asdfasdf'); + t.truthy(shell.error()); + t.is('ls: no such file or directory: /asdfasdf', result.stderr); +}); + +test('non-normalized paths are still ok with -R', t => { + const result = shell.ls('-R', 'test/resources/./ls/../ls'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(result.includes('a_dir')); + t.truthy(result.includes('a_dir/b_dir')); + t.truthy(result.includes('a_dir/b_dir/z')); + t.is(result.length, 9); +}); diff --git a/test/make.js b/test/make.js deleted file mode 100644 index b54c0af13..000000000 --- a/test/make.js +++ /dev/null @@ -1,20 +0,0 @@ -var shell = require('..'), - child = require('child_process'), - assert = require('assert'); - -shell.mkdir('-p', 'tmp'); -var file = 'tmp/tempscript'+Math.random()+'.js', - script = 'require(\'../../make.js\');' + - 'target.all=function(){' + - ' echo("first"); '+ - ' cp("this_file_doesnt_exist", ".");' + - ' echo("second");' + - '}'; - -shell.ShellString(script).to(file); -child.exec(JSON.stringify(process.execPath)+' '+file, function(err, stdout) { - assert.ok(stdout.match('first')); - assert.ok(!stdout.match('second')); // Make should die on errors, so this should never get echoed - - shell.exit(123); -}); diff --git a/test/mkdir.js b/test/mkdir.js index 7d51af29e..f37b9f047 100644 --- a/test/mkdir.js +++ b/test/mkdir.js @@ -1,103 +1,203 @@ -var shell = require('..'); +const fs = require('fs'); -var assert = require('assert'), - fs = require('fs'), - numLines = require('./utils/utils').numLines; +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const common = require('../src/common'); +const utils = require('./utils/utils'); + +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.mkdir(t.context.tmp); +}); + +test.afterEach.always(t => { + shell.rm('-rf', t.context.tmp); +}); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); // // Invalids // -var result = shell.mkdir(); -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mkdir: no paths given'); - -var mtime = fs.statSync('tmp').mtime.toString(); -result = shell.mkdir('tmp'); // dir already exists -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mkdir: path already exists: tmp'); -assert.equal(fs.statSync('tmp').mtime.toString(), mtime); // didn't mess with dir - -// Can't overwrite a broken link -mtime = fs.lstatSync('resources/badlink').mtime.toString(); -result = shell.mkdir('resources/badlink'); -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mkdir: path already exists: resources/badlink'); -assert.equal(fs.lstatSync('resources/badlink').mtime.toString(), mtime); // didn't mess with file - -assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check -result = shell.mkdir('/asdfasdf/foobar'); // root path does not exist -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mkdir: no such file or directory: /asdfasdf'); -assert.equal(fs.existsSync('/asdfasdf'), false); -assert.equal(fs.existsSync('/asdfasdf/foobar'), false); +test('no args', t => { + const result = shell.mkdir(); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mkdir: no paths given'); +}); + +test('dir already exists', t => { + const mtime = common.statFollowLinks(t.context.tmp).mtime.toString(); + const result = shell.mkdir(t.context.tmp); // dir already exists + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, `mkdir: path already exists: ${t.context.tmp}`); + t.is(common.statFollowLinks(t.context.tmp).mtime.toString(), mtime); // didn't mess with dir +}); + +test("Can't overwrite a broken link", t => { + const mtime = common.statNoFollowLinks('test/resources/badlink').mtime.toString(); + const result = shell.mkdir('test/resources/badlink'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mkdir: path already exists: test/resources/badlink'); + t.is(common.statNoFollowLinks('test/resources/badlink').mtime.toString(), mtime); // didn't mess with file +}); + +test('root path does not exist', t => { + t.falsy(fs.existsSync('/asdfasdf')); // sanity check + const result = shell.mkdir('/asdfasdf/foobar'); // root path does not exist + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mkdir: no such file or directory: /asdfasdf'); + t.falsy(fs.existsSync('/asdfasdf')); + t.falsy(fs.existsSync('/asdfasdf/foobar')); +}); + +test('try to overwrite file', t => { + t.truthy(common.statFollowLinks('test/resources/file1').isFile()); + const result = shell.mkdir('test/resources/file1'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mkdir: path already exists: test/resources/file1'); + t.truthy(common.statFollowLinks('test/resources/file1').isFile()); +}); + +test('try to overwrite file, with -p', t => { + t.truthy(common.statFollowLinks('test/resources/file1').isFile()); + const result = shell.mkdir('-p', 'test/resources/file1'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mkdir: cannot create directory test/resources/file1: File exists'); + t.truthy(common.statFollowLinks('test/resources/file1').isFile()); +}); + +test('try to make a subdirectory of a file', t => { + t.truthy(common.statFollowLinks('test/resources/file1').isFile()); + const result = shell.mkdir('test/resources/file1/subdir'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mkdir: cannot create directory test/resources/file1/subdir: Not a directory'); + t.truthy(common.statFollowLinks('test/resources/file1').isFile()); + t.falsy(fs.existsSync('test/resources/file1/subdir')); +}); + +test('Check for invalid permissions', t => { + utils.skipOnWin(t, () => { + // This test case only works on unix, but should work on Windows as well + const dirName = `${t.context.tmp}/nowritedir`; + shell.mkdir(dirName); + t.falsy(shell.error()); + shell.chmod('-w', dirName); + const result = shell.mkdir(dirName + '/foo'); + t.is(result.code, 1); + t.is( + result.stderr, + `mkdir: cannot create directory ${t.context.tmp}/nowritedir/foo: Permission denied` + ); + t.truthy(shell.error()); + t.falsy(fs.existsSync(dirName + '/foo')); + }); +}); // // Valids // -assert.equal(fs.existsSync('tmp/t1'), false); -result = shell.mkdir('tmp/t1'); // simple dir -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/t1'), true); - -assert.equal(fs.existsSync('tmp/t2'), false); -assert.equal(fs.existsSync('tmp/t3'), false); -result = shell.mkdir('tmp/t2', 'tmp/t3'); // multiple dirs -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/t2'), true); -assert.equal(fs.existsSync('tmp/t3'), true); - -assert.equal(fs.existsSync('tmp/t1'), true); -assert.equal(fs.existsSync('tmp/t4'), false); -result = shell.mkdir('tmp/t1', 'tmp/t4'); // one dir exists, one doesn't -assert.equal(numLines(shell.error()), 1); -assert.equal(fs.existsSync('tmp/t1'), true); -assert.equal(fs.existsSync('tmp/t4'), true); - -assert.equal(fs.existsSync('tmp/a'), false); -result = shell.mkdir('-p', 'tmp/a/b/c'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/a/b/c'), true); -shell.rm('-Rf', 'tmp/a'); // revert - -// multiple dirs -result = shell.mkdir('-p', 'tmp/zzza', 'tmp/zzzb', 'tmp/zzzc'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/zzza'), true); -assert.equal(fs.existsSync('tmp/zzzb'), true); -assert.equal(fs.existsSync('tmp/zzzc'), true); - -// multiple dirs, array syntax -result = shell.mkdir('-p', ['tmp/yyya', 'tmp/yyyb', 'tmp/yyyc']); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/yyya'), true); -assert.equal(fs.existsSync('tmp/yyyb'), true); -assert.equal(fs.existsSync('tmp/yyyc'), true); - -// globbed dir -result = shell.mkdir('-p', 'tmp/mydir'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/mydir'), true); -result = shell.mkdir('-p', 'tmp/m*ir'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/mydir'), true); -assert.equal(fs.existsSync('tmp/m*ir'), false); // doesn't create literal name - -shell.exit(123); +test('basic usage', t => { + t.falsy(fs.existsSync(`${t.context.tmp}/t1`)); + const result = shell.mkdir(`${t.context.tmp}/t1`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/t1`)); +}); + +test('multiple dirs', t => { + t.falsy(fs.existsSync(`${t.context.tmp}/t2`)); + t.falsy(fs.existsSync(`${t.context.tmp}/t3`)); + const result = shell.mkdir(`${t.context.tmp}/t2`, `${t.context.tmp}/t3`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/t2`)); + t.truthy(fs.existsSync(`${t.context.tmp}/t3`)); +}); + +test('one dir exists, the other does not', t => { + shell.mkdir(`${t.context.tmp}/t1`); + t.truthy(fs.existsSync(`${t.context.tmp}/t1`)); + t.falsy(fs.existsSync(`${t.context.tmp}/t4`)); + const result = shell.mkdir(`${t.context.tmp}/t1`, `${t.context.tmp}/t4`); + t.is(result.code, 1); + t.is(utils.numLines(shell.error()), 1); + t.truthy(fs.existsSync(`${t.context.tmp}/t1`)); + t.truthy(fs.existsSync(`${t.context.tmp}/t4`)); +}); + +test('-p flag', t => { + t.falsy(fs.existsSync(`${t.context.tmp}/a`)); + const result = shell.mkdir('-p', `${t.context.tmp}/a/b/c`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/a/b/c`)); +}); + +test('-p flag: multiple dirs', t => { + const result = shell.mkdir('-p', `${t.context.tmp}/zzza`, + `${t.context.tmp}/zzzb`, `${t.context.tmp}/zzzc`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/zzza`)); + t.truthy(fs.existsSync(`${t.context.tmp}/zzzb`)); + t.truthy(fs.existsSync(`${t.context.tmp}/zzzc`)); +}); + +test('-p flag: multiple dirs, array syntax', t => { + const result = shell.mkdir('-p', [`${t.context.tmp}/yyya`, + `${t.context.tmp}/yyyb`, `${t.context.tmp}/yyyc`]); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/yyya`)); + t.truthy(fs.existsSync(`${t.context.tmp}/yyyb`)); + t.truthy(fs.existsSync(`${t.context.tmp}/yyyc`)); +}); + +test('-p flag: subdirectory already exists', t => { + t.falsy(fs.existsSync(`${t.context.tmp}/d1`)); + shell.mkdir('-p', `${t.context.tmp}/d1/d2/d3`); + t.truthy(fs.existsSync(`${t.context.tmp}/d1/d2/d3`)); + const result = shell.mkdir('-p', `${t.context.tmp}/d1/d2`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/d1/d2/d3`)); +}); + +test('-p flag: create directory in subdirectory', t => { + t.falsy(fs.existsSync(`${t.context.tmp}/d1`)); + shell.mkdir('-p', `${t.context.tmp}/d1/d2`); + t.truthy(fs.existsSync(`${t.context.tmp}/d1/d2`)); + const result = shell.mkdir('-p', `${t.context.tmp}/d1/d2/d3`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/d1/d2/d3`)); +}); + +test('globbed dir', t => { + let result = shell.mkdir('-p', `${t.context.tmp}/mydir`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/mydir`)); + result = shell.mkdir('-p', `${t.context.tmp}/m*ir`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/mydir`)); + t.falsy(fs.existsSync(`${t.context.tmp}/m*ir`)); // doesn't create literal name +}); + +test('non-normalized paths are still ok with -p', t => { + const result = shell.mkdir('-p', `${t.context.tmp}/asdf/../asdf/./`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.truthy(fs.existsSync(`${t.context.tmp}/asdf`)); +}); diff --git a/test/mv.js b/test/mv.js index a519bf40e..f6c34e6aa 100644 --- a/test/mv.js +++ b/test/mv.js @@ -1,166 +1,235 @@ -var shell = require('..'); +const fs = require('fs'); -var assert = require('assert'), - fs = require('fs'), - numLines = require('./utils/utils').numLines; +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const utils = require('./utils/utils'); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +const CWD = process.cwd(); +const numLines = utils.numLines; + +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.cp('-r', 'test/resources', t.context.tmp); + shell.cd(t.context.tmp); +}); + +test.afterEach.always(t => { + process.chdir(CWD); + shell.rm('-rf', t.context.tmp); +}); -// Prepare tmp/ -shell.cp('resources/*', 'tmp'); // // Invalids // -var result = shell.mv(); -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mv: missing and/or '); - -result = shell.mv('file1'); -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mv: missing and/or '); - -result = shell.mv('-f'); -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mv: missing and/or '); - -result = shell.mv('-Z', 'tmp/file1', 'tmp/file1'); // option not supported -assert.ok(shell.error()); -assert.equal(fs.existsSync('tmp/file1'), true); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mv: option not recognized: Z'); - -result = shell.mv('asdfasdf', 'tmp'); // source does not exist -assert.ok(shell.error()); -assert.equal(numLines(shell.error()), 1); -assert.equal(fs.existsSync('tmp/asdfasdf'), false); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mv: no such file or directory: asdfasdf'); - -result = shell.mv('asdfasdf1', 'asdfasdf2', 'tmp'); // sources do not exist -assert.ok(shell.error()); -assert.equal(numLines(shell.error()), 2); -assert.equal(fs.existsSync('tmp/asdfasdf1'), false); -assert.equal(fs.existsSync('tmp/asdfasdf2'), false); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mv: no such file or directory: asdfasdf1\nmv: no such file or directory: asdfasdf2'); - -result = shell.mv('asdfasdf1', 'asdfasdf2', 'tmp/file1'); // too many sources (dest is file) -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mv: dest is not a directory (too many sources)'); - -// -n is no-force/no-clobber -result = shell.mv('-n', 'tmp/file1', 'tmp/file2'); // dest already exists -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mv: dest file already exists: tmp/file2'); - -// -f is the default behavior -shell.cp('tmp/file1', 'tmp/tmp_file'); -result = shell.mv('tmp/tmp_file', 'tmp/file2'); // dest already exists (but that's ok) -assert.ok(!shell.error()); -assert.ok(!result.stderr); -assert.equal(result.code, 0); - -// -fn is the same as -n -result = shell.mv('-fn', 'tmp/file1', 'tmp/file2'); -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mv: dest file already exists: tmp/file2'); - -result = shell.mv('tmp/file1', 'tmp/file2', 'tmp/a_file'); // too many sources (exist, but dest is file) -assert.ok(shell.error()); -assert.equal(fs.existsSync('tmp/a_file'), false); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mv: dest is not a directory (too many sources)'); - -result = shell.mv('tmp/file*', 'tmp/file1'); // can't use wildcard when dest is file -assert.ok(shell.error()); -assert.equal(fs.existsSync('tmp/file1'), true); -assert.equal(fs.existsSync('tmp/file2'), true); -assert.equal(fs.existsSync('tmp/file1.js'), true); -assert.equal(fs.existsSync('tmp/file2.js'), true); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'mv: dest is not a directory (too many sources)'); +test('no args', t => { + const result = shell.mv(); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mv: missing and/or '); +}); + +test('one arg', t => { + const result = shell.mv('file1'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mv: missing and/or '); +}); + +test('option only', t => { + const result = shell.mv('-f'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mv: missing and/or '); +}); + +test('option not supported', t => { + t.truthy(fs.existsSync('file1')); // precondition + const result = shell.mv('-Z', 'file1', 'file1'); + t.truthy(shell.error()); + t.truthy(fs.existsSync('file1')); + t.is(result.code, 1); + t.is(result.stderr, 'mv: option not recognized: Z'); +}); + +test('source does not exist', t => { + const result = shell.mv('asdfasdf', '..'); + t.truthy(shell.error()); + t.is(numLines(shell.error()), 1); + t.falsy(fs.existsSync('../asdfasdf')); + t.is(result.code, 1); + t.is(result.stderr, 'mv: no such file or directory: asdfasdf'); +}); + +test('sources do not exist', t => { + const result = shell.mv('asdfasdf1', 'asdfasdf2', '..'); + t.truthy(shell.error()); + t.is(numLines(shell.error()), 2); + t.falsy(fs.existsSync('../asdfasdf1')); + t.falsy(fs.existsSync('../asdfasdf2')); + t.is(result.code, 1); + t.is( + result.stderr, + 'mv: no such file or directory: asdfasdf1\nmv: no such file or directory: asdfasdf2' + ); +}); + +test('too many sources (dest is file)', t => { + const result = shell.mv('asdfasdf1', 'asdfasdf2', 'file1'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mv: dest is not a directory (too many sources)'); +}); + +test('-n is no-force/no-clobber', t => { + const result = shell.mv('-n', 'file1', 'file2'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mv: dest file already exists: file2'); +}); + +test('-n option with a directory as the destination', t => { + shell.cp('file1', 'cp'); // copy it so we're sure it's already there + const result = shell.mv('-n', 'file1', 'cp'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mv: dest file already exists: cp/file1'); +}); + +test('-f is the default behavior', t => { + const result = shell.mv('file1', 'file2'); // dest already exists (but that's ok) + t.falsy(shell.error()); + t.falsy(result.stderr); + t.is(result.code, 0); +}); + +test('-fn is the same as -n', t => { + const result = shell.mv('-fn', 'file1', 'file2'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'mv: dest file already exists: file2'); +}); + +test('too many sources (exist, but dest is file)', t => { + const result = shell.mv('file1', 'file2', 'a_file'); + t.truthy(shell.error()); + t.falsy(fs.existsSync('a_file')); + t.is(result.code, 1); + t.is(result.stderr, 'mv: dest is not a directory (too many sources)'); +}); + +test("can't use wildcard when dest is file", t => { + const result = shell.mv('file*', 'file1'); + t.truthy(shell.error()); + t.truthy(fs.existsSync('file1')); + t.truthy(fs.existsSync('file2')); + t.truthy(fs.existsSync('file1.js')); + t.truthy(fs.existsSync('file2.js')); + t.is(result.code, 1); + t.is(result.stderr, 'mv: dest is not a directory (too many sources)'); +}); // // Valids // -shell.cd('tmp'); - -// handles self OK -shell.mkdir('tmp2'); -result = shell.mv('*', 'tmp2'); // has to handle self (tmp2 --> tmp2) without throwing error -assert.ok(shell.error()); // there's an error, but not fatal -assert.equal(fs.existsSync('tmp2/file1'), true); // moved OK -assert.equal(result.code, 1); -result = shell.mv('tmp2/*', '.'); // revert -assert.equal(fs.existsSync('file1'), true); // moved OK -assert.equal(result.code, 0); - -result = shell.mv('file1', 'file3'); // one source -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('file1'), false); -assert.equal(fs.existsSync('file3'), true); -result = shell.mv('file3', 'file1'); // revert -assert.equal(shell.error(), null); -assert.equal(fs.existsSync('file1'), true); -assert.equal(result.code, 0); - -// two sources -shell.rm('-rf', 't'); -shell.mkdir('-p', 't'); -result = shell.mv('file1', 'file2', 't'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('file1'), false); -assert.equal(fs.existsSync('file2'), false); -assert.equal(fs.existsSync('t/file1'), true); -assert.equal(fs.existsSync('t/file2'), true); -result = shell.mv('t/*', '.'); // revert -assert.equal(result.code, 0); -assert.equal(fs.existsSync('file1'), true); -assert.equal(fs.existsSync('file2'), true); - -// two sources, array style -shell.rm('-rf', 't'); -shell.mkdir('-p', 't'); -result = shell.mv(['file1', 'file2'], 't'); // two sources -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('file1'), false); -assert.equal(fs.existsSync('file2'), false); -assert.equal(fs.existsSync('t/file1'), true); -assert.equal(fs.existsSync('t/file2'), true); -result = shell.mv('t/*', '.'); // revert -assert.equal(fs.existsSync('file1'), true); -assert.equal(fs.existsSync('file2'), true); - -result = shell.mv('file*.js', 't'); // wildcard -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('file1.js'), false); -assert.equal(fs.existsSync('file2.js'), false); -assert.equal(fs.existsSync('t/file1.js'), true); -assert.equal(fs.existsSync('t/file2.js'), true); -result = shell.mv('t/*', '.'); // revert -assert.equal(fs.existsSync('file1.js'), true); -assert.equal(fs.existsSync('file2.js'), true); - -result = shell.mv('-f', 'file1', 'file2'); // dest exists, but -f given -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('file1'), false); -assert.equal(fs.existsSync('file2'), true); - -shell.exit(123); +test('handles self OK', t => { + const tmp2 = `${t.context.tmp}-2`; + shell.mkdir(tmp2); + let result = shell.mv('*', tmp2); // has to handle self (tmp2 --> tmp2) without throwing error + t.truthy(shell.error()); // there's an error, but not fatal + t.truthy(fs.existsSync(`${tmp2}/file1`)); // moved OK + t.is(result.code, 1); + result = shell.mv(`${tmp2}/*`, '.'); // revert + t.truthy(fs.existsSync('file1')); // moved OK + t.is(result.code, 0); +}); + +test('one source', t => { + let result = shell.mv('file1', 'file3'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(fs.existsSync('file1')); + t.truthy(fs.existsSync('file3')); + result = shell.mv('file3', 'file1'); // revert + t.falsy(shell.error()); + t.truthy(fs.existsSync('file1')); + t.is(result.code, 0); +}); + +test('two sources', t => { + const result = shell.mv('file1', 'file2', 'cp'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(fs.existsSync('file1')); + t.falsy(fs.existsSync('file2')); + t.truthy(fs.existsSync('cp/file1')); + t.truthy(fs.existsSync('cp/file2')); +}); + +test('two sources, array style', t => { + shell.rm('-rf', 't'); + shell.mkdir('-p', 't'); + let result = shell.mv(['file1', 'file2'], 't'); // two sources + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(fs.existsSync('file1')); + t.falsy(fs.existsSync('file2')); + t.truthy(fs.existsSync('t/file1')); + t.truthy(fs.existsSync('t/file2')); + result = shell.mv('t/*', '.'); // revert + t.truthy(fs.existsSync('file1')); + t.truthy(fs.existsSync('file2')); +}); + +test('wildcard', t => { + shell.mkdir('-p', 't'); + let result = shell.mv('file*.js', 't'); // wildcard + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(fs.existsSync('file1.js')); + t.falsy(fs.existsSync('file2.js')); + t.truthy(fs.existsSync('t/file1.js')); + t.truthy(fs.existsSync('t/file2.js')); + result = shell.mv('t/*', '.'); // revert + t.truthy(fs.existsSync('file1.js')); + t.truthy(fs.existsSync('file2.js')); +}); + +test('dest exists, but -f given', t => { + const result = shell.mv('-f', 'file1', 'file2'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(fs.existsSync('file1')); + t.truthy(fs.existsSync('file2')); +}); + +test('should not overwrite recently created files', t => { + shell.mkdir('-p', 't'); + const result = shell.mv('file1', 'cp/file1', 't/'); + t.truthy(shell.error()); + t.is(result.code, 1); + + // Ensure First file is copied + t.is(shell.cat('t/file1').toString(), 'test1'); + t.is( + result.stderr, + "mv: will not overwrite just-created 't/file1' with 'cp/file1'" + ); + t.truthy(fs.existsSync('cp/file1')); +}); + + +test('should not overwrite recently created files (not give error no-force mode)', t => { + shell.mkdir('-p', 't'); + const result = shell.mv('-n', 'file1', 'cp/file1', 't/'); + t.falsy(shell.error()); + t.is(result.code, 0); + + // Ensure First file is moved + t.is(shell.cat('t/file1').toString(), 'test1'); + t.truthy(fs.existsSync('cp/file1')); +}); diff --git a/test/pipe.js b/test/pipe.js index 5bc273942..a8aabb928 100644 --- a/test/pipe.js +++ b/test/pipe.js @@ -1,78 +1,80 @@ -var shell = require('..'); +const test = require('ava'); -var assert = require('assert'); +const shell = require('..'); shell.config.silent = true; -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); - // // Invalids // -// commands like `rm` can't be on the right side of pipes -assert.equal(typeof shell.ls('.').rm, 'undefined'); -assert.equal(typeof shell.cat('resources/file1.txt').rm, 'undefined'); +test('commands like `rm` cannot be on the right side of pipes', t => { + t.is(shell.ls('.').rm, undefined); + t.is(shell.cat('test/resources/file1.txt').rm, undefined); +}); // // Valids // -// piping to cat() should return roughly the same thing -assert.strictEqual(shell.cat('resources/file1.txt').cat().toString(), - shell.cat('resources/file1.txt').toString()); +test('piping to cat() should return roughly the same thing', t => { + t.is( + shell.cat('test/resources/file1.txt').cat().toString(), + shell.cat('test/resources/file1.txt').toString() + ); +}); + +test('piping ls() into cat() converts to a string-like object', t => { + t.is(shell.ls('test/resources/').cat().toString(), shell.ls('test/resources/').stdout); +}); + +test('grep works in a pipe', t => { + const result = shell.ls('test/resources/').grep('file1'); + t.is(result.toString(), 'file1\nfile1.js\nfile1.txt\n'); +}); -// piping ls() into cat() converts to a string -assert.strictEqual(shell.ls('resources/').cat().toString(), - shell.ls('resources/').stdout); +test('multiple pipes work', t => { + const result = shell.ls('test/resources/').cat().grep('file1'); + t.is(result.toString(), 'file1\nfile1.js\nfile1.txt\n'); +}); -var result; -result = shell.ls('resources/').grep('file1'); -assert.equal(result + '', 'file1\nfile1.js\nfile1.txt\n'); +test('Equivalent to a simple grep() test case', t => { + const result = shell.cat('test/resources/grep/file').grep(/alpha*beta/); + t.falsy(shell.error()); + t.is(result.toString(), 'alphaaaaaaabeta\nalphbeta\n'); +}); -result = shell.ls('resources/').cat().grep('file1'); -assert.equal(result + '', 'file1\nfile1.js\nfile1.txt\n'); +test('Equivalent to a simple sed() test case', t => { + const result = shell.cat('test/resources/grep/file').sed(/l*\.js/, ''); + t.falsy(shell.error()); + t.is( + result.toString(), + 'alphaaaaaaabeta\nhowareyou\nalphbeta\nthis line ends in\n\n' + ); +}); -// Equivalent to a simple grep() test case -result = shell.cat('resources/grep/file').grep(/alpha*beta/); -assert.equal(shell.error(), null); -assert.equal(result.toString(), 'alphaaaaaaabeta\nalphbeta\n'); +test('Sort a file by frequency of each line', t => { + const result = shell.sort('test/resources/uniq/pipe').uniq('-c').sort('-n'); + t.falsy(shell.error()); + t.is(result.toString(), shell.cat('test/resources/uniq/pipeSorted').toString()); +}); -// Equivalent to a simple sed() test case -var result = shell.cat('resources/grep/file').sed(/l*\.js/, ''); -assert.ok(!shell.error()); -assert.equal(result.toString(), 'alphaaaaaaabeta\nhowareyou\nalphbeta\nthis line ends in\n\n'); +test('Synchronous exec', t => { + const result = shell.cat('test/resources/grep/file').exec('shx grep "alpha*beta"'); + t.falsy(shell.error()); + t.is(result.toString(), 'alphaaaaaaabeta\nalphbeta\n'); +}); -// Synchronous exec -// TODO: add windows tests -if (process.platform !== 'win32') { - // unix-specific - if (shell.which('grep').stdout) { - result = shell.cat('resources/grep/file').exec("grep 'alpha*beta'"); - assert.equal(shell.error(), null); - assert.equal(result, 'alphaaaaaaabeta\nalphbeta\n'); - } else { - console.error('Warning: Cannot verify piped exec'); - } -} else { - console.error('Warning: Cannot verify piped exec'); -} +test('Asynchronous exec', async t => { + const promise = new Promise(resolve => { + shell.cat('test/resources/grep/file') + .exec('shx grep "alpha*beta"', (code, stdout, stderr) => { + resolve({ code, stdout, stderr }); + }); + }); -// Async exec -// TODO: add windows tests -if (process.platform !== 'win32') { - // unix-specific - if (shell.which('grep').stdout) { - shell.cat('resources/grep/file').exec("grep 'alpha*beta'", function(code, stdout) { - assert.equal(code, 0); - assert.equal(stdout, 'alphaaaaaaabeta\nalphbeta\n'); - shell.exit(123); - }); - } else { - console.error('Warning: Cannot verify piped exec'); - } -} else { - console.error('Warning: Cannot verify piped exec'); - shell.exit(123); -} + const result = await promise; + t.is(result.code, 0); + t.is(result.stdout, 'alphaaaaaaabeta\nalphbeta\n'); + t.is(result.stderr, ''); +}); diff --git a/test/plugin.js b/test/plugin.js new file mode 100644 index 000000000..59626c1c0 --- /dev/null +++ b/test/plugin.js @@ -0,0 +1,167 @@ +const test = require('ava'); + +// This is the supported order for importing these files +const plugin = require('../plugin'); +const shell = require('..'); + +let data = 0; +let fname; + +function fooImplementation(options, arg) { + // Some sort of side effect, so we know when this is called + if (arg) { + fname = arg; + } else { + fname = plugin.readFromPipe(); + } + + if (arg === 'exitWithCode5') { + plugin.error('Exited with code 5', 5); + } else if (arg === 'changePrefix') { + plugin.error('prefix was changed', { + prefix: 'prefix: ', + }); + } else if (arg === 'continue') { + plugin.error('Error, but continuing', { + continue: true, + }); + } + + if (options.flag) { + data = 12; + } else { + data++; + } + return 'hello world'; +} + +test.beforeEach(() => { + shell.config.resetForTesting(); +}); + +// +// Invalids +// + +test('Unable to register a plugin with unknown options', t => { + t.throws(() => { + plugin.register('foo', fooImplementation, { + foobar: true, + }); + }, { instanceOf: Error }); +}); + +test('Unable to register a plugin with wrong option types', t => { + t.throws(() => { + plugin.register('foo', fooImplementation, { + wrapOutput: 'true', // should be a boolean + }); + }, { instanceOf: TypeError }); +}); + +// +// Valids +// + +test('All plugin utils exist', t => { + t.is(typeof plugin.error, 'function'); + t.is(typeof plugin.parseOptions, 'function'); + t.is(typeof plugin.readFromPipe, 'function'); + t.is(typeof plugin.register, 'function'); +}); + +test("The plugin does not exist before it's registered", t => { + t.falsy(shell.foo); +}); + +test('Register the plugin', t => { + plugin.register('foo', fooImplementation, { + cmdOptions: { + f: 'flag', + }, + wrapOutput: true, + canReceivePipe: true, + }); + t.pass(); +}); + +test('The plugin exists after registering', t => { + t.is(typeof shell.foo, 'function'); +}); + +test('The command fails for invalid options', t => { + const result = shell.foo('-n', 'filename'); + t.is(result.code, 1); + t.is(result.stdout, ''); + t.is(result.stderr, 'foo: option not recognized: n'); + t.is(shell.error(), 'foo: option not recognized: n'); +}); + +test('The command succeeds for normal calls', t => { + t.is(data, 0); + shell.foo('filename'); + t.is(data, 1); + t.is(fname, 'filename'); + shell.foo('filename2'); + t.is(data, 2); + t.is(fname, 'filename2'); +}); + +test('The command parses options', t => { + shell.foo('-f', 'filename'); + t.is(data, 12); + t.is(fname, 'filename'); +}); + +test('The command supports globbing by default', t => { + shell.foo('-f', 'test/re*u?ces'); + t.is(data, 12); + t.is(fname, 'test/resources'); +}); + +test('Plugins are also compatible with shelljs/global', t => { + require('../global'); + t.is(typeof global.foo, 'function'); + t.is(global.foo, shell.foo); +}); + +test('Plugins can be added as methods to ShellStrings', t => { + const result = shell.ShellString('hello world\n'); + t.is(result.toString(), 'hello world\n'); + t.is(typeof result.grep, 'function'); // existing methods persist + t.is(typeof result.foo, 'function'); + result.foo(); + t.is(fname, 'hello world\n'); // readFromPipe() works +}); + +test('Plugins can signal errors', t => { + const result = shell.foo('exitWithCode5'); + t.is(result.code, 5); + t.is(result.stdout, ''); + t.is(result.stderr, 'foo: Exited with code 5'); + t.is(shell.error(), 'foo: Exited with code 5'); +}); + +test('Plugins can change the prefix', t => { + const result = shell.foo('changePrefix'); + t.is(result.code, 1); + t.is(result.stdout, ''); + t.is(result.stderr, 'prefix: prefix was changed'); + t.is(shell.error(), 'prefix: prefix was changed'); +}); + +test('Plugins can continue from errors', t => { + const result = shell.foo('continue'); + t.is(result.code, 1); + t.is(result.stdout, 'hello world'); + t.is(result.stderr, 'foo: Error, but continuing'); + t.is(shell.error(), 'foo: Error, but continuing'); +}); + +test('Cannot overwrite an existing command', t => { + const oldCat = shell.cat; + t.throws(() => { + plugin.register('cat', fooImplementation); + }, { message: 'Command `cat` already exists' }); + t.is(shell.cat, oldCat); +}); diff --git a/test/popd.js b/test/popd.js index cb7371c25..e02880b75 100644 --- a/test/popd.js +++ b/test/popd.js @@ -1,114 +1,159 @@ -var shell = require('..'); +const path = require('path'); -var assert = require('assert'), - path = require('path'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const mocks = require('./utils/mocks'); -var root = path.resolve(), trail; +const rootDir = path.resolve(); function reset() { - shell.dirs('-c'); - shell.cd(root); + shell.dirs('-c'); + shell.cd(rootDir); } -// Valid -shell.pushd('resources/pushd'); -trail = shell.popd(); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ root ]); - -shell.pushd('resources/pushd'); -shell.pushd('a'); -trail = shell.popd(); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd'), - root -]); - -shell.pushd('b'); -trail = shell.popd(); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd'), - root -]); - -shell.pushd('b'); -shell.pushd('c'); -trail = shell.popd(); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd/b'), - path.resolve(root, 'resources/pushd'), - root -]); - -trail = shell.popd(); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd'), - root -]); - -trail = shell.popd(); -assert.equal(shell.error(), null); -assert.equal(trail.length, 1); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ root ]); - -// Valid by index -shell.pushd('resources/pushd'); -trail = shell.popd('+0'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ root ]); - -shell.pushd('resources/pushd'); -trail = shell.popd('+1'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ path.resolve(root, 'resources/pushd') ]); - -reset(); shell.pushd('resources/pushd'); -trail = shell.popd('-0'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ path.resolve(root, 'resources/pushd') ]); - -reset(); shell.pushd('resources/pushd'); -trail = shell.popd('-1'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ root ]); - - -reset(); shell.pushd('resources/pushd'); -trail = shell.popd('-n'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ path.resolve(root, 'resources/pushd') ]); - -// Invalid -trail = shell.popd(); -assert.ok(shell.error('popd: directory stack empty\n')); - -// Test that the root dir is not stored -shell.cd('resources/pushd'); -shell.pushd('b'); -trail = shell.popd(); -assert.equal(shell.error(), null); -assert.equal(trail[0], path.resolve(root, 'resources/pushd')); -assert.equal(process.cwd(), trail[0]); -shell.popd(); -assert.ok(shell.error(), null); - -shell.cd(root); - -shell.exit(123); \ No newline at end of file +test.beforeEach(() => { + shell.config.resetForTesting(); + reset(); +}); + + +test.after.always(() => { + reset(); +}); + +// +// Valids +// + +test('basic usage', t => { + shell.pushd('test/resources/pushd'); + const trail = shell.popd(); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [rootDir]); +}); + +test('two directories on the stack', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + const trail = shell.popd(); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + ]); +}); + +test('three directories on the stack', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('b'); + shell.pushd('c'); + const trail = shell.popd(); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd/b'), + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + ]); +}); + +test('Valid by index', t => { + shell.pushd('test/resources/pushd'); + const trail = shell.popd('+0'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [rootDir]); +}); + +test('Using +1 option', t => { + shell.pushd('test/resources/pushd'); + const trail = shell.popd('+1'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [path.resolve(rootDir, 'test/resources/pushd')]); +}); + +test('Using -0 option', t => { + shell.pushd('test/resources/pushd'); + const trail = shell.popd('-0'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [path.resolve(rootDir, 'test/resources/pushd')]); +}); + +test('Using -1 option', t => { + shell.pushd('test/resources/pushd'); + const trail = shell.popd('-1'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [rootDir]); +}); + +test('Using -n option', t => { + shell.pushd('test/resources/pushd'); + const trail = shell.popd('-n'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [path.resolve(rootDir, 'test/resources/pushd')]); +}); + +test('Popping an empty stack', t => { + shell.popd(); + t.truthy(shell.error('popd: directory stack empty\n')); +}); + +test('Test that rootDir is not stored', t => { + shell.cd('test/resources/pushd'); + shell.pushd('b'); + const trail = shell.popd(); + t.falsy(shell.error()); + t.is(trail[0], path.resolve(rootDir, 'test/resources/pushd')); + t.is(process.cwd(), trail[0]); + shell.popd(); // no more in the stack + t.truthy(shell.error()); +}); + +test('quiet mode off', t => { + try { + shell.pushd('test/resources/pushd'); + shell.config.silent = false; + mocks.stdout.init(); + mocks.stderr.init(); + const trail = shell.popd(); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(stdout, ''); + t.is(stderr, `${rootDir}\n`); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [rootDir]); + } finally { + shell.config.silent = true; + mocks.stdout.restore(); + mocks.stderr.restore(); + } +}); + +test('quiet mode on', t => { + try { + shell.pushd('test/resources/pushd'); + shell.config.silent = false; + mocks.stdout.init(); + mocks.stderr.init(); + const trail = shell.popd('-q'); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(stdout, ''); + t.is(stderr, ''); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [rootDir]); + } finally { + shell.config.silent = true; + mocks.stdout.restore(); + mocks.stderr.restore(); + } +}); diff --git a/test/pushd.js b/test/pushd.js index d588c13ff..987dc093d 100644 --- a/test/pushd.js +++ b/test/pushd.js @@ -1,224 +1,377 @@ -var shell = require('..'); +const path = require('path'); -var assert = require('assert'), - path = require('path'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const mocks = require('./utils/mocks'); -var root = path.resolve(), trail; +const rootDir = path.resolve(); function reset() { - shell.dirs('-c'); - shell.cd(root); + shell.dirs('-c'); + shell.cd(rootDir); } -// Push valid directories -trail = shell.pushd('resources/pushd'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd'), - root -]); - -trail = shell.pushd('a'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd'), - root -]); - -trail = shell.pushd('../b'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd/b'), - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd'), - root -]); - -trail = shell.pushd('c'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd/b/c'), - path.resolve(root, 'resources/pushd/b'), - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd'), - root -]); - -// Push stuff around with positive indices -trail = shell.pushd('+0'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd/b/c'), - path.resolve(root, 'resources/pushd/b'), - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd'), - root -]); - -trail = shell.pushd('+1'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd/b'), - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd'), - root, - path.resolve(root, 'resources/pushd/b/c') -]); - -trail = shell.pushd('+2'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd'), - root, - path.resolve(root, 'resources/pushd/b/c'), - path.resolve(root, 'resources/pushd/b'), - path.resolve(root, 'resources/pushd/a') -]); - -trail = shell.pushd('+3'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd/b'), - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd'), - root, - path.resolve(root, 'resources/pushd/b/c') -]); - -trail = shell.pushd('+4'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd/b/c'), - path.resolve(root, 'resources/pushd/b'), - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd'), - root -]); - -// Push stuff around with negative indices -trail = shell.pushd('-0'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - root, - path.resolve(root, 'resources/pushd/b/c'), - path.resolve(root, 'resources/pushd/b'), - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd') -]); - -trail = shell.pushd('-1'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd'), - root, - path.resolve(root, 'resources/pushd/b/c'), - path.resolve(root, 'resources/pushd/b') -]); - -trail = shell.pushd('-2'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - root, - path.resolve(root, 'resources/pushd/b/c'), - path.resolve(root, 'resources/pushd/b'), - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd') -]); - -trail = shell.pushd('-3'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd/b/c'), - path.resolve(root, 'resources/pushd/b'), - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd'), - root -]); - -trail = shell.pushd('-4'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - path.resolve(root, 'resources/pushd/b/c'), - path.resolve(root, 'resources/pushd/b'), - path.resolve(root, 'resources/pushd/a'), - path.resolve(root, 'resources/pushd'), - root -]); - -// Push without changing directory or resolving paths -reset(); trail = shell.pushd('-n', 'resources/pushd'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - root, - 'resources/pushd' -]); - -trail = shell.pushd('-n', 'resources/pushd/a'); -assert.equal(shell.error(), null); -assert.equal(process.cwd(), trail[0]); -assert.deepEqual(trail, [ - root, - 'resources/pushd/a', - 'resources/pushd' -]); - -// Push invalid directory -shell.pushd('does/not/exist'); -assert.equal(shell.error(), 'pushd: no such file or directory: ' + path.resolve('.', 'does/not/exist')); -assert.equal(process.cwd(), trail[0]); - -// Push without arguments should swap top two directories when stack length is 2 -reset(); trail = shell.pushd('resources/pushd'); -assert.equal(shell.error(), null); -assert.equal(trail.length, 2); -assert.equal(path.relative(root, trail[0]), path.join('resources', 'pushd')); -assert.equal(trail[1], root); -assert.equal(process.cwd(), trail[0]); -trail = shell.pushd(); -assert.equal(shell.error(), null); -assert.equal(trail.length, 2); -assert.equal(trail[0], root); -assert.equal(path.relative(root, trail[1]), path.join('resources', 'pushd')); -assert.equal(process.cwd(), trail[0]); - -// Push without arguments should swap top two directories when stack length is > 2 -trail = shell.pushd('resources/pushd/a'); -assert.equal(shell.error(), null); -assert.equal(trail.length, 3); -assert.equal(path.relative(root, trail[0]), path.join('resources', 'pushd', 'a')); -assert.equal(trail[1], root); -assert.equal(path.relative(root, trail[2]), path.join('resources', 'pushd')); -assert.equal(process.cwd(), trail[0]); - -trail = shell.pushd(); -assert.equal(shell.error(), null); -assert.equal(trail.length, 3); -assert.equal(trail[0], root); -assert.equal(path.relative(root, trail[1]), path.join('resources', 'pushd', 'a')); -assert.equal(path.relative(root, trail[2]), path.join('resources', 'pushd')); -assert.equal(process.cwd(), trail[0]); - -// Push without arguments invalid when stack is empty -reset(); shell.pushd(); -assert.equal(shell.error(), 'pushd: no other directory'); - -shell.exit(123); +test.beforeEach(() => { + shell.config.resetForTesting(); + reset(); +}); + +test.after.always(() => { + reset(); +}); + +// +// Valids +// + +test('Push valid directories', t => { + const trail = shell.pushd('test/resources/pushd'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + ]); +}); + +test('Two directories', t => { + shell.pushd('test/resources/pushd'); + const trail = shell.pushd('a'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd/a'), + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + ]); +}); + +test('Three directories', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + const trail = shell.pushd('../b'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd/b'), + path.resolve(rootDir, 'test/resources/pushd/a'), + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + ]); +}); + +test('Four directories', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + shell.pushd('../b'); + const trail = shell.pushd('c'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd/b/c'), + path.resolve(rootDir, 'test/resources/pushd/b'), + path.resolve(rootDir, 'test/resources/pushd/a'), + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + ]); +}); + +test('Push stuff around with positive indices', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + shell.pushd('../b'); + shell.pushd('c'); + const trail = shell.pushd('+0'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd/b/c'), + path.resolve(rootDir, 'test/resources/pushd/b'), + path.resolve(rootDir, 'test/resources/pushd/a'), + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + ]); +}); + +test('+1 option', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + shell.pushd('../b'); + shell.pushd('c'); + const trail = shell.pushd('+1'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd/b'), + path.resolve(rootDir, 'test/resources/pushd/a'), + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + path.resolve(rootDir, 'test/resources/pushd/b/c'), + ]); +}); + +test('+2 option', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + shell.pushd('../b'); + shell.pushd('c'); + const trail = shell.pushd('+2'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd/a'), + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + path.resolve(rootDir, 'test/resources/pushd/b/c'), + path.resolve(rootDir, 'test/resources/pushd/b'), + ]); +}); + +test('+3 option', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + shell.pushd('../b'); + shell.pushd('c'); + const trail = shell.pushd('+3'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + path.resolve(rootDir, 'test/resources/pushd/b/c'), + path.resolve(rootDir, 'test/resources/pushd/b'), + path.resolve(rootDir, 'test/resources/pushd/a'), + ]); +}); + +test('+4 option', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + shell.pushd('../b'); + shell.pushd('c'); + const trail = shell.pushd('+4'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + rootDir, + path.resolve(rootDir, 'test/resources/pushd/b/c'), + path.resolve(rootDir, 'test/resources/pushd/b'), + path.resolve(rootDir, 'test/resources/pushd/a'), + path.resolve(rootDir, 'test/resources/pushd'), + ]); +}); + +test('Push stuff around with negative indices', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + shell.pushd('../b'); + shell.pushd('c'); + const trail = shell.pushd('-0'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + rootDir, + path.resolve(rootDir, 'test/resources/pushd/b/c'), + path.resolve(rootDir, 'test/resources/pushd/b'), + path.resolve(rootDir, 'test/resources/pushd/a'), + path.resolve(rootDir, 'test/resources/pushd'), + ]); +}); + +test('-1 option', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + shell.pushd('../b'); + shell.pushd('c'); + const trail = shell.pushd('-1'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + path.resolve(rootDir, 'test/resources/pushd/b/c'), + path.resolve(rootDir, 'test/resources/pushd/b'), + path.resolve(rootDir, 'test/resources/pushd/a'), + ]); +}); + +test('-2 option', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + shell.pushd('../b'); + shell.pushd('c'); + const trail = shell.pushd('-2'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd/a'), + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + path.resolve(rootDir, 'test/resources/pushd/b/c'), + path.resolve(rootDir, 'test/resources/pushd/b'), + ]); +}); + +test('-3 option', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + shell.pushd('../b'); + shell.pushd('c'); + const trail = shell.pushd('-3'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd/b'), + path.resolve(rootDir, 'test/resources/pushd/a'), + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + path.resolve(rootDir, 'test/resources/pushd/b/c'), + ]); +}); + +test('-4 option', t => { + shell.pushd('test/resources/pushd'); + shell.pushd('a'); + shell.pushd('../b'); + shell.pushd('c'); + const trail = shell.pushd('-4'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd/b/c'), + path.resolve(rootDir, 'test/resources/pushd/b'), + path.resolve(rootDir, 'test/resources/pushd/a'), + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + ]); +}); + +test('Push without changing directory or resolving paths', t => { + const trail = shell.pushd('-n', 'test/resources/pushd'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + rootDir, + 'test/resources/pushd', + ]); +}); + +test('Using the -n option with a non-empty stack', t => { + shell.pushd('-n', 'test/resources/pushd'); + const trail = shell.pushd('-n', 'test/resources/pushd/a'); + t.falsy(shell.error()); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + rootDir, + 'test/resources/pushd/a', + 'test/resources/pushd', + ]); +}); + +test('Push invalid directory', t => { + const oldCwd = process.cwd(); + shell.pushd('does/not/exist'); + t.is( + shell.error(), + `pushd: no such file or directory: ${path.resolve('.', 'does/not/exist') + .replace(/\\/g, '/')}` + ); + t.is(process.cwd(), oldCwd); +}); + +test( + 'Push without args swaps top two directories when stack length is 2', + t => { + let trail = shell.pushd('test/resources/pushd'); + t.falsy(shell.error()); + t.is(trail.length, 2); + t.is(path.relative(rootDir, trail[0]), path.join('test/resources', 'pushd')); + t.is(trail[1], rootDir); + t.is(process.cwd(), trail[0]); + trail = shell.pushd(); + t.falsy(shell.error()); + t.is(trail.length, 2); + t.is(trail[0], rootDir); + t.is(path.relative(rootDir, trail[1]), path.join('test/resources', 'pushd')); + t.is(process.cwd(), trail[0]); + } +); + +test( + 'Push without args swaps top two directories for larger stacks', + t => { + shell.pushd('test/resources/pushd'); + shell.pushd(); + const trail = shell.pushd('test/resources/pushd/a'); + t.falsy(shell.error()); + t.is(trail.length, 3); + t.is(path.relative(rootDir, trail[0]), path.join('test/resources', 'pushd', 'a')); + t.is(trail[1], rootDir); + t.is(path.relative(rootDir, trail[2]), path.join('test/resources', 'pushd')); + t.is(process.cwd(), trail[0]); + } +); + +test('Pushing with no args', t => { + shell.pushd('-n', 'test/resources/pushd'); + shell.pushd('test/resources/pushd/a'); + const trail = shell.pushd(); + t.falsy(shell.error()); + t.is(trail.length, 3); + t.is(trail[0], rootDir); + t.is(path.relative(rootDir, trail[1]), path.join('test/resources', 'pushd', 'a')); + t.is(path.relative(rootDir, trail[2]), path.join('test/resources', 'pushd')); + t.is(process.cwd(), trail[0]); +}); + +test('Push without arguments invalid when stack is empty', t => { + shell.pushd(); + t.is(shell.error(), 'pushd: no other directory'); +}); + +test('quiet mode off', t => { + try { + shell.config.silent = false; + mocks.stdout.init(); + mocks.stderr.init(); + const trail = shell.pushd('test/resources/pushd'); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(stdout, ''); + t.is(stderr, `${path.resolve(rootDir, 'test/resources/pushd')} ${rootDir}\n`); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + ]); + } finally { + shell.config.silent = true; + mocks.stdout.restore(); + mocks.stderr.restore(); + } +}); + +test('quiet mode on', t => { + try { + shell.config.silent = false; + mocks.stdout.init(); + mocks.stderr.init(); + const trail = shell.pushd('-q', 'test/resources/pushd'); + const stdout = mocks.stdout.getValue(); + const stderr = mocks.stderr.getValue(); + t.falsy(shell.error()); + t.is(stdout, ''); + t.is(stderr, ''); + t.is(process.cwd(), trail[0]); + t.deepEqual(trail, [ + path.resolve(rootDir, 'test/resources/pushd'), + rootDir, + ]); + } finally { + shell.config.silent = true; + mocks.stdout.restore(); + mocks.stderr.restore(); + } +}); diff --git a/test/pwd.js b/test/pwd.js index fe4652597..6e6534cc5 100644 --- a/test/pwd.js +++ b/test/pwd.js @@ -1,28 +1,41 @@ -var shell = require('..'); +const path = require('path'); -var assert = require('assert'), - path = require('path'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const utils = require('./utils/utils'); + +const cur = process.cwd(); + +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.mkdir(t.context.tmp); +}); + +test.afterEach.always(t => { + process.chdir(cur); + shell.rm('-rf', t.context.tmp); +}); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); // // Valids // -var _pwd = shell.pwd(); -assert.equal(shell.error(), null); -assert.equal(_pwd.code, 0); -assert.ok(!_pwd.stderr); -assert.equal(_pwd, path.resolve('.')); - -shell.cd('tmp'); -_pwd = shell.pwd(); -assert.equal(_pwd.code, 0); -assert.ok(!_pwd.stderr); -assert.equal(shell.error(), null); -assert.equal(path.basename(_pwd.toString()), 'tmp'); +test('initial directory', t => { + const cwd = shell.pwd(); + t.falsy(shell.error()); + t.is(cwd.code, 0); + t.falsy(cwd.stderr); + t.is(cwd.toString(), path.resolve('.')); +}); -shell.exit(123); +test('after changing directory', t => { + shell.cd(t.context.tmp); + const cwd = shell.pwd(); + t.is(cwd.code, 0); + t.falsy(cwd.stderr); + t.falsy(shell.error()); + t.is(path.basename(cwd.toString()), t.context.tmp); +}); diff --git a/test/resources/cat/file3 b/test/resources/cat/file3 new file mode 100644 index 000000000..29f446afe --- /dev/null +++ b/test/resources/cat/file3 @@ -0,0 +1 @@ +test3 \ No newline at end of file diff --git a/test/resources/cat/file4 b/test/resources/cat/file4 new file mode 100644 index 000000000..f4538a5cc --- /dev/null +++ b/test/resources/cat/file4 @@ -0,0 +1,12 @@ +test4-01 +test4-02 +test4-03 +test4-04 +test4-05 +test4-06 +test4-07 +test4-08 +test4-09 +test4-10 +test4-11 +test4-12 diff --git a/test/resources/cat/file5 b/test/resources/cat/file5 new file mode 100644 index 000000000..e69de29bb diff --git a/test/resources/cp/file1 b/test/resources/cp/file1 new file mode 100644 index 000000000..180cf8328 --- /dev/null +++ b/test/resources/cp/file1 @@ -0,0 +1 @@ +test2 diff --git a/test/resources/exec/slow.js b/test/resources/exec/slow.js index cffff43f5..d8f26becd 100644 --- a/test/resources/exec/slow.js +++ b/test/resources/exec/slow.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +console.log('fast'); setTimeout(function() { console.log('slow'); }, parseInt(process.argv[2], 10)); diff --git a/test/resources/find/dir2_link b/test/resources/find/dir2_link new file mode 120000 index 000000000..1e039be90 --- /dev/null +++ b/test/resources/find/dir2_link @@ -0,0 +1 @@ +dir2 \ No newline at end of file diff --git a/test/resources/grep/case1 b/test/resources/grep/case1 new file mode 100644 index 000000000..b08f7b082 --- /dev/null +++ b/test/resources/grep/case1 @@ -0,0 +1 @@ +Test3 diff --git a/test/resources/grep/case1.js b/test/resources/grep/case1.js new file mode 100644 index 000000000..df6b0d2bc --- /dev/null +++ b/test/resources/grep/case1.js @@ -0,0 +1 @@ +test3 diff --git a/test/resources/grep/case1.txt b/test/resources/grep/case1.txt new file mode 100644 index 000000000..bac87cc8e --- /dev/null +++ b/test/resources/grep/case1.txt @@ -0,0 +1 @@ +TEST3 diff --git a/test/resources/grep/file2 b/test/resources/grep/file2 new file mode 100644 index 000000000..0c37715dc --- /dev/null +++ b/test/resources/grep/file2 @@ -0,0 +1,3 @@ +-v +-vv +-a diff --git a/test/resources/grep/file3 b/test/resources/grep/file3 new file mode 100644 index 000000000..e4bee9f8c --- /dev/null +++ b/test/resources/grep/file3 @@ -0,0 +1,15 @@ +line1 +line2 test line +line3 test line +line4 +line5 +line6 +line7 +line8 +line9 +line10 test line +line11 +line12 +line13 +line14 +line15 test line diff --git a/test/resources/sed/empty.txt b/test/resources/sed/empty.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test/resources/shjs/a-file.js b/test/resources/shjs/a-file.js deleted file mode 100644 index 3050cd9a6..000000000 --- a/test/resources/shjs/a-file.js +++ /dev/null @@ -1,2 +0,0 @@ -console.log('OK!'); -process.exit(0); diff --git a/test/resources/shjs/coffeescript.coffee b/test/resources/shjs/coffeescript.coffee deleted file mode 100644 index 5b2766405..000000000 --- a/test/resources/shjs/coffeescript.coffee +++ /dev/null @@ -1,2 +0,0 @@ -msg = 'CoffeeScript: OK!' -console.log msg diff --git a/test/resources/shjs/exit-0.js b/test/resources/shjs/exit-0.js deleted file mode 100644 index dcbbff6c9..000000000 --- a/test/resources/shjs/exit-0.js +++ /dev/null @@ -1 +0,0 @@ -process.exit(0); diff --git a/test/resources/shjs/exit-codes.js b/test/resources/shjs/exit-codes.js deleted file mode 100644 index 6427ca060..000000000 --- a/test/resources/shjs/exit-codes.js +++ /dev/null @@ -1 +0,0 @@ -process.exit(42); diff --git a/test/resources/shjs/stdout-stderr.js b/test/resources/shjs/stdout-stderr.js deleted file mode 100644 index 220cb58f8..000000000 --- a/test/resources/shjs/stdout-stderr.js +++ /dev/null @@ -1,2 +0,0 @@ -console.log('stdout: OK!'); -console.error('stderr: OK!'); diff --git a/test/resources/uniq/file1 b/test/resources/uniq/file1 new file mode 100644 index 000000000..8687fd613 --- /dev/null +++ b/test/resources/uniq/file1 @@ -0,0 +1,4 @@ +foo +bar +bar +baz diff --git a/test/resources/uniq/file1c b/test/resources/uniq/file1c new file mode 100644 index 000000000..7162bb424 --- /dev/null +++ b/test/resources/uniq/file1c @@ -0,0 +1,3 @@ + 1 foo + 2 bar + 1 baz diff --git a/test/resources/uniq/file1d b/test/resources/uniq/file1d new file mode 100644 index 000000000..5716ca598 --- /dev/null +++ b/test/resources/uniq/file1d @@ -0,0 +1 @@ +bar diff --git a/test/resources/uniq/file1t b/test/resources/uniq/file1t new file mode 100644 index 000000000..86e041dad --- /dev/null +++ b/test/resources/uniq/file1t @@ -0,0 +1,3 @@ +foo +bar +baz diff --git a/test/resources/uniq/file1u b/test/resources/uniq/file1u new file mode 100644 index 000000000..86e041dad --- /dev/null +++ b/test/resources/uniq/file1u @@ -0,0 +1,3 @@ +foo +bar +baz diff --git a/test/resources/uniq/file2 b/test/resources/uniq/file2 new file mode 100644 index 000000000..2bf57bbeb --- /dev/null +++ b/test/resources/uniq/file2 @@ -0,0 +1,4 @@ +foo +bar +Bar +baz diff --git a/test/resources/uniq/file2u b/test/resources/uniq/file2u new file mode 100644 index 000000000..86e041dad --- /dev/null +++ b/test/resources/uniq/file2u @@ -0,0 +1,3 @@ +foo +bar +baz diff --git a/test/resources/uniq/file3 b/test/resources/uniq/file3 new file mode 100644 index 000000000..86e041dad --- /dev/null +++ b/test/resources/uniq/file3 @@ -0,0 +1,3 @@ +foo +bar +baz diff --git a/test/resources/uniq/pipe b/test/resources/uniq/pipe new file mode 100644 index 000000000..b7eb19584 --- /dev/null +++ b/test/resources/uniq/pipe @@ -0,0 +1,50 @@ +bar +foo +foo +baz +foo +foo +bar +foo +baz +baz +foo +baz +baz +baz +bar +baz +bar +bar +baz +bar +baz +foo +baz +foo +baz +foo +bar +baz +bar +bar +bar +foo +foo +foo +foo +baz +foo +baz +foo +foo +bar +foo +foo +bar +foo +foo +baz +foo +foo +foo diff --git a/test/resources/uniq/pipeSorted b/test/resources/uniq/pipeSorted new file mode 100644 index 000000000..0e1845d37 --- /dev/null +++ b/test/resources/uniq/pipeSorted @@ -0,0 +1,3 @@ + 12 bar + 15 baz + 23 foo diff --git a/test/resources/which/node b/test/resources/which/node new file mode 100644 index 000000000..0b917ba31 --- /dev/null +++ b/test/resources/which/node @@ -0,0 +1 @@ +text file, not an executable diff --git a/test/rm.js b/test/rm.js index ffe133940..01efd7c0b 100644 --- a/test/rm.js +++ b/test/rm.js @@ -1,244 +1,310 @@ -var shell = require('..'); +const fs = require('fs'); +const path = require('path'); -var assert = require('assert'), - path = require('path'), - fs = require('fs'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const utils = require('./utils/utils'); + +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.cp('-r', 'test/resources', t.context.tmp); +}); + +test.afterEach.always(t => { + shell.rm('-rf', t.context.tmp); +}); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); // // Invalids // -var contents; -var result; - -result = shell.rm(); -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'rm: no paths given'); - -result = shell.rm('asdfasdf'); // file does not exist -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'rm: no such file or directory: asdfasdf'); - -result = shell.rm('-f'); // no file -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(result.stderr, 'rm: no paths given'); - -result = shell.rm('-@', 'resources/file1'); // invalid option -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.equal(fs.existsSync('resources/file1'), true); -assert.equal(result.stderr, 'rm: option not recognized: @'); +test('no args', t => { + const result = shell.rm(); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'rm: no paths given'); +}); + +test('file does not exist', t => { + const result = shell.rm('asdfasdf'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'rm: no such file or directory: asdfasdf'); +}); + +test('cannot delete a directory without recursive flag', t => { + const result = shell.rm(`${t.context.tmp}/rm`); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'rm: path is a directory'); +}); + +test('only an option', t => { + const result = shell.rm('-f'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'rm: no paths given'); +}); + +test('invalid option', t => { + const result = shell.rm('-@', 'test/resources/file1'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.truthy(fs.existsSync('test/resources/file1')); + t.is(result.stderr, 'rm: option not recognized: @'); +}); // // Valids // -// file does not exist, but -f specified -result = shell.rm('-f', 'asdfasdf'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); - -// directory does not exist, but -fr specified -result = shell.rm('-fr', 'fake_dir/'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); - -// directory does not exist, but *only -f* specified -result = shell.rm('-f', 'fake_dir/'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); - -// file (in fake dir) does not exist, but -f specified -result = shell.rm('-f', 'fake_dir/asdfasdf'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); - -// dir (in fake dir) does not exist, but -fr specified -result = shell.rm('-fr', 'fake_dir/sub/'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); - -// simple rm -shell.cp('-f', 'resources/file1', 'tmp/file1'); -assert.equal(fs.existsSync('tmp/file1'), true); -result = shell.rm('tmp/file1'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/file1'), false); - -// recursive dir removal - small-caps '-r' -shell.mkdir('-p', 'tmp/a/b/c'); -assert.equal(fs.existsSync('tmp/a/b/c'), true); -result = shell.rm('-rf', 'tmp/a'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/a'), false); - -// recursive dir removal - capital '-R' -shell.mkdir('-p', 'tmp/a/b/c'); -assert.equal(fs.existsSync('tmp/a/b/c'), true); -result = shell.rm('-Rf', 'tmp/a'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/a'), false); - -// recursive dir removal - absolute path -shell.mkdir('-p', 'tmp/a/b/c'); -assert.equal(fs.existsSync('tmp/a/b/c'), true); -result = shell.rm('-Rf', path.resolve('./tmp/a')); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/a'), false); - -// wildcard -shell.cp('-f', 'resources/file*', 'tmp'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/file1'), true); -assert.equal(fs.existsSync('tmp/file2'), true); -assert.equal(fs.existsSync('tmp/file1.js'), true); -assert.equal(fs.existsSync('tmp/file2.js'), true); -result = shell.rm('tmp/file*'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/file1'), false); -assert.equal(fs.existsSync('tmp/file2'), false); -assert.equal(fs.existsSync('tmp/file1.js'), false); -assert.equal(fs.existsSync('tmp/file2.js'), false); - -// recursive dir removal -shell.mkdir('-p', 'tmp/a/b/c'); -shell.mkdir('-p', 'tmp/b'); -shell.mkdir('-p', 'tmp/c'); -shell.mkdir('-p', 'tmp/.hidden'); -assert.equal(fs.existsSync('tmp/a/b/c'), true); -assert.equal(fs.existsSync('tmp/b'), true); -assert.equal(fs.existsSync('tmp/c'), true); -assert.equal(fs.existsSync('tmp/.hidden'), true); -result = shell.rm('-rf', 'tmp/*'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -contents = fs.readdirSync('tmp'); -assert.equal(contents.length, 1); -assert.equal(contents[0], '.hidden'); // shouldn't remove hiddden if no .* given - -// recursive dir removal -shell.mkdir('-p', 'tmp/a/b/c'); -shell.mkdir('-p', 'tmp/b'); -shell.mkdir('-p', 'tmp/c'); -shell.mkdir('-p', 'tmp/.hidden'); -assert.equal(fs.existsSync('tmp/a/b/c'), true); -assert.equal(fs.existsSync('tmp/b'), true); -assert.equal(fs.existsSync('tmp/c'), true); -assert.equal(fs.existsSync('tmp/.hidden'), true); -result = shell.rm('-rf', 'tmp/*', 'tmp/.*'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -contents = fs.readdirSync('tmp'); -assert.equal(contents.length, 0); - -// recursive dir removal - array-syntax -shell.mkdir('-p', 'tmp/a/b/c'); -shell.mkdir('-p', 'tmp/b'); -shell.mkdir('-p', 'tmp/c'); -shell.mkdir('-p', 'tmp/.hidden'); -assert.equal(fs.existsSync('tmp/a/b/c'), true); -assert.equal(fs.existsSync('tmp/b'), true); -assert.equal(fs.existsSync('tmp/c'), true); -assert.equal(fs.existsSync('tmp/.hidden'), true); -result = shell.rm('-rf', ['tmp/*', 'tmp/.*']); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -contents = fs.readdirSync('tmp'); -assert.equal(contents.length, 0); - -// removal of a read-only file (unforced) -shell.mkdir('-p', 'tmp/readonly'); -shell.ShellString('asdf').to('tmp/readonly/file1'); -fs.chmodSync('tmp/readonly/file1', '0444'); // -r--r--r-- -result = shell.rm('tmp/readonly/file1'); -assert.equal(fs.existsSync('tmp/readonly/file1'), true); // bash's rm always asks before removing read-only files - // here we just assume "no" - -// removal of a read-only file (forced) -shell.mkdir('-p', 'tmp/readonly'); -shell.ShellString('asdf').to('tmp/readonly/file2'); -fs.chmodSync('tmp/readonly/file2', '0444'); // -r--r--r-- -result = shell.rm('-f', 'tmp/readonly/file2'); -assert.equal(fs.existsSync('tmp/readonly/file2'), false); - -// removal of a tree containing read-only files (unforced) -shell.mkdir('-p', 'tmp/tree2'); -shell.ShellString('asdf').to('tmp/tree2/file1'); -shell.ShellString('asdf').to('tmp/tree2/file2'); -fs.chmodSync('tmp/tree2/file1', '0444'); // -r--r--r-- -result = shell.rm('-r', 'tmp/tree2'); -assert.equal(fs.existsSync('tmp/tree2/file1'), true); -assert.equal(fs.existsSync('tmp/tree2/file2'), false); - -// removal of a tree containing read-only files (forced) -shell.mkdir('-p', 'tmp/tree'); -shell.ShellString('asdf').to('tmp/tree/file1'); -shell.ShellString('asdf').to('tmp/tree/file2'); -fs.chmodSync('tmp/tree/file1', '0444'); // -r--r--r-- -result = shell.rm('-rf', 'tmp/tree'); -assert.equal(fs.existsSync('tmp/tree'), false); - -// removal of a sub-tree containing read-only and hidden files - rm('dir/*') -shell.mkdir('-p', 'tmp/tree3'); -shell.mkdir('-p', 'tmp/tree3/subtree'); -shell.mkdir('-p', 'tmp/tree3/.hidden'); -shell.ShellString('asdf').to('tmp/tree3/subtree/file'); -shell.ShellString('asdf').to('tmp/tree3/.hidden/file'); -shell.ShellString('asdf').to('tmp/tree3/file'); -fs.chmodSync('tmp/tree3/file', '0444'); // -r--r--r-- -fs.chmodSync('tmp/tree3/subtree/file', '0444'); // -r--r--r-- -fs.chmodSync('tmp/tree3/.hidden/file', '0444'); // -r--r--r-- -result = shell.rm('-rf', 'tmp/tree3/*', 'tmp/tree3/.*'); // erase dir contents -assert.equal(shell.ls('tmp/tree3').length, 0); - -// removal of a sub-tree containing read-only and hidden files - rm('dir') -shell.mkdir('-p', 'tmp/tree4'); -shell.mkdir('-p', 'tmp/tree4/subtree'); -shell.mkdir('-p', 'tmp/tree4/.hidden'); -shell.ShellString('asdf').to('tmp/tree4/subtree/file'); -shell.ShellString('asdf').to('tmp/tree4/.hidden/file'); -shell.ShellString('asdf').to('tmp/tree4/file'); -fs.chmodSync('tmp/tree4/file', '0444'); // -r--r--r-- -fs.chmodSync('tmp/tree4/subtree/file', '0444'); // -r--r--r-- -fs.chmodSync('tmp/tree4/.hidden/file', '0444'); // -r--r--r-- -result = shell.rm('-rf', 'tmp/tree4'); // erase dir contents -assert.equal(fs.existsSync('tmp/tree4'), false); - -// remove symbolic link to a dir -result = shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); -shell.cp('-R', 'resources/rm', 'tmp'); -result = shell.rm('-f', 'tmp/rm/link_to_a_dir'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(fs.existsSync('tmp/rm/link_to_a_dir'), false); -assert.equal(fs.existsSync('tmp/rm/a_dir'), true); - -// remove broken symbolic link -if (process.platform !== 'win32') { - result = shell.rm('-rf', 'tmp'); - shell.mkdir('tmp'); - shell.cp('-R', 'resources/rm', 'tmp'); - assert.ok(shell.test('-L', 'tmp/rm/fake.lnk')); - result = shell.rm('tmp/rm/fake.lnk'); - assert.equal(shell.error(), null); - assert.equal(result.code, 0); - assert.ok(!shell.test('-L', 'tmp/rm/fake.lnk')); - assert.equal(fs.existsSync('tmp/rm/fake.lnk'), false); -} - -shell.exit(123); +test('file does not exist, but -f specified', t => { + const result = shell.rm('-f', 'asdfasdf'); + t.falsy(shell.error()); + t.is(result.code, 0); +}); + +test('directory does not exist, but -fr specified', t => { + const result = shell.rm('-fr', 'fake_dir/'); + t.falsy(shell.error()); + t.is(result.code, 0); +}); + +test('directory does not exist, but *only -f* specified', t => { + const result = shell.rm('-f', 'fake_dir/'); + t.falsy(shell.error()); + t.is(result.code, 0); +}); + +test('file (in fake dir) does not exist, but -f specified', t => { + const result = shell.rm('-f', 'fake_dir/asdfasdf'); + t.falsy(shell.error()); + t.is(result.code, 0); +}); + +test('dir (in fake dir) does not exist, but -fr specified', t => { + const result = shell.rm('-fr', 'fake_dir/sub/'); + t.falsy(shell.error()); + t.is(result.code, 0); +}); + +test('simple rm', t => { + t.truthy(fs.existsSync(`${t.context.tmp}/file1`)); + const result = shell.rm(`${t.context.tmp}/file1`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(fs.existsSync(`${t.context.tmp}/file1`)); +}); + +test('recursive dir removal: -r option', t => { + shell.mkdir('-p', `${t.context.tmp}/a/b/c`); + t.truthy(fs.existsSync(`${t.context.tmp}/a/b/c`)); + const result = shell.rm('-rf', `${t.context.tmp}/a`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(fs.existsSync(`${t.context.tmp}/a`)); +}); + +test('-R option does the same thing', t => { + shell.mkdir('-p', `${t.context.tmp}/a/b/c`); + t.truthy(fs.existsSync(`${t.context.tmp}/a/b/c`)); + const result = shell.rm('-Rf', `${t.context.tmp}/a`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(fs.existsSync(`${t.context.tmp}/a`)); +}); + +test('recursive dir removal - absolute path', t => { + shell.mkdir('-p', `${t.context.tmp}/a/b/c`); + t.truthy(fs.existsSync(`${t.context.tmp}/a/b/c`)); + const result = shell.rm('-Rf', path.resolve(`./${t.context.tmp}/a`)); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(fs.existsSync(`${t.context.tmp}/a`)); +}); + +test('wildcard', t => { + const result = shell.rm(`${t.context.tmp}/file*`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(fs.existsSync(`${t.context.tmp}/file1`)); + t.falsy(fs.existsSync(`${t.context.tmp}/file2`)); + t.falsy(fs.existsSync(`${t.context.tmp}/file1.js`)); + t.falsy(fs.existsSync(`${t.context.tmp}/file2.js`)); +}); + +test('recursive dir removal', t => { + shell.mkdir('-p', `${t.context.tmp}/a/b/c`); + shell.mkdir('-p', `${t.context.tmp}/b`); + shell.mkdir('-p', `${t.context.tmp}/c`); + shell.mkdir('-p', `${t.context.tmp}/.hidden`); + t.truthy(fs.existsSync(`${t.context.tmp}/a/b/c`)); + t.truthy(fs.existsSync(`${t.context.tmp}/b`)); + t.truthy(fs.existsSync(`${t.context.tmp}/c`)); + t.truthy(fs.existsSync(`${t.context.tmp}/.hidden`)); + const result = shell.rm('-rf', `${t.context.tmp}/*`); + t.falsy(shell.error()); + t.is(result.code, 0); + const contents = fs.readdirSync(t.context.tmp); + t.is(contents.length, 1); + t.is(contents[0], '.hidden'); // shouldn't remove hidden if no .* given +}); + +test('recursive dir removal #2', t => { + shell.mkdir('-p', `${t.context.tmp}/a/b/c`); + shell.mkdir('-p', `${t.context.tmp}/b`); + shell.mkdir('-p', `${t.context.tmp}/c`); + shell.mkdir('-p', `${t.context.tmp}/.hidden`); + t.truthy(fs.existsSync(`${t.context.tmp}/a/b/c`)); + t.truthy(fs.existsSync(`${t.context.tmp}/b`)); + t.truthy(fs.existsSync(`${t.context.tmp}/c`)); + t.truthy(fs.existsSync(`${t.context.tmp}/.hidden`)); + const result = shell.rm('-rf', `${t.context.tmp}/*`, `${t.context.tmp}/.*`); + t.falsy(shell.error()); + t.is(result.code, 0); + const contents = fs.readdirSync(t.context.tmp); + t.is(contents.length, 0); +}); + +test('recursive dir removal - array-syntax', t => { + shell.mkdir('-p', `${t.context.tmp}/a/b/c`); + shell.mkdir('-p', `${t.context.tmp}/b`); + shell.mkdir('-p', `${t.context.tmp}/c`); + shell.mkdir('-p', `${t.context.tmp}/.hidden`); + t.truthy(fs.existsSync(`${t.context.tmp}/a/b/c`)); + t.truthy(fs.existsSync(`${t.context.tmp}/b`)); + t.truthy(fs.existsSync(`${t.context.tmp}/c`)); + t.truthy(fs.existsSync(`${t.context.tmp}/.hidden`)); + const result = shell.rm('-rf', [`${t.context.tmp}/*`, `${t.context.tmp}/.*`]); + t.falsy(shell.error()); + t.is(result.code, 0); + const contents = fs.readdirSync(t.context.tmp); + t.is(contents.length, 0); +}); + +test('removal of a read-only file (unforced)', t => { + shell.mkdir('-p', `${t.context.tmp}/readonly`); + shell.ShellString('asdf').to(`${t.context.tmp}/readonly/file1`); + fs.chmodSync(`${t.context.tmp}/readonly/file1`, '0444'); // -r--r--r-- + shell.rm(`${t.context.tmp}/readonly/file1`); + t.truthy(shell.error()); + t.truthy(fs.existsSync(`${t.context.tmp}/readonly/file1`)); // bash's rm always asks before removing read-only files + // here we just assume "no" +}); + +test('removal of a read-only file (forced)', t => { + shell.mkdir('-p', `${t.context.tmp}/readonly`); + shell.ShellString('asdf').to(`${t.context.tmp}/readonly/file2`); + fs.chmodSync(`${t.context.tmp}/readonly/file2`, '0444'); // -r--r--r-- + shell.rm('-f', `${t.context.tmp}/readonly/file2`); + t.falsy(shell.error()); + t.falsy(fs.existsSync(`${t.context.tmp}/readonly/file2`)); +}); + +test('removal of a tree containing read-only files (unforced)', t => { + shell.mkdir('-p', `${t.context.tmp}/tree2`); + shell.ShellString('asdf').to(`${t.context.tmp}/tree2/file1`); + shell.ShellString('asdf').to(`${t.context.tmp}/tree2/file2`); + fs.chmodSync(`${t.context.tmp}/tree2/file1`, '0444'); // -r--r--r-- + shell.rm('-r', `${t.context.tmp}/tree2`); + t.truthy(shell.error()); + t.truthy(fs.existsSync(`${t.context.tmp}/tree2/file1`)); + t.falsy(fs.existsSync(`${t.context.tmp}/tree2/file2`)); +}); + +test('removal of a tree containing read-only files (forced)', t => { + shell.mkdir('-p', `${t.context.tmp}/tree`); + shell.ShellString('asdf').to(`${t.context.tmp}/tree/file1`); + shell.ShellString('asdf').to(`${t.context.tmp}/tree/file2`); + fs.chmodSync(`${t.context.tmp}/tree/file1`, '0444'); // -r--r--r-- + shell.rm('-rf', `${t.context.tmp}/tree`); + t.falsy(shell.error()); + t.falsy(fs.existsSync(`${t.context.tmp}/tree`)); +}); + +test( + 'removal of a sub-tree containing read-only and hidden files - glob', + t => { + shell.mkdir('-p', `${t.context.tmp}/tree3`); + shell.mkdir('-p', `${t.context.tmp}/tree3/subtree`); + shell.mkdir('-p', `${t.context.tmp}/tree3/.hidden`); + shell.ShellString('asdf').to(`${t.context.tmp}/tree3/subtree/file`); + shell.ShellString('asdf').to(`${t.context.tmp}/tree3/.hidden/file`); + shell.ShellString('asdf').to(`${t.context.tmp}/tree3/file`); + fs.chmodSync(`${t.context.tmp}/tree3/file`, '0444'); // -r--r--r-- + fs.chmodSync(`${t.context.tmp}/tree3/subtree/file`, '0444'); // -r--r--r-- + fs.chmodSync(`${t.context.tmp}/tree3/.hidden/file`, '0444'); // -r--r--r-- + shell.rm('-rf', `${t.context.tmp}/tree3/*`, `${t.context.tmp}/tree3/.*`); // erase dir contents + t.is(shell.ls(`${t.context.tmp}/tree3`).length, 0); + } +); + +test( + 'removal of a sub-tree containing read-only and hidden files - without glob', + t => { + shell.mkdir('-p', `${t.context.tmp}/tree4`); + shell.mkdir('-p', `${t.context.tmp}/tree4/subtree`); + shell.mkdir('-p', `${t.context.tmp}/tree4/.hidden`); + shell.ShellString('asdf').to(`${t.context.tmp}/tree4/subtree/file`); + shell.ShellString('asdf').to(`${t.context.tmp}/tree4/.hidden/file`); + shell.ShellString('asdf').to(`${t.context.tmp}/tree4/file`); + fs.chmodSync(`${t.context.tmp}/tree4/file`, '0444'); // -r--r--r-- + fs.chmodSync(`${t.context.tmp}/tree4/subtree/file`, '0444'); // -r--r--r-- + fs.chmodSync(`${t.context.tmp}/tree4/.hidden/file`, '0444'); // -r--r--r-- + shell.rm('-rf', `${t.context.tmp}/tree4`); // erase dir contents + t.falsy(fs.existsSync(`${t.context.tmp}/tree4`)); + } +); + +test('remove symbolic link to a dir', t => { + const result = shell.rm(`${t.context.tmp}/rm/link_to_a_dir`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(fs.existsSync(`${t.context.tmp}/rm/link_to_a_dir`)); + t.truthy(fs.existsSync(`${t.context.tmp}/rm/a_dir`)); +}); + +test('rm -rf on a symbolic link to a dir deletes its contents', t => { + utils.skipOnWin(t, () => { + const result = shell.rm('-rf', `${t.context.tmp}/rm/link_to_a_dir/`); + t.falsy(shell.error()); + t.is(result.code, 0); + + // Both the link and original dir should remain, but contents are deleted + t.truthy(fs.existsSync(`${t.context.tmp}/rm/link_to_a_dir`)); + t.truthy(fs.existsSync(`${t.context.tmp}/rm/a_dir`)); + t.falsy(fs.existsSync(`${t.context.tmp}/rm/a_dir/a_file`)); + }); +}); + +test('remove broken symbolic link', t => { + utils.skipOnWin(t, () => { + t.truthy(shell.test('-L', `${t.context.tmp}/rm/fake.lnk`)); + const result = shell.rm(`${t.context.tmp}/rm/fake.lnk`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(shell.test('-L', `${t.context.tmp}/rm/fake.lnk`)); + t.falsy(fs.existsSync(`${t.context.tmp}/rm/fake.lnk`)); + }); +}); + +test('recursive dir removal, for non-normalized path', t => { + shell.mkdir('-p', `${t.context.tmp}/a/b/c`); + t.truthy(fs.existsSync(`${t.context.tmp}/a/b/c`)); + const result = shell.rm('-rf', `${t.context.tmp}/a/.././a`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.falsy(fs.existsSync(`${t.context.tmp}/a`)); +}); + +test('remove fifo', t => { + utils.skipOnWin(t, () => { + const fifo = utils.mkfifo(t.context.tmp); + const result = shell.rm(fifo); + t.falsy(shell.error()); + t.is(result.code, 0); + }); +}); diff --git a/test/sed.js b/test/sed.js index 3ff041b08..fa0d41810 100644 --- a/test/sed.js +++ b/test/sed.js @@ -1,137 +1,182 @@ -var shell = require('..'); +const fs = require('fs'); -var assert = require('assert'), - fs = require('fs'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const utils = require('./utils/utils'); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.cp('-r', 'test/resources', t.context.tmp); +}); + +test.afterEach.always(t => { + shell.rm('-rf', t.context.tmp); +}); -var result; // // Invalids // -result = shell.sed(); -assert.ok(shell.error()); -assert.equal(result.code, 1); -assert.ok(result.stderr); - -result = shell.sed(/asdf/g); // too few args -assert.ok(shell.error()); -assert.equal(result.code, 1); - -result = shell.sed(/asdf/g, 'nada'); // too few args -assert.ok(shell.error()); -assert.equal(result.code, 1); - -assert.equal(fs.existsSync('asdfasdf'), false); // sanity check -result = shell.sed(/asdf/g, 'nada', 'asdfasdf'); // no such file -assert.ok(shell.error()); -assert.equal(result.code, 2); -assert.equal(result.stderr, 'sed: no such file or directory: asdfasdf'); - -// if at least one file is missing, this should be an error -shell.cp('-f', 'resources/file1', 'tmp/file1'); -assert.equal(fs.existsSync('asdfasdf'), false); // sanity check -assert.equal(fs.existsSync('tmp/file1'), true); // sanity check -result = shell.sed(/asdf/g, 'nada', 'tmp/file1', 'asdfasdf'); -assert.ok(shell.error()); -assert.equal(result.code, 2); -assert.equal(result.stderr, 'sed: no such file or directory: asdfasdf'); +test('no arguments', t => { + const result = shell.sed(); + t.truthy(shell.error()); + t.is(result.code, 1); + t.truthy(result.stderr); +}); + +test('only one argument', t => { + const result = shell.sed(/asdf/g); + t.truthy(shell.error()); + t.is(result.code, 1); +}); + +test('only two arguments', t => { + const result = shell.sed(/asdf/g, 'nada'); + t.truthy(shell.error()); + t.is(result.code, 1); +}); + +test('no such file', t => { + t.falsy(fs.existsSync('asdfasdf')); // sanity check + const result = shell.sed(/asdf/g, 'nada', 'asdfasdf'); + t.truthy(shell.error()); + t.is(result.code, 2); + t.is(result.stderr, 'sed: no such file or directory: asdfasdf'); +}); + +// TODO(nate): flaky test +test('if at least one file is missing, this should be an error', t => { + t.falsy(fs.existsSync('asdfasdf')); // sanity check + t.truthy(fs.existsSync(`${t.context.tmp}/file1`)); // sanity check + const result = shell.sed(/asdf/g, 'nada', `${t.context.tmp}/file1`, 'asdfasdf'); + t.truthy(shell.error()); + t.is(result.code, 2); + t.is(result.stderr, 'sed: no such file or directory: asdfasdf'); +}); // // Valids // -shell.cp('-f', 'resources/file1', 'tmp/file1'); -result = shell.sed('test1', 'hello', 'tmp/file1'); // search string -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.toString(), 'hello'); - -result = shell.sed(/test1/, 'hello', 'tmp/file1'); // search regex -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.toString(), 'hello'); - -result = shell.sed(/test1/, 1234, 'tmp/file1'); // numeric replacement -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.toString(), '1234'); - -var replaceFun = function (match) { - return match.toUpperCase() + match; -}; -result = shell.sed(/test1/, replaceFun, 'tmp/file1'); // replacement function -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.toString(), 'TEST1test1'); - -result = shell.sed('-i', /test1/, 'hello', 'tmp/file1'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.toString(), 'hello'); -assert.equal(shell.cat('tmp/file1').toString(), 'hello'); - -// make sure * in regex is not globbed -result = shell.sed(/alpha*beta/, 'hello', 'resources/grep/file'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.toString(), 'hello\nhowareyou\nhello\nthis line ends in.js\nlllllllllllllllll.js\n'); - -// make sure * in string-regex is not globbed -result = shell.sed('alpha*beta', 'hello', 'resources/grep/file'); -assert.ok(!shell.error()); -assert.equal(result.code, 0); -assert.equal(result.toString(), 'hello\nhowareyou\nhello\nthis line ends in.js\nlllllllllllllllll.js\n'); - -// make sure * in regex is not globbed -result = shell.sed(/l*\.js/, '', 'resources/grep/file'); -assert.ok(!shell.error()); -assert.equal(result.code, 0); -assert.equal(result.toString(), 'alphaaaaaaabeta\nhowareyou\nalphbeta\nthis line ends in\n\n'); - -// make sure * in string-regex is not globbed -result = shell.sed('l*\\.js', '', 'resources/grep/file'); -assert.ok(!shell.error()); -assert.equal(result.code, 0); -assert.equal(result.toString(), 'alphaaaaaaabeta\nhowareyou\nalphbeta\nthis line ends in\n\n'); - -shell.cp('-f', 'resources/file1', 'tmp/file1'); -shell.cp('-f', 'resources/file2', 'tmp/file2'); - -// multiple file names -result = shell.sed('test', 'hello', 'tmp/file1', 'tmp/file2'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.toString(), 'hello1\nhello2'); - -// array of file names (and try it out with a simple regex) -result = shell.sed(/t.*st/, 'hello', ['tmp/file1', 'tmp/file2']); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.toString(), 'hello1\nhello2'); - -// multiple file names, with in-place-replacement -result = shell.sed('-i', 'test', 'hello', ['tmp/file1', 'tmp/file2']); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.toString(), 'hello1\nhello2'); -assert.equal(shell.cat('tmp/file1').toString(), 'hello1'); -assert.equal(shell.cat('tmp/file2').toString(), 'hello2'); - -// glob file names, with in-place-replacement -shell.cp('resources/file*.txt', 'tmp/'); -assert.equal(shell.cat('tmp/file1.txt').toString(), 'test1\n'); -assert.equal(shell.cat('tmp/file2.txt').toString(), 'test2\n'); -result = shell.sed('-i', 'test', 'hello', 'tmp/file*.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result.toString(), 'hello1\n\nhello2\n'); // TODO: fix sed's behavior -assert.equal(shell.cat('tmp/file1.txt').toString(), 'hello1\n'); -assert.equal(shell.cat('tmp/file2.txt').toString(), 'hello2\n'); - -shell.exit(123); +test('search with a string', t => { + const result = shell.sed('test1', 'hello', `${t.context.tmp}/file1`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'hello'); +}); + +test('search with a regex', t => { + const result = shell.sed(/test1/, 'hello', `${t.context.tmp}/file1`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'hello'); +}); + +test('replace with a number instead of a string', t => { + const result = shell.sed(/test1/, 1234, `${t.context.tmp}/file1`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), '1234'); +}); + +test('replace using a function', t => { + const replaceFun = match => match.toUpperCase() + match; + const result = shell.sed(/test1/, replaceFun, `${t.context.tmp}/file1`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'TEST1test1'); +}); + +test('-i option', t => { + const result = shell.sed('-i', /test1/, 'hello', `${t.context.tmp}/file1`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), ''); + t.is(shell.cat(`${t.context.tmp}/file1`).toString(), 'hello'); +}); + +test('make sure * in regex is not globbed', t => { + const result = shell.sed(/alpha*beta/, 'hello', 'test/resources/grep/file'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is( + result.toString(), + 'hello\nhowareyou\nhello\nthis line ends in.js\nlllllllllllllllll.js\n' + ); +}); + +test('make sure * in string-regex is not globbed', t => { + const result = shell.sed('alpha*beta', 'hello', 'test/resources/grep/file'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is( + result.toString(), + 'hello\nhowareyou\nhello\nthis line ends in.js\nlllllllllllllllll.js\n' + ); +}); + +test('make sure * in regex is not globbed (matches something)', t => { + const result = shell.sed(/l*\.js/, '', 'test/resources/grep/file'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is( + result.toString(), + 'alphaaaaaaabeta\nhowareyou\nalphbeta\nthis line ends in\n\n' + ); +}); + +test('make sure * in string-regex is not globbed (matches something)', t => { + const result = shell.sed('l*\\.js', '', 'test/resources/grep/file'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is( + result.toString(), + 'alphaaaaaaabeta\nhowareyou\nalphbeta\nthis line ends in\n\n' + ); +}); + +test('multiple file names', t => { + const result = shell.sed('test', 'hello', `${t.context.tmp}/file1`, + `${t.context.tmp}/file2`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'hello1\nhello2'); +}); + +test('array of file names (and try it out with a simple regex)', t => { + const result = shell.sed(/t.*st/, 'hello', [`${t.context.tmp}/file1`, + `${t.context.tmp}/file2`]); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'hello1\nhello2'); +}); + +test('multiple file names, with in-place-replacement', t => { + const result = shell.sed('-i', 'test', 'hello', [`${t.context.tmp}/file1`, + `${t.context.tmp}/file2`]); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), ''); + t.is(shell.cat(`${t.context.tmp}/file1`).toString(), 'hello1'); + t.is(shell.cat(`${t.context.tmp}/file2`).toString(), 'hello2'); +}); + +test('glob file names, with in-place-replacement', t => { + t.is(shell.cat(`${t.context.tmp}/file1.txt`).toString(), 'test1\n'); + t.is(shell.cat(`${t.context.tmp}/file2.txt`).toString(), 'test2\n'); + const result = shell.sed('-i', 'test', 'hello', `${t.context.tmp}/file*.txt`); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), ''); + t.is(shell.cat(`${t.context.tmp}/file1.txt`).toString(), 'hello1\n'); + t.is(shell.cat(`${t.context.tmp}/file2.txt`).toString(), 'hello2\n'); +}); + +test('empty file', t => { + const result = shell.sed('widget', 'wizzle', 'test/resources/sed/empty.txt'); + t.is(result.code, 0); + t.is(result.toString(), ''); +}); diff --git a/test/set.js b/test/set.js index d64036f3f..94c0a0bfa 100644 --- a/test/set.js +++ b/test/set.js @@ -1,68 +1,77 @@ -var shell = require('..'); +const test = require('ava'); -var assert = require('assert'); +const shell = require('..'); +const utils = require('./utils/utils'); -var oldConfigSilent = shell.config.silent; -shell.config.silent = true; +const oldConfigSilent = shell.config.silent; +const uncaughtErrorExitCode = 1; -shell.rm('-rf', 'tmp'); +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.cp('-r', 'test/resources', t.context.tmp); +}); + +test.afterEach.always(t => { + shell.rm('-rf', t.context.tmp); +}); // // Valids // -var result; - -// initial values -assert.strictEqual(oldConfigSilent, false); -assert.strictEqual(shell.config.verbose, false); -assert.strictEqual(shell.config.fatal, false); -assert.strictEqual(shell.config.noglob, false); - -shell.cp('-R', 'resources/', 'tmp'); - -// default behavior -result = shell.exec(JSON.stringify(process.execPath)+' -e \"require(\'../global\'); ls(\'file_doesnt_exist\'); echo(1234);\"'); -assert.equal(result.code, 0); -assert.equal(result.stdout, '1234\n'); -assert.equal(result.stderr, 'ls: no such file or directory: file_doesnt_exist\n'); - -// set -e -result = shell.exec(JSON.stringify(process.execPath)+' -e \"require(\'../global\'); set(\'-e\'); ls(\'file_doesnt_exist\'); echo(1234);\"'); -var nodeVersion = process.versions.node.split('.').map(function(str) { return parseInt(str, 10); }); -var uncaughtErrorExitCode = (nodeVersion[0] === 0 && nodeVersion[1] < 11) ? 8 : 1; -assert.equal(result.code, uncaughtErrorExitCode); -assert.equal(result.stdout, ''); -assert(result.stderr.indexOf('Error: ls: no such file or directory: file_doesnt_exist') >= 0); - -// set -v -result = shell.exec(JSON.stringify(process.execPath)+' -e \"require(\'../global\'); set(\'-v\'); ls(\'file_doesnt_exist\'); echo(1234);\"'); -assert.equal(result.code, 0); -assert.equal(result.stdout, '1234\n'); -assert.equal(result.stderr, 'ls file_doesnt_exist\nls: no such file or directory: file_doesnt_exist\necho 1234\n'); - -// set -ev -result = shell.exec(JSON.stringify(process.execPath)+' -e \"require(\'../global\'); set(\'-ev\'); ls(\'file_doesnt_exist\'); echo(1234);\"'); -assert.equal(result.code, uncaughtErrorExitCode); -assert.equal(result.stdout, ''); -assert(result.stderr.indexOf('Error: ls: no such file or directory: file_doesnt_exist') >= 0); -assert(result.stderr.indexOf('ls file_doesnt_exist\n') >= 0); -assert.equal(result.stderr.indexOf('echo 1234\n'), -1); - -// set -e, set +e -result = shell.exec(JSON.stringify(process.execPath)+' -e \"require(\'../global\'); set(\'-e\'); set(\'+e\'); ls(\'file_doesnt_exist\'); echo(1234);\"'); -assert.equal(result.code, 0); -assert.equal(result.stdout, '1234\n'); -assert.equal(result.stderr, 'ls: no such file or directory: file_doesnt_exist\n'); - -// set -f -shell.set('-f'); // disable globbing -shell.rm('tmp/*.txt'); -assert.ok(shell.error()); // file '*.txt' doesn't exist, so rm() fails -shell.set('+f'); -shell.rm('tmp/*.txt'); -assert.ok(!shell.error()); // globbing works, so rm succeeds - -shell.exit(123); - - +test('initial values', t => { + t.false(oldConfigSilent); + t.false(shell.config.verbose); + t.false(shell.config.fatal); + t.false(shell.config.noglob); +}); + +test('default behavior', t => { + const result = shell.exec(JSON.stringify(shell.config.execPath) + ` -e "require('./global'); ls('file_doesnt_exist'); echo(1234);"`); + t.is(result.code, 0); + t.is(result.stdout, '1234\n'); + t.is(result.stderr, 'ls: no such file or directory: file_doesnt_exist\n'); +}); + +test('set -e', t => { + const result = shell.exec(JSON.stringify(shell.config.execPath) + ` -e "require('./global'); set('-e'); ls('file_doesnt_exist'); echo(1234);"`); + t.is(result.code, uncaughtErrorExitCode); + t.is(result.stdout, ''); + t.truthy(result.stderr.includes('Error: ls: no such file or directory: file_doesnt_exist')); +}); + +test('set -v', t => { + const result = shell.exec(JSON.stringify(shell.config.execPath) + ` -e "require('./global'); set('-v'); ls('file_doesnt_exist'); echo(1234);"`); + t.is(result.code, 0); + t.is(result.stdout, '1234\n'); + t.is( + result.stderr, + 'ls file_doesnt_exist\nls: no such file or directory: file_doesnt_exist\necho 1234\n' + ); +}); + +test('set -ev', t => { + const result = shell.exec(JSON.stringify(shell.config.execPath) + ` -e "require('./global'); set('-ev'); ls('file_doesnt_exist'); echo(1234);"`); + t.is(result.code, uncaughtErrorExitCode); + t.is(result.stdout, ''); + t.truthy(result.stderr.includes('Error: ls: no such file or directory: file_doesnt_exist')); + t.truthy(result.stderr.includes('ls file_doesnt_exist\n')); + t.falsy(result.stderr.includes('echo 1234\n')); +}); + +test('set -e, set +e', t => { + const result = shell.exec(JSON.stringify(shell.config.execPath) + ` -e "require('./global'); set('-e'); set('+e'); ls('file_doesnt_exist'); echo(1234);"`); + t.is(result.code, 0); + t.is(result.stdout, '1234\n'); + t.is(result.stderr, 'ls: no such file or directory: file_doesnt_exist\n'); +}); + +test('set -f', t => { + shell.set('-f'); // disable globbing + shell.rm(`${t.context.tmp}/*.txt`); + t.truthy(shell.error()); // file '*.txt' doesn't exist, so rm() fails + shell.set('+f'); + shell.rm(`${t.context.tmp}/*.txt`); + t.falsy(shell.error()); // globbing works, so rm succeeds +}); diff --git a/test/shjs.js b/test/shjs.js deleted file mode 100644 index 8a257ac8f..000000000 --- a/test/shjs.js +++ /dev/null @@ -1,30 +0,0 @@ -var shell = require('..'); -var path = require('path'); -var assert = require('assert'); - -function runScript(name) { - // prefix with 'node ' for Windows, don't prefix for OSX/Linux - var cmd = (process.platform === 'win32' ? JSON.stringify(process.execPath)+ ' ' : '') + path.resolve(__dirname, '../bin/shjs'); - var script = path.resolve(__dirname, 'resources', 'shjs', name); - return shell.exec(cmd + ' ' + script, { silent: true }); -} - -// Exit Codes -assert.equal(runScript('exit-codes.js').code, 42, 'exit code works'); -assert.equal(runScript('exit-0.js').code, 0, 'exiting 0 works'); - -// Stdout/Stderr -var stdioRet = runScript('stdout-stderr.js'); -assert.equal(stdioRet.stdout, 'stdout: OK!\n', 'stdout works'); -assert.equal(stdioRet.stderr, 'stderr: OK!\n', 'stderr works'); - -// CoffeeScript -assert.equal(runScript('coffeescript.coffee').stdout, 'CoffeeScript: OK!\n'); - - -// Extension detection -var extDetectRet = runScript('a-file'); -assert.equal(extDetectRet.code, 0, 'error code works'); -assert.equal(extDetectRet.stdout, 'OK!\n', 'stdout works'); - -shell.exit(123); diff --git a/test/sort.js b/test/sort.js index 6e6b6202b..ac716e87c 100644 --- a/test/sort.js +++ b/test/sort.js @@ -1,94 +1,108 @@ -var shell = require('..'); +const fs = require('fs'); -var assert = require('assert'), - fs = require('fs'); +const test = require('ava'); -shell.config.silent = true; - -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +const shell = require('..'); +const common = require('../src/common'); -var result; +shell.config.silent = true; -var doubleSorted = shell.cat('resources/sort/sorted') +const doubleSorted = shell.cat('test/resources/sort/sorted') .trimRight() .split('\n') - .reduce(function(prev, cur) { - return prev.concat([cur, cur]); - }, []) + .reduce((prev, cur) => prev.concat([cur, cur]), []) .join('\n') + '\n'; + // // Invalids // -result = shell.sort(); -assert.ok(shell.error()); -assert.ok(result.code); - -assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check -result = shell.sort('/adsfasdf'); // file does not exist -assert.ok(shell.error()); -assert.ok(result.code); +test('no args', t => { + const result = shell.sort(); + t.truthy(shell.error()); + t.truthy(result.code); +}); + +test('file does not exist', t => { + t.falsy(fs.existsSync('/asdfasdf')); // sanity check + const result = shell.sort('/asdfasdf'); + t.truthy(shell.error()); + t.truthy(result.code); +}); + +test('directory', t => { + t.truthy(common.statFollowLinks('test/resources/').isDirectory()); // sanity check + const result = shell.sort('test/resources/'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'sort: read failed: test/resources/: Is a directory'); +}); // // Valids // -// simple -result = shell.sort('resources/sort/file1'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result + '', shell.cat('resources/sort/sorted')); - -// simple -result = shell.sort('resources/sort/file2'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result + '', shell.cat('resources/sort/sorted')); - -// multiple files -result = shell.sort('resources/sort/file2', 'resources/sort/file1'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result + '', doubleSorted); - -// multiple files, array syntax -result = shell.sort(['resources/sort/file2', 'resources/sort/file1']); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result + '', doubleSorted); - -// Globbed file -result = shell.sort('resources/sort/file?'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result + '', doubleSorted); - -// With '-n' option -result = shell.sort('-n', 'resources/sort/file2'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result + '', shell.cat('resources/sort/sortedDashN')); - -// With '-r' option -result = shell.sort('-r', 'resources/sort/file2'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result + '', shell.cat('resources/sort/sorted') - .trimRight() - .split('\n') - .reverse() - .join('\n') + '\n'); - -// With '-rn' option -result = shell.sort('-rn', 'resources/sort/file2'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result + '', shell.cat('resources/sort/sortedDashN') - .trimRight() - .split('\n') - .reverse() - .join('\n') + '\n'); - -shell.exit(123); +test('simple', t => { + const result = shell.sort('test/resources/sort/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), shell.cat('test/resources/sort/sorted').toString()); +}); + +test('simple #2', t => { + const result = shell.sort('test/resources/sort/file2'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), shell.cat('test/resources/sort/sorted').toString()); +}); + +test('multiple files', t => { + const result = shell.sort('test/resources/sort/file2', 'test/resources/sort/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), doubleSorted); +}); + +test('multiple files, array syntax', t => { + const result = shell.sort(['test/resources/sort/file2', 'test/resources/sort/file1']); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), doubleSorted); +}); + +test('Globbed file', t => { + const result = shell.sort('test/resources/sort/file?'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), doubleSorted); +}); + +test("With '-n' option", t => { + const result = shell.sort('-n', 'test/resources/sort/file2'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), shell.cat('test/resources/sort/sortedDashN').toString()); +}); + +test("With '-r' option", t => { + const result = shell.sort('-r', 'test/resources/sort/file2'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), shell.cat('test/resources/sort/sorted') + .trimRight() + .split('\n') + .reverse() + .join('\n') + '\n'); +}); + +test("With '-rn' option", t => { + const result = shell.sort('-rn', 'test/resources/sort/file2'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), shell.cat('test/resources/sort/sortedDashN') + .trimRight() + .split('\n') + .reverse() + .join('\n') + '\n'); +}); diff --git a/test/tail.js b/test/tail.js index de37f17ab..158b45d8f 100644 --- a/test/tail.js +++ b/test/tail.js @@ -1,105 +1,168 @@ -var shell = require('..'); +const fs = require('fs'); -var assert = require('assert'); -var fs = require('fs'); +const test = require('ava'); -shell.config.silent = true; - -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +const shell = require('..'); +const common = require('../src/common'); -var result; +shell.config.silent = true; // // Invalids // -result = shell.tail(); -assert.ok(shell.error()); -assert.equal(result.code, 1); - -assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check -result = shell.tail('/adsfasdf'); // file does not exist -assert.ok(shell.error()); -assert.equal(result.code, 1); +test('no args', t => { + const result = shell.tail(); + t.truthy(shell.error()); + t.is(result.code, 1); +}); + +test('file does not exist', t => { + t.falsy(fs.existsSync('/asdfasdf')); // sanity check + const result = shell.tail('/asdfasdf'); + t.truthy(shell.error()); + t.is(result.code, 1); +}); + +test('directory', t => { + t.truthy(common.statFollowLinks('test/resources/').isDirectory()); // sanity check + const result = shell.tail('test/resources/'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, "tail: error reading 'test/resources/': Is a directory"); +}); // // Valids // -var bottomOfFile1 = ['file1 50', 'file1 49', 'file1 48', 'file1 47', 'file1 46', - 'file1 45', 'file1 44', 'file1 43', 'file1 42', 'file1 41', - 'file1 40', 'file1 39', 'file1 38', 'file1 37', 'file1 36', - 'file1 35', 'file1 34', 'file1 33', 'file1 32', 'file1 31']; -var bottomOfFile2 = ['file2 50', 'file2 49', 'file2 48', 'file2 47', 'file2 46', - 'file2 45', 'file2 44', 'file2 43', 'file2 42', 'file2 41', - 'file2 40', 'file2 39', 'file2 38', 'file2 37', 'file2 36', - 'file2 35', 'file2 34', 'file2 33', 'file2 32', 'file2 31']; - -// simple -result = shell.tail('resources/head/file1.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, bottomOfFile1.slice(0, 10).reverse().join('\n')+'\n'); - -// multiple files -result = shell.tail('resources/head/file2.txt', 'resources/head/file1.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, bottomOfFile2.slice(0, 10).reverse().concat( - bottomOfFile1.slice(0, 10).reverse() - ).join('\n')+'\n'); - -// multiple files, array syntax -result = shell.tail(['resources/head/file2.txt', 'resources/head/file1.txt']); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, bottomOfFile2.slice(0, 10).reverse().concat( - bottomOfFile1.slice(0, 10).reverse() - ).join('\n')+'\n'); - -// reading more lines than are in the file (no trailing newline) -result = shell.tail('resources/file2', 'resources/file1'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, 'test2\ntest1'); // these files only have one line (no \n) - -// reading more lines than are in the file (with trailing newline) -result = shell.tail('resources/head/shortfile2', 'resources/head/shortfile1'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, 'short2\nshort1\n'); // these files only have one line (with \n) - -// Globbed file -result = shell.tail('resources/head/file?.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, bottomOfFile1.slice(0, 10).reverse().concat( - bottomOfFile2.slice(0, 10).reverse() - ).join('\n')+'\n'); - -// With `'-n' ` option -result = shell.tail('-n', 4, 'resources/head/file2.txt', 'resources/head/file1.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, bottomOfFile2.slice(0, 4).reverse().concat( - bottomOfFile1.slice(0, 4).reverse() - ).join('\n')+'\n'); - -// With `{'-n': }` option -result = shell.tail({'-n': 4}, 'resources/head/file2.txt', 'resources/head/file1.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, bottomOfFile2.slice(0, 4).reverse().concat( - bottomOfFile1.slice(0, 4).reverse() - ).join('\n')+'\n'); - -// negative values are the same as positive values -result = shell.tail('-n', -4, 'resources/head/file2.txt', 'resources/head/file1.txt'); -assert.equal(shell.error(), null); -assert.equal(result.code, 0); -assert.equal(result, bottomOfFile2.slice(0, 4).reverse().concat( - bottomOfFile1.slice(0, 4).reverse() - ).join('\n')+'\n'); - -shell.exit(123); +const bottomOfFile1 = ['file1 50', 'file1 49', 'file1 48', 'file1 47', 'file1 46', + 'file1 45', 'file1 44', 'file1 43', 'file1 42', 'file1 41', + 'file1 40', 'file1 39', 'file1 38', 'file1 37', 'file1 36', + 'file1 35', 'file1 34', 'file1 33', 'file1 32', 'file1 31']; +const bottomOfFile2 = ['file2 50', 'file2 49', 'file2 48', 'file2 47', 'file2 46', + 'file2 45', 'file2 44', 'file2 43', 'file2 42', 'file2 41', + 'file2 40', 'file2 39', 'file2 38', 'file2 37', 'file2 36', + 'file2 35', 'file2 34', 'file2 33', 'file2 32', 'file2 31']; + +test('simple', t => { + const result = shell.tail('test/resources/head/file1.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), bottomOfFile1.slice(0, 10).reverse().join('\n') + '\n'); +}); + +test('multiple files', t => { + const result = shell.tail('test/resources/head/file2.txt', 'test/resources/head/file1.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), + bottomOfFile2 + .slice(0, 10) + .reverse() + .concat(bottomOfFile1.slice(0, 10).reverse()) + .join('\n') + '\n'); +}); + +test('multiple files, array syntax', t => { + const result = shell.tail(['test/resources/head/file2.txt', 'test/resources/head/file1.txt']); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), + bottomOfFile2 + .slice(0, 10) + .reverse() + .concat(bottomOfFile1.slice(0, 10).reverse()) + .join('\n') + '\n'); +}); + +test('reading more lines than are in the file (no trailing newline)', t => { + const result = shell.tail('test/resources/file2', 'test/resources/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'test2\ntest1'); // these files only have one line (no \n) +}); + +test('reading more lines than are in the file (with trailing newline)', t => { + const result = shell.tail('test/resources/head/shortfile2', 'test/resources/head/shortfile1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), 'short2\nshort1\n'); // these files only have one line (with \n) +}); + +test('globbed file', t => { + const result = shell.tail('test/resources/head/file?.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), + bottomOfFile1 + .slice(0, 10) + .reverse() + .concat(bottomOfFile2.slice(0, 10).reverse()) + .join('\n') + '\n'); +}); + +test("with '-n ' option", t => { + const result = shell.tail('-n', 4, 'test/resources/head/file2.txt', + 'test/resources/head/file1.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), + bottomOfFile2 + .slice(0, 4) + .reverse() + .concat(bottomOfFile1.slice(0, 4).reverse()) + .join('\n') + '\n'); +}); + +test("with '-n +' option", t => { + const result = shell.tail('-n', '+48', 'test/resources/head/file2.txt', + 'test/resources/head/file1.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), + bottomOfFile2 + .slice(0, 3) + .reverse() + .concat(bottomOfFile1.slice(0, 3).reverse()) + .join('\n') + '\n'); +}); + +test("with '{-n: }' option", t => { + const result = shell.tail({ '-n': 4 }, 'test/resources/head/file2.txt', + 'test/resources/head/file1.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), + bottomOfFile2 + .slice(0, 4) + .reverse() + .concat(bottomOfFile1.slice(0, 4).reverse()) + .join('\n') + '\n'); +}); + +test("with '{-n: +}' option", t => { + const result = shell.tail({ '-n': '+48' }, 'test/resources/head/file2.txt', + 'test/resources/head/file1.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), + bottomOfFile2 + .slice(0, 3) + .reverse() + .concat(bottomOfFile1.slice(0, 3).reverse()) + .join('\n') + '\n'); +}); + +test('negative values are the same as positive values', t => { + const result = shell.tail('-n', -4, 'test/resources/head/file2.txt', + 'test/resources/head/file1.txt'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), + bottomOfFile2 + .slice(0, 4) + .reverse() + .concat(bottomOfFile1.slice(0, 4).reverse()) + .join('\n') + '\n'); +}); diff --git a/test/tempdir.js b/test/tempdir.js index e47ddd8ff..5590667e7 100644 --- a/test/tempdir.js +++ b/test/tempdir.js @@ -1,19 +1,31 @@ -var shell = require('..'); +const fs = require('fs'); -var assert = require('assert'), - fs = require('fs'); +const test = require('ava'); + +const shell = require('..'); +const { isCached, clearCache } = require('../src/tempdir'); shell.config.silent = true; -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); // // Valids // -var tmp = shell.tempdir(); -assert.equal(shell.error(), null); -assert.equal(fs.existsSync(tmp), true); +test('basic usage', t => { + const tmp = shell.tempdir(); + t.falsy(shell.error()); + t.truthy(fs.existsSync(tmp)); + + // It's a directory + t.truthy(shell.test('-d', tmp)); +}); -shell.exit(123); +test('cache', t => { + clearCache(); // In case this runs after any test which relies on tempdir(). + t.falsy(isCached()); + const tmp1 = shell.tempdir(); + t.truthy(isCached()); + const tmp2 = shell.tempdir(); + t.is(tmp1, tmp2); +}); diff --git a/test/test.js b/test/test.js index 8fc9e8c10..1b42057ae 100644 --- a/test/test.js +++ b/test/test.js @@ -1,90 +1,131 @@ -var shell = require('..'); -var common = require('../src/common'); +const test = require('ava'); -var assert = require('assert'); +const shell = require('..'); +const utils = require('./utils/utils'); shell.config.silent = true; -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); - // // Invalids // -var result = shell.test(); // no expression given -assert.ok(shell.error()); +test('no expression given', t => { + shell.test(); + t.truthy(shell.error()); +}); -var result = shell.test('asdf'); // bad expression -assert.ok(shell.error()); +test('bad expression', t => { + shell.test('asdf'); + t.truthy(shell.error()); +}); -var result = shell.test('f', 'resources/file1'); // bad expression -assert.ok(shell.error()); +test('bad expression #2', t => { + shell.test('f', 'test/resources/file1'); + t.truthy(shell.error()); +}); -var result = shell.test('-f'); // no file -assert.ok(shell.error()); +test('no file', t => { + shell.test('-f'); + t.truthy(shell.error()); +}); // // Valids // -//exists -var result = shell.test('-e', 'resources/file1'); -assert.equal(shell.error(), null); -assert.equal(result, true);//true - -var result = shell.test('-e', 'resources/404'); -assert.equal(shell.error(), null); -assert.equal(result, false); - -//directory -var result = shell.test('-d', 'resources'); -assert.equal(shell.error(), null); -assert.equal(result, true);//true - -var result = shell.test('-f', 'resources'); -assert.equal(shell.error(), null); -assert.equal(result, false); - -var result = shell.test('-L', 'resources'); -assert.equal(shell.error(), null); -assert.equal(result, false); - -//file -var result = shell.test('-d', 'resources/file1'); -assert.equal(shell.error(), null); -assert.equal(result, false); - -var result = shell.test('-f', 'resources/file1'); -assert.equal(shell.error(), null); -assert.equal(result, true);//true - -var result = shell.test('-L', 'resources/file1'); -assert.equal(shell.error(), null); -assert.equal(result, false); - -//link -// Windows is weird with links so skip these tests -if (common.platform !== 'win') { - var result = shell.test('-d', 'resources/link'); - assert.equal(shell.error(), null); - assert.equal(result, false); - - var result = shell.test('-f', 'resources/link'); - assert.equal(shell.error(), null); - assert.equal(result, true);//true - - var result = shell.test('-L', 'resources/link'); - assert.equal(shell.error(), null); - assert.equal(result, true);//true - - var result = shell.test('-L', 'resources/badlink'); - assert.equal(shell.error(), null); - assert.equal(result, true);//true - - var result = shell.test('-L', 'resources/404'); - assert.equal(shell.error(), null); - assert.equal(result, false);//false -} - -shell.exit(123); + +test('-e option succeeds for files', t => { + const result = shell.test('-e', 'test/resources/file1'); + t.falsy(shell.error()); + t.truthy(result); +}); + +test('-e option fails if it does not exist', t => { + const result = shell.test('-e', 'test/resources/404'); + t.falsy(shell.error()); + t.falsy(result); +}); + +test('-d option succeeds for a directory', t => { + const result = shell.test('-d', 'test/resources'); + t.falsy(shell.error()); + t.truthy(result); +}); + +test('-f option fails for a directory', t => { + const result = shell.test('-f', 'test/resources'); + t.falsy(shell.error()); + t.falsy(result); +}); + +test('-L option fails for a directory', t => { + const result = shell.test('-L', 'test/resources'); + t.falsy(shell.error()); + t.falsy(result); +}); + +test('-d option fails for a file', t => { + const result = shell.test('-d', 'test/resources/file1'); + t.falsy(shell.error()); + t.falsy(result); +}); + +test('-f option succeeds for a file', t => { + const result = shell.test('-f', 'test/resources/file1'); + t.falsy(shell.error()); + t.truthy(result); +}); + +test('-L option fails for a file', t => { + const result = shell.test('-L', 'test/resources/file1'); + t.falsy(shell.error()); + t.falsy(result); +}); + +test('test command is not globbed', t => { + // regression #529 + const result = shell.test('-f', 'test/resources/**/*.js'); + t.falsy(shell.error()); + t.falsy(result); +}); + +// TODO(nate): figure out a way to test links on Windows +test('-d option fails for a link', t => { + utils.skipOnWin(t, () => { + const result = shell.test('-d', 'test/resources/link'); + t.falsy(shell.error()); + t.falsy(result); + }); +}); + +test('-f option succeeds for a link', t => { + utils.skipOnWin(t, () => { + const result = shell.test('-f', 'test/resources/link'); + t.falsy(shell.error()); + t.truthy(result); + }); +}); + +test('-L option succeeds for a symlink', t => { + utils.skipOnWin(t, () => { + const result = shell.test('-L', 'test/resources/link'); + t.falsy(shell.error()); + t.truthy(result); + }); +}); + +test('-L option works for broken symlinks', t => { + utils.skipOnWin(t, () => { + const result = shell.test('-L', 'test/resources/badlink'); + t.falsy(shell.error()); + t.truthy(result); + }); +}); + +test('-L option fails for missing files', t => { + utils.skipOnWin(t, () => { + const result = shell.test('-L', 'test/resources/404'); + t.falsy(shell.error()); + t.falsy(result); + }); +}); diff --git a/test/to.js b/test/to.js index b8ca7f1b7..4669052eb 100644 --- a/test/to.js +++ b/test/to.js @@ -1,46 +1,60 @@ -var shell = require('..'); +const fs = require('fs'); -var assert = require('assert'), - fs = require('fs'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const utils = require('./utils/utils'); + +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.mkdir(t.context.tmp); +}); + +test.afterEach.always(t => { + shell.rm('-rf', t.context.tmp); +}); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); // // Invalids // -// Normal strings don't have '.to()' anymore -var str = 'hello world'; -assert.ok(typeof str.to === 'undefined'); +test("Normal strings don't have '.to()' anymore", t => { + const str = 'hello world'; + t.is(str.to, undefined); +}); -shell.ShellString('hello world').to(); -assert.ok(shell.error()); +test('no file argument', t => { + shell.ShellString('hello world').to(); + t.truthy(shell.error()); +}); -assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check -shell.ShellString('hello world').to('/asdfasdf/file'); -assert.ok(shell.error()); +test('cannot write to a non-existent directory', t => { + t.falsy(fs.existsSync('/asdfasdf')); // sanity check + shell.ShellString('hello world').to('/asdfasdf/file'); + t.truthy(shell.error()); +}); // // Valids // -var result; - -shell.ShellString('hello world').to('tmp/to1').to('tmp/to2'); -result = shell.cat('tmp/to1'); -assert.equal(shell.error(), null); -assert.equal(result, 'hello world'); -result = shell.cat('tmp/to2'); -assert.equal(shell.error(), null); -assert.equal(result, 'hello world'); - -// With a glob -shell.ShellString('goodbye').to('tmp/t*1'); -result = shell.cat('tmp/to1'); -assert.equal(shell.error(), null); -assert.equal(result, 'goodbye'); - -shell.exit(123); +test('can be chained', t => { + shell.ShellString('hello world').to(`${t.context.tmp}/to1`).to(`${t.context.tmp}/to2`); + let result = shell.cat(`${t.context.tmp}/to1`); + t.falsy(shell.error()); + t.is(result.toString(), 'hello world'); + result = shell.cat(`${t.context.tmp}/to2`); + t.falsy(shell.error()); + t.is(result.toString(), 'hello world'); +}); + +test('With a glob', t => { + shell.touch(`${t.context.tmp}/to1`); + shell.ShellString('goodbye').to(`${t.context.tmp}/t*1`); + t.falsy(fs.existsSync(`${t.context.tmp}/t*1`), 'globs are not interpreted literally'); + const result = shell.cat(`${t.context.tmp}/to1`); + t.falsy(shell.error()); + t.is(result.toString(), 'goodbye'); +}); diff --git a/test/toEnd.js b/test/toEnd.js index dce6250fe..4e8417396 100644 --- a/test/toEnd.js +++ b/test/toEnd.js @@ -1,48 +1,76 @@ -var shell = require('..'); +const fs = require('fs'); -var assert = require('assert'), - fs = require('fs'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const utils = require('./utils/utils'); + +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.mkdir(t.context.tmp); +}); + +test.afterEach.always(t => { + shell.rm('-rf', t.context.tmp); +}); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); // // Invalids // -// Normal strings don't have '.toEnd()' anymore -var str = 'hello world'; -assert.ok(typeof str.toEnd === 'undefined'); +test("Normal strings don't have '.toEnd()' anymore", t => { + const str = 'hello world'; + t.is(str.toEnd, undefined); +}); + +test('missing file argument', t => { + shell.ShellString('hello world').toEnd(); + t.truthy(shell.error()); +}); -shell.ShellString('hello world').toEnd(); -assert.ok(shell.error()); +test('cannot write to a non-existent directory', t => { + t.falsy(fs.existsSync('/asdfasdf')); // sanity check + shell.ShellString('hello world').toEnd('/asdfasdf/file'); + t.truthy(shell.error()); +}); -assert.equal(fs.existsSync('/asdfasdf'), false); // sanity check -assert.ok(shell.error()); // // Valids // -var result; -assert.equal(fs.existsSync('tmp/toEnd1'), false); //Check file toEnd() creates does not already exist -assert.equal(fs.existsSync('tmp/toEnd2'), false); -shell.ShellString('hello ').toEnd('tmp/toEnd1'); -assert.equal(fs.existsSync('tmp/toEnd1'), true); //Check that file was created -shell.ShellString('world').toEnd('tmp/toEnd1').toEnd('tmp/toEnd2'); //Write some more to the file -result = shell.cat('tmp/toEnd1'); -assert.equal(shell.error(), null); -assert.equal(result, 'hello world'); //Check that the result is what we expect -result = shell.cat('tmp/toEnd2'); -assert.equal(shell.error(), null); -assert.equal(result, 'world'); //Check that the result is what we expect - -// With a glob -shell.ShellString('good').to('tmp/toE*1'); -shell.ShellString('bye').toEnd('tmp/toE*1'); -result = shell.cat('tmp/toEnd1'); -assert.equal(shell.error(), null); -assert.equal(result, 'goodbye'); - -shell.exit(123); +test('creates a new file', t => { + t.falsy(fs.existsSync(`${t.context.tmp}/toEnd1`)); // Check file toEnd() creates does not already exist + shell.ShellString('hello ').toEnd(`${t.context.tmp}/toEnd1`); + t.truthy(fs.existsSync(`${t.context.tmp}/toEnd1`)); // Check that file was created + const result = shell.cat(`${t.context.tmp}/toEnd1`); + t.is(result.toString(), 'hello '); +}); + +test('can be chained', t => { + t.falsy(fs.existsSync(`${t.context.tmp}/toEnd1`)); + t.falsy(fs.existsSync(`${t.context.tmp}/toEnd2`)); + shell.ShellString('hello ').toEnd(`${t.context.tmp}/toEnd1`); + shell.ShellString('world') + .toEnd(`${t.context.tmp}/toEnd1`) + .toEnd(`${t.context.tmp}/toEnd2`); // Write some more to the file + const result1 = shell.cat(`${t.context.tmp}/toEnd1`); + t.falsy(shell.error()); + t.is(result1.toString(), 'hello world'); // Check that the result is what we expect + const result2 = shell.cat(`${t.context.tmp}/toEnd2`); + t.falsy(shell.error()); + t.is(result2.toString(), 'world'); // Check that the result is what we expect +}); + +test('With a glob', t => { + shell.touch(`${t.context.tmp}/toEnd1`); + shell.ShellString('good').to(`${t.context.tmp}/toE*1`); + shell.ShellString('bye').toEnd(`${t.context.tmp}/toE*1`); + t.falsy( + fs.existsSync(`${t.context.tmp}/toE*1`) + ); + const result = shell.cat(`${t.context.tmp}/toEnd1`); + t.falsy(shell.error()); + t.is(result.toString(), 'goodbye'); +}); diff --git a/test/touch.js b/test/touch.js index e9cccd436..c767eafcd 100644 --- a/test/touch.js +++ b/test/touch.js @@ -1,136 +1,33 @@ -var shell = require('..'); -var assert = require('assert'); -var fs = require('fs'); -var crypto = require('crypto'); - -shell.config.silent = true; -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); - -var oldStat; -var testFile; - -// should handle args -var result = shell.touch(); -assert.ok(shell.error()); -assert.equal(result.code, 1); - -result = shell.touch(1); -assert.ok(shell.error()); -assert.equal(result.code, 1); - -// exits without error when trying to touch a directory -result = shell.touch('tmp/'); -assert.ok(!shell.error()); -assert.equal(result.code, 0); -result = shell.touch('tmp'); -assert.ok(!shell.error()); -assert.equal(result.code, 0); - -// creates new files -var testFile = tmpFile(); -result = shell.touch(testFile); -assert(fs.existsSync(testFile)); - -// does not create a file if told not to -var testFile = tmpFile(true); -result = shell.touch('-c', testFile); -assert.equal(result.code, 0); -assert.ok(!fs.existsSync(testFile)); - -// handles globs correctly -result = shell.touch('tmp/file.txt'); -result = shell.touch('tmp/file.js'); -result = shell.touch('tmp/file*'); -assert.equal(result.code, 0); -var files = shell.ls('tmp/file*'); -assert.ok(files.indexOf('tmp/file.txt') > -1); -assert.ok(files.indexOf('tmp/file.js') > -1); -assert.equal(files.length, 2); - -// errors if reference file is not found -var testFile = tmpFile(); -var refFile = tmpFile(true); -result = shell.touch({'-r': refFile}, testFile); -assert.equal(result.code, 1); -assert.ok(shell.error()); - -// uses a reference file for mtime -var testFile = tmpFile(false); -var testFile2 = tmpFile(); -shell.touch(testFile2); -shell.exec(JSON.stringify(process.execPath)+' resources/exec/slow.js 3000'); -result = shell.touch(testFile); -assert.ok(!shell.error()); -assert.equal(result.code, 0); -assert.notEqual(fs.statSync(testFile).mtime.getTime(), fs.statSync(testFile2).mtime.getTime()); -assert.notEqual(fs.statSync(testFile).atime.getTime(), fs.statSync(testFile2).atime.getTime()); -result = shell.touch({'-r': testFile2}, testFile); -assert.ok(!shell.error()); -assert.equal(result.code, 0); -assert.equal(fs.statSync(testFile).mtime.getTime(), fs.statSync(testFile2).mtime.getTime()); -assert.equal(fs.statSync(testFile).atime.getTime(), fs.statSync(testFile2).atime.getTime()); - -// sets mtime -testFile = tmpFile(); -oldStat = resetUtimes(testFile); -result = shell.touch(testFile); -assert.equal(result.code, 0); -assert(oldStat.mtime < fs.statSync(testFile).mtime); -// sets atime -assert(oldStat.atime < fs.statSync(testFile).atime); - -// does not sets mtime if told not to -testFile = tmpFile(); -oldStat = resetUtimes(testFile); -result = shell.touch('-a', testFile); -assert.equal(result.code, 0); -assert.equal(oldStat.mtime.getTime(), fs.statSync(testFile).mtime.getTime()); - -// does not sets atime if told not to -testFile = tmpFile(); -oldStat = resetUtimes(testFile); -result = shell.touch('-m', testFile); -assert.equal(result.code, 0); -assert.equal(oldStat.atime.getTime(), fs.statSync(testFile).atime.getTime()); - -// multiple files -testFile = tmpFile(true); -testFile2 = tmpFile(true); -shell.rm('-f', testFile, testFile2); -result = shell.touch(testFile, testFile2); -assert.equal(result.code, 0); -assert(fs.existsSync(testFile)); -assert(fs.existsSync(testFile2)); - -// file array -testFile = tmpFile(true); -testFile2 = tmpFile(true); -shell.rm('-f', testFile, testFile2); -result = shell.touch([testFile, testFile2]); -assert.equal(result.code, 0); -assert(fs.existsSync(testFile)); -assert(fs.existsSync(testFile2)); - -// touching broken link creates a new file -if (process.platform !== 'win32') { - result = shell.touch('resources/badlink'); - assert.equal(result.code, 0); - assert.ok(!shell.error()); - assert.ok(fs.existsSync('resources/not_existed_file')); - shell.rm('resources/not_existed_file'); -} +const crypto = require('crypto'); +const fs = require('fs'); + +const test = require('ava'); + +const shell = require('..'); +const common = require('../src/common'); +const utils = require('./utils/utils'); +test.beforeEach(t => { + t.context.tmp = utils.getTempDir(); + shell.config.resetForTesting(); + shell.mkdir(t.context.tmp); +}); + +test.afterEach.always(t => { + shell.rm('-rf', t.context.tmp); +}); + +// Helper functions function resetUtimes(f) { - var d = new Date(); + const d = new Date(); d.setYear(2000); fs.utimesSync(f, d, d); - return fs.statSync(f); + return common.statFollowLinks(f); } -function tmpFile(noCreate) { - var str = crypto.randomBytes(Math.ceil(10 / 2)).toString('hex'); - var file = 'tmp/' + str; +function tmpFile(t, noCreate) { + const str = crypto.randomBytes(Math.ceil(10 / 2)).toString('hex'); + const file = `${t.context.tmp}/${str}`; if (!noCreate) { fs.closeSync(fs.openSync(file, 'a')); } @@ -138,6 +35,161 @@ function tmpFile(noCreate) { } -// required for the test runner -shell.exit(123); - +// +// Valids +// + +test('should handle args', t => { + const result = shell.touch(); + t.truthy(shell.error()); + t.is(result.code, 1); +}); + +test('arguments must be strings', t => { + const result = shell.touch(1); + t.truthy(shell.error()); + t.is(result.code, 1); +}); + +test('exits without error when trying to touch a directory', t => { + const result = shell.touch(t.context.tmp); + t.falsy(shell.error()); + t.is(result.code, 0); +}); + +test('creates new files', t => { + const testFile = tmpFile(t); + const result = shell.touch(testFile); + t.truthy(fs.existsSync(testFile)); + t.is(result.code, 0); +}); + +test('does not create a file if told not to', t => { + const testFile = tmpFile(t, true); + const result = shell.touch('-c', testFile); + t.is(result.code, 0); + t.falsy(fs.existsSync(testFile)); +}); + +test('handles globs correctly', t => { + shell.touch(`${t.context.tmp}/file.txt`); + shell.touch(`${t.context.tmp}/file.js`); + const result = shell.touch(`${t.context.tmp}/file*`); + t.is(result.code, 0); + const files = shell.ls(`${t.context.tmp}/file*`); + t.truthy(files.includes(`${t.context.tmp}/file.txt`)); + t.truthy(files.includes(`${t.context.tmp}/file.js`)); + t.is(files.length, 2); +}); + +test('errors if reference file is not found', t => { + const testFile = tmpFile(t); + const refFile = tmpFile(t, true); + const result = shell.touch({ '-r': refFile }, testFile); + t.is(result.code, 1); + t.truthy(shell.error()); +}); + +test('uses a reference file for mtime', t => { + const testFile = tmpFile(t); + const testFile2 = tmpFile(t); + shell.touch(testFile2); + utils.sleep(1000); + let result = shell.touch(testFile); + t.falsy(shell.error()); + t.is(result.code, 0); + t.not( + common.statFollowLinks(testFile).mtime.getTime(), + common.statFollowLinks(testFile2).mtime.getTime() + ); + t.not( + common.statFollowLinks(testFile).atime.getTime(), + common.statFollowLinks(testFile2).atime.getTime() + ); + result = shell.touch({ '-r': testFile2 }, testFile); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is( + common.statFollowLinks(testFile).mtime.getTime(), + common.statFollowLinks(testFile2).mtime.getTime() + ); + t.is( + common.statFollowLinks(testFile).atime.getTime(), + common.statFollowLinks(testFile2).atime.getTime() + ); +}); + +test('accepts -d flag', t => { + const testFile = tmpFile(t); + const date = new Date('December 17, 1995 03:24:00'); + const result = shell.touch({ '-d': date }, testFile); + t.is(result.code, 0); + // Compare getTime(), because Date can't be compared with triple-equals. + t.is(common.statFollowLinks(testFile).mtime.getTime(), date.getTime()); + t.is(common.statFollowLinks(testFile).atime.getTime(), date.getTime()); +}); + +test('accepts long option (--date)', t => { + const testFile = tmpFile(t); + const someDate = new Date('December 17, 1995 03:24:00'); + const result = shell.touch({ date: someDate }, testFile); + t.is(result.code, 0); + // Compare getTime(), because Date can't be compared with triple-equals. + t.is(common.statFollowLinks(testFile).mtime.getTime(), someDate.getTime()); + t.is(common.statFollowLinks(testFile).atime.getTime(), someDate.getTime()); +}); + +test('sets mtime and atime by default', t => { + const testFile = tmpFile(t); + const oldStat = resetUtimes(testFile); + const result = shell.touch(testFile); + t.is(result.code, 0); + t.truthy(oldStat.mtime < common.statFollowLinks(testFile).mtime); + t.truthy(oldStat.atime < common.statFollowLinks(testFile).atime); +}); + +test('does not set mtime if told not to', t => { + const testFile = tmpFile(t); + const oldStat = resetUtimes(testFile); + const result = shell.touch('-a', testFile); + t.is(result.code, 0); + t.is(oldStat.mtime.getTime(), common.statFollowLinks(testFile).mtime.getTime()); +}); + +test('does not set atime if told not to', t => { + const testFile = tmpFile(t); + const oldStat = resetUtimes(testFile); + const result = shell.touch('-m', testFile); + t.is(result.code, 0); + t.is(oldStat.atime.getTime(), common.statFollowLinks(testFile).atime.getTime()); +}); + +test('multiple files', t => { + const testFile = tmpFile(t, true); + const testFile2 = tmpFile(t, true); + shell.rm('-f', testFile, testFile2); + const result = shell.touch(testFile, testFile2); + t.is(result.code, 0); + t.truthy(fs.existsSync(testFile)); + t.truthy(fs.existsSync(testFile2)); +}); + +test('file array', t => { + const testFile = tmpFile(t, true); + const testFile2 = tmpFile(t, true); + shell.rm('-f', testFile, testFile2); + const result = shell.touch([testFile, testFile2]); + t.is(result.code, 0); + t.truthy(fs.existsSync(testFile)); + t.truthy(fs.existsSync(testFile2)); +}); + +test('touching broken link creates a new file', t => { + utils.skipOnWin(t, () => { + shell.ln('-s', 'not_existed_file', `${t.context.tmp}/badlink2`); + const result = shell.touch(`${t.context.tmp}/badlink2`); + t.is(result.code, 0); + t.falsy(shell.error()); + t.truthy(fs.existsSync(`${t.context.tmp}/not_existed_file`)); + }); +}); diff --git a/test/uniq.js b/test/uniq.js new file mode 100644 index 000000000..72611df83 --- /dev/null +++ b/test/uniq.js @@ -0,0 +1,104 @@ +const fs = require('fs'); + +const test = require('ava'); + +const shell = require('..'); +const common = require('../src/common'); + +shell.config.silent = true; + +// +// Invalids +// + +test('no args', t => { + const result = shell.uniq(); + t.truthy(shell.error()); + t.truthy(result.code); +}); + +test('file does not exist', t => { + t.falsy(fs.existsSync('/asdfasdf')); // sanity check + const result = shell.uniq('/asdfasdf'); + t.truthy(shell.error()); + t.truthy(result.code); +}); + +test('directory', t => { + t.truthy(common.statFollowLinks('test/resources/').isDirectory()); // sanity check + const result = shell.uniq('test/resources/'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, "uniq: error reading 'test/resources/'"); +}); + +test('output directory', t => { + t.truthy(common.statFollowLinks('test/resources/').isDirectory()); // sanity check + const result = shell.uniq('test/resources/file1.txt', 'test/resources/'); + t.truthy(shell.error()); + t.is(result.code, 1); + t.is(result.stderr, 'uniq: test/resources/: Is a directory'); +}); + +test('file does not exist with output directory', t => { + t.falsy(fs.existsSync('/asdfasdf')); // sanity check + const result = shell.uniq('/asdfasdf', 'test/resources/'); + t.is(result.code, 1); + t.truthy(shell.error()); +}); + +// +// Valids +// + +test('uniq file1', t => { + const result = shell.uniq('test/resources/uniq/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), shell.cat('test/resources/uniq/file1u').toString()); +}); + +test('uniq -i file2', t => { + const result = shell.uniq('-i', 'test/resources/uniq/file2'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), shell.cat('test/resources/uniq/file2u').toString()); +}); + +test('with glob character', t => { + const result = shell.uniq('-i', 'test/resources/uniq/fi?e2'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), shell.cat('test/resources/uniq/file2u').toString()); +}); + +test('uniq file1 file2', t => { + const result = shell.uniq('test/resources/uniq/file1', 'test/resources/uniq/file1t'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is( + shell.cat('test/resources/uniq/file1u').toString(), + shell.cat('test/resources/uniq/file1t').toString() + ); +}); + +test('cat file1 |uniq', t => { + const result = shell.cat('test/resources/uniq/file1').uniq(); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), shell.cat('test/resources/uniq/file1u').toString()); +}); + +test('uniq -c file1', t => { + const result = shell.uniq('-c', 'test/resources/uniq/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), shell.cat('test/resources/uniq/file1c').toString()); +}); + +test('uniq -d file1', t => { + const result = shell.uniq('-d', 'test/resources/uniq/file1'); + t.falsy(shell.error()); + t.is(result.code, 0); + t.is(result.toString(), shell.cat('test/resources/uniq/file1d').toString()); +}); diff --git a/test/utils/mocks.js b/test/utils/mocks.js new file mode 100644 index 000000000..a55e6d298 --- /dev/null +++ b/test/utils/mocks.js @@ -0,0 +1,68 @@ +function addToString(str, val) { + if (Buffer.isBuffer(val)) { + return str + val.toString(); + } + return str + val; +} + +function joinData(data) { + return data.reduce(addToString, ''); +} + +function wrapWrite(target) { + return function write(val) { + target.push(val); + return true; + }; +} + +const stdout = { + original: process.stdout.write, + value: [], + init: function init() { + this.value = []; + process.stdout.write = wrapWrite(this.value); + }, + restore: function restore() { + process.stdout.write = this.original; + }, + getValue: function getValue() { + return joinData(this.value); + } +}; + +const stderr = { + original: process.stderr.write, + value: [], + init: function init() { + this.value = []; + process.stderr.write = wrapWrite(this.value); + }, + restore: function restore() { + process.stderr.write = this.original; + }, + getValue: function getValue() { + return joinData(this.value); + } +}; + +const exit = { + original: process.exit, + value: undefined, + init: function init() { + this.value = undefined; + process.exit = (newCode) => { + this.value = newCode; + }; + }, + restore: function restore() { + process.exit = this.original; + }, + getValue: function getValue() { + return this.value; + } +}; + +exports.stdout = stdout; +exports.stderr = stderr; +exports.exit = exit; diff --git a/test/utils/utils.js b/test/utils/utils.js index 98bd8a2d0..9c20e5db3 100644 --- a/test/utils/utils.js +++ b/test/utils/utils.js @@ -1,5 +1,76 @@ -function _numLines(str) { - return typeof str === 'string' ? (str.match(/\n/g)||[]).length+1 : 0; +const child = require('child_process'); +const path = require('path'); +const { promisify } = require('node:util'); + +const chalk = require('chalk'); + +const common = require('../../src/common'); + +// Capture process.stderr.write, otherwise we have a conflict with mocks.js +const _processStderrWrite = process.stderr.write.bind(process.stderr); + +function numLines(str) { + return typeof str === 'string' ? (str.match(/\n/g) || []).length + 1 : 0; +} +exports.numLines = numLines; + +function getTempDir() { + // a very random directory + return ('tmp' + Math.random() + Math.random()).replace(/\./g, ''); +} +exports.getTempDir = getTempDir; + +// On Windows, symlinks for files need admin permissions. This helper +// skips certain tests if we are on Windows and got an EPERM error +function skipOnWinForEPERM(action, testCase) { + const ret = action(); + const error = ret.code; + const isWindows = process.platform === 'win32'; + if (isWindows && error && /EPERM:/.test(error)) { + _processStderrWrite('Got EPERM when testing symlinks on Windows. Assuming non-admin environment and skipping test.\n'); + } else { + testCase(); + } +} +exports.skipOnWinForEPERM = skipOnWinForEPERM; + +function runScript(script) { + const promiseExec = promisify(child.execFile); + return promiseExec(common.config.execPath, ['-e', script]); +} +exports.runScript = runScript; + +function sleep(time) { + const testDirectoryPath = path.dirname(__dirname); + child.execFileSync(common.config.execPath, [ + path.join(testDirectoryPath, 'resources', 'exec', 'slow.js'), + time.toString(), + ]); +} +exports.sleep = sleep; + +function mkfifo(dir) { + if (process.platform !== 'win32') { + const fifo = dir + 'fifo'; + child.execFileSync('mkfifo', [fifo]); + return fifo; + } + return null; +} +exports.mkfifo = mkfifo; + +function skipIfTrue(booleanValue, t, closure) { + if (booleanValue) { + _processStderrWrite( + chalk.yellow('Warning: skipping platform-dependent test ') + + chalk.bold.white(`'${t.title}'`) + + '\n' + ); + t.truthy(true); // dummy assertion to satisfy ava v0.19+ + } else { + closure(); + } } -exports.numLines = _numLines; +exports.skipOnUnix = skipIfTrue.bind(module.exports, process.platform !== 'win32'); +exports.skipOnWin = skipIfTrue.bind(module.exports, process.platform === 'win32'); diff --git a/test/which.js b/test/which.js index 70ba5db47..c49eb06d4 100644 --- a/test/which.js +++ b/test/which.js @@ -1,41 +1,96 @@ -var shell = require('..'); +const fs = require('fs'); +const path = require('path'); -var assert = require('assert'), - fs = require('fs'); +const test = require('ava'); -shell.config.silent = true; +const shell = require('..'); +const utils = require('./utils/utils'); -shell.rm('-rf', 'tmp'); -shell.mkdir('tmp'); +shell.config.silent = true; // // Invalids // -shell.which(); -assert.ok(shell.error()); +test('no args', t => { + shell.which(); + t.truthy(shell.error()); +}); -var result = shell.which('asdfasdfasdfasdfasdf'); // what are the odds... -assert.ok(!shell.error()); -assert.ok(!result); +test('command does not exist in the path', t => { + const result = shell.which('asdfasdfasdfasdfasdf'); // what are the odds... + t.falsy(shell.error()); + t.falsy(result); +}); // // Valids // -var node = shell.which('node'); -assert.equal(node.code, 0); -assert.ok(!node.stderr); -assert.ok(!shell.error()); -assert.ok(fs.existsSync(node + '')); +// TODO(nate): make sure this does not have a false negative if 'git' is missing +test('basic usage', t => { + const git = shell.which('git'); + t.is(git.code, 0); + t.falsy(git.stderr); + t.falsy(shell.error()); + t.truthy(fs.existsSync(git.toString())); +}); -if (process.platform === 'win32') { +test('Windows can search with or without a .exe extension', t => { + utils.skipOnUnix(t, () => { // This should be equivalent on Windows - var nodeExe = shell.which('node.exe'); - assert.ok(!shell.error()); + const node = shell.which('node'); + const nodeExe = shell.which('node.exe'); + t.falsy(shell.error()); // If the paths are equal, then this file *should* exist, since that's // already been checked. - assert.equal(node + '', nodeExe + ''); -} + t.is(node.toString(), nodeExe.toString()); + }); +}); + +test('Searching with -a flag returns an array', t => { + const commandName = 'node'; // Should be an existing command + const result = shell.which('-a', commandName); + t.falsy(shell.error()); + t.truthy(result); + t.not(result.length, 0); +}); + +test('Searching with -a flag for not existing command returns an empty array', t => { + const notExist = '6ef25c13209cb28ae465852508cc3a8f3dcdc71bc7bcf8c38379ba38me'; + const result = shell.which('-a', notExist); + t.falsy(shell.error()); + t.is(result.length, 0); +}); + +test('Searching with -a flag returns an array with first item equals to the regular search', t => { + const commandName = 'node'; // Should be an existing command + const resultForWhich = shell.which(commandName); + const resultForWhichA = shell.which('-a', commandName); + t.falsy(shell.error()); + t.truthy(resultForWhich); + t.truthy(resultForWhichA); + t.is(resultForWhich.toString(), resultForWhichA[0]); +}); + +test('None executable files does not appear in the result list', t => { + const commandName = 'node'; // Should be an existing command + const extraPath = path.resolve(__dirname, 'resources', 'which'); + const matchingFile = path.resolve(extraPath, commandName); + const pathEnv = process.env.PATH; + + // make sure that file is exists (will throw error otherwise) + t.truthy(fs.existsSync(matchingFile)); + + process.env.PATH = extraPath + path.delimiter + process.env.PATH; + const resultForWhich = shell.which(commandName); + const resultForWhichA = shell.which('-a', commandName); + t.falsy(shell.error()); + t.truthy(resultForWhich); + t.truthy(resultForWhichA); + t.truthy(resultForWhichA.length); + t.not(resultForWhich.toString(), matchingFile); + t.falsy(resultForWhichA.includes(matchingFile)); -shell.exit(123); + process.env.PATH = pathEnv; +});