From 8ab7e02ad413e7ce1ba9945d81e845d6bbd916b6 Mon Sep 17 00:00:00 2001 From: Drew Immerman <30954849+DrewImm@users.noreply.github.com> Date: Sun, 30 Mar 2025 20:16:04 -0400 Subject: [PATCH 1/5] #15 Default option values aren't loaded (#17) --- src/command-options.ts | 5 +++++ src/command-runner.ts | 1 + src/command.ts | 19 ++++++++++++++++++- test/spec/command.spec.ts | 21 +++++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/command-options.ts b/src/command-options.ts index b7f6916..0a77e1d 100644 --- a/src/command-options.ts +++ b/src/command-options.ts @@ -14,3 +14,8 @@ export interface CommandOption { default?: ArgumentValue; choices?: number[] | string[]; } + +export const defaultCommandOptions: Partial = { + type: OptionType.string, + description: '', +}; diff --git a/src/command-runner.ts b/src/command-runner.ts index 168ca0b..7a24d3d 100644 --- a/src/command-runner.ts +++ b/src/command-runner.ts @@ -42,6 +42,7 @@ export class CommandRunner { protected async handle( parsedArgs: ParsedArguments ): Promise { + this.command.init(); return await this.command.handle(parsedArgs); } } diff --git a/src/command.ts b/src/command.ts index 2e78c23..7f81037 100644 --- a/src/command.ts +++ b/src/command.ts @@ -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; @@ -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; public help() { + this.init(); this.commandHelp.help(); } diff --git a/test/spec/command.spec.ts b/test/spec/command.spec.ts index 8dca82f..5b314c6 100644 --- a/test/spec/command.spec.ts +++ b/test/spec/command.spec.ts @@ -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; @@ -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({ + key: 'arg1', + type: OptionType.string, + description: '', + }); + + expect(command.options[0]).toEqual({ + key: 'opt1', + type: OptionType.string, + description: '', + }); + }); + it('should log help information', () => { const command = new MockCommand(); command.key = 'test-command'; From 501961f994edf66ace897dacf9e4b3b1fd84c845 Mon Sep 17 00:00:00 2001 From: Drew Immerman <30954849+DrewImm@users.noreply.github.com> Date: Sun, 30 Mar 2025 20:21:32 -0400 Subject: [PATCH 2/5] #16 Command help should show type, default value & choices (#19) --- src/command-help.ts | 37 +++++++++++++-- src/command-options.ts | 6 +-- test/spec/command-help.spec.ts | 85 ++++++++++++++++++++++++++++++++-- 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/src/command-help.ts b/src/command-help.ts index 0ff6a2c..94ddc06 100644 --- a/src/command-help.ts +++ b/src/command-help.ts @@ -1,4 +1,5 @@ import { Command } from './command'; +import { CommandOption, OptionType } from './command-options'; export class CommandHelp { public constructor(public command: Command) {} @@ -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() {} } diff --git a/src/command-options.ts b/src/command-options.ts index 0a77e1d..96e8344 100644 --- a/src/command-options.ts +++ b/src/command-options.ts @@ -1,9 +1,9 @@ import { ArgumentValue } from './argument-parser'; export enum OptionType { - boolean, - number, - string, + boolean = 'bool', + number = 'number', + string = 'string', } export interface CommandOption { diff --git a/test/spec/command-help.spec.ts b/test/spec/command-help.spec.ts index 1e0c18b..b304e8c 100644 --- a/test/spec/command-help.spec.ts +++ b/test/spec/command-help.spec.ts @@ -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 }, ]; @@ -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' ); }); From 153126579861da556ff2a4d49da53652c82e18c5 Mon Sep 17 00:00:00 2001 From: Drew Immerman <30954849+DrewImm@users.noreply.github.com> Date: Sun, 30 Mar 2025 20:29:59 -0400 Subject: [PATCH 3/5] #18 Params with default values should not fail validation (#20) --- src/argument-parser.ts | 15 +++++++++++++-- test/spec/argument-parser.spec.ts | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/argument-parser.ts b/src/argument-parser.ts index a43ef2a..1542141 100644 --- a/src/argument-parser.ts +++ b/src/argument-parser.ts @@ -52,9 +52,20 @@ export class ArgumentParser { matches[option.key] = outputValue; } - if (this.positionalIndex < this.command.positional.length) { + const requiredPositionals: CommandOption[] = []; + + for (const positional of this.command.positional) { + 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(', ')}` diff --git a/test/spec/argument-parser.spec.ts b/test/spec/argument-parser.spec.ts index 5111084..b22a3b3 100644 --- a/test/spec/argument-parser.spec.ts +++ b/test/spec/argument-parser.spec.ts @@ -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( From 30aab6e47a15318c1d1223132c24c58a640b3d2e Mon Sep 17 00:00:00 2001 From: Drew Immerman Date: Sun, 30 Mar 2025 20:32:10 -0400 Subject: [PATCH 4/5] Ignore invalid coverage warning --- src/argument-parser.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/argument-parser.ts b/src/argument-parser.ts index 1542141..08a5c56 100644 --- a/src/argument-parser.ts +++ b/src/argument-parser.ts @@ -55,6 +55,7 @@ export class ArgumentParser { const requiredPositionals: CommandOption[] = []; for (const positional of this.command.positional) { + /* istanbul ignore else */ if (positional.default === undefined) { requiredPositionals.push(positional); } From 3ad1ef59b3c1decaaa30984729620176f99e98da Mon Sep 17 00:00:00 2001 From: Drew Immerman Date: Sun, 30 Mar 2025 20:32:39 -0400 Subject: [PATCH 5/5] v2.0.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f6e3e06..2660d0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ts-commands", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ts-commands", - "version": "2.0.0", + "version": "2.0.1", "license": "MIT", "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", diff --git a/package.json b/package.json index 6cbdc82..6bcc8d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-commands", - "version": "2.0.0", + "version": "2.0.1", "description": "ts-commands", "private": "true", "typescript-template": {