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

Generate help by flag descriptions #140

Closed
wants to merge 15 commits into from
93 changes: 93 additions & 0 deletions generate-help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict';
Copy link
Owner

Choose a reason for hiding this comment

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

This file needs to be added to files in package.json.

const {EOL} = require('os');
const decamelizeKeys = require('decamelize-keys');
const trimNewlines = require('trim-newlines');
const redent = require('redent');

function flagName(name, alias, type) {
let result = `--${name}`;
if (alias) {
result += `, -${alias}`;
}

if (type && type !== 'boolean') {
result += ` <${type}>`;
}

return result;
}

function flagsSection(flags) {
flags = {...flags, help: {type: 'boolean', description: 'Show help'}};

const entries = Object.entries(decamelizeKeys(flags, '-')).map(([name, def]) => {
Copy link
Owner

Choose a reason for hiding this comment

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

Don't use abbreviations (regarding def, and other cases).

const type = def.type || def;

const entry = {
name: flagName(name, def.alias, type),
desc: def.description || ''
};
if (typeof def === 'object' && def !== null && 'default' in def) {
entry.desc += ` [default: ${def.default}]`;
}

entry.desc = entry.desc.trim();

return entry;
});

const maxNameLengh = Math.max(...entries.map(({name}) => name.length));

const lines = entries.map(({name, desc}) => {
Copy link
Owner

Choose a reason for hiding this comment

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

Don't use abbreviations for variable/parameter names.

if (!desc) {
return name;
}

const spaces = 4;
const padding = ' '.repeat(maxNameLengh - name.length + spaces);

let [firstLine, ...restLines] = desc.split(/\r?\n/);
if (restLines.length === 0) {
return `${name}${padding}${firstLine}`;
}

const fullPadding = ' '.repeat(maxNameLengh + spaces);
restLines = restLines.map(line => fullPadding + line).join(EOL);

return `${name}${padding}${firstLine}${EOL}${restLines}`;
});

return lines;
}

