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

Add --shell-auto-fallback #7

Merged
merged 5 commits into from
May 31, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ By default, `<command>` will be installed prior to execution. An optional `@vers

* `-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 @@ -58,6 +60,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 === '') {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't this always be true? parseArgs() defaults to ''

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requireArg is false! So sometimes the option will return null/undefined/false/whatever yargs returns for a non-provided option. But '' is falsy, so I do need to check.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't having a default mean that you always get '' if you don't provide the option, though? I'm so confused.

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'],
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ''?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll re-test, but I think yargs refused to parse the option if the default wasn't in the choices. Although. Given the difficulty of figuring out the shell from a subprocess, maybe I should just remove both the autodetecting code and the default value here, and just do it based on the passed value.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just don't give it a default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As to why I made the default ''... err. I don't remember. I'll have a look at the code again in its entirety to see if I can figure it out.

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()
})
)