Skip to content
This repository has been archived by the owner on Jul 6, 2019. It is now read-only.

Commit

Permalink
feat(opts): add --shell-auto-fallback (#7)
Browse files Browse the repository at this point in the history
Generates shell code that hooks into the "command not found" mechanism
and attempts to run the command with npx instead of failing. The option
has an optional argument that forces generation of a particular variant,
otherwise it attempts to autodetect the shell. However, autodetecting is
actually very flaky, so it's better to always specify the argument.

To use, place this in relevant shell config file:

For Bash:

    source <(npx --shell-auto-fallback bash)

For Zsh:

    source <(npx --shell-auto-fallback zsh)

For Fish:

    source (npx --shell-auto-fallback fish | psub)

As Seen On Twitter: https://twitter.com/passcod/status/869469928474107906
  • Loading branch information
passcod authored and zkat committed May 31, 2017
1 parent e41f1fc commit ac9cb40
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 2 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ If a version specifier is included, or if `--package` is used, npx will ignore t

* `-c <string>` - Execute `<string>` with delayed environment variable evaluation.

* `--shell-auto-fallback [shell]` - Generates shell code to override your shell's "command not found" handler with one that calls `npx`. Tries to figure out your shell, or you can pass its name (either `bash`, `fish`, or `zsh`) as an option. See below for how to install.

* `-v, --version` - Show the current npx version.

## EXAMPLES
Expand Down Expand Up @@ -60,6 +62,30 @@ $ cat package.json
...webpack added to "devDependencies"
```

## SHELL AUTO FALLBACK

To install permanently, add the relevant line to your `~/.bashrc`, `~/.zshrc`, `~/.config/fish/config.fish`, or as needed. To install just for the shell session, simply run the line.

Be warned that this _will_ send (almost) all your missed commands over the internet, then fetch and execute code automatically.

### For Bash:

```
$ source <(npx --shell-auto-fallback bash)
```

### For Fish:

```
$ source (npx --shell-auto-fallback fish | psub)
```

### For Zsh:

```
$ source <(npx --shell-auto-fallback zsh)
```

## ACKNOWLEDGEMENTS

Huge thanks to [Kwyn Meagher](https://blog.kwyn.io) for generously donating the package name in the main npm registry. Previously `npx` was used for a Tessel board Neopixels library, which can now be found under [`npx-tessel`](https://npm.im/npx-tessel).
Expand Down
46 changes: 46 additions & 0 deletions auto-fallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict'

const POSIX = `
command_not_found_handler() {
# Do not run within a pipe
if test ! -t 1; then
echo "command not found: $1"
return 127
fi
echo "Trying with npx..."
npx $*
return $?
}`

const FISH = `
function __fish_command_not_found_on_interactive --on-event fish_prompt
functions --erase __fish_command_not_found_handler
functions --erase __fish_command_not_found_setup
function __fish_command_not_found_handler --on-event fish_command_not_found
echo "Trying with npx..."
npx $argv
end
functions --erase __fish_command_not_found_on_interactive
end`

module.exports = function autoFallback (shell) {
const SHELL = process.env.SHELL || ''

if (shell === 'bash' || SHELL.includes('bash')) {
return POSIX.replace('handler()', 'handle()')
}

if (shell === 'zsh' || SHELL.includes('zsh')) {
return POSIX
}

if (shell === 'fish' || SHELL.includes('fish')) {
return FISH
}

console.error('Only Bash, Zsh, and Fish shells are supported :(')
process.exit(1)
}
7 changes: 7 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

const BB = require('bluebird')

const autoFallback = require('./auto-fallback.js')
const cp = require('child_process')
const getPrefix = require('./get-prefix.js')
const parseArgs = require('./parse-args.js')
Expand All @@ -18,6 +19,12 @@ updateNotifier({pkg}).notify()
main(parseArgs())

function main (argv) {
const shell = argv['shell-auto-fallback']
if (shell || shell === '') {
console.log(autoFallback(shell))
process.exit(0)
}

if (!argv.command || !argv.package) {
console.error('\nERROR: You must supply a command.\n')
yargs.showHelp()
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

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

8 changes: 7 additions & 1 deletion parse-args.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const npa = require('npm-package-arg')
const yargs = require('yargs')

const usage = `$0 [--package|-p <package>] [--cache <path>] [--save-dev|-D] [--save-prod|-P] [--save-optional|-O] [--save-bundle|-B] [--save-exact|-E] [--global|-g] [--prefix|-C] [--userconfig <path>] [-c <string>] [--version|-v] [--] <command>[@version] [command-arg]...`
const usage = `$0 [--package|-p <package>] [--cache <path>] [--save-dev|-D] [--save-prod|-P] [--save-optional|-O] [--save-bundle|-B] [--save-exact|-E] [--global|-g] [--prefix|-C] [--userconfig <path>] [-c <string>] [--shell-auto-fallback [shell]] [--version|-v] [--] <command>[@version] [command-arg]...`

module.exports = parseArgs
function parseArgs () {
Expand Down Expand Up @@ -62,6 +62,12 @@ function parseArgs () {
type: 'string',
describe: 'execute string as if inside `npm run-script`'
})
.option('shell-auto-fallback', {
choices: ['', 'bash', 'fish', 'zsh'],
describe: 'generate shell code to use npx as the "command not found" fallback',
requireArg: false,
type: 'string'
})
.version()
.alias('version', 'v')

Expand Down
112 changes: 112 additions & 0 deletions test/auto-fallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use strict'

const exec = require('child_process').exec
const test = require('tap').test

test('not called with option', (t) =>
exec('node .', (err, stdout, stderr) => {
t.equal(err.code, 1)
t.notOk(stdout)
t.match(stderr, /--shell-auto-fallback/)
t.end()
})
)

test('detect: SHELL ~= fish', (t) =>
exec('node . --shell-auto-fallback', {
env: {
SHELL: '/usr/bin/fish'
}
}, (err, stdout, stderr) => {
if (err) { throw err }
t.match(stdout, /function __fish_command_not_found/)
t.notOk(stderr)
t.end()
})
)

test('detect: SHELL ~= bash', (t) =>
exec('node . --shell-auto-fallback', {
env: {
SHELL: '/bin/bash'
}
}, (err, stdout, stderr) => {
if (err) { throw err }
t.match(stdout, /command_not_found_handle\(/)
t.notOk(stderr)
t.end()
})
)

test('detect: SHELL ~= zsh', (t) =>
exec('node . --shell-auto-fallback', {
env: {
SHELL: '/usr/local/bin/zsh'
}
}, (err, stdout, stderr) => {
if (err) { throw err }
t.match(stdout, /command_not_found_handler\(/)
t.notOk(stderr)
t.end()
})
)

test('detect: no SHELL', (t) =>
exec('node . --shell-auto-fallback', {
env: {}
}, (err, stdout, stderr) => {
t.equal(err.code, 1)
t.notOk(stdout)
t.match(stderr, /Only .+ shells are supported :\(/)
t.end()
})
)

test('detect: SHELL ~= unsupported', (t) =>
exec('node . --shell-auto-fallback', {
env: {
SHELL: '/sbin/nope'
}
}, (err, stdout, stderr) => {
t.equal(err.code, 1)
t.notOk(stdout)
t.match(stderr, /Only .+ shells are supported :\(/)
t.end()
})
)

test('given: fish', (t) =>
exec('node . --shell-auto-fallback fish', (err, stdout, stderr) => {
if (err) { throw err }
t.match(stdout, /function __fish_command_not_found/)
t.notOk(stderr)
t.end()
})
)

test('given: bash', (t) =>
exec('node . --shell-auto-fallback bash', (err, stdout, stderr) => {
if (err) { throw err }
t.match(stdout, /command_not_found_handle\(/)
t.notOk(stderr)
t.end()
})
)

test('given: zsh', (t) =>
exec('node . --shell-auto-fallback zsh', (err, stdout, stderr) => {
if (err) { throw err }
t.match(stdout, /command_not_found_handler\(/)
t.notOk(stderr)
t.end()
})
)

test('given: unsupported', (t) =>
exec('node . --shell-auto-fallback nope', (err, stdout, stderr) => {
t.equal(err.code, 1)
t.notOk(stdout)
t.match(stderr, /Invalid values:\s+Argument: shell-auto-fallback/)
t.end()
})
)

0 comments on commit ac9cb40

Please sign in to comment.