Repository: yargs/yargs
Pull request #983 (2017-10-16 01:34:02)

[Discussion] Replacing "$0" with the executing file's base name
This PR changes the behavior of the `$0` token (when used within the contents of the `usage`, `epilog` and/or `example` methods), so that it displays the executing file's basename, as opposed to the full path. This has the affect of providing default help text that is closer to what the end-user would actually run, since they would likely be running the CLI on their path (or via an `package.json` script), as opposed to using the CLI's entrypoint script path.

This is a breaking change, but since #975 introduces a breaking change for the `usage` method, I figured I'd put this PR out for discussion's sake. I updated all of the tests to ensure they still pass, but there may be other scenarios that this change would break, so I'd love some feedback.

I'm also not entirely sure this is the right behavior (e.h. as opposed to introducing a new token ala `$$0` or something, etc.), and I'm sure this conversation has been had a bunch of times already, so I'd love feedback and/or pointers to an existing/better solution for this. Thanks!

---

**Before this change (using a Yargs-based CLI named `kyte`):**

<img width="586" alt="screen shot 2017-10-15 at 1 42 25 pm" src="https://user-images.githubusercontent.com/116461/31589008-04238874-b1af-11e7-9d2b-2443e734bcf4.png">

**After this change:**

<img width="578" alt="screen shot 2017-10-15 at 6 16 02 pm" src="https://user-images.githubusercontent.com/116461/31591545-8d94efe6-b1d5-11e7-856e-086f8a155b48.png">

CC @bcoe 

bcoe: @lostintangent I like this idea. I might be tempted to apply the `path.basename` logic in `yargs.js` when we initially populate `$0`, so that we don't need to apply the replacement multiple times.

What does the output look like when you're running a command line script that's not installed globally, e.g., `node ./foo.js --help`?


lostintangent: It would display the file name (without the path), like so:

<img width="583" alt="screen shot 2017-10-15 at 6 57 38 pm" src="https://user-images.githubusercontent.com/116461/31592102-c99d7e18-b1da-11e7-9738-81ca3ec5b40d.png">

bcoe: @lostintangent I like this change, I think it's much more readable.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b5c810..6bd1752 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -52,6 +52,7 @@ All notable changes to this project will be documented in this file. See [standa
 ### BREAKING CHANGES
 
 * version() and help() are now enabled by default, and show up in help output; the implicit help command can no longer be enabled/disabled independently from the help command itself (which can now be disabled).
+* parse() now behaves as an alias for .argv, unless a parseCallback is provided.
 
 
 
diff --git a/lib/command.js b/lib/command.js
index b528df7..b094ad5 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -293,13 +293,26 @@ module.exports = function command (yargs, usage, validation) {
         return `--${key} ${value}`
       }))
     })
+
+    // short-circuit parse.
+    if (!unparsed.length) return
+
     const parsed = Parser.detailed(unparsed.join(' '), options)
 
     if (parsed.error) {
       yargs.getUsageInstance().fail(parsed.error.message, parsed.error)
     } else {
+      // only copy over positional keys (don't overwrite
+      // flag arguments that were already parsed).
+      const positionalKeys = Object.keys(positionalMap)
+      Object.keys(positionalMap).forEach((key) => {
+        [].push.apply(positionalKeys, parsed.aliases[key])
+      })
+
       Object.keys(parsed.argv).forEach((key) => {
-        if (key !== '_') argv[key] = parsed.argv[key]
+        if (positionalKeys.indexOf(key) !== -1) {
+          argv[key] = parsed.argv[key]
+        }
       })
     }
   }
diff --git a/lib/usage.js b/lib/usage.js
index c742f8d..24a9a9a 100644
--- a/lib/usage.js
+++ b/lib/usage.js
@@ -3,6 +3,7 @@
 // failures, etc. keeps logging in one place.
 const stringWidth = require('string-width')
 const objFilter = require('./obj-filter')
+const path = require('path')
 const setBlocking = require('set-blocking')
 const YError = require('./yerror')
 
