Skip to content

Commit 733fa6a

Browse files
authored
Merge pull request #5409 from NomicFoundation/feat/help-option
Implement the `--help` command
2 parents ffc1468 + bb8dfe3 commit 733fa6a

File tree

14 files changed

+1077
-54
lines changed

14 files changed

+1077
-54
lines changed

pnpm-lock.yaml

+4-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

v-next/core/test/internal/hook-manager.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe("HookManager", () => {
9090
plugins: [],
9191
},
9292
hooks: hookManager,
93-
globalArguments: {},
93+
globalOptions: {},
9494
interruptions: userInterruptionsManager,
9595
});
9696

@@ -295,7 +295,7 @@ describe("HookManager", () => {
295295
plugins: [],
296296
},
297297
hooks: hookManager,
298-
globalArguments: {},
298+
globalOptions: {},
299299
interruptions: userInterruptionsManager,
300300
});
301301

@@ -509,7 +509,7 @@ describe("HookManager", () => {
509509
plugins: [],
510510
},
511511
hooks: hookManager,
512-
globalArguments: {},
512+
globalOptions: {},
513513
interruptions: userInterruptionsManager,
514514
});
515515

@@ -634,7 +634,7 @@ describe("HookManager", () => {
634634
plugins: [],
635635
},
636636
hooks: hookManager,
637-
globalArguments: {},
637+
globalOptions: {},
638638
interruptions: userInterruptionsManager,
639639
});
640640

@@ -778,7 +778,7 @@ describe("HookManager", () => {
778778
plugins: [],
779779
},
780780
hooks: hookManager,
781-
globalArguments: {},
781+
globalOptions: {},
782782
interruptions: userInterruptionsManager,
783783
});
784784

@@ -935,7 +935,7 @@ function buildMockHardhatRuntimeEnvironment(
935935
plugins: [],
936936
},
937937
tasks: mockTaskManager,
938-
globalArguments: {},
938+
globalOptions: {},
939939
interruptions: mockInteruptionManager,
940940
};
941941

v-next/hardhat/example.config.ts

+43-5
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,51 @@ import {
33
overrideTask,
44
configVariable,
55
task,
6+
emptyTask,
67
} from "./src/config.js";
78

