mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-12-20 13:37:49 +08:00
fix: cli framework help option not work
This commit is contained in:
@@ -94,30 +94,36 @@ function processAndExecute<TContext extends object>(
|
|||||||
): Result<void, CliError> {
|
): Result<void, CliError> {
|
||||||
const { command, commandPath, options, remaining } = parseResult;
|
const { command, commandPath, options, remaining } = parseResult;
|
||||||
|
|
||||||
// Handle requests for help on a specific command.
|
// Unified Help Check:
|
||||||
if (shouldShowHelp([...remaining, ...Object.keys(options)])) {
|
// A command should show its help page if:
|
||||||
writer(generateHelp(command, commandPath));
|
// 1. A help flag is explicitly passed (`--help` or `-h`). This has the highest priority.
|
||||||
return Ok.EMPTY;
|
// 2. It's a command group that was called without a subcommand (i.e., it has no action).
|
||||||
}
|
const isHelpFlagPassed = shouldShowHelp([
|
||||||
|
...remaining,
|
||||||
// If a command has subcommands but no action, show its help page.
|
...Object.keys(options),
|
||||||
if (
|
]);
|
||||||
command.subcommands &&
|
const isCommandGroupWithoutAction =
|
||||||
|
command.subcommands !== undefined &&
|
||||||
command.subcommands.size > 0 &&
|
command.subcommands.size > 0 &&
|
||||||
command.action === undefined
|
command.action === undefined;
|
||||||
) {
|
|
||||||
|
if (isHelpFlagPassed || isCommandGroupWithoutAction) {
|
||||||
writer(generateHelp(command, commandPath));
|
writer(generateHelp(command, commandPath));
|
||||||
return Ok.EMPTY;
|
return Ok.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A command that is meant to be executed must have an action.
|
// If we are here, it's a runnable command. It must have an action.
|
||||||
if (command.action === undefined) {
|
if (command.action === undefined) {
|
||||||
|
// This case should ideally not be reached if the parser and the logic above are correct.
|
||||||
|
// It would mean a command has no action and no subcommands, which is a configuration error.
|
||||||
return new Err({
|
return new Err({
|
||||||
kind: "NoAction",
|
kind: "NoAction",
|
||||||
commandPath: [...commandPath, command.name],
|
commandPath: [...commandPath, command.name],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now we know it's a runnable command, and no help flag was passed.
|
||||||
|
// We can now safely process the remaining items as arguments.
|
||||||
return processArguments(command.args ?? [], remaining)
|
return processArguments(command.args ?? [], remaining)
|
||||||
.andThen((args) => {
|
.andThen((args) => {
|
||||||
return processOptions(
|
return processOptions(
|
||||||
|
|||||||
@@ -103,5 +103,5 @@ export function generateCommandList<TContext extends object>(
|
|||||||
* @returns `true` if a help flag is found, otherwise `false`.
|
* @returns `true` if a help flag is found, otherwise `false`.
|
||||||
*/
|
*/
|
||||||
export function shouldShowHelp(argv: string[]): boolean {
|
export function shouldShowHelp(argv: string[]): boolean {
|
||||||
return argv.includes("--help") || argv.includes("-h");
|
return argv.includes("help") || argv.includes("h");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,22 +87,49 @@ export function parseArguments<TContext extends object>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let currentCommand = rootCommand;
|
let currentCommand = rootCommand;
|
||||||
let i = 0;
|
|
||||||
let inOptions = false;
|
let inOptions = false;
|
||||||
|
|
||||||
const optionMapCache = new OptionMapCache();
|
const optionMapCache = new OptionMapCache();
|
||||||
const getCurrentOptionMaps = () =>
|
|
||||||
getOptionMaps(optionMapCache, currentCommand);
|
|
||||||
|
|
||||||
while (i < argv.length) {
|
// Cache option maps for current command - only updated when command changes
|
||||||
|
let currentOptionMaps = getOptionMaps(optionMapCache, currentCommand);
|
||||||
|
|
||||||
|
// Helper function to update command context and refresh option maps
|
||||||
|
const updateCommand = (
|
||||||
|
newCommand: Command<TContext>,
|
||||||
|
commandName: string,
|
||||||
|
) => {
|
||||||
|
currentCommand = newCommand;
|
||||||
|
result.command = currentCommand;
|
||||||
|
result.commandPath.push(commandName);
|
||||||
|
currentOptionMaps = getOptionMaps(optionMapCache, currentCommand);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to process option value
|
||||||
|
const processOption = (optionName: string, i: number): number => {
|
||||||
|
const optionDef = currentOptionMaps.optionMap.get(optionName);
|
||||||
|
const nextArg = argv[i + 1];
|
||||||
|
const isKnownBooleanOption =
|
||||||
|
optionDef !== undefined && optionDef.defaultValue === undefined;
|
||||||
|
const nextArgLooksLikeValue =
|
||||||
|
nextArg !== undefined && nextArg !== null && !nextArg.startsWith("-");
|
||||||
|
|
||||||
|
if (nextArgLooksLikeValue && !isKnownBooleanOption) {
|
||||||
|
result.options[optionName] = nextArg;
|
||||||
|
return i + 1; // Skip the value argument
|
||||||
|
} else {
|
||||||
|
result.options[optionName] = true;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Single pass through argv
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
const arg = argv[i];
|
const arg = argv[i];
|
||||||
|
|
||||||
if (arg === undefined || arg === null) {
|
// Skip null/undefined arguments
|
||||||
i++;
|
if (!arg) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle double dash (--) - everything after is treated as a remaining argument
|
// Handle double dash (--) - everything after is treated as remaining
|
||||||
if (arg === "--") {
|
if (arg === "--") {
|
||||||
result.remaining.push(...argv.slice(i + 1));
|
result.remaining.push(...argv.slice(i + 1));
|
||||||
break;
|
break;
|
||||||
@@ -121,70 +148,33 @@ export function parseArguments<TContext extends object>(
|
|||||||
} else {
|
} else {
|
||||||
// --option [value] format
|
// --option [value] format
|
||||||
const optionName = arg.slice(2);
|
const optionName = arg.slice(2);
|
||||||
const optionDef = getCurrentOptionMaps().optionMap.get(optionName);
|
i = processOption(optionName, i);
|
||||||
|
|
||||||
// Check if this is a known boolean option or if next arg looks like a value
|
|
||||||
const nextArg = argv[i + 1];
|
|
||||||
const isKnownBooleanOption =
|
|
||||||
optionDef !== undefined && optionDef.defaultValue === undefined;
|
|
||||||
const nextArgLooksLikeValue =
|
|
||||||
nextArg !== undefined && nextArg !== null && !nextArg.startsWith("-");
|
|
||||||
|
|
||||||
if (nextArgLooksLikeValue && !isKnownBooleanOption) {
|
|
||||||
result.options[optionName] = nextArg;
|
|
||||||
i++; // Skip the value argument
|
|
||||||
} else {
|
|
||||||
// Boolean flag or no value available
|
|
||||||
result.options[optionName] = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle short options (-o or -o value)
|
// Handle short options (-o or -o value)
|
||||||
else if (arg.startsWith("-") && arg.length > 1) {
|
else if (arg.startsWith("-") && arg.length > 1) {
|
||||||
inOptions = true;
|
inOptions = true;
|
||||||
const shortName = arg.slice(1);
|
const shortName = arg.slice(1);
|
||||||
|
const optionName =
|
||||||
// Get option maps for the new command (lazy loaded and cached)
|
currentOptionMaps.shortNameMap.get(shortName) ?? shortName;
|
||||||
const maps = getCurrentOptionMaps();
|
i = processOption(optionName, i);
|
||||||
const optionName = maps.shortNameMap.get(shortName) ?? shortName;
|
|
||||||
const optionDef = maps.optionMap.get(optionName);
|
|
||||||
|
|
||||||
// Check if this is a known boolean option or if next arg looks like a value
|
|
||||||
const nextArg = argv[i + 1];
|
|
||||||
const isKnownBooleanOption =
|
|
||||||
optionDef !== undefined && optionDef.defaultValue === undefined;
|
|
||||||
const nextArgLooksLikeValue =
|
|
||||||
nextArg !== undefined && nextArg !== null && !nextArg.startsWith("-");
|
|
||||||
|
|
||||||
if (nextArgLooksLikeValue && !isKnownBooleanOption) {
|
|
||||||
result.options[optionName] = nextArg;
|
|
||||||
i++; // Skip the value argument
|
|
||||||
} else {
|
|
||||||
// Boolean flag or no value available
|
|
||||||
result.options[optionName] = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Handle positional arguments and command resolution
|
// Handle positional arguments and command resolution
|
||||||
else {
|
else {
|
||||||
if (!inOptions) {
|
if (!inOptions) {
|
||||||
// Try to find this as a subcommand of the current command
|
// Try to find this as a subcommand of the current command
|
||||||
const subcommand = currentCommand.subcommands?.get(arg);
|
const subcommand = currentCommand.subcommands?.get(arg);
|
||||||
if (subcommand !== undefined) {
|
if (subcommand) {
|
||||||
// Found a subcommand, move deeper
|
updateCommand(subcommand, arg);
|
||||||
currentCommand = subcommand;
|
|
||||||
result.command = currentCommand;
|
|
||||||
result.commandPath.push(arg);
|
|
||||||
} else {
|
} else {
|
||||||
// Not a subcommand, treat as remaining argument
|
// Not a subcommand, treat as remaining argument
|
||||||
result.remaining.push(arg);
|
result.remaining.push(arg);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// After options have started, treat as a remaining argument
|
// After options have started, treat as remaining argument
|
||||||
result.remaining.push(arg);
|
result.remaining.push(arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Ok(result);
|
return new Ok(result);
|
||||||
|
|||||||
Reference in New Issue
Block a user