Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
156 changes: 117 additions & 39 deletions src/bargs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,22 +760,42 @@ const createCliBuilder = <V, P extends readonly unknown[]>(

parse(
args: string[] = process.argv.slice(2),
): ParseResult<V, P> & { command?: string } {
const result = parseCore(state, args, false);
if (isThenable(result)) {
throw new BargsError(
'Async transform or handler detected. Use parseAsync() instead of parse().',
);
): ParseResult<V, P> & { command?: string; helpShown?: boolean } {
try {
const result = parseCore(state, args, false);
if (isThenable(result)) {
throw new BargsError(
'Async transform or handler detected. Use parseAsync() instead of parse().',
);
}
return result as ParseResult<V, P> & { command?: string };
} catch (error) {
if (error instanceof HelpError) {
return handleHelpError(error, state) as ParseResult<V, P> & {
command?: string;
helpShown: true;
};
}
throw error;
}
return result as ParseResult<V, P> & { command?: string };
},

async parseAsync(
args: string[] = process.argv.slice(2),
): Promise<ParseResult<V, P> & { command?: string }> {
return parseCore(state, args, true) as Promise<
ParseResult<V, P> & { command?: string }
>;
): Promise<ParseResult<V, P> & { command?: string; helpShown?: boolean }> {
try {
return (await parseCore(state, args, true)) as ParseResult<V, P> & {
command?: string;
};
} catch (error) {
if (error instanceof HelpError) {
return handleHelpError(error, state) as ParseResult<V, P> & {
command?: string;
helpShown: true;
Comment thread
boneskull marked this conversation as resolved.
Outdated
};
}
throw error;
}
},
};

Expand All @@ -799,7 +819,22 @@ const parseCore = (
> => {
const { aliasMap, commands, options, theme } = state;

/* c8 ignore start -- help/version output calls process.exit() */
/**
* Helper to create an early-exit result (for help, version, completions).
* Sets process.exitCode and returns a result with helpShown: true.
*
* @function
*/
const earlyExit = (
exitCode: number,
): ParseResult<unknown, readonly unknown[]> & {
command?: string;
helpShown: true;
} => {
process.exitCode = exitCode;
return { command: undefined, helpShown: true, positionals: [], values: {} };
Comment thread
boneskull marked this conversation as resolved.
Outdated
};

// Handle --help
if (args.includes('--help') || args.includes('-h')) {
// Check for command-specific help
Expand Down Expand Up @@ -832,27 +867,25 @@ const parseCore = (
values: {},
};
// This will trigger the nested builder's help handling
// and call process.exit(0) if --help is handled
void internalNestedBuilder.__parseWithParentGlobals(
return internalNestedBuilder.__parseWithParentGlobals(
nestedArgs,
emptyGlobals,
true,
);
}

// If no more args, show help for this nested command group
showNestedCommandHelp(state, commandName);
// showNestedCommandHelp calls process.exit(0)
return showNestedCommandHelp(state, commandName);
}

// Regular command help
console.log(generateCommandHelpNew(state, commandName, theme));
process.exit(0);
return earlyExit(0);
}
}

console.log(generateHelpNew(state, theme));
process.exit(0);
return earlyExit(0);
}

// Handle --version
Expand All @@ -863,7 +896,7 @@ const parseCore = (
} else {
console.log('Version information not available');
}
process.exit(0);
return earlyExit(0);
}

// Handle shell completion (when enabled)
Expand All @@ -876,15 +909,15 @@ const parseCore = (
console.error(
'Error: --completion-script requires a shell argument (bash, zsh, or fish)',
);
process.exit(1);
return earlyExit(1);
}
try {
const shell = validateShell(shellArg);
console.log(generateCompletionScript(state.name, shell));
process.exit(0);
return earlyExit(0);
} catch (err) {
console.error(`Error: ${(err as Error).message}`);
process.exit(1);
return earlyExit(1);
}
}

Expand All @@ -894,7 +927,7 @@ const parseCore = (
const shellArg = args[getCompletionsIndex + 1];
if (!shellArg) {
// No shell specified, output nothing
process.exit(0);
return earlyExit(0);
}
try {
const shell = validateShell(shellArg);
Expand All @@ -904,14 +937,13 @@ const parseCore = (
if (candidates.length > 0) {
console.log(candidates.join('\n'));
}
process.exit(0);
return earlyExit(0);
} catch {
// Invalid shell, output nothing
process.exit(0);
return earlyExit(0);
}
}
}
/* c8 ignore stop */