@@ -165,7 +166,7 @@ module.exports = function usage (yargs, y18n) {
 
     // the usage string.
     if (usages.length && !usageDisabled) {
-      const u = self.getUsage().replace(/\$0/g, yargs.$0)
+      const u = self.getUsage().replace(/\$0/g, path.basename(yargs.$0))
       ui.div(`${u}`)
     }
 
@@ -275,7 +276,7 @@ module.exports = function usage (yargs, y18n) {
       ui.div(__('Examples:'))
 
       examples.forEach((example) => {
-        example[0] = example[0].replace(/\$0/g, yargs.$0)
+        example[0] = example[0].replace(/\$0/g, path.basename(yargs.$0))
       })
 
       examples.forEach((example) => {
@@ -304,7 +305,7 @@ module.exports = function usage (yargs, y18n) {
 
     // the usage string.
     if (epilog) {
-      const e = epilog.replace(/\$0/g, yargs.$0)
+      const e = epilog.replace(/\$0/g, path.basename(yargs.$0))
       ui.div(`${e}\n`)
     }
 
diff --git a/locales/pl.json b/locales/pl.json
index 1c834f6..6926a45 100644
--- a/locales/pl.json
+++ b/locales/pl.json
@@ -35,5 +35,8 @@
   "Path to JSON config file": "Ścieżka do pliku konfiguracyjnego JSON",
   "Show help": "Pokaż pomoc",
   "Show version number": "Pokaż numer wersji",
-  "Did you mean %s?": "Czy chodziło Ci o %s?"
+  "Did you mean %s?": "Czy chodziło Ci o %s?",
+  "Arguments %s and %s are mutually exclusive": "Argumenty %s i %s wzajemnie się wykluczają",
+  "Positionals:": "Pozycyjne:",
+  "command": "polecenie"
 }
diff --git a/locales/tr.json b/locales/tr.json
index 000060b..9b06c52 100644
--- a/locales/tr.json
+++ b/locales/tr.json
@@ -35,5 +35,7 @@
   "Path to JSON config file": "JSON yapılandırma dosya konumu",
   "Show help": "Yardım detaylarını göster",
   "Show version number": "Versiyon detaylarını göster",
-  "Did you mean %s?": "Bunu mu demek istediniz: %s?"
+  "Did you mean %s?": "Bunu mu demek istediniz: %s?",
+  "Positionals:": "Sıralılar:",
+  "command": "komut"
 }


Test patch:
diff --git a/test/command.js b/test/command.js
index d468f90..6656716 100644
--- a/test/command.js
+++ b/test/command.js
@@ -102,6 +102,15 @@ describe('Command', () => {
       expect(handlers.wat).to.not.exist
       command.getCommands().should.deep.equal(['foo', 'wat'])
     })
+
+    it('does not overwrite existing values in argv for keys that are not positional', () => {
+      const argv = yargs('foo foo.js --reporter=html')
+        .command('foo <file>')
+        .default('reporter', 'text')
+        .argv
+      argv.file.should.equal('foo.js')
+      argv.reporter.should.equal('html')
+    })
   })
 
   describe('variadic', () => {
@@ -523,7 +532,7 @@ describe('Command', () => {
       r.should.have.property('errors').with.length(0)
       r.should.have.property('logs')
       r.logs[0].split(/\n+/).should.deep.equal([
-        './command dream [command] [opts]',
+        'command dream [command] [opts]',
         'Commands:',
         '  of-memory <memory>               Dream about a specific memory',
         '  within-a-dream [command] [opts]  Dream within a dream',
@@ -595,7 +604,7 @@ describe('Command', () => {
       r.should.have.property('errors').with.length(0)
       r.should.have.property('logs')
       r.logs.join('\n').split(/\n+/).should.deep.equal([
-        './command cyclic',
+        'command cyclic',
         'Options:',
         '  --help     Show help  [boolean]',
         '  --version  Show version number  [boolean]',
@@ -665,7 +674,7 @@ describe('Command', () => {
           .argv, [ './command' ])
 
       const expectedCmd = [
-        './command cmd <sub>',
+        'command cmd <sub>',
         'Commands:',
         '  sub  Run the subcommand',
         'Options:',
@@ -675,7 +684,7 @@ describe('Command', () => {
       ]
 
       const expectedSub = [
-        './command cmd sub',
+        'command cmd sub',
         'Options:',
         '  --help     Show help  [boolean]',
         '  --version  Show version number  [boolean]',
diff --git a/test/usage.js b/test/usage.js
index c97592a..201ea7f 100644
--- a/test/usage.js
+++ b/test/usage.js
@@ -30,7 +30,7 @@ describe('usage tests', () => {
         r.result.should.have.property('z', 20)
         r.result.should.have.property('_').with.length(0)
         r.errors.join('\n').split(/\n+/).should.deep.equal([
-          'Usage: ./usage -x NUM -y NUM',
+          'Usage: usage -x NUM -y NUM',
           'Options:',
           '  --help     Show help  [boolean]',
           '  --version  Show version number  [boolean]',
@@ -54,7 +54,7 @@ describe('usage tests', () => {
         r.result.should.have.property('y', 10)
         r.result.should.have.property('_').with.length(1)
         r.errors.join('\n').split(/\n+/).should.deep.equal([
-          'Usage: ./usage -w NUM -m NUM',
+          'Usage: usage -w NUM -m NUM',
           'Options:',
           '  --help     Show help  [boolean]',
           '  --version  Show version number  [boolean]',
@@ -78,7 +78,7 @@ describe('usage tests', () => {
         r.result.should.have.property('y', 10)
         r.result.should.have.property('_').with.length(0)
         r.errors.join('\n').split(/\n+/).should.deep.equal([
-          'Usage: ./usage -w NUM -m NUM',
+          'Usage: usage -w NUM -m NUM',
           'Options:',
           '  --help     Show help  [boolean]',
           '  --version  Show version number  [boolean]',
@@ -119,7 +119,7 @@ describe('usage tests', () => {
           r.result.should.have.property('z', 20)
           r.result.should.have.property('_').with.length(0)
           r.errors.join('\n').split(/\n+/).should.deep.equal([
-            'Usage: ./usage -x NUM -y NUM',
+            'Usage: usage -x NUM -y NUM',
             'Options:',
             '  --help     Show help  [boolean]',
             '  --version  Show version number  [boolean]',
@@ -142,7 +142,7 @@ describe('usage tests', () => {
           r.result.should.have.property('y', 10)
           r.result.should.have.property('_').with.length(1)
           r.errors.join('\n').split(/\n+/).should.deep.equal([
-            'Usage: ./usage -w NUM -m NUM',
+            'Usage: usage -w NUM -m NUM',
             'Options:',
             '  --help     Show help  [boolean]',
             '  --version  Show version number  [boolean]',
@@ -167,7 +167,7 @@ describe('usage tests', () => {
         r.result.should.have.property('y', 10)
         r.result.should.have.property('_').with.length(0)
         r.errors.join('\n').split(/\n+/).should.deep.equal([
-          'Usage: ./usage -w NUM -m NUM',
+          'Usage: usage -w NUM -m NUM',
           'Options:',
           '  --help     Show help  [boolean]',
           '  --version  Show version number  [boolean]',
@@ -190,7 +190,7 @@ describe('usage tests', () => {
       r.result.should.have.property('z', 20)
       r.result.should.have.property('_').with.length(0)
       r.errors.join('\n').split(/\n+/).should.deep.equal([
-        'Usage: ./usage -x NUM -y NUM',
+        'Usage: usage -x NUM -y NUM',
         'Options:',
         '  --help     Show help  [boolean]',
         '  --version  Show version number  [boolean]',
@@ -347,7 +347,7 @@ describe('usage tests', () => {
     r.result.should.have.property('z', 20)
     r.result.should.have.property('_').with.length(0)
     r.errors.join('\n').split(/\n+/).should.deep.equal([
-      'Usage: ./usage -x NUM -y NUM',
+      'Usage: usage -x NUM -y NUM',
       'Options:',
       '  --help     Show help  [boolean]',
       '  --version  Show version number  [boolean]',
@@ -375,7 +375,7 @@ describe('usage tests', () => {
     r.should.have.property('exit').and.be.ok
     r.should.have.property('errors')
     r.errors.join('\n').split(/\n+/).should.deep.equal([
-      'Usage: ./usage -x NUM -y NUM',
+      'Usage: usage -x NUM -y NUM',
       'Options:',
       '  --help     Show help  [boolean]',
       '  --version  Show version number  [boolean]',
@@ -419,7 +419,7 @@ describe('usage tests', () => {
     r.should.have.property('exit').and.be.ok
     r.should.have.property('errors')
     r.errors.join('\n').split(/\n+/).join('\n').should.equal(
-      'Usage: ./usage -x NUM -y NUM\n' +
+      'Usage: usage -x NUM -y NUM\n' +
       'Options:\n' +
       '  --help     Show help  [boolean]\n' +
       '  --version  Show version number  [boolean]\n' +
@@ -446,7 +446,7 @@ describe('usage tests', () => {
           }
         })
         r.errors.join('\n').split(/\n+/).should.deep.equal([
-          'Usage: ./usage -x NUM -y NUM',
+          'Usage: usage -x NUM -y NUM',
           'Options:',
           '  --help     Show help  [boolean]',
           '  --version  Show version number  [boolean]',
@@ -579,7 +579,7 @@ describe('usage tests', () => {
     r.result.should.have.property('moo', true)
     r.should.have.property('errors')
     r.errors.join('\n').split(/\n+/).should.deep.equal([
-      'Usage: ./usage [x] [y] [z] {OPTIONS}',
+      'Usage: usage [x] [y] [z] {OPTIONS}',
       'Options:',
       '  --help     Show help  [boolean]',
       '  --version  Show version number  [boolean]',
@@ -601,7 +601,7 @@ describe('usage tests', () => {
     r.result.should.have.property('moo', true)
     r.should.have.property('errors')
     r.errors.join('\n').split(/\n+/).should.deep.equal([
-      'Usage: ./usage [x] [y] [z] {OPTIONS} <src> <dest> [extra_files...]',
+      'Usage: usage [x] [y] [z] {OPTIONS} <src> <dest> [extra_files...]',
       'Options:',
       '  --help     Show help  [boolean]',
       '  --version  Show version number  [boolean]',
@@ -686,7 +686,7 @@ describe('usage tests', () => {
         r.should.have.property('logs').with.length(0)
         r.should.have.property('exit').and.be.ok
         r.errors.join('\n').split(/\n+/).should.deep.equal([
-          'Usage: ./usage [options]',
+          'Usage: usage [options]',
           'Options:',
           '  --help     Show help  [boolean]',
           '  --version  Show version number  [boolean]',
@@ -714,7 +714,7 @@ describe('usage tests', () => {
         r.should.have.property('logs').with.length(0)
         r.should.have.property('exit').and.be.ok
         r.errors.join('\n').split(/\n+/).should.deep.equal([
-          'Usage: ./usage [options]',
+          'Usage: usage [options]',
           'Options:',
           '  --help     Show help  [boolean]',
           '  --version  Show version number  [boolean]',
@@ -745,7 +745,7 @@ describe('usage tests', () => {
         r.should.have.property('logs').with.length(0)
         r.should.have.property('exit').and.be.ok
         r.errors.join('\n').split(/\n+/).should.deep.equal([
-          'Usage: ./usage [options]',
+          'Usage: usage [options]',
           'Options:',
           '  --help     Show help  [boolean]',
           '  --version  Show version number  [boolean]',
@@ -794,7 +794,7 @@ describe('usage tests', () => {
       r.result.should.have.property('baz', 30)
       r.should.have.property('errors')
       r.errors.join('\n').split(/\n+/).should.deep.equal([
-        'Usage: ./usage [options]',
+        'Usage: usage [options]',
         'Options:',
         '  --help     Show help  [boolean]',
         '  --version  Show version number  [boolean]',
@@ -829,7 +829,7 @@ describe('usage tests', () => {
       r.result.should.have.property('baz', 30)
       r.should.have.property('errors')
       r.errors.join('\n').split(/\n+/).should.deep.equal([
-        'Usage: ./usage [options]',
+        'Usage: usage [options]',
         'Options:',
         '  --help     Show help  [boolean]',
         '  --version  Show version number  [boolean]',
@@ -865,7 +865,7 @@ describe('usage tests', () => {
       r.result.should.have.property('q', 40)
       r.should.have.property('errors')
       r.errors.join('\n').split(/\n+/).should.deep.equal([
-        'Usage: ./usage [options]',
+        'Usage: usage [options]',
         'Options:',
         '  --help     Show help  [boolean]',
         '  --version  Show version number  [boolean]',
@@ -919,8 +919,8 @@ describe('usage tests', () => {
       '  --version  Show version number  [boolean]',
       '  -y  [required]',
       'Examples:',
-      '  ./usage something       description',
-      '  ./usage something else  other description',
+      '  usage something       description',
+      '  usage something else  other description',
       'Missing required argument: y'
     ])
   })
@@ -941,7 +941,7 @@ describe('usage tests', () => {
         r.result.should.have.property('z', 20)
         r.result.should.have.property('_').with.length(0)
         r.errors.join('\n').split(/\n/).should.deep.equal([
-          'Usage: ./usage -x NUM [-y NUM]',
+          'Usage: usage -x NUM [-y NUM]',
           '',
           'Options:',
           '  --help     Show help  [boolean]',
@@ -971,7 +971,7 @@ describe('usage tests', () => {
         r.result.should.have.property('z', 20)
         r.result.should.have.property('_').with.length(0)
         r.errors.join('\n').split(/\n/).should.deep.equal([
-          'Usage: ./usage -x NUM [-y NUM]',
+          'Usage: usage -x NUM [-y NUM]',
           '',
           'Options:',
           '  --help     Show help  [boolean]',
@@ -1038,7 +1038,7 @@ describe('usage tests', () => {
       r.should.have.property('errors').with.length(0)
       r.should.have.property('logs')
       r.logs.join('\n').split(/\n+/).should.deep.equal([
-        'Usage: ./usage options',
+        'Usage: usage options',
         'Options:',
         '  --help      Show help  [boolean]',
         '  --version   Show version number  [boolean]',
@@ -1066,7 +1066,7 @@ describe('usage tests', () => {
         r.should.have.property('errors').with.length(0)
         r.should.have.property('logs')
         r.logs.join('\n').split(/\n+/).should.deep.equal([
-          'Usage: ./usage options',
+          'Usage: usage options',
           'Options:',
           '  --help, -h  Show help  [boolean]',
           '  --version   Show version number  [boolean]',
@@ -1096,7 +1096,7 @@ describe('usage tests', () => {
         r.should.have.property('errors').with.length(0)
         r.should.have.property('logs')
         r.logs.join('\n').split(/\n+/).should.deep.equal([
-          'Usage: ./usage options',
+          'Usage: usage options',
           'Options:',
           '  --help, -h  Show help  [boolean]',
           '  --version   Show version number  [boolean]',
@@ -1495,7 +1495,7 @@ describe('usage tests', () => {
         ''
       ])
       commandHelp.logs[0].split('\n').should.deep.equal([
-        './usage upload <dest>',
+        'usage upload <dest>',
         '',
         'Options:',
         '  --help     Show help  [boolean]',
@@ -1537,7 +1537,7 @@ describe('usage tests', () => {
         ''
       ])
       commandHelp.logs[0].split('\n').should.deep.equal([
-        './usage upload <dest>',
+        'usage upload <dest>',
         '',
         'Options:',
         '  --help     Show help               [boolean]',
@@ -1563,7 +1563,7 @@ describe('usage tests', () => {
         )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage upload',
+        'usage upload',
         '',
         'Flags:',
         '  -q  [boolean]',
@@ -1601,7 +1601,7 @@ describe('usage tests', () => {
         )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage upload',
+        'usage upload',
         '',
         'Flags:',
         '  -q  [boolean]',
@@ -1635,7 +1635,7 @@ describe('usage tests', () => {
         )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage upload',
+        'usage upload',
         '',
         'Awesome Flags:',
         '  -i  [boolean]',
@@ -1673,7 +1673,7 @@ describe('usage tests', () => {
         )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage upload',
+        'usage upload',
         '',
         'Awesome Flags:',
         '  -i  [boolean]',
@@ -1756,7 +1756,7 @@ describe('usage tests', () => {
         )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage one two [next]',
+        'usage one two [next]',
         '',
         'Options:',
         '  --help     Show help  [boolean]',
@@ -1773,7 +1773,7 @@ describe('usage tests', () => {
         )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage one two [next]',
+        'usage one two [next]',
         '',
         'Options:',
         '  --help     Show help  [boolean]',
@@ -1826,8 +1826,8 @@ describe('usage tests', () => {
       )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage upload [something]',
-        './usage upload [something else]',
+        'usage upload [something]',
+        'usage upload [something else]',
         '',
         'Options:',
         '  --help     Show help  [boolean]',
@@ -1884,7 +1884,7 @@ describe('usage tests', () => {
         '  --help     Show help  [boolean]',
         '  --version  Show version number  [boolean]',
         '  -y  [required]',
-        'Try \'./usage --long-help\' for more information',
+        'Try \'usage --long-help\' for more information',
         'Missing required argument: y'
       ])
     })
@@ -2571,7 +2571,7 @@ describe('usage tests', () => {
       )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage list [pattern]',
+        'usage list [pattern]',
         '',
         'Positionals:',
         '  pattern  the pattern to list keys for',
@@ -2594,7 +2594,7 @@ describe('usage tests', () => {
       )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage list [pattern...]',
+        'usage list [pattern...]',
         '',
         'Positionals:',
         '  pattern  the pattern to list keys for                    [array] [default: []]',
@@ -2617,7 +2617,7 @@ describe('usage tests', () => {
       )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage list <pattern>',
+        'usage list <pattern>',
         '',
         'Positionals:',
         '  pattern  the pattern to list keys for                               [required]',
@@ -2640,7 +2640,7 @@ describe('usage tests', () => {
       )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage list [pattern|thingy]',
+        'usage list [pattern|thingy]',
         '',
         'Positionals:',
         '  pattern, thingy  the pattern to list keys for',
@@ -2664,7 +2664,7 @@ describe('usage tests', () => {
       )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage list [pattern]',
+        'usage list [pattern]',
         '',
         'Positionals:',
         '  pattern  the pattern to list keys for                                 [string]',
@@ -2688,7 +2688,7 @@ describe('usage tests', () => {
       )
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage list [pattern]',
+        'usage list [pattern]',
         '',
         'Positionals:',
         '  pattern  the pattern to list keys for                  [choices: "foo", "bar"]',
diff --git a/test/validation.js b/test/validation.js
index ff9a607..213cf7f 100644
--- a/test/validation.js
+++ b/test/validation.js
@@ -793,7 +793,7 @@ describe('validation tests', () => {
       r.should.have.property('exit').and.be.ok
       r.result.should.have.property('_').and.deep.equal(['src', 'dest'])
       r.errors.join('\n').split(/\n+/).should.deep.equal([
-        'Usage: ./usage [x] [y] [z] {OPTIONS} <src> <dest> [extra_files...]',
+        'Usage: usage [x] [y] [z] {OPTIONS} <src> <dest> [extra_files...]',
         'too many arguments are provided'
       ])
     })
diff --git a/test/yargs.js b/test/yargs.js
index a9f41a8..b2fd60e 100644
--- a/test/yargs.js
+++ b/test/yargs.js
@@ -369,7 +369,7 @@ describe('yargs dsl tests', () => {
       })
 
       r.logs[0].split('\n').should.deep.equal([
-        './usage blerg',
+        'usage blerg',
         '',
         'Commands:',
         '  snuh  snuh command',