├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── appveyor.yml ├── package.json ├── src ├── get-shell.js └── index.js └── test ├── data └── test.txt ├── test-all.js ├── test-basic.js ├── test-platform-shared.js └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [Makefile] 13 | indent_style = tab 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "mocha": true 5 | }, 6 | "rules": { 7 | "import/no-extraneous-dependencies": ["error", {"devDependencies": true}] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | # OS X 28 | .DS_Store 29 | 30 | # Vagrant directory 31 | .vagrant 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 5 5 | - 4 6 | notifications: 7 | email: 8 | - kimmobrunfeldt+node@gmail.com 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Pull requests and contributions are warmly welcome. 4 | Please follow existing code style and commit message conventions. 5 | Remember to keep documentation updated. 6 | 7 | **Pull requests:** You don't need to bump version numbers or modify anything 8 | related to releasing. That stuff is fully automated, just write the functionality. 9 | 10 | # Maintaining 11 | 12 | ## Release 13 | 14 | * Commit all changes 15 | * Run `./node_modules/.bin/releasor --bump minor`, which will create new tag and publish code to GitHub and npm 16 | 17 | See [releasor documentation](https://github.com/kimmobrunfeldt/releasor) 18 | for detailed usage. 19 | 20 | * Edit GitHub release notes 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kimmo Brunfeldt 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 | # spawn-default-shell 2 | 3 | > Spawn shell command with platform default shell 4 | 5 | [![Build Status](https://travis-ci.org/kimmobrunfeldt/spawn-default-shell.svg?branch=master)](https://travis-ci.org/kimmobrunfeldt/spawn-default-shell) [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/kimmobrunfeldt/spawn-default-shell?branch=master&svg=true)](https://ci.appveyor.com/project/kimmobrunfeldt/spawn-default-shell) *master branch status* 6 | 7 | [![NPM Badge](https://nodei.co/npm/spawn-default-shell.png?downloads=true)](https://www.npmjs.com/package/spawn-default-shell) 8 | 9 | Like `child_process.spawn` with `shell: true` option but a bit more 10 | convenient and customizable. You can just pass the command as a string, 11 | and it will be executed in the platform default shell. Used in [concurrently](https://github.com/kimmobrunfeldt/concurrently). 12 | 13 | ```js 14 | // If we are in Linux / Mac, this will work 15 | const defaultShell = require('spawn-default-shell'); 16 | const child = defaultShell.spawn('cat src/index.js | grep function'); 17 | ``` 18 | 19 | Platform | Default command spawned 20 | ---------|---------- 21 | Windows | `cmd.exe /c "..."`. If `COMSPEC` env variable is defined, it is used as shell path. 22 | Mac | `/bin/bash -l -c "..."` 23 | Linux | `/bin/sh -l -c "..."` 24 | 25 | You can always override the shell path by defining these two environment variables: 26 | 27 | * `SHELL=/bin/zsh` 28 | * `SHELL_EXECUTE_FLAGS=-l -c` **Warning: execute flag must be the last flag.** 29 | 30 | All `sh` variants will be called with `-l` flag (--login). It invokes the shell 31 | as a non-interactive login shell. In bash it means: 32 | 33 | > When bash is invoked as an interactive login shell, or as a non-inter- 34 | > active shell with the --login option, it first reads and executes commands 35 | > from the file /etc/profile, if that file exists. After reading 36 | > that file, it looks for ~/.bash_profile, ~/.bash_login, and ~/.profile, 37 | > in that order, and reads and executes commands from the first one that 38 | > exists and is readable. The --noprofile option may be used when the 39 | > shell is started to inhibit this behavior. 40 | > 41 | > When a login shell exits, bash reads and executes commands from the 42 | > file ~/.bash_logout, if it exists. 43 | 44 | ## Install 45 | 46 | ```bash 47 | npm install spawn-default-shell --save 48 | ``` 49 | 50 | ## API 51 | 52 | ### .spawn(command, [opts]) 53 | 54 | Spawns a new process of the platform default shell using the given command. 55 | 56 | For all options, see [child_process](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) 57 | documentation. 58 | 59 | ## License 60 | 61 | MIT 62 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '6' 4 | - nodejs_version: '4' 5 | install: 6 | - ps: Install-Product node $env:nodejs_version 7 | - set CI=true 8 | - npm -g install npm@latest 9 | - set PATH=%APPDATA%\npm;%PATH% 10 | - npm install 11 | build: off 12 | test_script: 13 | - node --version 14 | - npm --version 15 | - npm test 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spawn-default-shell", 3 | "version": "2.0.0", 4 | "description": "Spawn shell command with platform default shell", 5 | "main": "src/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/kimmobrunfeldt/spawn-default-shell.git" 9 | }, 10 | "keywords": [ 11 | "shell", 12 | "exec", 13 | "bash", 14 | "sh", 15 | "command", 16 | "cross-platform", 17 | "windows", 18 | "linux", 19 | "mac" 20 | ], 21 | "author": "Kimmo Brunfeldt", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/kimmobrunfeldt/spawn-default-shell/issues" 25 | }, 26 | "homepage": "https://github.com/kimmobrunfeldt/spawn-default-shell#readme", 27 | "devDependencies": { 28 | "eslint": "^3.5.0", 29 | "eslint-config-airbnb-base": "^7.1.0", 30 | "eslint-plugin-import": "^1.15.0", 31 | "lodash": "^4.16.2", 32 | "mocha": "^3.0.2", 33 | "releasor": "^1.2.1" 34 | }, 35 | "scripts": { 36 | "test": "npm run test-debug-print && mocha", 37 | "test-debug-print": "node -e \"var a = require('./src/get-shell')(); console.log('> getShell()\\n' + JSON.stringify(a, null, 2));\"", 38 | "lint": "eslint ./src ./test" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/get-shell.js: -------------------------------------------------------------------------------- 1 | const DETECT_CMD_REGEX = /cmd.exe/; 2 | // All sh variant names I found end with "sh": 3 | // https://en.wikipedia.org/wiki/List_of_command-line_interpreters 4 | const DETECT_SH_REGEX = /sh$/; 5 | 6 | function detectPlatformShell() { 7 | if (process.platform === 'darwin') { 8 | return process.env.SHELL || '/bin/bash'; 9 | } 10 | 11 | if (process.platform === 'win32') { 12 | return process.env.SHELL || process.env.COMSPEC || 'cmd.exe'; 13 | } 14 | 15 | return process.env.SHELL || '/bin/sh'; 16 | } 17 | 18 | function detectExecuteFlags(shell) { 19 | if (process.env.SHELL_EXECUTE_FLAGS) { 20 | return process.env.SHELL_EXECUTE_FLAGS; 21 | } 22 | 23 | if (shell.match(DETECT_CMD_REGEX)) { 24 | return '/c'; 25 | } else if (shell.match(DETECT_SH_REGEX)) { 26 | return '-l -c'; 27 | } 28 | 29 | throw new Error('Unable to detect platform shell type. Please set SHELL_EXECUTE_FLAGS env variable.'); 30 | } 31 | 32 | function getShell() { 33 | const shell = detectPlatformShell(); 34 | 35 | return { 36 | shell: shell, 37 | executeFlags: detectExecuteFlags(shell), 38 | }; 39 | } 40 | 41 | module.exports = getShell; 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process'); 2 | const getShell = require('./get-shell'); 3 | 4 | function spawn(command, spawnOpts) { 5 | const shellDetails = getShell(); 6 | 7 | const args = shellDetails.executeFlags.split(' '); 8 | return childProcess.spawn( 9 | shellDetails.shell, 10 | args.concat([command]), 11 | spawnOpts 12 | ); 13 | } 14 | 15 | module.exports = { 16 | spawn: spawn, 17 | }; 18 | -------------------------------------------------------------------------------- /test/data/test.txt: -------------------------------------------------------------------------------- 1 | 0 2 | 1 äö☃ 3 | 2 4 | 3 5 | 4 6 | -------------------------------------------------------------------------------- /test/test-all.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const testBasic = require('./test-basic'); 3 | const testPlatformShared = require('./test-platform-shared'); 4 | 5 | const PLATFORMS = ['darwin', 'freebsd', 'linux', 'sunos', 'win32']; 6 | const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); 7 | 8 | describe('spawn-default-shell', () => { 9 | testBasic(); 10 | }); 11 | 12 | describe('shared tests on each platform (mocking)', () => { 13 | _.each(PLATFORMS, (platform) => { 14 | describe(`process.platform = "${platform}"`, () => { 15 | before(() => { 16 | Object.defineProperty(process, 'platform', { value: platform }); 17 | }); 18 | 19 | after(() => { 20 | Object.defineProperty(process, 'platform', originalPlatform); 21 | }); 22 | 23 | testPlatformShared(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/test-basic.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const defaultShell = require('../src/index'); 3 | const getShell = require('../src/get-shell'); 4 | const withEnv = require('./utils').withEnv; 5 | 6 | const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); 7 | 8 | function testBasic() { 9 | it('piping should work', (done) => { 10 | const child = defaultShell.spawn('cat test/data/test.txt | grep 1', { 11 | stdio: 'pipe', 12 | }); 13 | 14 | child.stdout.on('data', (data) => { 15 | assert.strictEqual(data.toString('utf8'), '1 äö☃\n'); 16 | }); 17 | 18 | child.on('close', (code) => { 19 | assert.strictEqual(code, 0); 20 | done(); 21 | }); 22 | }); 23 | 24 | it('&& operator should work', (done) => { 25 | const child = defaultShell.spawn('echo 1 && node -e "process.exit(42)"'); 26 | 27 | child.on('close', (code) => { 28 | assert.strictEqual(code, 42); 29 | done(); 30 | }); 31 | }); 32 | 33 | describe('process.platform = "darwin"', () => { 34 | before(() => { 35 | Object.defineProperty(process, 'platform', { value: 'darwin' }); 36 | }); 37 | 38 | after(() => { 39 | Object.defineProperty(process, 'platform', originalPlatform); 40 | }); 41 | 42 | it('shell resolution order should be 1. SHELL 2. /bin/bash', () => { 43 | withEnv({ SHELL: '' }, () => { 44 | assert.strictEqual(getShell().shell, '/bin/bash'); 45 | assert.strictEqual(getShell().executeFlags, '-l -c'); 46 | }); 47 | 48 | withEnv({ SHELL: 'zsh' }, () => { 49 | assert.strictEqual(getShell().shell, 'zsh'); 50 | assert.strictEqual(getShell().executeFlags, '-l -c'); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('process.platform = "win32"', () => { 56 | before(() => { 57 | Object.defineProperty(process, 'platform', { value: 'win32' }); 58 | }); 59 | 60 | after(() => { 61 | Object.defineProperty(process, 'platform', originalPlatform); 62 | }); 63 | 64 | it('shell resolution order should be 1. SHELL 2. COMSPEC 3. cmd.exe', () => { 65 | withEnv({ SHELL: '', COMSPEC: '' }, () => { 66 | assert.strictEqual(getShell().shell, 'cmd.exe'); 67 | assert.strictEqual(getShell().executeFlags, '/c'); 68 | }); 69 | 70 | withEnv({ SHELL: '', COMSPEC: '\\C:\\cmd.exe' }, () => { 71 | assert.strictEqual(getShell().shell, '\\C:\\cmd.exe'); 72 | assert.strictEqual(getShell().executeFlags, '/c'); 73 | }); 74 | 75 | withEnv({ SHELL: 'bash', COMSPEC: '\\C:\\cmd.exe' }, () => { 76 | assert.strictEqual(getShell().shell, 'bash'); 77 | assert.strictEqual(getShell().executeFlags, '-l -c'); 78 | }); 79 | }); 80 | }); 81 | 82 | describe('process.platform = "linux" (other than win32 or darwin)', () => { 83 | before(() => { 84 | Object.defineProperty(process, 'platform', { value: 'linux' }); 85 | }); 86 | 87 | after(() => { 88 | Object.defineProperty(process, 'platform', originalPlatform); 89 | }); 90 | 91 | it('shell resolution order should be 1. SHELL 2. /bin/sh', () => { 92 | withEnv({ SHELL: '' }, () => { 93 | assert.strictEqual(getShell().shell, '/bin/sh'); 94 | assert.strictEqual(getShell().executeFlags, '-l -c'); 95 | }); 96 | 97 | withEnv({ SHELL: 'zsh' }, () => { 98 | assert.strictEqual(getShell().shell, 'zsh'); 99 | assert.strictEqual(getShell().executeFlags, '-l -c'); 100 | }); 101 | }); 102 | }); 103 | } 104 | 105 | module.exports = testBasic; 106 | -------------------------------------------------------------------------------- /test/test-platform-shared.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const defaultShell = require('../src/index'); 3 | const getShell = require('../src/get-shell'); 4 | const withEnv = require('./utils').withEnv; 5 | 6 | function sharedTests() { 7 | it('custom /bin/zsh shell should work', () => { 8 | withEnv({ SHELL: '/bin/zsh' }, () => { 9 | assert.strictEqual(getShell().shell, '/bin/zsh'); 10 | assert.strictEqual(getShell().executeFlags, '-l -c'); 11 | }); 12 | }); 13 | 14 | it('custom execute flag should override default', () => { 15 | withEnv({ SHELL_EXECUTE_FLAGS: '--execute' }, () => { 16 | assert.strictEqual(getShell().executeFlags, '--execute'); 17 | }); 18 | }); 19 | 20 | it('customizing whole command should work', () => { 21 | withEnv({ SHELL: '/bin/verycustomshell', SHELL_EXECUTE_FLAGS: '-x' }, () => { 22 | assert.strictEqual(getShell().shell, '/bin/verycustomshell'); 23 | assert.strictEqual(getShell().executeFlags, '-x'); 24 | }); 25 | }); 26 | 27 | it('unknown shell without execution flag should throw error', () => { 28 | withEnv({ SHELL: '/bin/false' }, () => { 29 | assert.throws( 30 | () => { 31 | defaultShell.spawn('echo test'); 32 | }, 33 | /Unable to detect platform shell type/ 34 | ); 35 | }); 36 | }); 37 | } 38 | 39 | module.exports = sharedTests; 40 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | function withEnv(env, func) { 4 | const originals = _.map(env, (val, key) => ({ key: key, val: process.env[key] })); 5 | _.each(env, (newVal, key) => { 6 | process.env[key] = newVal; 7 | }); 8 | 9 | try { 10 | func(); 11 | } finally { 12 | _.each(originals, (item) => { 13 | if (!item.val) { 14 | delete process.env[item.key]; 15 | } else { 16 | process.env[item.key] = item.val; 17 | } 18 | }); 19 | } 20 | } 21 | 22 | module.exports = { 23 | withEnv: withEnv, 24 | }; 25 | --------------------------------------------------------------------------------