// If we have commands, dispatch to the appropriate one
if (commands.size > 0) {
Expand All @@ -927,15 +959,25 @@ const parseCore = (
*
* @function
*/
/* c8 ignore start -- only called from help paths that call process.exit() */
const showNestedCommandHelp = (
state: InternalCliState,
commandName: string,
): void => {
):
| (ParseResult<unknown, readonly unknown[]> & {
command?: string;
helpShown?: boolean;
})
| Promise<
ParseResult<unknown, readonly unknown[]> & {
command?: string;
helpShown?: boolean;
}
> => {
const commandEntry = state.commands.get(commandName);
if (!commandEntry || commandEntry.type !== 'nested') {
console.log(`Unknown command group: ${commandName}`);
process.exit(1);
console.error(`Unknown command group: ${commandName}`);
process.exitCode = 1;
return { command: undefined, helpShown: true, positionals: [], values: {} };
}

// Delegate to nested builder with --help
Expand All @@ -948,21 +990,20 @@ const showNestedCommandHelp = (
values: {},
};

// This will show the nested builder's help and call process.exit(0)
void internalNestedBuilder.__parseWithParentGlobals(
// This will show the nested builder's help
return internalNestedBuilder.__parseWithParentGlobals(
['--help'],
emptyGlobals,
true,
);
};
/* c8 ignore stop */

/**
* Generate command-specific help.
*
* @function
*/
/* c8 ignore start -- only called from help paths that call process.exit() */
/* c8 ignore start -- only called from help paths */
const generateCommandHelpNew = (
state: InternalCliState,
commandName: string,
Expand All @@ -973,11 +1014,10 @@ const generateCommandHelpNew = (
return `Unknown command: ${commandName}`;
}

// Handle nested commands - this shouldn't be reached as nested commands
// delegate to showNestedCommandHelp in parseCore, but handle it gracefully
// Nested commands are handled by showNestedCommandHelp in parseCore,
// so this function should never be called for nested commands
if (commandEntry.type === 'nested') {
showNestedCommandHelp(state, commandName);
return ''; // Never reached, showNestedCommandHelp calls process.exit
return `${commandName} is a command group. Use --help after a subcommand.`;
}

// Regular command help
Expand All @@ -1004,7 +1044,7 @@ const generateCommandHelpNew = (
*
* @function
*/
/* c8 ignore start -- only called from help paths that call process.exit() */
/* c8 ignore start -- only called from help paths */
const generateHelpNew = (state: InternalCliState, theme: Theme): string => {
// Build options schema, adding built-in options
let options = state.globalParser?.__optionsSchema;
Expand Down Expand Up @@ -1053,6 +1093,44 @@ const generateHelpNew = (state: InternalCliState, theme: Theme): string => {
};
/* c8 ignore stop */

/**
* Handle a HelpError by displaying the error message and help text to stderr,
* setting the exit code, and returning a result indicating help was shown.
*
* This prevents HelpError from bubbling up to global exception handlers while
* still providing useful feedback to the user.
*
* @function
*/
const handleHelpError = (
error: HelpError,
state: InternalCliState,
): ParseResult<unknown, readonly unknown[]> & {
command?: string;
helpShown: true;
} => {
const { theme } = state;

// Write error message to stderr
process.stderr.write(`Error: ${error.message}\n\n`);

// Generate and write help text to stderr
const helpText = generateHelpNew(state, theme);
process.stderr.write(helpText);
process.stderr.write('\n');

// Set exit code to indicate error (don't call process.exit())
process.exitCode = 1;

// Return a result indicating help was shown
return {
command: error.command,
helpShown: true,
positionals: [],
values: {},
};
};

/**
* Check if something is a Parser (has __brand: 'Parser'). Parsers can be either
* objects or functions (CallableParser).
Expand Down
24 changes: 17 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,20 +162,30 @@ export interface CliBuilder<
* Parse arguments synchronously and run handlers.
*
* Throws if any transform or handler returns a Promise.
*
* When a HelpError occurs (e.g., unknown command, no command specified), help
* text is displayed to stderr, process.exitCode is set to 1, and a result
* with `helpShown: true` is returned instead of throwing.
*/
parse(
args?: string[],
): ParseResult<TGlobalValues, TGlobalPositionals> & { command?: string };
parse(args?: string[]): ParseResult<TGlobalValues, TGlobalPositionals> & {
command?: string;
helpShown?: boolean;
};

/**
* Parse arguments asynchronously and run handlers.
*
* Supports async transforms and handlers.
*
* When a HelpError occurs (e.g., unknown command, no command specified), help
* text is displayed to stderr, process.exitCode is set to 1, and a result
* with `helpShown: true` is returned instead of rejecting.
*/
parseAsync(
args?: string[],
): Promise<
ParseResult<TGlobalValues, TGlobalPositionals> & { command?: string }
parseAsync(args?: string[]): Promise<
ParseResult<TGlobalValues, TGlobalPositionals> & {
command?: string;
helpShown?: boolean;
}
>;
}

Expand Down
Loading
Loading