Skip to content

Commit

Permalink
feat(shell): exec and sh
Browse files Browse the repository at this point in the history
  • Loading branch information
seb-cr committed May 4, 2023
1 parent 6a2cbbd commit a68ade7
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 26 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,32 @@ Install via npm:
```sh
npm i @sebalon/scripting
```

Then import the functions you need into your scripts.

```ts
import { /* ... */ } from '@sebalon/scripting';
```

### Shell

Run shell commands using `sh`. It returns a `Promise` for the command's output.

```ts
const output = await sh('echo hello');
// => 'hello'
```

By default, leading and trailing whitespace is trimmed from the output. You can disable this by passing `trim: false` in the options.

```ts
const output = await sh('echo hello', { trim: false });
// => 'hello\n'
```

If you also need to inspect `stderr`, use `exec` which is the promisified version of [Node's `child_process.exec`](https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback).

```ts
const output = await exec('echo hello');
// => { stdout: 'hello\n', stderr: '' }
```
41 changes: 41 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
"@comicrelief/eslint-config": "^2.0.3",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@types/chai": "^4.3.4",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^10.0.1",
"@types/node": "^18.15.13",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"eslint": "^8.38.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsdoc": "^39.9.1",
Expand Down
9 changes: 1 addition & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1 @@
/**
* Returns a greeting.
*
* @param name Optional name to greet.
*/
export function greet(name?: string): string {
return `Hello ${name || 'world'}!`;
}
export * from './shell';
55 changes: 55 additions & 0 deletions src/shell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ExecOptions, exec as execWithCallback } from 'child_process';
import { promisify } from 'util';

/**
* Promisified version of `child_process.exec`.
*
* Executes `command` within a shell and return an object containing stdout and
* stderr, or throws an error if `command` completes with a non-zero exit code.
*
* See the official Node documentation here for more details:
* https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback
*
* ```ts
* import { exec } from '@sebalon/scripting';
*
* const output = await exec('echo hello');
* // => { stdout: 'hello\n', stderr: '' }
* ```
*/
export const exec = promisify(execWithCallback);

/**
* Options for `sh`.
*/
export type ShOptions = ExecOptions & {
/**
* Trim leading and trailing whitespace from the output.
*
* Default: true
*/
trim?: boolean;
};

/**
* Executes `command` within a shell and returns its output (stdout), or throws
* an error if `command` completes with a non-zero exit code.
*
* ```ts
* import { sh } from '@sebalon/scripting';
*
* const output = await sh('echo hello');
* // => 'hello'
* ```
*
* @param command Command to run.
* @param options Options to be passed to `exec`.
*/
export async function sh(command: string, options?: ShOptions): Promise<string> {
const result = await exec(command, options);
let output = result.stdout.toString();
if (options?.trim ?? true) {
output = output.trim();
}
return output;
}
17 changes: 0 additions & 17 deletions tests/greet.spec.ts

This file was deleted.

5 changes: 4 additions & 1 deletion tests/setup.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
// add test setup here, e.g. load dotenv, register chai plugins, ...
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';

chai.use(chaiAsPromised);
38 changes: 38 additions & 0 deletions tests/shell.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { expect } from 'chai';

import { exec, sh } from '@/src';

describe('exec', () => {
it('should run a command and return stdout and stderr', async () => {
const output = await exec('echo "hello stdout"; echo "hello stderr" >&2');
expect(output.stdout).to.equal('hello stdout\n');
expect(output.stderr).to.equal('hello stderr\n');
});

it('should throw if the command fails', async () => {
await expect(exec('false')).to.eventually.be.rejected;
});
});

describe('sh', () => {
it('should run a command and return its stdout, trimmed by default', async () => {
const output = await sh('echo hello');
expect(output).to.equal('hello');
});

it('should throw if the command fails', async () => {
await expect(sh('false')).to.eventually.be.rejected;
});

describe('options.trim', () => {
it('if true, should remove leading and trailing spaces', async () => {
const output = await sh('echo " hello "', { trim: true });
expect(output).to.equal('hello');
});

it('if false, should keep leading and trailing spaces', async () => {
const output = await sh('echo " hello "', { trim: false });
expect(output).to.equal(' hello \n');
});
});
});

0 comments on commit a68ade7

Please sign in to comment.