diff --git a/src/args.ts b/src/args.ts index 3e37952..0fe2f34 100644 --- a/src/args.ts +++ b/src/args.ts @@ -1,6 +1,10 @@ import type { GlobalFlags } from './types/flags'; import type { OptionDef } from './command'; +/** Recognised spellings for an explicit boolean flag value, e.g. `--flag=false`. */ +const BOOLEAN_TRUE_VALUES = new Set(['true', '1', 'yes', 'on']); +const BOOLEAN_FALSE_VALUES = new Set(['false', '0', 'no', 'off']); + function kebabToCamel(str: string): string { return str.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); } @@ -114,7 +118,23 @@ export function parseFlags(argv: string[], options: OptionDef[]): GlobalFlags { const camelKey = kebabToCamel(key); if (schema.booleans.has(camelKey)) { - (flags as Record)[camelKey] = true; + // A bare boolean flag (`--flag`) is true. Honour an explicit value + // (`--flag=false`, `--flag=0`, ...) and reject an unrecognised one so a + // typo cannot silently enable the flag, mirroring numeric-flag handling. + if (value === undefined) { + (flags as Record)[camelKey] = true; + } else { + const normalized = value.trim().toLowerCase(); + if (BOOLEAN_TRUE_VALUES.has(normalized)) { + (flags as Record)[camelKey] = true; + } else if (BOOLEAN_FALSE_VALUES.has(normalized)) { + (flags as Record)[camelKey] = false; + } else { + throw new Error( + `Flag --${key} requires a boolean value (e.g. true/false), got "${value}".`, + ); + } + } i++; continue; } diff --git a/test/args.test.ts b/test/args.test.ts index e71ac2c..e196d3e 100644 --- a/test/args.test.ts +++ b/test/args.test.ts @@ -5,6 +5,7 @@ import type { OptionDef } from '../src/command'; const OPTIONS: OptionDef[] = [ { flag: '--timeout ', description: 'Request timeout', type: 'number' }, { flag: '--message ', description: 'Message text', type: 'array' }, + { flag: '--verbose', description: 'Verbose output' }, ]; describe('parseFlags', () => { @@ -25,4 +26,30 @@ describe('parseFlags', () => { expect(flags.timeout).toBe(1.5); }); + + it('treats a bare boolean flag as true', () => { + expect(parseFlags(['--verbose'], OPTIONS).verbose).toBe(true); + }); + + it('honours explicit false-like values on a boolean flag', () => { + // Regression: `--flag=false` / `--flag=0` were silently set to true. + for (const v of ['false', '0', 'no', 'off', ' OFF ', 'False']) { + expect(parseFlags([`--verbose=${v}`], OPTIONS).verbose).toBe(false); + } + }); + + it('honours explicit true-like values on a boolean flag', () => { + for (const v of ['true', '1', 'yes', 'on', 'TRUE']) { + expect(parseFlags([`--verbose=${v}`], OPTIONS).verbose).toBe(true); + } + }); + + it('rejects an unrecognised explicit boolean value', () => { + expect(() => parseFlags(['--verbose=maybe'], OPTIONS)).toThrow( + 'Flag --verbose requires a boolean value', + ); + expect(() => parseFlags(['--verbose='], OPTIONS)).toThrow( + 'Flag --verbose requires a boolean value', + ); + }); });