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

Commit ac9cb40

Browse files
passcodzkat
authored andcommitted
feat(opts): add --shell-auto-fallback (#7)
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
1 parent e41f1fc commit ac9cb40

File tree

6 files changed

+199
-2
lines changed

6 files changed

+199
-2
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ If a version specifier is included, or if `--package` is used, npx will ignore t
3232

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

35+
* `--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.
36+
3537
* `-v, --version` - Show the current npx version.
3638

3739
## EXAMPLES
@@ -60,6 +62,30 @@ $ cat package.json
6062
...webpack added to "devDependencies"
6163
```
6264

65+
## SHELL AUTO FALLBACK
66+
67+
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.
68+
69+
Be warned that this _will_ send (almost) all your missed commands over the internet, then fetch and execute code automatically.
70+
71+
### For Bash:
72+
73+
```
74+
$ source <(npx --shell-auto-fallback bash)
75+
```
76+
77+
### For Fish:
78+
79+
```
80+
$ source (npx --shell-auto-fallback fish | psub)
81+
```
82+
83+
### For Zsh:
84+
85+
```
86+
$ source <(npx --shell-auto-fallback zsh)
87+
```
88+
6389
## ACKNOWLEDGEMENTS
6490

6591
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).

auto-fallback.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use strict'
2+
3+
const POSIX = `
4+
command_not_found_handler() {
5+
# Do not run within a pipe
6+
if test ! -t 1; then
7+
echo "command not found: $1"
8+
return 127
9+
fi
10+
11+
echo "Trying with npx..."
12+
npx $*
13+
return $?
14+
}`
15+
16+
const FISH = `
17+
function __fish_command_not_found_on_interactive --on-event fish_prompt
18+
functions --erase __fish_command_not_found_handler
19+
functions --erase __fish_command_not_found_setup
20+
21+
function __fish_command_not_found_handler --on-event fish_command_not_found
22+
echo "Trying with npx..."
23+
npx $argv
24+
end
25+
26+
functions --erase __fish_command_not_found_on_interactive
27+
end`
28+
29+
module.exports = function autoFallback (shell) {
30+
const SHELL = process.env.SHELL || ''
31+
32+
if (shell === 'bash' || SHELL.includes('bash')) {
33+
return POSIX.replace('handler()', 'handle()')
34+
}
35+
36+
if (shell === 'zsh' || SHELL.includes('zsh')) {
37+
return POSIX
38+
}
39+
40+
if (shell === 'fish' || SHELL.includes('fish')) {
41+
return FISH
42+
}
43+
44+
console.error('Only Bash, Zsh, and Fish shells are supported :(')
45+
process.exit(1)
46+
}

index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
const BB = require('bluebird')
55

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

2021
function main (argv) {
22+
const shell = argv['shell-auto-fallback']
23+
if (shell || shell === '') {
24+
console.log(autoFallback(shell))
25+
process.exit(0)
26+
}
27+
2128
if (!argv.command || !argv.package) {
2229
console.error('\nERROR: You must supply a command.\n')
2330
yargs.showHelp()

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

parse-args.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const npa = require('npm-package-arg')
44
const yargs = require('yargs')
55

6-
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]...`
6+
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]...`
77

88
module.exports = parseArgs
99
function parseArgs () {
@@ -62,6 +62,12 @@ function parseArgs () {
6262
type: 'string',
6363
describe: 'execute string as if inside `npm run-script`'
6464
})
65+
.option('shell-auto-fallback', {
66+
choices: ['', 'bash', 'fish', 'zsh'],
67+
describe: 'generate shell code to use npx as the "command not found" fallback',
68+
requireArg: false,
69+
type: 'string'
70+
})
6571
.version()
6672
.alias('version', 'v')
6773

test/auto-fallback.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use strict'
2+
3+
const exec = require('child_process').exec
4+
const test = require('tap').test
5+
6+
test('not called with option', (t) =>
7+
exec('node .', (err, stdout, stderr) => {
8+
t.equal(err.code, 1)
9+
t.notOk(stdout)
10+
t.match(stderr, /--shell-auto-fallback/)
11+
t.end()
12+
})
13+
)
14+
15+
test('detect: SHELL ~= fish', (t) =>
16+
exec('node . --shell-auto-fallback', {
17+
env: {
18+
SHELL: '/usr/bin/fish'
19+
}
20+
}, (err, stdout, stderr) => {
21+
if (err) { throw err }
22+
t.match(stdout, /function __fish_command_not_found/)
23+
t.notOk(stderr)
24+
t.end()
25+
})
26+
)
27+
28+
test('detect: SHELL ~= bash', (t) =>
29+
exec('node . --shell-auto-fallback', {
30+
env: {
31+
SHELL: '/bin/bash'
32+
}
33+
}, (err, stdout, stderr) => {
34+
if (err) { throw err }
35+
t.match(stdout, /command_not_found_handle\(/)
36+
t.notOk(stderr)
37+
t.end()
38+
})
39+
)
40+
41+
test('detect: SHELL ~= zsh', (t) =>
42+
exec('node . --shell-auto-fallback', {
43+
env: {
44+
SHELL: '/usr/local/bin/zsh'
45+
}
46+
}, (err, stdout, stderr) => {
47+
if (err) { throw err }
48+
t.match(stdout, /command_not_found_handler\(/)
49+
t.notOk(stderr)
50+
t.end()
51+
})
52+
)
53+
54+
test('detect: no SHELL', (t) =>
55+
exec('node . --shell-auto-fallback', {
56+
env: {}
57+
}, (err, stdout, stderr) => {
58+
t.equal(err.code, 1)
59+
t.notOk(stdout)
60+
t.match(stderr, /Only .+ shells are supported :\(/)
61+
t.end()
62+
})
63+
)
64+
65+
test('detect: SHELL ~= unsupported', (t) =>
66+
exec('node . --shell-auto-fallback', {
67+
env: {
68+
SHELL: '/sbin/nope'
69+
}
70+
}, (err, stdout, stderr) => {
71+
t.equal(err.code, 1)
72+
t.notOk(stdout)
73+
t.match(stderr, /Only .+ shells are supported :\(/)
74+
t.end()
75+
})
76+
)
77+
78+
test('given: fish', (t) =>
79+
exec('node . --shell-auto-fallback fish', (err, stdout, stderr) => {
80+
if (err) { throw err }
81+
t.match(stdout, /function __fish_command_not_found/)
82+
t.notOk(stderr)
83+
t.end()
84+
})
85+
)
86+
87+
test('given: bash', (t) =>
88+
exec('node . --shell-auto-fallback bash', (err, stdout, stderr) => {
89+
if (err) { throw err }
90+
t.match(stdout, /command_not_found_handle\(/)
91+
t.notOk(stderr)
92+
t.end()
93+
})
94+
)
95+
96+
test('given: zsh', (t) =>
97+
exec('node . --shell-auto-fallback zsh', (err, stdout, stderr) => {
98+
if (err) { throw err }
99+
t.match(stdout, /command_not_found_handler\(/)
100+
t.notOk(stderr)
101+
t.end()
102+
})
103+
)
104+
105+
test('given: unsupported', (t) =>
106+
exec('node . --shell-auto-fallback nope', (err, stdout, stderr) => {
107+
t.equal(err.code, 1)
108+
t.notOk(stdout)
109+
t.match(stderr, /Invalid values:\s+Argument: shell-auto-fallback/)
110+
t.end()
111+
})
112+
)

0 commit comments

Comments
 (0)