Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
arshaw committed Sep 22, 2021
0 parents commit 3944596
Show file tree
Hide file tree
Showing 11 changed files with 734 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# http://editorconfig.org

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
quote_type = single
trim_semicolon = true
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: CI
on: [push]
jobs:
CI:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Restore Dependencies
uses: actions/cache@v2
id: node-cache
with:
# https://dev.to/mpocock1/how-to-cache-nodemodules-in-github-actions-with-yarn-24eh
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Install Dependencies
run: yarn install
- name: Build Project
run: yarn run build
- name: Run Tests
run: yarn run test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
105 changes: 105 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@

# shelljs-live

Execute a shell command while piping output directly to your console.

Motivated by [shelljs] exec's [inability to preserve colors](https://github.com/shelljs/shelljs/issues/86).
Also, great for watcher tasks, as output is not buffered.

Very portable (uses [cross-spawn]), especially when specifying `command` as an array of strings (more below).

## Installation

```sh
npm install shelljs shelljs-live
```

The `shelljs` package is a peerDependency of `shelljs-live` and must be installed.

## Traditional API

```
live(command [, options] [, callback]) => statusCode
```

- `command` - A string **OR** an array of strings:
- If a string, run as a shell statement. The shell might expand globs or have opinions about escaping. This is not very portable.
- If an *array* of strings, all arguments are piped directly to a command and no escaping is necessary. **This is recommended.**
- `options` - *Optional*. [More info](#options).
- `callback` - *Optional*. Called on success/failure. Receives the `statusCode`. Implies the `async:true` option.
- `statusCode` - A number, or in some cases `null`. Success means `statusCode === 0`.

Synchronous usage:

```js
const { live } = require('shelljs-live')

const statusCode = live(['ps', '-ax']) // live('ps -ax') works too. not recommended
if (statusCode === 0) {
console.log('Success')
} else {
console.log('Failure')
}
```

Asynchronous usage:

```js
const { live } = require('shelljs-live')

live(['ps', '-ax'], (statusCode) => {
if (statusCode === 0) {
console.log('Success')
} else {
console.log('Failure')
}
})
```

## Promise API

```
live(command [, options]) => promise
```

- `command`: A string **OR** an array of strings:
- If a string, run as a shell statement. The shell might expand globs or have opinions about escaping. This is not very portable.
- If an *array* of strings, all arguments are piped directly to a command and no escaping is necessary. **This is recommended.**
- `options`: *Optional*. [More info](#options).
- `promise`: A [Promise] that triggers success when status code equals `0`, failure otherwise. Neither handler receives the status code.

Usage:

```js
const { live } = require('shelljs-live/promise')

live(['ps', '-ax']).then(() => {
console.log('Success')
}, () => {
console.log('Failure')
})
```

Or if you want to use `await` and don't care about handling errors:

```js
const { live } = require('shelljs-live/promise')

await live(['ps', '-ax'])
console.log('Success')
```

## Options

- `async`: Asynchronous execution. If a callback is provided, or using the Promise API, it will be set to true, regardless of the passed value (default: `false`).
- `fatal`: Exit upon error (default: `false`, inherits from [ShellJS Config][shelljs-config]).
- `silent`: Do not echo program output to console (default: `false`, inherits from [ShellJS Config][shelljs-config]).

Any other option, such as `cwd`, is passed directly to [spawn].


[shelljs]: https://documentup.com/shelljs/shelljs
[shelljs-config]: https://documentup.com/shelljs/shelljs#configuration
[cross-spawn]: https://www.npmjs.com/package/cross-spawn
[Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[spawn]: https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
32 changes: 32 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "shelljs-live",
"version": "0.0.1",
"repository": "https://github.com/arshaw/shelljs-live.git",
"author": "Adam Shaw",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./promise": "./dist/promise.js"
},
"scripts": {
"clean": "rm -rf dist",
"build": "tsc",
"watch": "tsc --watch",
"test": "./test.sh"
},
"peerDependencies": {
"shelljs": "^0.8.4"
},
"dependencies": {
"cross-spawn": "^7.0.3"
},
"devDependencies": {
"@types/cross-spawn": "^6.0.2",
"@types/shelljs": "^0.8.9",
"shelljs": "^0.8.4",
"typescript": "^4.4.3",
"yargs": "^17.1.1"
}
}
76 changes: 76 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { SpawnOptions } from 'child_process'
import * as spawn from 'cross-spawn'
import { config } from 'shelljs'

export type Callback = (status: number | null) => void
export type Options = SpawnOptions & { async?: boolean, fatal?: boolean, silent?: boolean }

export function live(command: string | string[], options?: Options): number | null
export function live(command: string | string[], callback: Callback): number | null
export function live(
command: string | string[],
options: Options | undefined,
callback: Callback,
): number | null
export function live(
command: string | string[],
optionsOrCallback?: Options | Callback,
callback?: Callback,
): number | null {
let options: Options

if (typeof optionsOrCallback === 'function') {
callback = optionsOrCallback
options = {}
} else {
options = optionsOrCallback || {}
}

let command0: string
let args: string[]
let shell: boolean

if (Array.isArray(command)) {
command0 = command[0]
args = command.slice(1)
shell = false
} else {
command0 = command
args = []
shell = true
}

if (!command0) {
throw new Error('Must specify a command')
}

const fatal = options.fatal ?? config.fatal
const silent = options.silent ?? config.silent
const spawnOptions: SpawnOptions = {
...(silent ? {} : { stdio: 'inherit' }),
...options,
shell,
}

function handleStatus(status: number | null) {
if (status === null || status !== 0) {
if (fatal) {
console.error(`Command '${command0}' failed with status code ${status}`)
process.exit(status || 1)
}
}
if (callback) {
callback(status)
}
}

if (options.async || callback) {
const childProcess = spawn(command0, args, spawnOptions)
childProcess.on('close', handleStatus)
return null
} else {
const { status } = spawn.sync(command0, args, spawnOptions)
handleStatus(status)
return status
}
}
15 changes: 15 additions & 0 deletions src/promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { live as origLive, Options } from './'

export function live(tokens: string[], options?: Options): Promise<void> {
return new Promise((resolve, reject) => {
origLive(tokens, options, (status) => {
if (status === 0) {
resolve()
} else {
reject(new Error(`Command '${tokens[0]}' failed with status code ${status}`))
}
})
})
}

export { Options } from './'
88 changes: 88 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const yargs = require('yargs/yargs')
const { hideBin } = require('yargs/helpers')
const shell = require('shelljs')
const { live } = require('./dist/index')
const { live: promiseLive } = require('./dist/promise')

const argv = yargs(hideBin(process.argv)).argv
shell.config.silent = Boolean(argv.silent)
shell.config.fatal = Boolean(argv.fatal)

// TODO: test silent/fatal passed-in as options

let successStatusA = live(argv.shell ? 'ls -al && cd .' : ['ls', '-al'])
if (successStatusA !== 0) {
console.error('should succeed')
process.exit(successStatusA)
}

let successStatusB = live(argv.shell ? 'ls -al && cd .' : ['ls', '-al'], {
cwd: argv.cwd || '.'
})
if (successStatusB !== 0) {
console.error('should succeed in different CWD')
process.exit(successStatusB)
}

let failureStatusA = live(['asdfasdfasdf'])
if (failureStatusA === 0) {
console.error('status should be non-zero when command does not exist')
process.exit(1)
}

let failureStatusB = live(['ls', 'asdfasdfasdf'])
if (failureStatusB === 0) {
console.error('status should be non-zero when command fails')
process.exit(1)
}

function testSuccessAsync() {
let start = Date.now()
return new Promise((resolve, reject) => {
live(['sleep', '1'], (status) => {
let end = Date.now()
let dur = end - start
if (status !== 0) {
reject(new Error('async did not succeed'))
} else if (dur < 1000) {
reject(new Error('async is not async'))
} else {
resolve()
}
})
})
}

function testSuccessPromise() {
let start = Date.now()
return promiseLive(['sleep', '1']).then(() => {
let end = Date.now()
let dur = end - start
if (dur < 1000) {
throw new new Error('promise is not async')
}
}, () => {
throw new Error('promise did not fail')
})
}

function testFailurePromise() {
return promiseLive(['ls', 'asdfasdfasdf']).then(() => {
throw new Error('async should fail')
}, () => {
// handle error by doing nothing
})
}

Promise.all([
testSuccessAsync(),
testSuccessPromise(),
testFailurePromise(),
]).then(() => {
if (!argv.silent) {
console.log('Successfully ran all tests')
}
}, (err) => {
console.error(err.toString())
process.exit(1)
})
Loading

0 comments on commit 3944596

Please sign in to comment.