Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Argument parsing (>= node 16) #283

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
68 changes: 40 additions & 28 deletions cli.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import { globbySync } from 'globby'
import fs from 'node:fs'
import { parseArgs } from 'node:util'
aarondill marked this conversation as resolved.
Show resolved Hide resolved
import sortPackageJson from './index.js'
import Reporter from './reporter.js'

Expand All @@ -27,6 +28,25 @@ If file/glob is omitted, './package.json' file will be processed.
)
}

function parseCliArguments() {
const { values: options, positionals: patterns } = parseArgs({
options: {
check: { type: 'boolean', short: 'c', default: false },
quiet: { type: 'boolean', short: 'q', default: false },
version: { type: 'boolean', short: 'v', default: false },
help: { type: 'boolean', short: 'h', default: false },
keithamus marked this conversation as resolved.
Show resolved Hide resolved
},
allowPositionals: true,
strict: true,
})

if (!patterns.length) {
patterns[0] = 'package.json'
}

return { options, patterns }
}

function sortPackageJsonFile(file, reporter, isCheck) {
const original = fs.readFileSync(file, 'utf8')
const sorted = sortPackageJson(original)
Expand Down Expand Up @@ -58,41 +78,33 @@ function sortPackageJsonFiles(patterns, options) {
}

function run() {
const cliArguments = process.argv.slice(2)
let options, patterns
try {
;({ options, patterns } = parseCliArguments())
} catch (error) {
process.exitCode = 2
console.error(error.message)
if (
error.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION' ||
error.code === 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE'
) {
console.error(`Try 'sort-package-json --help' for more information.`)
Copy link
Owner

@keithamus keithamus Feb 2, 2023

Choose a reason for hiding this comment

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

Rather than telling the user to re-run the command with different arguments, can we just show the help?

Suggested change
console.error(`Try 'sort-package-json --help' for more information.`)
showHelpInformation()

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 prefer the current mode because it results in shorter output, making it more immediately clear what went wrong, rather than the user having to find the error message above the wall of text. If you are insistent that we should show the help menu, perhaps we move the error displaying to after the help, so the user can see what went wrong without scrolling

Copy link
Collaborator

Choose a reason for hiding this comment

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

I thought about show it directly too, but I agree error message is more important to tell user what's wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@keithamus, thoughts?

Copy link
Owner

@keithamus keithamus Feb 2, 2023

Choose a reason for hiding this comment

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

I'm happy displaying an error message but I think it's important to show the help. Perhaps a more explicit error of --foo is not a valid argument would also be better here?

To clarify - IMO we should show both help and an error telling them why they're seeing the help instead of the command running.

Copy link
Contributor Author

@aarondill aarondill Feb 2, 2023

Choose a reason for hiding this comment

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

Using the util.parseArgs function doesn't seem to expose a way to determine the invalid argument which was passed, other than parsing error.message; however error.message has no guarantee of consistency between versions (Errors | Node.js).

Regarding displaying help, would you suggest the following, or the error at the end? I feel that the help might be slightly overwhelming when just searching for the error.

$ sort-package-json --not-an-option
Unknown option '--not-an-option'. To specify a positional argument starting with a '-',
place it at the end of the command after '--', as in '-- "--not-an-option"
Usage: sort-package-json [options] [file/glob ...]

Sort package.json files.
If file/glob is omitted, './package.json' file will be processed.

  -c, --check   Check if files are sorted
  -q, --quiet   Don't output success messages
  -h, --help    Display this help
  -v, --version Display the package version

I personally prefer the shorter output (see below), as it directly tells the user what went wrong, without having to find it, as well as telling them how to find help if they aren't sure already how to fix their error.

$ sort-package-json --not-an-option
Unknown option '--not-an-option'. To specify a positional argument starting with a '-',
place it at the end of the command after '--', as in '-- "--not-an-option"
Try 'sort-package-json --help' for more information.

Additionally, this is the format followed by many default tools, such as ls, df, du, and diff:

$ df --not-an-option
df: unrecognized option '--not-an-option'
Try 'df --help' for more information.
$ ls --not-an-option
ls: unrecognized option '--not-an-option'
Try 'ls --help' for more information.
$ du --not-an-option
du: unrecognized option '--not-an-option'
Try 'du --help' for more information.
$ diff --not-an-option
diff: unrecognized option '--not-an-option'
diff: Try 'diff --help' for more information.

The other reasonable possibility is to display an abbreviated usage menu, such as the one displayed by git (below), however this forces the maintenance of both help menus when the arguments or usage of the cli changes.

$ git --not-an-option
unknown option: --not-an-option
usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           [--super-prefix=<path>] [--config-env=<name>=<envvar>]
           <command> [<args>]

}
return
}

if (
cliArguments.some((argument) => argument === '--help' || argument === '-h')
) {
if (options.help) {
return showHelpInformation()
}

if (
cliArguments.some(
(argument) => argument === '--version' || argument === '-v',
)
) {
if (options.version) {
return showVersion()
}

const patterns = []
let isCheck = false
let shouldBeQuiet = false

for (const argument of cliArguments) {
if (argument === '--check' || argument === '-c') {
isCheck = true
} else if (argument === '--quiet' || argument === '-q') {
shouldBeQuiet = true
} else {
patterns.push(argument)
}
}

if (!patterns.length) {
patterns[0] = 'package.json'
}

sortPackageJsonFiles(patterns, { isCheck, shouldBeQuiet })
sortPackageJsonFiles(patterns, {
isCheck: options.check,
shouldBeQuiet: options.quiet,
})
}

run()
4 changes: 2 additions & 2 deletions reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class Reporter {
return
}

const { isCheck, isQuiet } = this.#options
const { isCheck, shouldBeQuiet } = this.#options

if (isCheck && changedFilesCount) {
process.exitCode = 1
Expand All @@ -70,7 +70,7 @@ class Reporter {
process.exitCode = 2
}

if (isQuiet) {
if (shouldBeQuiet) {
return
}

Expand Down
47 changes: 47 additions & 0 deletions tests/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,31 @@ test('run `cli --help` with `--version`', macro.testCLI, {
message: 'Should prioritize help over version.',
})

test('run `cli --help=value`', macro.testCLI, {
args: ['--help=value'],
message: 'Should report illegal argument and suggest help.',
})

test('run `cli --version=true`', macro.testCLI, {
args: ['--version=true'],
message: 'Should report illegal argument and suggest help.',
})

test('run `cli --unknown-option`', macro.testCLI, {
args: ['--unknown-option'],
message: 'Should report unknown option and suggest help.',
})

test('run `cli -u` with unknown option', macro.testCLI, {
args: ['-u'],
message: 'Should report unknown option and suggest help.',
})

test('run `cli --no-version`', macro.testCLI, {
args: ['--no-version'],
message: 'A snapshot to show how `--no-*` works, not care about result.',
})

test('run `cli` with no patterns', macro.testCLI, {
fixtures: [
{
Expand All @@ -87,6 +112,11 @@ test('run `cli --quiet` with no patterns', macro.testCLI, {
message: 'Should format package.json without message.',
})

test('run `cli --quiet=value`', macro.testCLI, {
args: ['--quiet=value'],
message: 'Should report illegal argument and suggest help.',
})

test('run `cli -q` with no patterns', macro.testCLI, {
fixtures: [
{
Expand All @@ -111,6 +141,11 @@ test('run `cli --check` with no patterns', macro.testCLI, {
message: 'Should not sort package.json',
})

test('run `cli --check=value`', macro.testCLI, {
args: ['--check=value'],
message: 'Should report illegal argument and suggest help.',
})

test('run `cli --check --quiet` with no patterns', macro.testCLI, {
fixtures: [
{
Expand Down Expand Up @@ -147,6 +182,18 @@ test('run `cli -c -q` with no patterns', macro.testCLI, {
message: 'Should support `-q` alias',
})

test('run `cli -cq` with no patterns', macro.testCLI, {
fixtures: [
{
file: 'package.json',
content: badJson,
expect: badJson,
},
],
args: ['-cq'],
message: 'Should support option aggregation',
})

test('run `cli` on 1 bad file', macro.testCLI, {
fixtures: [
{
Expand Down
154 changes: 154 additions & 0 deletions tests/snapshots/cli.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,96 @@ Generated by [AVA](https://avajs.dev).
},
}

## run `cli --help=value`

> Should report illegal argument and suggest help.

{
args: [
'--help=value',
],
fixtures: [],
result: {
errorCode: 2,
stderr: `Option '-h, --help' does not take an argument␊
Try 'sort-package-json --help' for more information.␊
`,
stdout: '',
},
}

## run `cli --version=true`

> Should report illegal argument and suggest help.

{
args: [
'--version=true',
],
fixtures: [],
result: {
errorCode: 2,
stderr: `Option '-v, --version' does not take an argument␊
Try 'sort-package-json --help' for more information.␊
`,
stdout: '',
},
}

## run `cli --unknown-option`

> Should report unknown option and suggest help.

{
args: [
'--unknown-option',
],
fixtures: [],
result: {
errorCode: 2,
stderr: `Unknown option '--unknown-option'. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- "--unknown-option"␊
Try 'sort-package-json --help' for more information.␊
`,
stdout: '',
},
}

## run `cli -u` with unknown option

> Should report unknown option and suggest help.

{
args: [
'-u',
],
fixtures: [],
result: {
errorCode: 2,
stderr: `Unknown option '-u'. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- "-u"␊
Try 'sort-package-json --help' for more information.␊
`,
stdout: '',
},
}

## run `cli --no-version`

> A snapshot to show how `--no-*` works, not care about result.

{
args: [
'--no-version',
],
fixtures: [],
result: {
errorCode: 2,
stderr: `Unknown option '--no-version'. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- "--no-version"␊
Try 'sort-package-json --help' for more information.␊
`,
stdout: '',
},
}

## run `cli` with no patterns

> Should format package.json.
Expand Down Expand Up @@ -265,6 +355,24 @@ Generated by [AVA](https://avajs.dev).
},
}

## run `cli --quiet=value`

> Should report illegal argument and suggest help.

{
args: [
'--quiet=value',
],
fixtures: [],
result: {
errorCode: 2,
stderr: `Option '-q, --quiet' does not take an argument␊
Try 'sort-package-json --help' for more information.␊
`,
stdout: '',
},
}

## run `cli -q` with no patterns

> Should support -q alias.
Expand Down Expand Up @@ -325,6 +433,24 @@ Generated by [AVA](https://avajs.dev).
},
}

## run `cli --check=value`

> Should report illegal argument and suggest help.

{
args: [
'--check=value',
],
fixtures: [],
result: {
errorCode: 2,
stderr: `Option '-c, --check' does not take an argument␊
Try 'sort-package-json --help' for more information.␊
`,
stdout: '',
},
}

## run `cli --check --quiet` with no patterns

> Should not sort package.json or report a message.
Expand Down Expand Up @@ -415,6 +541,34 @@ Generated by [AVA](https://avajs.dev).
},
}

## run `cli -cq` with no patterns

> Should support option aggregation

{
args: [
'-cq',
],
fixtures: [
{
expect: `{␊
"version": "1.0.0",␊
"name": "sort-package-json"␊
}`,
file: 'package.json',
original: `{␊
"version": "1.0.0",␊
"name": "sort-package-json"␊
}`,
},
],
result: {
errorCode: 1,
stderr: '',
stdout: '',
},
}

## run `cli` on 1 bad file

> Should format 1 file.
Expand Down
Binary file modified tests/snapshots/cli.js.snap
Binary file not shown.