Skip to content

Commit fd40a96

Browse files
authored
fix #29; subcommand --help doesn't show global options (#119)
1 parent 20ae94b commit fd40a96

10 files changed

Lines changed: 260 additions & 46 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ build/
66
vendor/
77
tests/test_all
88
tests/test_nested_cmd
9+
tests/test_help

confutils.nim

Lines changed: 74 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -317,51 +317,47 @@ type
317317
defaultCmdOpts
318318
conditionalOpts
319319

320-
proc describeOptions(help: var string,
321-
cmd: CmdInfo, cmdInvocation: string,
322-
appInfo: HelpAppInfo, optionsType = normalOpts) =
323-
if cmd.hasOpts:
324-
case optionsType
325-
of normalOpts:
326-
helpOutput "\pThe following options are available:\p\p"
327-
of conditionalOpts:
328-
helpOutput ", the following additional options are available:\p\p"
329-
of defaultCmdOpts:
330-
discard
331-
332-
for opt in cmd.opts:
333-
if opt.kind == Arg or
334-
opt.kind == Discriminator or
335-
opt.isHidden: continue
336-
337-
if opt.separator.len > 0:
338-
helpOutput opt.separator
339-
helpOutput "\p"
320+
proc describeOptionsList(
321+
help: var string,
322+
cmd: CmdInfo,
323+
appInfo: HelpAppInfo
324+
) =
325+
for opt in cmd.opts:
326+
if opt.kind == Arg or
327+
opt.kind == Discriminator or
328+
opt.isHidden:
329+
continue
340330

341-
# Indent all command-line switches
342-
helpOutput " "
331+
if opt.separator.len > 0:
332+
helpOutput opt.separator
333+
helpOutput "\p"
343334

344-
if opt.abbr.len > 0:
345-
helpOutput fgOption, styleBright, "-", opt.abbr, ", "
346-
elif appInfo.hasAbbrs:
347-
# Add additional indentatition, so all names are aligned
348-
helpOutput " "
335+
# Indent all command-line switches
336+
helpOutput " "
349337

350-
if opt.name.len > 0:
351-
let switch = "--" & opt.name
352-
helpOutput fgOption, styleBright,
353-
switch, padding(switch, appInfo.namesWidth)
354-
else:
355-
helpOutput spaces(2 + appInfo.namesWidth)
338+
if opt.abbr.len > 0:
339+
helpOutput fgOption, styleBright, "-", opt.abbr, ", "
340+
elif appInfo.hasAbbrs:
341+
# Add additional indentatition, so all names are aligned
342+
helpOutput " "
343+
344+
if opt.name.len > 0:
345+
let switch = "--" & opt.name
346+
helpOutput fgOption, styleBright,
347+
switch, padding(switch, appInfo.namesWidth)
348+
else:
349+
helpOutput spaces(2 + appInfo.namesWidth)
356350

357-
if opt.desc.len > 0:
358-
help.writeDesc appInfo,
359-
opt.desc.replace("%t", opt.typename),
360-
opt.defaultInHelpText
361-
help.writeLongDesc appInfo, opt.longDesc
351+
if opt.desc.len > 0:
352+
help.writeDesc appInfo,
353+
opt.desc.replace("%t", opt.typename),
354+
opt.defaultInHelpText
355+
help.writeLongDesc appInfo, opt.longDesc
362356

363-
helpOutput "\p"
357+
helpOutput "\p"
364358

