├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── SECURITY.md └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── common.js ├── index.js ├── package-lock.json ├── package.json └── test └── test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": "airbnb-base", 6 | "rules": { 7 | "import/no-mutable-exports": 0, 8 | "global-require": 0, 9 | "vars-on-top": 0, 10 | "spaced-comment": [2, "always", { "markers": ["@", "@include"], "exceptions": ["@"] }], 11 | "no-param-reassign": 0, 12 | "no-console": 0, 13 | "curly": 0, 14 | "no-var": 2, 15 | "prefer-const": 2, 16 | "new-cap": [2, { 17 | "capIsNewExceptions": [ 18 | "ShellString" 19 | ]} 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Node version (or tell us if you're using electron or some other framework): 2 | 3 | ### ShellJS version (the most recent version/GitHub branch you see the bug on): 4 | 5 | ### Operating system: 6 | 7 | ### Description of the bug: 8 | 9 | ### Example ShellJS command to reproduce the error: 10 | 11 | ```javascript 12 | 13 | ``` 14 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Thank you for reaching out about security! Please note that this project is 4 | maintained on a best-effort basis, however I still intend to prioritize 5 | reviewing and addressing security issues. 6 | 7 | ## Supported Versions 8 | 9 | I generally only support the latest release (see 10 | https://www.npmjs.com/package/shelljs-exec-proxy). My goal is to release 11 | security fixes as patch releases on top of whatever was most recently shipped. 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Please report security vulnerabilities to ntfschr@gmail.com. I should respond 16 | within a few days. Although it's not strictly required, it helps me out if you 17 | can include any proof of concept exploit code, suggested fix, etc. 18 | 19 | **Please do not publicly disclose the suspected vulnerability** until I have a 20 | chance to review your report. I'd like a chance to patch the code before the 21 | issue is known to the public. 22 | 23 | Please **only** use this email for security issues. It's also OK to use the 24 | email if you're legitimately unsure if this is a security issue (better safe 25 | than sorry). But for all other non-security issues, please use the GitHub issue 26 | tracker. 27 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 18 14 | - 20 15 | - 22 16 | os: 17 | - ubuntu-latest 18 | - macos-latest 19 | - windows-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm install 26 | - run: npm run test 27 | - name: Upload coverage reports to Codecov 28 | uses: codecov/codecov-action@v4 29 | with: 30 | token: ${{ secrets.CODECOV_TOKEN }} 31 | fail_ci_if_error: true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .nyc_output/ 4 | 5 | test_data/ 6 | 7 | *~ 8 | *.swp 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nathan Fischer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShellJS Exec Proxy 2 | 3 | [![GitHub Actions](https://img.shields.io/github/actions/workflow/status/nfischer/shelljs-exec-proxy/main.yml?style=flat-square&logo=github)](https://github.com/nfischer/shelljs-exec-proxy/actions/workflows/main.yml) 4 | [![Codecov](https://img.shields.io/codecov/c/github/nfischer/shelljs-exec-proxy.svg?style=flat-square)](https://codecov.io/gh/nfischer/shelljs-exec-proxy) 5 | [![npm](https://img.shields.io/npm/v/shelljs-exec-proxy.svg?style=flat-square)](https://www.npmjs.com/package/shelljs-exec-proxy) 6 | [![npm downloads](https://img.shields.io/npm/dm/shelljs-exec-proxy.svg?style=flat-square)](https://www.npmjs.com/package/shelljs-exec-proxy) 7 | 8 | Unleash the power of unlimited ShellJS commands... *with ES6 Proxies!* 9 | 10 | Do you like [ShellJS](https://github.com/shelljs/shelljs), but wish it had your 11 | favorite commands? Skip the weird `exec()` calls by using `shelljs-exec-proxy`: 12 | 13 | ```javascript 14 | // Our goal: make a commit: `$ git commit -am "I'm updating the \"foo\" module to be more secure"` 15 | // Standard ShellJS requires the exec function, with confusing string escaping: 16 | shell.exec('git commit -am "I\'m updating the \\"foo\\" module to be more secure"'); 17 | // Skip the extra string escaping with shelljs-exec-proxy! 18 | shell.git.commit('-am', `I'm updating the "foo" module to be more secure`); 19 | ``` 20 | 21 | ## Installation 22 | 23 | ``` 24 | $ npm install --save shelljs-exec-proxy 25 | ``` 26 | 27 | ## Get that JavaScript feeling back in your code 28 | 29 | ```javascript 30 | const shell = require('shelljs-exec-proxy'); 31 | shell.git.status(); 32 | shell.git.add('.'); 33 | shell.git.commit('-am', 'Fixed issue #1'); 34 | shell.git.push('origin', 'main'); 35 | ``` 36 | 37 | ## Security improvements 38 | 39 | Current versions of ShellJS export the `.exec()` method, which if not used 40 | carefully, could introduce command injection Vulnerabilities to your module. 41 | Here's an insecure code snippet: 42 | 43 | ```javascript 44 | shell.ls('dir/*.txt').forEach(file => { 45 | shell.exec('git add ' + file); 46 | } 47 | ``` 48 | 49 | This leaves you vulnerable to files like: 50 | 51 | | Example file name | Unintended behavior | 52 | |------------------ | ------------- | 53 | | `File 1.txt` | This tries to add both `File` and `1.txt`, instead of `File 1.txt` | 54 | | `foo;rm -rf *` | This executes both `git add foo` and `rm -rf *`, unexpectedly deleting your files! | 55 | | `ThisHas"quotes'.txt` | This tries running `git add ThisHas"quotes'.txt`, producing a Bash syntax error | 56 | 57 | `shelljs-exec-proxy` solves all these problems: 58 | 59 | ```javascript 60 | shell.ls('dir/*.txt').forEach(file => { 61 | shell.git.add(file); 62 | } 63 | ``` 64 | 65 | | Example file name | Behavior | 66 | |------------------ | ------------ | 67 | | `File 1.txt` | Arguments are automatically quoted, so spaces aren't an issue | 68 | | `foo;rm -rf *` | Only one command runs at a time (semicolons are treated literally) and wildcards aren't expanded | 69 | | `ThisHas"quotes'.txt` | Quote characters are automatically escaped for you, so there are never any issues | 70 | -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | module.exports.cmdArrayAttr = '__cmdStart__'; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const origShell = require('shelljs'); 2 | const { cmdArrayAttr } = require('./common'); 3 | 4 | const proxyifyCmd = (t, ...cmdStart) => { 5 | // Create the target (or use the one passed in) 6 | t = t || function _t(...args) { 7 | // Wrap all the arguments in quotes 8 | const newArgs = cmdStart 9 | .concat(args) 10 | .map((x) => JSON.stringify(x)); 11 | // Run this command in the shell 12 | return origShell.exec.call(this.stdout, newArgs.join(' ')); 13 | }; 14 | // Store the list of commands, in case we have a subcommand chain 15 | t[cmdArrayAttr] = cmdStart; 16 | 17 | // Create the handler 18 | const handler = { 19 | // Don't delete reserved attributes 20 | deleteProperty: (target, methodName) => { 21 | if (methodName === cmdArrayAttr) { 22 | throw new Error(`Cannot delete reserved attribute '${methodName}'`); 23 | } 24 | delete target[methodName]; 25 | }, 26 | 27 | // Don't override reserved attributes 28 | set: (target, methodName, value) => { 29 | if (methodName === cmdArrayAttr) { 30 | throw new Error(`Cannot modify reserved attribute '${methodName}'`); 31 | } 32 | target[methodName] = value; 33 | return target[methodName]; 34 | }, 35 | 36 | // Always defer to `target` 37 | has: (target, methodName) => (methodName in target), 38 | ownKeys: (target) => Object.keys(target), 39 | 40 | // Prefer the existing attribute, otherwise return another Proxy 41 | get: (target, methodName) => { 42 | // Don't Proxy-ify these attributes, no matter what 43 | const noProxyifyList = ['inspect', 'valueOf']; 44 | 45 | // Return the attribute, either if it exists or if it's in the 46 | // `noProxyifyList`, otherwise return a new Proxy 47 | if (methodName in target || noProxyifyList.includes(methodName)) { 48 | return target[methodName]; 49 | } 50 | return proxyifyCmd(null, ...target[cmdArrayAttr], methodName); 51 | }, 52 | }; 53 | 54 | // Each command and subcommand is a Proxy 55 | return new Proxy(t, handler); 56 | }; 57 | 58 | // TODO(nate): put hooks in ShellString so that I can Proxy-ify it to allow new 59 | // commands on the right hand side of pipes 60 | 61 | // const OrigShellString = origShell.ShellString; 62 | // // modify prototypes 63 | // function ShellStringProxy(...args) { 64 | // return proxyifyCmd(new OrigShellString(...args)); 65 | // } 66 | // origShell.ShellString = ShellStringProxy; 67 | 68 | // export the modified shell 69 | const proxifiedShell = proxyifyCmd(origShell); 70 | 71 | // Allow access to native commands, bypassing ShellJS builtins. Useful for 72 | // testing, but most usecases should prefer calling the proxifiedShell directly 73 | // which prefers ShellJS builtins when available. Store this under an unusual 74 | // name to limit the risk of name conflicts with real commands. 75 | // eslint-disable-next-line no-underscore-dangle 76 | proxifiedShell.__native = proxyifyCmd({}); 77 | module.exports = proxifiedShell; 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shelljs-exec-proxy", 3 | "version": "0.2.1", 4 | "description": "Unlimited shelljs commands with ES6 proxies", 5 | "main": "index.js", 6 | "scripts": { 7 | "posttest": "npm run lint", 8 | "test": "nyc --reporter=text --reporter=lcov mocha", 9 | "lint": "eslint .", 10 | "changelog": "shelljs-changelog", 11 | "release:major": "shelljs-release major", 12 | "release:minor": "shelljs-release minor", 13 | "release:patch": "shelljs-release patch" 14 | }, 15 | "keywords": [ 16 | "shelljs", 17 | "exec", 18 | "proxy", 19 | "es6", 20 | "git" 21 | ], 22 | "author": "Nate Fischer (https://github.com/nfischer)", 23 | "license": "MIT", 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/nfischer/shelljs-exec-proxy.git" 27 | }, 28 | "dependencies": { 29 | "shelljs": "^0.10.0" 30 | }, 31 | "files": [ 32 | "index.js", 33 | "common.js" 34 | ], 35 | "devDependencies": { 36 | "eslint": "^8.57.1", 37 | "eslint-config-airbnb-base": "^15.0.0", 38 | "eslint-plugin-import": "^2.26.0", 39 | "mocha": "^11.1.0", 40 | "nyc": "^17.1.0", 41 | "shelljs-changelog": "^0.2.6", 42 | "shelljs-release": "^0.5.3", 43 | "should": "^13.2.3" 44 | }, 45 | "engines": { 46 | "node": ">=18" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* globals describe, it, before, beforeEach, afterEach */ 2 | const origShell = require('shelljs'); 3 | const assert = require('assert'); 4 | const fs = require('fs'); 5 | const os = require('os'); 6 | const shell = require('../index'); 7 | const { cmdArrayAttr } = require('../common'); 8 | 9 | /* eslint-disable no-underscore-dangle */ 10 | // Disable lint rule for '__native'. 11 | 12 | require('should'); 13 | 14 | function assertShellStringEqual(a, b) { 15 | a.stdout.should.equal(b.stdout); 16 | a.stderr.should.equal(b.stderr); 17 | a.code.should.equal(b.code); 18 | } 19 | 20 | // returns true if on Unix 21 | function unix() { 22 | return process.platform !== 'win32'; 23 | } 24 | 25 | describe('proxy', function describeproxy() { 26 | this.timeout(10000); // shell.exec() is slow 27 | 28 | before(() => { 29 | shell.config.silent = true; 30 | }); 31 | 32 | beforeEach(() => { 33 | shell.mkdir('-p', 'test_data'); 34 | shell.cd('test_data'); 35 | }); 36 | 37 | afterEach(() => { 38 | shell.cd('..'); 39 | shell.rm('-rf', 'test_data'); 40 | }); 41 | 42 | it('appropriately handles inspect() and valueOf()', () => { 43 | assert.strictEqual(shell.inspect, origShell.inspect); 44 | assert.strictEqual(shell.valueOf, origShell.valueOf); 45 | 46 | const oldInspect = shell.inspect; 47 | shell.inspect = () => 'foo'; 48 | shell.inspect().should.equal('foo'); 49 | if ('inspect' in shell) { 50 | shell.inspect = oldInspect; 51 | } else { 52 | delete shell.inspect; 53 | } 54 | 55 | const oldValueOf = shell.valueOf; 56 | shell.valueOf = () => 'bar'; 57 | shell.valueOf().should.equal('bar'); 58 | if ('valueOf' in shell) { 59 | shell.valueOf = oldValueOf; 60 | } else { 61 | delete shell.valueOf; 62 | } 63 | }); 64 | 65 | it('returns appropriate keys', () => { 66 | Object.keys(shell).should.deepEqual(Object.keys(origShell)); 67 | }); 68 | 69 | it('does not override existing commands', () => { 70 | ('ls' in origShell).should.equal(true); 71 | ('ls' in shell).should.equal(true); 72 | (typeof shell.ls).should.equal('function'); 73 | }); 74 | 75 | it('does not mess up non-command properties', () => { 76 | ('env' in origShell).should.equal(true); 77 | ('env' in shell).should.equal(true); 78 | (typeof shell.env).should.equal('object'); 79 | (typeof shell.env.PATH).should.equal('string'); 80 | }); 81 | 82 | it('does not allow overriding reserved attributes', () => { 83 | assert.throws(() => { 84 | shell[cmdArrayAttr] = 'foo'; 85 | }, Error); 86 | }); 87 | 88 | it('does not allow deleting reserved attributes', () => { 89 | assert.throws(() => { 90 | delete shell[cmdArrayAttr]; 91 | }, Error); 92 | }); 93 | 94 | it("doesn't claim to have properties that don't exist in target", () => { 95 | ('foobar' in origShell).should.equal(false); 96 | ('foobar' in shell).should.equal(false); 97 | }); 98 | 99 | it('allows adding new attributes', () => { 100 | Object.prototype.hasOwnProperty.call(shell, 'version').should.equal(false); 101 | shell.version = 'Proxy'; 102 | shell.version.should.equal('Proxy'); 103 | delete shell.version; 104 | Object.prototype.hasOwnProperty.call(shell, 'version').should.equal(false); 105 | }); 106 | 107 | describe('commands', () => { 108 | it('runs whoami', () => { 109 | if (shell.which('whoami')) { 110 | const ret1 = shell.whoami(); 111 | const ret2 = shell.exec('whoami'); 112 | assertShellStringEqual(ret1, ret2); 113 | } else { 114 | console.log('skipping test'); 115 | } 116 | }); 117 | 118 | it('runs wc', () => { 119 | if (shell.which('wc')) { 120 | const fname = 'file.txt'; 121 | shell.ShellString('This is a file\nthat has 2 lines\n').to(fname); 122 | const ret1 = shell.wc(fname); 123 | const ret2 = shell.exec(`wc ${fname}`); 124 | assertShellStringEqual(ret1, ret2); 125 | } else { 126 | console.log('skipping test'); 127 | } 128 | }); 129 | 130 | it('runs du', () => { 131 | if (shell.which('du')) { 132 | const fname = 'file.txt'; 133 | shell.touch(fname); 134 | const ret1 = shell.du(fname); 135 | const ret2 = shell.exec(`du ${fname}`); 136 | assertShellStringEqual(ret1, ret2); 137 | // Don't assert the file size, since that may be OS dependent. 138 | // Note: newline should be '\n', because we're checking a JS string, not 139 | // something from the file system. 140 | ret1.stdout.should.endWith(`\t${fname}\n`); 141 | } else { 142 | console.log('skipping test'); 143 | } 144 | }); 145 | 146 | it('runs rmdir', () => { 147 | if (shell.which('rmdir')) { 148 | const dirName = 'sub'; 149 | shell.mkdir(dirName); 150 | const ret1 = shell.rmdir(dirName); 151 | ret1.stdout.should.equal(''); 152 | ret1.stderr.should.equal(''); 153 | ret1.code.should.equal(0); 154 | fs.existsSync(dirName).should.equal(false); 155 | } else { 156 | console.log('skipping test'); 157 | } 158 | }); 159 | 160 | it('runs true', () => { 161 | if (shell.which('true')) { 162 | const ret1 = shell.true(); 163 | const ret2 = shell.exec('true'); 164 | assertShellStringEqual(ret1, ret2); 165 | } else { 166 | console.log('skipping test'); 167 | } 168 | }); 169 | 170 | it('runs printf', (done) => { 171 | if (shell.which('printf')) { 172 | const ret1 = shell.printf('first second third').to('file1.txt'); 173 | shell.cat('file1.txt').toString().should.equal('first second third'); 174 | const ret2 = shell.printf('first second third').to('file2.txt'); 175 | shell.cat('file2.txt').toString().should.equal('first second third'); 176 | assertShellStringEqual(ret1, ret2); 177 | } else { 178 | console.log('skipping test'); 179 | } 180 | done(); 181 | }); 182 | 183 | it('runs garbage commands', (done) => { 184 | const randCmd = 'alsdkfjlaskdfjlaskjdffksjdf'; 185 | assert.ok(!shell.which(randCmd)); // don't run anything real! 186 | shell[randCmd]().code.should.not.equal(0); 187 | done(); 188 | }); 189 | 190 | it('handles ShellStrings as arguments', (done) => { 191 | if (!unix()) { 192 | // See the TODO below. 193 | console.log('Skipping unix-only test case'); 194 | done(); 195 | return; 196 | } 197 | shell.touch('file.txt'); 198 | fs.existsSync('file.txt').should.equal(true); 199 | if (unix()) { 200 | shell.__native.rm(shell.ShellString('file.txt')); 201 | } else { 202 | shell.del(shell.ShellString('file.txt')); 203 | } 204 | // TODO(nfischer): this fails on Windows 205 | fs.existsSync('file.txt').should.equal(false); 206 | done(); 207 | }); 208 | }); 209 | 210 | describe('subcommands', () => { 211 | it('can use subcommands', (done) => { 212 | const ret = shell.git.status(); 213 | ret.code.should.equal(0); 214 | ret.stderr.should.equal(''); 215 | done(); 216 | }); 217 | 218 | it('can use subcommands with options', (done) => { 219 | fs.existsSync('../package.json').should.equal(true); 220 | 221 | // don't actually remove this file, but do a dry run 222 | const ret = shell.git.rm('-qrnf', '../package.json'); 223 | ret.code.should.equal(0); 224 | ret.stdout.should.equal(''); 225 | ret.stderr.should.equal(''); 226 | done(); 227 | }); 228 | 229 | it('runs very long subcommand chains', (done) => { 230 | const ret = shell.__native.echo.one.two.three.four.five.six('seven'); 231 | ret.stdout.should.equal('one two three four five six seven\n'); 232 | ret.stderr.should.equal(''); 233 | ret.code.should.equal(0); 234 | done(); 235 | }); 236 | }); 237 | 238 | describe('security', () => { 239 | it('handles unsafe filenames', (done) => { 240 | if (!unix()) { 241 | // See the TODO below. 242 | console.log('Skipping unix-only test case'); 243 | done(); 244 | return; 245 | } 246 | const fa = 'a.txt'; 247 | const fb = 'b.txt'; 248 | const fname = `${fa};${fb}`; 249 | shell.exec('echo hello world').to(fa); 250 | shell.exec('echo hello world').to(fb); 251 | shell.exec('echo hello world').to(fname); 252 | 253 | // All three files should exist at this point. 254 | fs.existsSync(fname).should.equal(true); 255 | fs.existsSync(fa).should.equal(true); 256 | fs.existsSync(fb).should.equal(true); 257 | 258 | if (unix()) { 259 | shell.__native.rm(fname); 260 | } else { 261 | shell.del(fname); 262 | } 263 | // TODO(nfischer): this line fails on Windows 264 | fs.existsSync(fname).should.equal(false); 265 | shell.cat(fa).toString().should.equal(`hello world${os.EOL}`); 266 | 267 | // These files are still ok 268 | fs.existsSync(fa).should.equal(true); 269 | fs.existsSync(fb).should.equal(true); 270 | done(); 271 | }); 272 | 273 | it('avoids globs', (done) => { 274 | const fa = 'a.txt'; 275 | const fglob = '*.txt'; 276 | shell.exec('echo hello world').to(fa); 277 | shell.exec('echo hello world').to(fglob); 278 | 279 | if (unix()) { 280 | shell.__native.rm(fglob); 281 | } else { 282 | shell.del(fglob); 283 | } 284 | fs.existsSync(fglob).should.equal(false); 285 | shell.cat(fa).toString().should.equal(`hello world${os.EOL}`); 286 | 287 | // These files are still ok 288 | fs.existsSync(fa).should.equal(true); 289 | done(); 290 | }); 291 | 292 | it('escapes quotes', (done) => { 293 | if (!unix()) { 294 | // Windows doesn't support `"` as a character in a filename, see 295 | // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx 296 | console.log('Skipping unix-only test case'); 297 | done(); 298 | return; 299 | } 300 | 301 | const fquote = 'thisHas"Quotes.txt'; 302 | shell.exec('echo hello world').to(fquote); 303 | fs.existsSync(fquote).should.equal(true); 304 | if (unix()) { 305 | shell.__native.rm(fquote); 306 | } else { 307 | shell.del(fquote); 308 | } 309 | fs.existsSync(fquote).should.equal(false); 310 | done(); 311 | }); 312 | }); 313 | }); 314 | --------------------------------------------------------------------------------