Package Exports
- @nksaraf/nsh
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@nksaraf/nsh) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Shellac
A tool to make invoking a series of shell commands safer & better-looking.
Usage
import shellac from 'shellac'
test('morty', async () => await shellac`
$ echo "End-to-end CLI testing made nice"
$ node -p "5 * 9"
stdout >> ${ answer => expect(Number(answer)).toBeGreaterThan(40) }
`)Syntax
Basic commands
await shellac`
// To execute a command, use $
$ my command here
// If you want the output piped through to process.stdout/err, use $$
$$ echo "This command will print to terminal"
// Use stdout/err and >> to check the output of the last command
stdout >> ${ last_cmd_stdout => {
expect(last_cmd_stdout).toBe("This command will print to terminal")
}}
`Returning output
Shellac returns the stdout/err of the last command in a block as { stdout, stderr }
const { stdout, stderr } = await shellac`
$ echo "This command will run but its output will be lost"
$ echo "The last command executed returns its stdout/err"
`
expect(stdout).toBe("The last command executed returns its stdout/err")You can also return named captures from a series of commands:
const { current_sha, current_branch } = await shellac`
$ git rev-parse --short HEAD
stdout >> current_sha
$ git rev-parse --abbrev-ref HEAD
stdout >> current_branch
`Branching
You can use if ${ ... } { ... } else { ... } to run conditionally based on the value of an interpolation:
await shellac`
if ${ process.env.CLEAN_RUN } {
$ yarn create react-app
} else {
$ git reset --hard
$ git clean -df
}
$$ npx fab init -y
// ...
`Changing directory
You can either use an in directive:
await shellac`
// Change directory for the duration of the block:
in ${ __dirname } {
$ pwd
stdout >> ${ cwd => expect(cwd).toBe(__dirname) }
}
// By default we run in process.cwd()
$ pwd
stdout >> ${ cwd => expect(cwd).toBe(process.cwd()) }
`If the whole script needs to run in one place, use shellac.in(dir):
import tmp from 'tmp-promise'
const dir = await tmp.dir()
await shellac.in(dir.path)`
$ pwd
stdout >> ${ cwd => expect(cwd).toBe(dir.path) }
`Async
Use the await declaration to invoke & wait for some JS inline with your script. It works great when Bash doesn't quite do what you need.
import fs from 'fs-extra'
await shellac.in(cwd)`
await ${ async () => {
await fs.writeFile(
path.join(cwd, 'bigfile.dat'),
huge_data
)
}}
$ ls -l
stdout >> ${(files) => expect(files).toMatch('bigfile.dat')}
`Interpolated commands
Inside a $ command you can use string interpolation like normal:
await shellac.in(cwd)`
$ echo "${JSON.stringify({ current_sha, current_branch })}" > git_info.json
`These can even be promises or async functions:
const getAllPackageNames = async () => { /* ... */ }
await shellac.in(cwd)`
// You can pass a promise and it will be awaited
$ yarn link ${ getAllPackageNames() }
// ...
// Or pass an async function and shellac will call and await it
$ yarn unlink ${ async () => getAllPackageNames() }
`Persistence between commands
A shellac call invokes a single instance of bash for the duration, so changes you make are reflected later in the script:
await shellac`
$ echo $LOL
stdout >> ${lol => expect(lol).toBe('') }
$ LOL=boats
$ echo $LOL
stdout >> ${lol => expect(lol).toBe('boats') }
`Note: the current working directory is only configured by shellac.in() or the in ${} { ... } directive:
const cwd = __dirname
const parent_dir = path.resolve(cwd, '..')
await shellac.in(cwd)`
// Normal behaviour
$ pwd
stdout >> ${pwd => expect(pwd).toBe(cwd) }
// Has no effect on the remaining commands
$ cd ..
$ pwd
stdout >> ${pwd => expect(pwd).toBe(cwd) }
// If you want to change dir use in {}
in ${ parent_dir } {
$ pwd
stdout >> ${pwd => expect(pwd).toBe(parent_dir) }
}
// Or do it on a single line
$ cd .. && pwd
stdout >> ${pwd => expect(pwd).toBe(parent_dir) }
// Joining commands with ; also works
$ cd ..; pwd
stdout >> ${(pwd) => expect(pwd).toBe(parent_dir)}
`Comments
All these examples are valid, since // single-line-comments are ignored as expected.
Example
Works great with ts-jest:
// ts-jest-example.test.js
import shellac from 'shellac'
describe('my CLI tool', () => {
it('should do everything I need', async () => {
await shellac`
$ echo "Hello, world!"
stdout >> ${(echo) => {
expect(echo).toBe('Hello, world!')
}}
$ rm -rf working-dir
$ mkdir -p working-dir/example
$ cp -R fixtures/run-1/* working-dir/example
await ${async () => {
// generate some more test data
}}
in ${'working-dir/example'} {
$ ls -l
stdout >> ${(files) => {
expect(files).toMatch('package.json')
}}
$ yarn
$$ run-app
}
`
})
})Using CommonJS, import it like:
const test = require('ava')
const shellac = require('shellac').default
test('plugin should be installable', async (t) => {
await shellac.default`
$ echo "Hello, world!"
stdout >> ${(echo) => {
t.is(echo, 'Hello, world!')
}}
`
})Snippets
Use double-$ $$ for logging while the test runs:
shellac.in(cwd)`
$$ ls -al
`is the same as:
shellac.in(cwd)`
$ ls -al
stdout >> ${console.log}
`Confirm a file is present:
shellac`
$ ls -l
stdout >> ${files => expect(files).toMatch('fab.zip')}
`Acknowledgements
@kitten for reghex which is genuinely incredible and the only reason this library is possible at all.
@superhighfives for coming up with the name!