Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-commands",
"version": "2.0.0",
"version": "2.0.1",
"description": "ts-commands",
"private": "true",
"typescript-template": {
Expand Down
16 changes: 14 additions & 2 deletions src/argument-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,21 @@ export class ArgumentParser {
matches[option.key] = outputValue;
}

if (this.positionalIndex < this.command.positional.length) {
const requiredPositionals: CommandOption[] = [];

for (const positional of this.command.positional) {
/* istanbul ignore else */
if (positional.default === undefined) {
requiredPositionals.push(positional);
}
else if (matches[positional.key] === undefined) {
matches[positional.key] = positional.default;
}
}

if (this.positionalIndex < requiredPositionals.length) {
throw new ArgumentError(
`Missing positional arguments: ${this.command.positional
`Missing positional arguments: ${requiredPositionals
.slice(this.positionalIndex)
.map((o) => o.key)
.join(', ')}`
Expand Down
37 changes: 33 additions & 4 deletions src/command-help.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Command } from './command';
import { CommandOption, OptionType } from './command-options';

export class CommandHelp {
public constructor(public command: Command) {}
Expand Down Expand Up @@ -37,17 +38,45 @@ export class CommandHelp {

public showPositional() {
for (const positional of this.command.positional) {
console.log(` <${positional.key}>: ${positional.description}`);
this.showCommandOption(positional, true);
}
}

public showOptions() {
for (const option of this.command.options) {
const key = `--${option.key}`;
const alias = option.alias ? ` -${option.alias}` : '';
console.log(` ${key}${alias}: ${option.description}`);
this.showCommandOption(option);
}
}

public showCommandOption(option: CommandOption, isPositional = false) {
let key = '';

if (isPositional) {
key = `<${option.key}>`;
}
else {
if (option.alias) {
key = `-${option.alias}, `;
}

key += `--${option.key}`;
}

const type =
option.type && option.type !== OptionType.string
? ` {${option.type}}`
: '';
const defaultValue =
option.default !== undefined ? ` (default: ${option.default})` : '';
const choices = option.choices
? ` [choices: ${option.choices.join(', ')}]`
: '';
const description = option.description ?? '';
const metadata = `${type}${defaultValue}${choices}`;
const fullDescription = `${description}${metadata}`;

console.log(` ${key}: ${fullDescription}`);
}

public footer() {}
}
11 changes: 8 additions & 3 deletions src/command-options.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ArgumentValue } from './argument-parser';

export enum OptionType {
boolean,
number,
string,
boolean = 'bool',
number = 'number',
string = 'string',
}

export interface CommandOption {
Expand All @@ -14,3 +14,8 @@ export interface CommandOption {
default?: ArgumentValue;
choices?: number[] | string[];
}

export const defaultCommandOptions: Partial<CommandOption> = {
type: OptionType.string,
description: '',
};
1 change: 1 addition & 0 deletions src/command-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class CommandRunner {
protected async handle(
parsedArgs: ParsedArguments
): Promise<undefined | number> {
this.command.init();
return <undefined | number>await this.command.handle(parsedArgs);
}
}
19 changes: 18 additions & 1 deletion src/command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ParsedArguments } from './argument-parser';
import { CommandHelp } from './command-help';
import { CommandOption } from './command-options';
import { CommandOption, defaultCommandOptions } from './command-options';

export abstract class Command {
public key: string;
Expand All @@ -11,9 +11,26 @@ export abstract class Command {

public commandHelp = new CommandHelp(this);

public init() {
for (let i = 0; i < this.positional.length; i++) {
this.positional[i] = {
...defaultCommandOptions,
...this.positional[i],
};
}

for (let i = 0; i < this.options.length; i++) {
this.options[i] = {
...defaultCommandOptions,
...this.options[i],
};
}
}

public abstract handle(args: ParsedArguments): Promise<void | number>;

public help() {
this.init();
this.commandHelp.help();
}

Expand Down
15 changes: 15 additions & 0 deletions test/spec/argument-parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,21 @@ describe('ArgumentParser', () => {
);
});

it('allows optional positional options', () => {
command.positional = [
{ key: 'filename' },
{ key: 'optional', default: 'default.txt' },
];
command.init();

const result = parser.parse(['input.txt']);

expect(result).toEqual({
filename: 'input.txt',
optional: 'default.txt',
});
});

it('throws an error for invalid option value', () => {
const args = ['--verbose=invalid.txt', 'input.txt'];
expect(() => parser.parse(args)).toThrowError(
Expand Down
85 changes: 82 additions & 3 deletions test/spec/command-help.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { MockConsole } from 'ts-jasmine-spies';
import { MockCommand } from '../mock/mock-command';
import { CommandHelp } from '../../src/command-help';
import { CommandOption, OptionType } from '../../src/command-options';

class HelpTestCommand extends MockCommand {
override key = 'test';
override description = 'Test command description';

override positional = [
override positional: CommandOption[] = [
{ key: 'arg1', description: 'First argument' },
{ key: 'arg2', description: 'Second argument' },
];

override options = [
override options: CommandOption[] = [
{ key: 'opt1', description: 'First option', alias: 'o' },
{ key: 'opt2', description: 'Second option', alias: undefined },
];
Expand Down Expand Up @@ -53,7 +54,85 @@ describe('CommandHelp', () => {
it('can show optional arguments', () => {
commandHelp.showOptions();
mockConsole.expectStdout(
' --opt1 -o: First option\n --opt2: Second option\n'
' -o, --opt1: First option\n --opt2: Second option\n'
);
});

it('shows option default value', () => {
mockCommand.options = [
{
key: 'opt1',
description: 'First option',
default: 'defaultValue',
},
];

commandHelp.showOptions();
mockConsole.expectStdout(
' --opt1: First option (default: defaultValue)\n'
);
});

it('can show option without description', () => {
mockCommand.options = [{ key: 'opt1', default: 'defaultValue' }];
commandHelp.showOptions();
mockConsole.expectStdout(' --opt1: (default: defaultValue)\n');
});

it('shows option choices', () => {
mockCommand.options = [
{
key: 'opt1',
description: 'First option',
choices: ['choice1', 'choice2'],
},
];

commandHelp.showOptions();
mockConsole.expectStdout(
' --opt1: First option [choices: choice1, choice2]\n'
);
});

it('shows option type', () => {
mockCommand.options = [
{
key: 'opt1',
description: 'First option',
type: OptionType.number,
},
];

commandHelp.showOptions();
mockConsole.expectStdout(' --opt1: First option {number}\n');
});

it('shows skips option type for strings', () => {
mockCommand.options = [
{
key: 'opt1',
description: 'First option',
},
];

commandHelp.showOptions();
mockConsole.expectStdout(' --opt1: First option\n');
});

it('shows default value and choices', () => {
mockCommand.options = [
{
key: 'opt1',
description: 'First option',
default: 'defaultValue',
choices: ['choice1', 'choice2'],
type: OptionType.string,
},
];
commandHelp.showOptions();
mockConsole.expectStdout(
' --opt1: First option (default: defaultValue)' +
' [choices: choice1, choice2]\n'
);
});

Expand Down
21 changes: 21 additions & 0 deletions test/spec/command.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'jasmine';
import { MockConsole } from 'ts-jasmine-spies';
import { MockCommand } from '../mock/mock-command';
import { CommandOption, OptionType } from '../../src/command-options';

describe('Command', () => {
let mockConsole: MockConsole;
Expand All @@ -17,6 +18,26 @@ describe('Command', () => {
expect(command.options).toEqual([]);
});

it('should init positional and options with default values', () => {
const command = new MockCommand();
command.positional = [{ key: 'arg1' }];
command.options = [{ key: 'opt1' }];

command.init();

expect(command.positional[0]).toEqual(<CommandOption>{
key: 'arg1',
type: OptionType.string,
description: '',
});

expect(command.options[0]).toEqual(<CommandOption>{
key: 'opt1',
type: OptionType.string,
description: '',
});
});

it('should log help information', () => {
const command = new MockCommand();
command.key = 'test-command';
Expand Down