359+
# TODO: this is not reached: https://github.com/status-im/nim-confutils/issues/39
360+
when false:
365361
if opt.kind == Discriminator:
366362
for i, subCmd in opt.subCmds:
367363
if not subCmd.hasOpts: continue
@@ -371,6 +367,34 @@ proc describeOptions(help: var string,
371367
if i == opt.defaultSubCmd: helpOutput " (default)"
372368
help.describeOptions subCmd, cmdInvocation, appInfo, conditionalOpts
373369

370+
proc describeOptions(
371+
help: var string,
372+
cmd: CmdInfo,
373+
cmdInvocation: string,
374+
appInfo: HelpAppInfo,
375+
optionsType = normalOpts,
376+
activeCmds: openArray[CmdInfo] = @[]
377+
) =
378+
var hasOpts = cmd.hasOpts
379+
for c in activeCmds:
380+
if c.hasOpts:
381+
hasOpts = true
382+
383+
if hasOpts:
384+
case optionsType
385+
of normalOpts:
386+
helpOutput "\pThe following options are available:\p\p"
387+
of conditionalOpts:
388+
helpOutput ", the following additional options are available:\p\p"
389+
of defaultCmdOpts:
390+
discard
391+
392+
if activeCmds.len > 0:
393+
for c in activeCmds:
394+
describeOptionsList(help, c, appInfo)
395+
else:
396+
describeOptionsList(help, cmd, appInfo)
397+
374398
let subCmdDiscriminator = cmd.getSubCmdDiscriminator
375399
if subCmdDiscriminator != nil:
376400
let defaultCmdIdx = subCmdDiscriminator.defaultSubCmd
@@ -396,11 +420,13 @@ proc showHelp(help: var string,
396420

397421
appInfo.maxNameLen = cmd.maxNameLen
398422
appInfo.hasAbbrs = cmd.hasAbbrs
399-
appInfo.terminalWidth =
423+
let termWidth =
400424
try:
401425
terminalWidth()
402426
except ValueError:
403427
int.high # https://github.com/nim-lang/Nim/pull/21968
428+
if appInfo.terminalWidth == 0:
429+
appInfo.terminalWidth = termWidth
404430
appInfo.namesWidth = min(minNameWidth, appInfo.maxNameLen) + descPadding
405431

406432
var cmdInvocation = appInfo.appInvocation
@@ -411,7 +437,7 @@ proc showHelp(help: var string,
411437
# Write out the app or script name
412438
helpOutput fgSection, "Usage: \p"
413439
help.describeInvocation cmd, cmdInvocation, appInfo
414-
help.describeOptions cmd, cmdInvocation, appInfo
440+
help.describeOptions cmd, cmdInvocation, appInfo, activeCmds = activeCmds
415441
helpOutput "\p"
416442

417443
flushOutputAndQuit QuitSuccess
@@ -920,7 +946,8 @@ proc loadImpl[C, SecondarySources](
920946
secondarySources: proc (
921947
config: Configuration, sources: ref SecondarySources
922948
) {.gcsafe, raises: [ConfigurationError].} = nil,
923-
envVarsPrefix = appInvocation()
949+
envVarsPrefix = appInvocation(),
950+
termWidth = 0
924951
): Configuration {.raises: [ConfigurationError].} =
925952
## Loads a program configuration by parsing command-line arguments
926953
## and a standard set of config files that can specify:
@@ -1094,7 +1121,8 @@ proc loadImpl[C, SecondarySources](
10941121
proc lazyHelpAppInfo: HelpAppInfo =
10951122
HelpAppInfo(
10961123
copyrightBanner: copyrightBanner,
1097-
appInvocation: appInvocation())
1124+
appInvocation: appInvocation(),
1125+
terminalWidth: termWidth)
10981126

10991127
template processHelpAndVersionOptions(optKey: string) =
11001128
let key = optKey
@@ -1200,12 +1228,13 @@ template load*(
12001228
quitOnFailure = true,
12011229
ignoreUnknown = false,
12021230
secondarySources: untyped = nil,
1203-
envVarsPrefix = appInvocation()): untyped =
1231+
envVarsPrefix = appInvocation(),
1232+
termWidth = 0): untyped =
12041233
block:
12051234
let secondarySourcesRef = generateSecondarySources(Configuration)
12061235
loadImpl(Configuration, cmdLine, version,
12071236
copyrightBanner, printUsage, quitOnFailure, ignoreUnknown,
1208-
secondarySourcesRef, secondarySources, envVarsPrefix)
1237+
secondarySourcesRef, secondarySources, envVarsPrefix, termWidth)
12091238

12101239
func defaults*(Configuration: type): Configuration =
12111240
load(Configuration, cmdLine = @[], printUsage = false, quitOnFailure = false)

tests/help/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/*
2+
!*/
3+
!/*.nim
4+
!.gitignore
5+
!README.md

tests/help/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## What?
2+
3+
The way to test --help output changes is to remove all files under `snapshot`
4+
and run `./tests/test_help.nim` so the snapshots get generated again with
5+
the new content. Then you can check what changed compared to the original file.
6+
7+
The snapshots are the output of the `*.nim --help` programs in this directory.
8+
9+
Once your done commit the changes including the new snapshots.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Usage:
2+
3+
test_nested_cmd [OPTIONS]... command
4+
5+
The following options are available:
6+
7+
--top-arg1 topArg1 desc [=topArg1 default].
8+
--lvl1-no-command-arg1 lvl1NoCommandArg1 desc [=lvl1NoCommandArg1 default].
9+
10+
Available sub-commands:
11+
12+
test_nested_cmd lvl1Cmd1 [OPTIONS]... command
13+
14+
The following options are available:
15+
16+
--lvl1-cmd1-arg1 lvl1Cmd1Arg1 desc [=lvl1Cmd1Arg1 default].
17+
18+
Available sub-commands:
19+
20+
test_nested_cmd lvl1Cmd1 lvl2Cmd1 [OPTIONS]...
21+
22+
The following options are available:
23+
24+
--lvl2-cmd1-arg1 lvl2Cmd1Arg1 desc [=lvl2Cmd1Arg1 default].
25+
26+
test_nested_cmd lvl1Cmd1 lvl2Cmd2 [OPTIONS]...
27+
28+
The following options are available:
29+
30+
--lvl2-cmd2-arg1 lvl2Cmd2Arg1 desc [=lvl2Cmd2Arg1 default].
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Usage:
2+
3+
test_nested_cmd lvl1Cmd1 [OPTIONS]... command
4+
5+
The following options are available:
6+
7+
--top-arg1 topArg1 desc [=topArg1 default].
8+
--lvl1-cmd1-arg1 lvl1Cmd1Arg1 desc [=lvl1Cmd1Arg1 default].
9+
10+
Available sub-commands:
11+
12+
test_nested_cmd lvl1Cmd1 lvl2Cmd1 [OPTIONS]...
13+
14+
The following options are available:
15+
16+
--lvl2-cmd1-arg1 lvl2Cmd1Arg1 desc [=lvl2Cmd1Arg1 default].
17+
18+
test_nested_cmd lvl1Cmd1 lvl2Cmd2 [OPTIONS]...
19+
20+
The following options are available:
21+
22+
--lvl2-cmd2-arg1 lvl2Cmd2Arg1 desc [=lvl2Cmd2Arg1 default].
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Usage:
2+
3+
test_nested_cmd lvl1Cmd1 lvl2Cmd2 [OPTIONS]...
4+
5+
The following options are available:
6+
7+
--top-arg1 topArg1 desc [=topArg1 default].
8+
--lvl1-cmd1-arg1 lvl1Cmd1Arg1 desc [=lvl1Cmd1Arg1 default].
9+
--lvl2-cmd2-arg1 lvl2Cmd2Arg1 desc [=lvl2Cmd2Arg1 default].

tests/help/test_nested_cmd.nim

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import ../../confutils
2+
3+
type
4+
Lvl1Cmd = enum
5+
lvl1NoCommand
6+
lvl1Cmd1
7+
8+
Lvl2Cmd = enum
9+
lvl2Cmd1
10+
lvl2Cmd2
11+
12+
TestConf = object
13+
topArg1 {.
14+
defaultValue: "topArg1 default"
15+
desc: "topArg1 desc"
16+
name: "top-arg1" }: string
17+
18+
case cmd {.
19+
command
20+
defaultValue: Lvl1Cmd.lvl1NoCommand }: Lvl1Cmd
21+
of Lvl1Cmd.lvl1NoCommand:
22+
lvl1NoCommandArg1 {.
23+
defaultValue: "lvl1NoCommandArg1 default"
24+
desc: "lvl1NoCommandArg1 desc"
25+
name: "lvl1-no-command-arg1" }: string
26+
of Lvl1Cmd.lvl1Cmd1:
27+
lvl1Cmd1Arg1 {.
28+
defaultValue: "lvl1Cmd1Arg1 default"
29+
desc: "lvl1Cmd1Arg1 desc"
30+
name: "lvl1-cmd1-arg1" }: string
31+
32+
case lvl2Cmd {.command.}: Lvl2Cmd
33+
of Lvl2Cmd.lvl2Cmd1:
34+
lvl2Cmd1Arg1 {.
35+
defaultValue: "lvl2Cmd1Arg1 default"
36+
desc: "lvl2Cmd1Arg1 desc"
37+
name: "lvl2-cmd1-arg1" }: string
38+
of Lvl2Cmd.lvl2Cmd2:
39+
lvl2Cmd2Arg1 {.
40+
defaultValue: "lvl2Cmd2Arg1 default"
41+
desc: "lvl2Cmd2Arg1 desc"
42+
name: "lvl2-cmd2-arg1" }: string
43+
44+
let c = TestConf.load(termWidth = int.high)

tests/test_all.nim

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import
1414
test_parsecmdarg,
1515
test_pragma,
1616
test_qualified_ident,
17-
test_nested_cmd
17+
test_nested_cmd,
18+
test_help
1819

1920
when defined(windows):
2021
import test_winreg

tests/test_help.nim

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# confutils
2+
# Copyright (c) 2018-2025 Status Research & Development GmbH
3+
# Licensed under either of
4+
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5+
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
6+
# at your option.
7+
# This file may not be copied, modified, or distributed except according to
8+
# those terms.
9+
10+
import
11+
std/[os, osproc, strutils],
12+
unittest2,
13+
stew/byteutils,
14+
../confutils
15+
16+
const helpPath = "tests" / "help"
17+
const snapshotsPath = helpPath / "snapshots"
18+
19+
func normalizeHelp(s: string): string =
20+
s.replace("\x1B[0m", "")
21+
.replace("\r\n", "\n")
22+
.strip(leading = false)
23+
24+
func cmdsToName(cmds: string): string =
25+
if cmds.len == 0:
26+
""
27+
else:
28+
"_" & cmds.replace(" ", "_")
29+
30+
proc cmdTest(cmdName: string, cmds = "") =
31+
let fname = helpPath / cmdName
32+
var build = "nim c --verbosity:0 --hints:off -d:confutilsNoColors"
33+
if NimMajor < 2:
34+
build.add " -d:nimOldCaseObjects"
35+
let buildRes = execCmdEx(build & " " & fname & ".nim")
36+
if buildRes.exitCode != 0:
37+
checkpoint "Build output: " & buildRes.output
38+
fail()
39+
else:
40+
let res = execCmdEx(fname & " " & cmds & " --help")
41+
let output = res.output.normalizeHelp()
42+
let snapshot = snapshotsPath / cmdName & cmds.cmdsToName() & ".txt"
43+
if res.exitCode != 0:
44+
checkpoint "Run output: " & res.output
45+
fail()
46+
elif not fileExists(snapshot):
47+
writeFile(snapshot, output)
48+
checkpoint "Snapshot created: " & snapshot
49+
fail()
50+
else:
51+
let expected = readFile(snapshot).normalizeHelp()
52+
checkpoint "Cmd output: " & $output.toBytes()
53+
checkpoint "Snapshot: " & $expected.toBytes()
54+
check output == expected
55+
56+
suite "test --help":
57+
test "test test_nested_cmd":
58+
cmdTest("test_nested_cmd", "")
59+
60+
test "test test_nested_cmd lvl1Cmd1":
61+
cmdTest("test_nested_cmd", "lvl1Cmd1")
62+
63+
test "test test_nested_cmd lvl1Cmd1 lvl2Cmd2":
64+
cmdTest("test_nested_cmd", "lvl1Cmd1 lvl2Cmd2")

0 commit comments

Comments
 (0)