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
9 changes: 8 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ declare namespace meow {
readonly type?: Type;
readonly alias?: string;
readonly default?: Default;
readonly description?: string;
}

type StringFlag = Flag<'string', string>;
Expand All @@ -31,7 +32,8 @@ declare namespace meow {
unicorn: {
type: 'string',
alias: 'u',
default: 'rainbow'
default: 'rainbow',
description: 'This is an unicorn option'
}
}
```
Expand All @@ -56,6 +58,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
51 changes: 50 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const meow = (helpText, options) => {
inferType: false,
input: 'string',
help: helpText,
helpOptions: false,
autoHelp: true,
autoVersion: true,
booleanDefault: false,
Expand Down Expand Up @@ -73,7 +74,8 @@ const meow = (helpText, options) => {

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

normalizePackageData(pkg);

Expand All @@ -86,6 +88,53 @@ const meow = (helpText, options) => {

help = (description ? `\n ${description}\n` : '') + (help ? `\n${help}\n` : '\n');

if (options.helpOptions) {
ybiquitous marked this conversation as resolved.
Show resolved Hide resolved
const cliOption = name => name.length === 1 ? `-${name}` : `--${name}`;
const indent = str => str ? (' '.repeat(indentSize) + str) : str;

let helpOptions = Object.entries(decamelizeKeys(options.flags, '-')).map(([name, definition]) => {
const type = definition.type || definition;
const {alias, default: defaultValue, description} = definition;

let firstLine = '';
switch (type) {
case 'boolean':
if (alias) {
firstLine = `${cliOption(alias)}, `;
}

firstLine += cliOption(name);
break;
case 'number':
case 'string':
if (alias) {
firstLine = `${cliOption(alias)} <${type}>, `;
}

firstLine += `${cliOption(name)} <${type}>`;
break;
default:
throw new Error(`Unexpected flag type: '${type}'`);
}

if (defaultValue !== null && defaultValue !== undefined) {
firstLine += ` (default: ${defaultValue})`;
}

const descLines = [];
if (description) {
descLines.push(...description.split(/\r?\n/));
}

return [firstLine, ...descLines.map(indent), ''];
});
helpOptions = [].concat(...helpOptions); // Flatten
helpOptions = ['Options:', ...helpOptions.map(indent)].map(indent);

help = help.replace(/\n+$/, '\n'); // Trim end
help += '\n' + helpOptions.join('\n');
}

const showHelp = code => {
console.log(help);
process.exit(typeof code === 'number' ? code : 2);
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
11 changes: 10 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,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.

Example:

Expand All @@ -105,7 +106,8 @@ flags: {
unicorn: {
type: 'string',
alias: 'u',
default: 'rainbow'
default: 'rainbow',
description: 'This is an unicorn option'
}
}
```
Expand All @@ -129,6 +131,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
100 changes: 100 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,106 @@ test('support help shortcut', t => {
t.is(cli.help, indentString('\nCLI app helper\n\nunicorn\ncat\n', 2));
});

test('help with no description', t => {
const cli = meow({description: false});
t.is(cli.help, '\n');
});

test('help with a custom message and description', t => {
const cli = meow('Unicorn', {description: 'Cat'});
t.is(cli.help, '\n Cat\n\n Unicorn\n');
});

test('help with a custom message and no description', t => {
const cli = meow('Unicorn', {description: false});
t.is(cli.help, '\n Unicorn\n');
});

test('disabled help options', t => {
const cli = meow('Unicorn', {
helpOptions: false,
flags: {
foo: {type: 'string', description: 'Foo'},
bar: {type: 'number', description: 'Bar'}
}
});
t.is(cli.help, '\n CLI app helper\n\n Unicorn\n');
});

test('auto-generated help options', t => {
const cli = meow({
helpOptions: true,
flags: {
foo: 'string',
output: {
type: 'string'
},
input: {
type: 'string',
default: 'stdin'
},
indent: {
type: 'number',
alias: 'i',
default: 2,
description: 'Indent level'
},
enabled: {
type: 'boolean',
default: false,
description: 'Enabled or not'
},
longWord: {
type: 'string',
alias: 'lw',
default: 'none',
description: 'Very very long long word.\nThis is the second line.'
}
}
});
t.is(cli.help, `
CLI app helper

Options:
--foo <string>

--output <string>

--input <string> (default: stdin)
ybiquitous marked this conversation as resolved.
Show resolved Hide resolved

-i <number>, --indent <number> (default: 2)
ybiquitous marked this conversation as resolved.
Show resolved Hide resolved
Indent level

--enabled (default: false)
Enabled or not

--lw <string>, --long-word <string> (default: none)
Very very long long word.
This is the second line.
`);
});

test('auto-generated help options without a description', t => {
const cli = meow({description: false, helpOptions: true, flags: {a: 'string'}});
t.is(cli.help, `

Options:
-a <string>
`);
});

test('auto-generated help options with a help message', t => {
const cli = meow({help: 'Usage: foo [options]', helpOptions: true, flags: {a: 'string'}});
t.is(cli.help, `
CLI app helper

Usage: foo [options]

Options:
-a <string>
`);
});

test('spawn cli and show version', async t => {
const {stdout} = await execa('./fixture.js', ['--version']);
t.is(stdout, pkg.version);
Expand Down