-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement the --help
command
#5409
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks"; | ||
|
||
import { getHardhatVersion } from "../../utils/package.js"; | ||
|
||
import { | ||
GLOBAL_NAME_PADDING, | ||
GLOBAL_OPTIONS, | ||
getLongestNameLength, | ||
getSection, | ||
parseTasks, | ||
} from "./utils.js"; | ||
|
||
export async function getGlobalHelpString( | ||
rootTasks: Map<string, Task>, | ||
): Promise<string> { | ||
const version = await getHardhatVersion(); | ||
|
||
const { tasks, subtasks } = parseTasks(rootTasks); | ||
|
||
const namePadding = | ||
getLongestNameLength([...tasks, ...subtasks, ...GLOBAL_OPTIONS]) + | ||
GLOBAL_NAME_PADDING; | ||
|
||
let output = `Hardhat version ${version} | ||
|
||
Usage: hardhat [GLOBAL OPTIONS] <TASK> [SUBTASK] [TASK OPTIONS] [--] [TASK ARGUMENTS] | ||
`; | ||
|
||
if (tasks.length > 0) { | ||
output += getSection("AVAILABLE TASKS", tasks, namePadding); | ||
} | ||
|
||
if (subtasks.length > 0) { | ||
output += getSection("AVAILABLE SUBTASKS", subtasks, namePadding); | ||
} | ||
|
||
output += getSection("GLOBAL OPTIONS", GLOBAL_OPTIONS, namePadding); | ||
|
||
output += `\nTo get help for a specific task run: npx hardhat <TASK> [SUBTASK] --help`; | ||
|
||
return output; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks"; | ||
|
||
import { | ||
GLOBAL_NAME_PADDING, | ||
parseOptions, | ||
getLongestNameLength, | ||
getSection, | ||
parseSubtasks, | ||
getUsageString, | ||
} from "./utils.js"; | ||
|
||
export async function getHelpString(task: Task): Promise<string> { | ||
const { default: chalk } = await import("chalk"); | ||
|
||
const { options, positionalArguments } = parseOptions(task); | ||
|
||
const subtasks = parseSubtasks(task); | ||
|
||
const namePadding = | ||
getLongestNameLength([...options, ...positionalArguments, ...subtasks]) + | ||
GLOBAL_NAME_PADDING; | ||
|
||
let output = `${chalk.bold(task.description)}`; | ||
|
||
if (task.isEmpty) { | ||
output += `\n\nUsage: hardhat [GLOBAL OPTIONS] ${task.id.join(" ")} <SUBTASK> [SUBTASK OPTIONS] [--] [SUBTASK POSITIONAL ARGUMENTS]\n`; | ||
|
||
if (subtasks.length > 0) { | ||
output += getSection("AVAILABLE SUBTASKS", subtasks, namePadding); | ||
|
||
output += `\nTo get help for a specific task run: npx hardhat ${task.id.join(" ")} <SUBTASK> --help`; | ||
} | ||
|
||
return output; | ||
} | ||
|
||
const usage = getUsageString(task, options, positionalArguments); | ||
|
||
output += `\n\n${usage}\n`; | ||
|
||
if (options.length > 0) { | ||
output += getSection("OPTIONS", options, namePadding); | ||
} | ||
|
||
if (positionalArguments.length > 0) { | ||
output += getSection( | ||
"POSITIONAL ARGUMENTS", | ||
positionalArguments, | ||
namePadding, | ||
); | ||
} | ||
|
||
if (subtasks.length > 0) { | ||
output += getSection("AVAILABLE SUBTASKS", subtasks, namePadding); | ||
} | ||
|
||
output += `\nFor global options help run: hardhat --help`; | ||
|
||
return output; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import type { ParameterType } from "@nomicfoundation/hardhat-core/config"; | ||
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks"; | ||
|
||
export const GLOBAL_OPTIONS = [ | ||
{ | ||
name: "--config", | ||
description: "A Hardhat config file.", | ||
}, | ||
{ | ||
name: "--help", | ||
description: "Shows this message, or a task's help if its name is provided", | ||
}, | ||
{ | ||
name: "--show-stack-traces", | ||
description: "Show stack traces (always enabled on CI servers).", | ||
}, | ||
{ | ||
name: "--version", | ||
description: "Shows hardhat's version.", | ||
}, | ||
]; | ||
|
||
export const GLOBAL_NAME_PADDING = 6; | ||
|
||
export function parseTasks(taskMap: Map<string, Task>): { | ||
tasks: Array<{ name: string; description: string }>; | ||
subtasks: Array<{ name: string; description: string }>; | ||
} { | ||
const tasks = []; | ||
const subtasks = []; | ||
|
||
for (const [taskName, task] of taskMap) { | ||
subtasks.push(...parseSubtasks(task)); | ||
|
||
if (task.isEmpty) { | ||
continue; | ||
} | ||
|
||
tasks.push({ name: taskName, description: task.description }); | ||
} | ||
|
||
return { tasks, subtasks }; | ||
} | ||
|
||
export function parseSubtasks( | ||
task: Task, | ||
): Array<{ name: string; description: string }> { | ||
const subtasks = []; | ||
|
||
for (const [, subtask] of task.subtasks) { | ||
subtasks.push({ | ||
name: subtask.id.join(" "), | ||
description: subtask.description, | ||
}); | ||
} | ||
|
||
return subtasks; | ||
} | ||
|
||
export function parseOptions(task: Task): { | ||
options: Array<{ name: string; description: string; type: ParameterType }>; | ||
positionalArguments: Array<{ | ||
name: string; | ||
description: string; | ||
isRequired: boolean; | ||
}>; | ||
} { | ||
const options = []; | ||
const positionalArguments = []; | ||
|
||
for (const [optionName, option] of task.options) { | ||
options.push({ | ||
name: formatOptionName(optionName), | ||
description: option.description, | ||
type: option.parameterType, | ||
}); | ||
} | ||
|
||
for (const argument of task.positionalParameters) { | ||
positionalArguments.push({ | ||
name: argument.name, | ||
description: argument.description, | ||
isRequired: argument.defaultValue === undefined, | ||
}); | ||
} | ||
|
||
return { options, positionalArguments }; | ||
} | ||
|
||
export function formatOptionName(str: string): string { | ||
return `--${str | ||
.split("") | ||
.map((letter, idx) => { | ||
return letter.toUpperCase() === letter | ||
? `${idx !== 0 ? "-" : ""}${letter.toLowerCase()}` | ||
: letter; | ||
}) | ||
Comment on lines
+93
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @schaable do we have a utility for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we have the opposite ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added an issue to track it #5465 |
||
.join("")}`; | ||
} | ||
|
||
export function getLongestNameLength(tasks: Array<{ name: string }>): number { | ||
return tasks.reduce((acc, { name }) => Math.max(acc, name.length), 0); | ||
} | ||
|
||
export function getSection( | ||
title: string, | ||
items: Array<{ name: string; description: string }>, | ||
namePadding: number, | ||
): string { | ||
return `\n${title}:\n\n${items.map(({ name, description }) => ` ${name.padEnd(namePadding)}${description}`).join("\n")}\n`; | ||
} | ||
|
||
export function getUsageString( | ||
task: Task, | ||
options: ReturnType<typeof parseOptions>["options"], | ||
positionalArguments: ReturnType<typeof parseOptions>["positionalArguments"], | ||
): string { | ||
let output = `Usage: hardhat [GLOBAL OPTIONS] ${task.id.join(" ")}`; | ||
|
||
if (options.length > 0) { | ||
output += ` ${options.map((o) => `[${o.name}${o.type === "BOOLEAN" ? "" : ` <${o.type}>`}]`).join(" ")}`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Options and positional arguments can be optional if they have a default value. In that case, they need to be wrapped around "[]`, otherwise they don't. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed, these options may all be, well... optional |
||
} | ||
|
||
if (positionalArguments.length > 0) { | ||
output += ` [--] ${positionalArguments.map((a) => (a.isRequired ? a.name : `[${a.name}]`)).join(" ")}`; | ||
} | ||
|
||
return output; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a note: I think we can handle this and reserved words differently so that they are more unified with the rest of the global options.
For a follow up pr though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created an issue for this