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

Commit 2404420

Browse files
authored
feat(cmd): do some heuristic guesswork on default command names (#23)
Fixes: #12
1 parent ed70a7b commit 2404420

File tree

3 files changed

+103
-4
lines changed

3 files changed

+103
-4
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Executes `<command>` either from a local `node_modules/.bin`, or from a central
1616

1717
By default, `npx` will check whether `<command>` exists in `$PATH`, or in the local project binaries, and execute that. If `<command>` is not found, it will be installed prior to execution.
1818

19+
Unless a `--package` option is specified, `npx` will try to guess the name of the binary to invoke depending on the specifier provided. All package specifiers understood by `npm` may be used with `npx`, including git specifiers, remote tarballs, local directories, or scoped packages.
20+
1921
An optional `@version` may be appended to specify the package version required, which defaults to `latest` only if `<command>` is not in the path. If the command is already present and no explicit version specifier was requested, the existing command will be used.
2022

2123
If a version specifier is included, or if `--package` is used, npx will ignore the version of the package in the current path, if it exists. This can also be forced with the `--ignore-existing` flag.
@@ -42,7 +44,7 @@ If a version specifier is included, or if `--package` is used, npx will ignore t
4244

4345
```
4446
$ npm i -D webpack
45-
$ npx webpack -- ...
47+
$ npx webpack ...
4648
```
4749

4850
### One-off invocation without local installation
@@ -54,6 +56,15 @@ $ cat package.json
5456
...webpack not in "devDependencies"...
5557
```
5658

59+
### Invoking a command from a github repository
60+
61+
```
62+
$ npx github:piuccio/cowsay
63+
...or...
64+
$ npx git+ssh://my.hosted.git:cowsay.git#semver:^1
65+
...etc...
66+
```
67+
5768
### Execute a full shell command using one npx call
5869

5970
```

parse-args.js

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

33
const npa = require('npm-package-arg')
4+
const path = require('path')
45
const yargs = require('yargs')
56

67
const usage = `$0 [--package|-p <package>] [--cache <path>] [--userconfig <path>] [-c <string>] [--shell <string>] [--shell-auto-fallback [<shell>]] [--ignore-existing] [--version|-v] [--] <command>[@version] [command-arg]...`
@@ -71,7 +72,7 @@ function parseArgs () {
7172
const parsedCmd = npa(process.argv[cmdIndex])
7273
parsed.command = parsed.package
7374
? process.argv[cmdIndex]
74-
: parsedCmd.name
75+
: guessCmdName(parsedCmd)
7576
parsed.cmdOpts = process.argv.slice(cmdIndex + 1)
7677
parsed.packageRequested = !!parsed.package
7778
parsed.cmdHadVersion = parsedCmd.name !== parsedCmd.raw
@@ -85,7 +86,7 @@ function parseArgs () {
8586
const parsedCmd = npa(splitCmd[0])
8687
parsed.command = parsed.package
8788
? splitCmd[0]
88-
: parsedCmd.name
89+
: guessCmdName(parsedCmd)
8990
parsed.cmdOpts = splitCmd.slice(1)
9091
parsed.packageRequested = !!parsed.package
9192
parsed.cmdHadVersion = parsedCmd.name !== parsedCmd.raw
@@ -96,11 +97,39 @@ function parseArgs () {
9697
const parsedCmd = npa(splitCmd[0])
9798
parsed.command = parsed.package
9899
? splitCmd[0]
99-
: parsedCmd.name
100+
: guessCmdName(parsedCmd)
100101
parsed.cmdOpts = splitCmd.slice(1)
101102
parsed.packageRequested = !!parsed.package
102103
parsed.cmdHadVersion = parsedCmd.name !== parsedCmd.raw
103104
}
104105
return parsed
105106
}
106107
}
108+
109+
module.exports._guessCmdName = guessCmdName
110+
function guessCmdName (spec) {
111+
if (typeof spec === 'string') { spec = npa(spec) }
112+
if (spec.scope) {
113+
return spec.name.slice(spec.scope.length + 1)
114+
} else if (spec.registry) {
115+
return spec.name
116+
} else if (spec.hosted && spec.hosted.project) {
117+
return spec.hosted.project
118+
} else if (spec.type === 'git') {
119+
const match = spec.fetchSpec.match(/([a-z0-9-]+)(?:\.git)?$/i)
120+
if (match && match[1]) {
121+
return match[1]
122+
}
123+
} else if (spec.type === 'directory') {
124+
return path.basename(spec.fetchSpec)
125+
} else if (spec.type === 'file' || spec.type === 'remote') {
126+
let ext = path.extname(spec.fetchSpec)
127+
if (ext === '.gz') {
128+
ext = path.extname(path.basename(spec.fetchSpec, ext)) + ext
129+
}
130+
return path.basename(spec.fetchSpec, ext).replace(/-\d+\.\d+\.\d+(?:-[a-z0-9.\-+]+)?$/i, '')
131+
}
132+
133+
console.error(`Unable to guess a binary name from ${spec.raw}. Please use --package.`)
134+
return null
135+
}

test/guess-command-name.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict'
2+
3+
const test = require('tap').test
4+
5+
const guessCmdName = require('../parse-args.js')._guessCmdName
6+
7+
test('guesses unscoped registry binaries', t => {
8+
t.equal(guessCmdName('foo'), 'foo')
9+
t.done()
10+
})
11+
12+
test('guesses scoped registry binaries', t => {
13+
t.equal(guessCmdName('@user/foo'), 'foo')
14+
t.done()
15+
})
16+
17+
test('guesses hosted git binaries', t => {
18+
t.equal(guessCmdName('user/foo'), 'foo')
19+
t.equal(guessCmdName('git+ssh://git@github.com:user/foo.git#semver:^1'), 'foo')
20+
t.done()
21+
})
22+
23+
test('guesses git binaries', t => {
24+
t.equal(guessCmdName('git+ssh://myhost.com:user/foo.git#semver:^1'), 'foo')
25+
t.equal(guessCmdName('git://myhost.com:foo'), 'foo')
26+
t.done()
27+
})
28+
29+
test('guesses local directory binaries', t => {
30+
t.equal(guessCmdName('./foo'), 'foo')
31+
t.equal(guessCmdName('./dir/foo'), 'foo')
32+
t.equal(guessCmdName('../../../dir/foo'), 'foo')
33+
t.equal(guessCmdName('C:\\Program Files\\node\\foo'), 'foo')
34+
t.done()
35+
})
36+
37+
test('guesses remote tarballs', t => {
38+
t.equal(guessCmdName('https://registry.npmjs.org/foo/-/foo-1.2.3.tgz'), 'foo')
39+
t.equal(guessCmdName('http://registry.npmjs.org/foo/-/foo-1.2.3-prerelease.5+buildnum.tgz'), 'foo')
40+
t.equal(guessCmdName('https://github.com/tarball/blah/foo.tar.gz'), 'foo')
41+
t.done()
42+
})
43+
44+
test('guesses local tarballs', t => {
45+
t.equal(guessCmdName('./foo-1.2.3.tgz'), 'foo')
46+
t.equal(guessCmdName('./dir/foo.tar.gz'), 'foo')
47+
t.equal(guessCmdName('C:\\Program Files\\dir/foo-bar.tar.gz'), 'foo-bar')
48+
t.done()
49+
})
50+
51+
test('warns when something could not be guessed', t => {
52+
const oldErr = console.error
53+
console.error = str => {
54+
t.match(str, /Unable to guess a binary name/)
55+
}
56+
guessCmdName({})
57+
console.error = oldErr
58+
t.done()
59+
})

0 commit comments

Comments
 (0)