Skip to content

Commit b0c3813

Browse files
authored
Merge pull request #21 from StatelessStudio/v2.0.1
V2.0.1
2 parents a8b13f9 + 3ad1ef5 commit b0c3813

File tree

10 files changed

+195
-16
lines changed

10 files changed

+195
-16
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ts-commands",
3-
"version": "2.0.0",
3+
"version": "2.0.1",
44
"description": "ts-commands",
55
"private": "true",
66
"typescript-template": {

src/argument-parser.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,21 @@ export class ArgumentParser {
5252
matches[option.key] = outputValue;
5353
}
5454

55-
if (this.positionalIndex < this.command.positional.length) {
55+
const requiredPositionals: CommandOption[] = [];
56+
57+
for (const positional of this.command.positional) {
58+
/* istanbul ignore else */
59+
if (positional.default === undefined) {
60+
requiredPositionals.push(positional);
61+
}
62+
else if (matches[positional.key] === undefined) {
63+
matches[positional.key] = positional.default;
64+
}
65+
}
66+
67+
if (this.positionalIndex < requiredPositionals.length) {
5668
throw new ArgumentError(
57-
`Missing positional arguments: ${this.command.positional
69+
`Missing positional arguments: ${requiredPositionals
5870
.slice(this.positionalIndex)
5971
.map((o) => o.key)
6072
.join(', ')}`

src/command-help.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Command } from './command';
2+
import { CommandOption, OptionType } from './command-options';
23

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

3839
public showPositional() {
3940
for (const positional of this.command.positional) {
40-
console.log(` <${positional.key}>: ${positional.description}`);
41+
this.showCommandOption(positional, true);
4142
}
4243
}
4344

4445
public showOptions() {
4546
for (const option of this.command.options) {
46-
const key = `--${option.key}`;
47-
const alias = option.alias ? ` -${option.alias}` : '';
48-
console.log(` ${key}${alias}: ${option.description}`);
47+
this.showCommandOption(option);
4948
}
5049
}
5150

51+
public showCommandOption(option: CommandOption, isPositional = false) {
52+
let key = '';
53+
54+
if (isPositional) {
55+
key = `<${option.key}>`;
56+
}
57+
else {
58+
if (option.alias) {
59+
key = `-${option.alias}, `;
60+
}
61+
62+
key += `--${option.key}`;
63+
}
64+
65+
const type =
66+
option.type && option.type !== OptionType.string
67+
? ` {${option.type}}`
68+
: '';
69+
const defaultValue =
70+
option.default !== undefined ? ` (default: ${option.default})` : '';
71+
const choices = option.choices
72+
? ` [choices: ${option.choices.join(', ')}]`
73+
: '';
74+
const description = option.description ?? '';
75+
const metadata = `${type}${defaultValue}${choices}`;
76+
const fullDescription = `${description}${metadata}`;
77+
78+
console.log(` ${key}: ${fullDescription}`);
79+
}
80+
5281
public footer() {}
5382
}

src/command-options.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { ArgumentValue } from './argument-parser';
22

33
export enum OptionType {
4-
boolean,
5-
number,
6-
string,
4+
boolean = 'bool',
5+
number = 'number',
6+
string = 'string',
77
}
88

99
export interface CommandOption {
@@ -14,3 +14,8 @@ export interface CommandOption {
1414
default?: ArgumentValue;
1515
choices?: number[] | string[];
1616
}
17+
18+
export const defaultCommandOptions: Partial<CommandOption> = {
19+
type: OptionType.string,
20+
description: '',
21+
};

src/command-runner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export class CommandRunner {
4242
protected async handle(
4343
parsedArgs: ParsedArguments
4444
): Promise<undefined | number> {
45+
this.command.init();
4546
return <undefined | number>await this.command.handle(parsedArgs);
4647
}
4748
}

src/command.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ParsedArguments } from './argument-parser';
22
import { CommandHelp } from './command-help';
3-
import { CommandOption } from './command-options';
3+
import { CommandOption, defaultCommandOptions } from './command-options';
44

55
export abstract class Command {
66
public key: string;
@@ -11,9 +11,26 @@ export abstract class Command {
1111

1212
public commandHelp = new CommandHelp(this);
1313

14+
public init() {
15+
for (let i = 0; i < this.positional.length; i++) {
16+
this.positional[i] = {
17+
...defaultCommandOptions,
18+
...this.positional[i],
19+
};
20+
}
21+
22+
for (let i = 0; i < this.options.length; i++) {
23+
this.options[i] = {
24+
...defaultCommandOptions,
25+
...this.options[i],
26+
};
27+
}
28+
}
29+
1430
public abstract handle(args: ParsedArguments): Promise<void | number>;
1531

1632
public help() {
33+
this.init();
1734
this.commandHelp.help();
1835
}
1936

test/spec/argument-parser.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,21 @@ describe('ArgumentParser', () => {
147147
);
148148
});
149149

150+
it('allows optional positional options', () => {
151+
command.positional = [
152+
{ key: 'filename' },
153+
{ key: 'optional', default: 'default.txt' },
154+
];
155+
command.init();
156+
157+
const result = parser.parse(['input.txt']);
158+
159+
expect(result).toEqual({
160+
filename: 'input.txt',
161+
optional: 'default.txt',
162+
});
163+
});
164+
150165
it('throws an error for invalid option value', () => {
151166
const args = ['--verbose=invalid.txt', 'input.txt'];
152167
expect(() => parser.parse(args)).toThrowError(

test/spec/command-help.spec.ts

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import { MockConsole } from 'ts-jasmine-spies';
22
import { MockCommand } from '../mock/mock-command';
33
import { CommandHelp } from '../../src/command-help';
4+
import { CommandOption, OptionType } from '../../src/command-options';
45

56
class HelpTestCommand extends MockCommand {
67
override key = 'test';
78
override description = 'Test command description';
89

9-
override positional = [
10+
override positional: CommandOption[] = [
1011
{ key: 'arg1', description: 'First argument' },
1112
{ key: 'arg2', description: 'Second argument' },
1213
];
1314

14-
override options = [
15+
override options: CommandOption[] = [
1516
{ key: 'opt1', description: 'First option', alias: 'o' },
1617
{ key: 'opt2', description: 'Second option', alias: undefined },
1718
];
@@ -53,7 +54,85 @@ describe('CommandHelp', () => {
5354
it('can show optional arguments', () => {
5455
commandHelp.showOptions();
5556
mockConsole.expectStdout(
56-
' --opt1 -o: First option\n --opt2: Second option\n'
57+
' -o, --opt1: First option\n --opt2: Second option\n'
58+
);
59+
});
60+
61+
it('shows option default value', () => {
62+
mockCommand.options = [
63+
{
64+
key: 'opt1',
65+
description: 'First option',
66+
default: 'defaultValue',
67+
},
68+
];
69+
70+
commandHelp.showOptions();
71+
mockConsole.expectStdout(
72+
' --opt1: First option (default: defaultValue)\n'
73+
);
74+
});
75+
76+
it('can show option without description', () => {
77+
mockCommand.options = [{ key: 'opt1', default: 'defaultValue' }];
78+
commandHelp.showOptions();
79+
mockConsole.expectStdout(' --opt1: (default: defaultValue)\n');
80+
});
81+
82+
it('shows option choices', () => {
83+
mockCommand.options = [
84+
{
85+
key: 'opt1',
86+
description: 'First option',
87+
choices: ['choice1', 'choice2'],
88+
},
89+
];
90+
91+
commandHelp.showOptions();
92+
mockConsole.expectStdout(
93+
' --opt1: First option [choices: choice1, choice2]\n'
94+
);
95+
});
96+
97+
it('shows option type', () => {
98+
mockCommand.options = [
99+
{
100+
key: 'opt1',
101+
description: 'First option',
102+
type: OptionType.number,
103+
},
104+
];
105+
106+
commandHelp.showOptions();
107+
mockConsole.expectStdout(' --opt1: First option {number}\n');
108+
});
109+
110+
it('shows skips option type for strings', () => {
111+
mockCommand.options = [
112+
{
113+
key: 'opt1',
114+
description: 'First option',
115+
},
116+
];
117+
118+
commandHelp.showOptions();
119+
mockConsole.expectStdout(' --opt1: First option\n');
120+
});
121+
122+
it('shows default value and choices', () => {
123+
mockCommand.options = [
124+
{
125+
key: 'opt1',
126+
description: 'First option',
127+
default: 'defaultValue',
128+
choices: ['choice1', 'choice2'],
129+
type: OptionType.string,
130+
},
131+
];
132+
commandHelp.showOptions();
133+
mockConsole.expectStdout(
134+
' --opt1: First option (default: defaultValue)' +
135+
' [choices: choice1, choice2]\n'
57136
);
58137
});
59138

test/spec/command.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'jasmine';
22
import { MockConsole } from 'ts-jasmine-spies';
33
import { MockCommand } from '../mock/mock-command';
4+
import { CommandOption, OptionType } from '../../src/command-options';
45

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

21+
it('should init positional and options with default values', () => {
22+
const command = new MockCommand();
23+
command.positional = [{ key: 'arg1' }];
24+
command.options = [{ key: 'opt1' }];
25+
26+
command.init();
27+
28+
expect(command.positional[0]).toEqual(<CommandOption>{
29+
key: 'arg1',
30+
type: OptionType.string,
31+
description: '',
32+
});
33+
34+
expect(command.options[0]).toEqual(<CommandOption>{
35+
key: 'opt1',
36+
type: OptionType.string,
37+
description: '',
38+
});
39+
});
40+
2041
it('should log help information', () => {
2142
const command = new MockCommand();
2243
command.key = 'test-command';

0 commit comments

Comments
 (0)