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
103 changes: 103 additions & 0 deletions generate-help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'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 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 buildFlagLines(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),
description: def.description || ''
};
if (typeof def === 'object' && def !== null && 'default' in def) {
entry.description += ` [default: ${def.default}]`;
}

entry.description = entry.description.trim();

return entry;
});

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

const lines = entries.map(({name, description}) => {
if (!description) {
return name;
}

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

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

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

return `${name}${padding}${firstLine}\n${restLines}`;
});

return lines;
}

module.exports = (options, defaultDescription) => {
let lines = [];

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

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

let flagLines;

const {help} = options;
if (typeof help === 'string' && help.length > 0) {
if (lines.length > 0) {
lines.push('');
}

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

flagLines = buildFlagLines(options.flags);
lines.push('Options:');
lines.push(flagLines.map(line => redent(line, 2)).join('\n'));
}

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

const content = lines.join('\n').trimEnd();
const wholeText = '\n' + trimNewlines(redent(content, 2)) + '\n';

if (typeof help === 'function') {
return help({wholeText, description, flagLines, flagOptions: options});
}

return wholeText;
};
35 changes: 34 additions & 1 deletion 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 All @@ -28,6 +29,37 @@ declare namespace meow {
type AnyFlag = StringFlag | BooleanFlag | NumberFlag;
type AnyFlags = {[key: string]: AnyFlag};

interface GenerateHelpOptions {
/**
A whole help text generated by default.
Copy link
Owner

Choose a reason for hiding this comment

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

Typo

*/
wholeText: string;

/**
A description of a command generated by default.
*/
description: string;

/**
A list of a line including each flag's name and description.
Copy link
Owner

Choose a reason for hiding this comment

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

Typo

*/
flagLines: readonly string[];

/**
An object including each flag information.
Copy link
Owner

Choose a reason for hiding this comment

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

Not clear to the user what "flag information" means.

*/
flagOptions: Readonly<Options<AnyFlags>>;
}

/**
Callback function to customize a help text you want.
Copy link
Owner

Choose a reason for hiding this comment

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

Typos


@param options - Options to help you customize.

@returns A help text to be shown in a console.
*/
type GenerateHelp = (options: Readonly<GenerateHelpOptions>) => string;

interface Options<Flags extends AnyFlags> {
/**
Define argument flags.
Expand All @@ -48,6 +80,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 @@ -78,7 +111,7 @@ declare namespace meow {

Set it to `false` to disable it altogether.
*/
readonly help?: string | false;
readonly help?: string | false | GenerateHelp;

/**
Set a custom version output. Default: The package.json `"version"` property.
Expand Down
11 changes: 2 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 @@ -133,18 +132,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.description);

const showHelp = code => {
console.log(help);
Expand Down
14 changes: 12 additions & 2 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {expectAssignable, expectType} from 'tsd';
import {PackageJson} from 'type-fest';
import meow = require('.');
import {Result} from '.';
import {Result, Options, AnyFlags} from '.';

expectType<Result<never>>(meow('Help text'));
expectType<Result<never>>(meow('Help text', {hardRejection: false}));
Expand Down Expand Up @@ -43,7 +43,7 @@ const result = meow('Help text', {
flags: {
foo: {type: 'boolean', alias: 'f'},
'foo-bar': {type: 'number'},
bar: {type: 'string', default: ''},
bar: {type: 'string', default: '', description: 'This is bar'},
abc: {type: 'string', isMultiple: true}
}
});
Expand All @@ -65,3 +65,13 @@ expectType<string[] | undefined>(result.unnormalizedFlags.abc);
result.showHelp();
result.showHelp(1);
result.showVersion();

meow({
help: ({wholeText, description, flagLines, flagOptions}) => {
expectType<string>(wholeText);
expectType<string>(description);
expectType<readonly string[]>(flagLines);
expectType<Readonly<Options<AnyFlags>>>(flagOptions);
return 'help text';
}
});
21 changes: 18 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Returns an `object` with:
- `flags` *(Object)* - Flags converted to camelCase excluding aliases
- `unnormalizedFlags` *(Object)* - Flags converted to camelCase including aliases
- `pkg` *(Object)* - The `package.json` object
- `help` *(string)* - The help text used with `--help`
- `help` *(string | boolean | Function)* - The help text used with `--help`
- `showHelp([exitCode=2])` *(Function)* - Show the help text and exit with `exitCode`
- `showVersion()` *(Function)* - Show the version text and exit

Expand All @@ -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 required 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 All @@ -175,14 +177,27 @@ Set it to `false` to disable it altogether.

##### help

Type: `string | boolean`
Type: `string | boolean | Function`

The help text you want shown.
The help text you want to show.

The input is reindented and starting/ending newlines are trimmed which means you can use a [template literal](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/template_strings) without having to care about using the correct amount of indent.

The description will be shown above your help text automatically.

Also, you can customize the auto-generated help text by giving the function as follows:
Copy link
Owner

Choose a reason for hiding this comment

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

The readme and index.d.ts should be in sync.


```js
meow({
help: ({wholeText, flagLines, description, options}) => {
return 'A help text you want...';
},
flags: {
rainbow: {type: 'boolean', alias: 'r'}
}
});
```

##### version

Type: `string | boolean`\
Expand Down
Loading