8-
const exampleTaskOverride = overrideTask("example")
9-
.setAction(async (args, _hre, runSuper) => {
9+
const exampleEmptyTask = emptyTask("empty", "An example empty task").build();
10+
11+
const exampleEmptySubtask = task(["empty", "task"])
12+
.setDescription("An example empty subtask task")
13+
.setAction(async (_, _hre) => {
14+
console.log("empty task");
15+
})
16+
.build();
17+
18+
const exampleTaskOverride = task("example2")
19+
.setAction(async (_, _hre) => {
1020
console.log("from an override");
11-
await runSuper(args);
21+
})
22+
.setDescription("An example task")
23+
.addVariadicParameter({
24+
name: "testFiles",
25+
description: "An optional list of files to test",
26+
// defaultValue: [],
27+
})
28+
.addOption({
29+
name: "noCompile",
30+
description: "Don't compile before running this task",
31+
})
32+
.addFlag({
33+
name: "parallel",
34+
description: "Run tests in parallel",
35+
})
36+
.addFlag({
37+
name: "bail",
38+
description: "Stop running tests after the first test failure",
39+
})
40+
.addOption({
41+
name: "grep",
42+
description: "Only run tests matching the given string or regexp",
1243
})
1344
.build();
1445

1546
const testTask = task("test", "Runs mocha tests")
1647
.addVariadicParameter({
1748
name: "testFiles",
1849
description: "An optional list of files to test",
19-
defaultValue: [],
50+
// defaultValue: [],
2051
})
2152
.addOption({
2253
name: "noCompile",
@@ -52,6 +83,13 @@ const testSolidityTask = task(["test", "solidity"], "Runs Solidity tests")
5283
.build();
5384

5485
export default {
55-
tasks: [exampleTaskOverride, testTask, testTaskOverride, testSolidityTask],
86+
tasks: [
87+
exampleTaskOverride,
88+
testTask,
89+
testTaskOverride,
90+
testSolidityTask,
91+
exampleEmptyTask,
92+
exampleEmptySubtask,
93+
],
5694
privateKey: configVariable("privateKey"),
5795
} satisfies HardhatUserConfig;

v-next/hardhat/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"@nomicfoundation/hardhat-errors": "workspace:^3.0.0",
7474
"@nomicfoundation/hardhat-utils": "workspace:^3.0.0",
7575
"@nomicfoundation/hardhat-zod-utils": "workspace:^3.0.0",
76+
"chalk": "^5.3.0",
7677
"tsx": "^4.11.0",
7778
"zod": "^3.23.8"
7879
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks";
2+
3+
import { getHardhatVersion } from "../../utils/package.js";
4+
5+
import {
6+
GLOBAL_NAME_PADDING,
7+
GLOBAL_OPTIONS,
8+
getLongestNameLength,
9+
getSection,
10+
parseTasks,
11+
} from "./utils.js";
12+
13+
export async function getGlobalHelpString(
14+
rootTasks: Map<string, Task>,
15+
): Promise<string> {
16+
const version = await getHardhatVersion();
17+
18+
const { tasks, subtasks } = parseTasks(rootTasks);
19+
20+
const namePadding =
21+
getLongestNameLength([...tasks, ...subtasks, ...GLOBAL_OPTIONS]) +
22+
GLOBAL_NAME_PADDING;
23+
24+
let output = `Hardhat version ${version}
25+
26+
Usage: hardhat [GLOBAL OPTIONS] <TASK> [SUBTASK] [TASK OPTIONS] [--] [TASK ARGUMENTS]
27+
`;
28+
29+
if (tasks.length > 0) {
30+
output += getSection("AVAILABLE TASKS", tasks, namePadding);
31+
}
32+
33+
if (subtasks.length > 0) {
34+
output += getSection("AVAILABLE SUBTASKS", subtasks, namePadding);
35+
}
36+
37+
output += getSection("GLOBAL OPTIONS", GLOBAL_OPTIONS, namePadding);
38+
39+
output += `\nTo get help for a specific task run: npx hardhat <TASK> [SUBTASK] --help`;
40+
41+
return output;
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks";
2+
3+
import {
4+
GLOBAL_NAME_PADDING,
5+
parseOptions,
6+
getLongestNameLength,
7+
getSection,
8+
parseSubtasks,
9+
getUsageString,
10+
} from "./utils.js";
11+
12+
export async function getHelpString(task: Task): Promise<string> {
13+
const { default: chalk } = await import("chalk");
14+
15+
const { options, positionalArguments } = parseOptions(task);
16+
17+
const subtasks = parseSubtasks(task);
18+
19+
const namePadding =
20+
getLongestNameLength([...options, ...positionalArguments, ...subtasks]) +
21+
GLOBAL_NAME_PADDING;
22+
23+
let output = `${chalk.bold(task.description)}`;
24+
25+
if (task.isEmpty) {
26+
output += `\n\nUsage: hardhat [GLOBAL OPTIONS] ${task.id.join(" ")} <SUBTASK> [SUBTASK OPTIONS] [--] [SUBTASK POSITIONAL ARGUMENTS]\n`;
27+
28+
if (subtasks.length > 0) {
29+
output += getSection("AVAILABLE SUBTASKS", subtasks, namePadding);
30+
31+
output += `\nTo get help for a specific task run: npx hardhat ${task.id.join(" ")} <SUBTASK> --help`;
32+
}
33+
34+
return output;
35+
}
36+
37+
const usage = getUsageString(task, options, positionalArguments);
38+
39+
output += `\n\n${usage}\n`;
40+
41+
if (options.length > 0) {
42+
output += getSection("OPTIONS", options, namePadding);
43+
}
44+
45+
if (positionalArguments.length > 0) {
46+
output += getSection(
47+
"POSITIONAL ARGUMENTS",
48+
positionalArguments,
49+
namePadding,
50+
);
51+
}
52+
53+
if (subtasks.length > 0) {
54+
output += getSection("AVAILABLE SUBTASKS", subtasks, namePadding);
55+
}
56+
57+
output += `\nFor global options help run: hardhat --help`;
58+
59+
return output;
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import type { ParameterType } from "@nomicfoundation/hardhat-core/config";
2+
import type { Task } from "@nomicfoundation/hardhat-core/types/tasks";
3+
4+
export const GLOBAL_OPTIONS = [
5+
{
6+
name: "--config",
7+
description: "A Hardhat config file.",
8+
},
9+
{
10+
name: "--help",
11+
description: "Shows this message, or a task's help if its name is provided",
12+
},
13+
{
14+
name: "--show-stack-traces",
15+
description: "Show stack traces (always enabled on CI servers).",
16+
},
17+
{
18+
name: "--version",
19+
description: "Shows hardhat's version.",
20+
},
21+
];
22+
23+
export const GLOBAL_NAME_PADDING = 6;
24+
25+
export function parseTasks(taskMap: Map<string, Task>): {
26+
tasks: Array<{ name: string; description: string }>;
27+
subtasks: Array<{ name: string; description: string }>;
28+
} {
29+
const tasks = [];
30+
const subtasks = [];
31+
32+
for (const [taskName, task] of taskMap) {
33+
subtasks.push(...parseSubtasks(task));
34+
35+
if (task.isEmpty) {
36+
continue;
37+
}
38+
39+
tasks.push({ name: taskName, description: task.description });
40+
}
41+
42+
return { tasks, subtasks };
43+
}
44+
45+
export function parseSubtasks(
46+
task: Task,
47+
): Array<{ name: string; description: string }> {
48+
const subtasks = [];
49+
50+
for (const [, subtask] of task.subtasks) {
51+
subtasks.push({
52+
name: subtask.id.join(" "),
53+
description: subtask.description,
54+
});
55+
}
56+
57+
return subtasks;
58+
}
59+
60+
export function parseOptions(task: Task): {
61+
options: Array<{ name: string; description: string; type: ParameterType }>;
62+
positionalArguments: Array<{
63+
name: string;
64+
description: string;
65+
isRequired: boolean;
66+
}>;
67+
} {
68+
const options = [];
69+
const positionalArguments = [];
70+
71+
for (const [optionName, option] of task.options) {
72+
options.push({
73+
name: formatOptionName(optionName),
74+
description: option.description,
75+
type: option.parameterType,
76+
});
77+
}
78+
79+
for (const argument of task.positionalParameters) {
80+
positionalArguments.push({
81+
name: argument.name,
82+
description: argument.description,
83+
isRequired: argument.defaultValue === undefined,
84+
});
85+
}
86+
87+
return { options, positionalArguments };
88+
}
89+
90+
export function formatOptionName(str: string): string {
91+
return `--${str
92+
.split("")
93+
.map((letter, idx) => {
94+
return letter.toUpperCase() === letter
95+
? `${idx !== 0 ? "-" : ""}${letter.toLowerCase()}`
96+
: letter;
97+
})
98+
.join("")}`;
99+
}
100+
101+
export function getLongestNameLength(tasks: Array<{ name: string }>): number {
102+
return tasks.reduce((acc, { name }) => Math.max(acc, name.length), 0);
103+
}
104+
105+
export function getSection(
106+
title: string,
107+
items: Array<{ name: string; description: string }>,
108+
namePadding: number,
109+
): string {
110+
return `\n${title}:\n\n${items.map(({ name, description }) => ` ${name.padEnd(namePadding)}${description}`).join("\n")}\n`;
111+
}
112+
113+
export function getUsageString(
114+
task: Task,
115+
options: ReturnType<typeof parseOptions>["options"],
116+
positionalArguments: ReturnType<typeof parseOptions>["positionalArguments"],
117+
): string {
118+
let output = `Usage: hardhat [GLOBAL OPTIONS] ${task.id.join(" ")}`;
119+
120+
if (options.length > 0) {
121+
output += ` ${options.map((o) => `[${o.name}${o.type === "BOOLEAN" ? "" : ` <${o.type}>`}]`).join(" ")}`;
122+
}
123+
124+
if (positionalArguments.length > 0) {
125+
output += ` [--] ${positionalArguments.map((a) => (a.isRequired ? a.name : `[${a.name}]`)).join(" ")}`;
126+
}
127+
128+
return output;
129+
}

0 commit comments

Comments
 (0)