module.exports = function ({description, help, flags}, pkg) {
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
module.exports = function ({description, help, flags}, pkg) {
module.exports = ({description, help, flags}, pkg) => {

let lines = [];

if (!description && description !== false) {
description = pkg.description;
}

if (description) {
lines.push(description);
}

if (help) {
if (lines.length > 0) {
lines.push('');
}

lines.push(help);
} else {
if (lines.length > 0) {
lines.push('');
}

lines.push('Options:');
lines.push(...flagsSection(flags).map(line => redent(line, 2)));
}

lines = lines.map(line => trimNewlines(line));

const content = lines.join(EOL).replace(/^\t+/gm, '').replace(/[\t ]+[\r\n]*$/gm, '');
Copy link
Owner

Choose a reason for hiding this comment

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

Instead of the regexes here, couldn't you use redent?

return EOL + trimNewlines(redent(content, 2)) + EOL;
Copy link
Owner

Choose a reason for hiding this comment

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

Use \n instead of EOL

};
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ declare namespace meow {
readonly type?: Type;
readonly alias?: string;
readonly default?: Default;
readonly description?: string;
readonly isRequired?: boolean | IsRequiredPredicate;
readonly isMultiple?: boolean;
}
Expand Down Expand Up @@ -47,6 +48,7 @@ declare namespace meow {
type: 'string',
alias: 'u',
default: ['rainbow', 'cat'],
description: 'This is an unicorn option'
isMultiple: true,
isRequired: (flags, input) => {
if (flags.otherFlag) {
Expand Down Expand Up @@ -79,6 +81,11 @@ declare namespace meow {
*/
readonly help?: string | false;

/**
Whether show the help text with defined descriptions for options (flags). Default: `false`.
*/
readonly helpOptions?: boolean;

/**
Set a custom version output. Default: The package.json `"version"` property.

Expand Down
12 changes: 3 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ const buildParserOptions = require('minimist-options');
const parseArguments = require('yargs-parser');
const camelCaseKeys = require('camelcase-keys');
const decamelizeKeys = require('decamelize-keys');
const trimNewlines = require('trim-newlines');
const redent = require('redent');
const readPkgUp = require('read-pkg-up');
const hardRejection = require('hard-rejection');
const normalizePackageData = require('normalize-package-data');
const generateHelp = require('./generate-help');

// Prevent caching of this module so module.parent is always accurate
delete require.cache[__filename];
Expand Down Expand Up @@ -100,6 +99,7 @@ const meow = (helpText, options) => {
inferType: false,
input: 'string',
help: helpText,
helpOptions: false,
autoHelp: true,
autoVersion: true,
booleanDefault: false,
Expand Down Expand Up @@ -133,18 +133,12 @@ const meow = (helpText, options) => {

const {pkg} = options;
const argv = parseArguments(options.argv, parserOptions);
let help = redent(trimNewlines((options.help || '').replace(/\t+\n*$/, '')), 2);

normalizePackageData(pkg);

process.title = pkg.bin ? Object.keys(pkg.bin)[0] : pkg.name;

let {description} = options;
if (!description && description !== false) {
({description} = pkg);
}

help = (description ? `\n ${description}\n` : '') + (help ? `\n${help}\n` : '\n');
const help = generateHelp(options, pkg);

const showHelp = code => {
console.log(help);
Expand Down
3 changes: 2 additions & 1 deletion index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ expectType<Result<never>>(meow({booleanDefault: undefined}));
expectType<Result<never>>(meow({hardRejection: false}));

const result = meow('Help text', {
helpOptions: true,
flags: {
foo: {type: 'boolean', alias: 'f'},
'foo-bar': {type: 'number'},
bar: {type: 'string', default: ''}
bar: {type: 'string', default: '', description: 'This is bar'}
}
});

Expand Down
9 changes: 9 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ The key is the flag name and the value is an object with any of:
- `type`: Type of value. (Possible values: `string` `boolean` `number`)
- `alias`: Usually used to define a short flag alias.
- `default`: Default value when the flag is not specified.
- `description`: Description of the flag.
- `isRequired`: Determine if the flag is required. (Default: false)
- If it's only known at runtime whether the flag is requried or not, you can pass a `Function` instead of a `boolean`, which based on the given flags and other non-flag arguments, should decide if the flag is required. Two arguments are passed to the function:
- The first argument is the **flags** object, which contains the flags converted to camel-case excluding aliases.
Expand All @@ -152,6 +153,7 @@ flags: {
type: 'string',
alias: 'u',
default: ['rainbow', 'cat'],
description: 'This is an unicorn option'
isMultiple: true,
isRequired: (flags, input) => {
if (flags.otherFlag) {
Expand Down Expand Up @@ -183,6 +185,13 @@ The input is reindented and starting/ending newlines are trimmed which means you

The description will be shown above your help text automatically.

##### helpOptions

Type: `boolean`
Default: `false`

Whether show the help text with defined descriptions for options (flags).

##### version

Type: `string | boolean`\
Expand Down
156 changes: 156 additions & 0 deletions test/help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import test from 'ava';
import meow from '..';

const inputHelpText = `
Usage: unicorn [options] <file>

Example: unicorn path/to/file.js
`;

test('when no arguments and options', t => {
const cli = meow();
t.is(cli.help, `
CLI app helper

Options:
--help Show help
`);
});

test('when shortcut and no description', t => {
const cli = meow(inputHelpText);
t.is(cli.help, `
CLI app helper

Usage: unicorn [options] <file>

Example: unicorn path/to/file.js
`);
});

test('when no shortcut and description:false', t => {
const cli = meow({description: false});
t.is(cli.help, `
Options:
--help Show help
`);
});

test('when shortcut and description', t => {
const cli = meow(inputHelpText, {description: 'A command for unicorns'});
t.is(cli.help, `
A command for unicorns

Usage: unicorn [options] <file>

Example: unicorn path/to/file.js
`);
});

test('when shortcut and description:false', t => {
const cli = meow(inputHelpText, {description: false});
t.is(cli.help, `
Usage: unicorn [options] <file>

Example: unicorn path/to/file.js
`);
});

test('when help:<string> and no description', t => {
const cli = meow({help: inputHelpText});
t.is(cli.help, `
CLI app helper

Usage: unicorn [options] <file>

Example: unicorn path/to/file.js
`);
});

test('when help:<string> and description:false', t => {
const cli = meow({help: inputHelpText, description: false});
t.is(cli.help, `
Usage: unicorn [options] <file>

Example: unicorn path/to/file.js
`);
});

test('when help:<string> and description:<string>', t => {
const cli = meow({help: inputHelpText, description: 'A command for unicorns'});
t.is(cli.help, `
A command for unicorns

Usage: unicorn [options] <file>

Example: unicorn path/to/file.js
`);
});

test('when description and flags', t => {
const cli = meow({
description: inputHelpText,
flags: {
format: 'string',
output: {
type: 'string',
alias: 'o'
},
input: {
type: 'string',
default: 'stdin',
description: 'Input file path'
},
indent: {
type: 'number',
alias: 'i',
default: 2,
description: 'Indent level'
},
verbose: {
type: 'boolean',
default: false,
description: 'Turn on verbose mode'
},
longLongOption: {
type: 'string',
alias: 'llo',
default: 'none',
description: 'A long long option.\nThis is the second line.'
}
}
});
t.is(cli.help, `
Usage: unicorn [options] <file>

Example: unicorn path/to/file.js

Options:
--format <string>
--output, -o <string>
--input <string> Input file path [default: stdin]
--indent, -i <number> Indent level [default: 2]
--verbose Turn on verbose mode [default: false]
--long-long-option, -llo <string> A long long option.
This is the second line. [default: none]
--help Show help
`);
});

test('when no description and flags', t => {
const cli = meow({
flags: {
input: {
type: 'string',
description: 'Input file path'
}
}
});
t.is(cli.help, `
CLI app helper

Options:
--input <string> Input file path
--help Show help
`);
});
10 changes: 1 addition & 9 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,6 @@ test('return object', t => {
t.is(cli.help, indentString('\nCLI app helper\n\nUsage\n foo <input>\n', 2));
});

test('support help shortcut', t => {
const cli = meow(`
unicorn
cat
`);
t.is(cli.help, indentString('\nCLI app helper\n\nunicorn\ncat\n', 2));
});

sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
test('spawn cli and show version', async t => {
const {stdout} = await execa(fixturePath, ['--version']);
t.is(stdout, pkg.version);
Expand All @@ -61,7 +53,7 @@ test('spawn cli and not show version', async t => {

test('spawn cli and show help screen', async t => {
const {stdout} = await execa(fixturePath, ['--help']);
t.is(stdout, indentString('\nCustom description\n\nUsage\n foo <input>\n\n', 2));
t.is(stdout, indentString('\nCustom description\n\nUsage\n foo <input>\n', 2));
});

test('spawn cli and disabled autoHelp', async t => {
Expand Down