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

};
59 changes: 2 additions & 57 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 @@ -134,66 +133,12 @@ const meow = (helpText, options) => {

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

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');

if (options.helpOptions) {
const cliOption = name => name.length === 1 ? `-${name}` : `--${name}`;
const indent = string => string ? (' '.repeat(indentSize) + string) : string;

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(line => indent(line)), ''];
});
helpOptions = [].concat(...helpOptions); // Flatten
helpOptions = ['Options:', ...helpOptions.map(opt => indent(opt))].map(line => indent(line));

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

const showHelp = code => {
console.log(help);
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
`);
});
Loading