Skip to content

Commit d10dcd6

Browse files
authored
completion: add configlet completion subcommand (#631)
With this commit, running e.g. configlet completion --shell fish will output the fish completion script to stdout. This allows us to distribute completion scripts to the user without adding them to the release assets, which avoids cluttering every directory in which the user runs `fetch-configlet`. For example, the user can enable completions for fish by running: configlet completion -s fish > ~/.config/fish/completions/configlet.fish Closes: #630
1 parent 1a295da commit d10dcd6

File tree

5 files changed

+85
-12
lines changed

5 files changed

+85
-12
lines changed

Diff for: README.md

+12-7
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ Usage:
1111
configlet [global-options] <command> [command-options]
1212
1313
Commands:
14-
fmt Format the exercise '.meta/config.json' files
15-
generate Generate Concept Exercise 'introduction.md' files from 'introduction.md.tpl' files
16-
info Print some information about the track
17-
lint Check the track configuration for correctness
18-
sync Check or update Practice Exercise docs, metadata, and tests from 'problem-specifications'.
19-
Check or populate missing 'files' values for Concept/Practice Exercises from the track 'config.json'.
20-
uuid Output new (version 4) UUIDs, suitable for the value of a 'uuid' key
14+
completion Output a completion script for a given shell
15+
fmt Format the exercise '.meta/config.json' files
16+
generate Generate Concept Exercise 'introduction.md' files from 'introduction.md.tpl' files
17+
info Print some information about the track
18+
lint Check the track configuration for correctness
19+
sync Check or update Practice Exercise docs, metadata, and tests from 'problem-specifications'.
20+
Check or populate missing 'files' values for Concept/Practice Exercises from the track 'config.json'.
21+
uuid Output new (version 4) UUIDs, suitable for the value of a 'uuid' key
22+
23+
Options for completion:
24+
-s, --shell <shell> Choose the shell type (required)
25+
Allowed values: b[ash], f[ish]
2126
2227
Options for fmt:
2328
-e, --exercise <slug> Only operate on this exercise

Diff for: src/cli.nim

+29-3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ import pkg/cligen/parseopt3
44
type
55
ActionKind* = enum
66
actNil = "nil"
7+
actCompletion = "completion"
78
actFmt = "fmt"
89
actGenerate = "generate"
910
actInfo = "info"
1011
actLint = "lint"
1112
actSync = "sync"
1213
actUuid = "uuid"
1314

15+
Shell* = enum
16+
sNil = "nil"
17+
sBash = "bash"
18+
sFish = "fish"
19+
1420
SyncKind* = enum
1521
skDocs = "docs"
1622
skFilepaths = "filepaths"
@@ -26,6 +32,8 @@ type
2632
case kind*: ActionKind
2733
of actNil, actGenerate, actLint:
2834
discard
35+
of actCompletion:
36+
shell*: Shell
2937
of actFmt:
3038
# We can't name these fields `exercise`, `update`, and `yes` because we
3139
# use those names in `actSync`, and Nim doesn't yet support duplicate
@@ -62,6 +70,9 @@ type
6270
optTrackDir = "trackDir"
6371
optVerbosity = "verbosity"
6472

73+
# Options for `completion`
74+
optCompletionShell = "shell"
75+
6576
# Options for both `fmt` and `sync`
6677
optFmtSyncExercise = "exercise"
6778
optFmtSyncUpdate = "update"
@@ -133,8 +144,10 @@ func genHelpText: string =
133144
## Returns a string that describes the allowed values for an enum `T`.
134145
result = "Allowed values: "
135146
for val in T:
136-
result.add &"{($val)[0]}"
137-
result.add &"[{($val)[1 .. ^1]}], "
147+
let s = $val
148+
if s != "nil":
149+
result.add s[0]
150+
result.add &"[{s[1 .. ^1]}], "
138151
setLen(result, result.len - 2)
139152

140153
func genSyntaxStrings: tuple[syntax: array[Opt, string], maxLen: int] =
@@ -147,6 +160,7 @@ func genHelpText: string =
147160
case opt
148161
of optTrackDir: "dir"
149162
of optVerbosity: "verbosity"
163+
of optCompletionShell: "shell"
150164
of optFmtSyncExercise: "slug"
151165
of optSyncTests: "mode"
152166
of optUuidNum: "int"
@@ -174,6 +188,7 @@ func genHelpText: string =
174188

175189
const actionDescriptions: array[ActionKind, string] = [
176190
actNil: "",
191+
actCompletion: "Output a completion script for a given shell",
177192
actFmt: "Format the exercise '.meta/config.json' files",
178193
actGenerate: "Generate Concept Exercise 'introduction.md' files from 'introduction.md.tpl' files",
179194
actInfo: "Print some information about the track",
@@ -199,6 +214,8 @@ func genHelpText: string =
199214
optTrackDir: "Specify a track directory to use instead of the current directory",
200215
optVerbosity: &"The verbosity of output.\n" &
201216
&"{paddingOpt}{allowedValues(Verbosity)} (default: normal)",
217+
optCompletionShell: &"Choose the shell type (required)\n" &
218+
&"{paddingOpt}{allowedValues(Shell)}",
202219
optFmtSyncExercise: "Only operate on this exercise",
203220
optFmtSyncUpdate: "Prompt to update the unsynced track data",
204221
optFmtSyncYes: &"Auto-confirm prompts from --{$optFmtSyncUpdate} for updating docs, filepaths, and metadata",
@@ -330,7 +347,7 @@ func formatOpt(kind: CmdLineKind, key: string, val = ""): string =
330347
func init*(T: typedesc[Action], actionKind: ActionKind,
331348
scope: set[SyncKind] = {}): T =
332349
case actionKind
333-
of actNil, actFmt, actGenerate, actInfo, actLint:
350+
of actNil, actCompletion, actFmt, actGenerate, actInfo, actLint:
334351
T(kind: actionKind)
335352
of actSync:
336353
T(kind: actionKind, scope: scope)
@@ -463,6 +480,12 @@ proc handleOption(conf: var Conf; kind: CmdLineKind; key, val: string) =
463480
case conf.action.kind
464481
of actNil, actGenerate, actLint:
465482
discard
483+
of actCompletion:
484+
case opt
485+
of optCompletionShell:
486+
setActionOpt(shell, parseVal[Shell](kind, key, val))
487+
else:
488+
discard
466489
of actFmt:
467490
case opt
468491
of optFmtSyncExercise:
@@ -533,6 +556,9 @@ proc processCmdLine*: Conf =
533556
case result.action.kind
534557
of actNil:
535558
showHelp()
559+
of actCompletion:
560+
if result.action.shell == sNil:
561+
showError("Please choose a shell. For example: `configlet completion -s bash`")
536562
of actFmt, actGenerate, actInfo, actLint, actUuid:
537563
discard
538564
of actSync:

Diff for: src/completion/completion.nim

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import std/[os, strformat]
2+
import ../cli
3+
4+
proc readCompletions: array[Shell, string] =
5+
const repoRootDir = currentSourcePath().parentDir().parentDir().parentDir()
6+
const completionsDir = repoRootDir / "completions"
7+
for shell in sBash .. result.high:
8+
result[shell] = staticRead(completionsDir / &"configlet.{shell}")
9+
10+
proc completion*(shellKind: Shell) =
11+
const completions = readCompletions()
12+
stdout.write completions[shellKind]

Diff for: src/configlet.nim

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import std/posix
2-
import "."/[cli, fmt/fmt, info/info, generate/generate, lint/lint, logger,
3-
sync/sync, uuid/uuid]
2+
import "."/[cli, completion/completion, fmt/fmt, info/info, generate/generate,
3+
lint/lint, logger, sync/sync, uuid/uuid]
44

55
proc main =
66
onSignal(SIGTERM):
@@ -13,6 +13,8 @@ proc main =
1313
case conf.action.kind
1414
of actNil:
1515
discard
16+
of actCompletion:
17+
completion(conf.action.shell)
1618
of actFmt:
1719
fmt(conf)
1820
of actLint:

Diff for: tests/test_binary.nim

+28
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,32 @@ proc testsForGenerate(binaryPath: string) =
929929
test "and writes the `introduction.md` file as expected":
930930
checkNoDiff(trackDir)
931931

932+
proc testsForCompletion(binaryPath: string) =
933+
suite "completion":
934+
const completionsDir = repoRootDir / "completions"
935+
for shell in ["bash", "fish"]:
936+
test shell:
937+
let c = shell[0]
938+
# Convert platform-specific line endings (e.g. CR+LF on Windows) to LF
939+
# before comparing. The below `replace` makes the tests pass on Windows.
940+
let expected = readFile(completionsDir / &"configlet.{shell}").replace("\p", "\n")
941+
execAndCheck(0, &"{binaryPath} completion --shell {shell}", expected)
942+
execAndCheck(0, &"{binaryPath} completion --shell {c}", expected)
943+
execAndCheck(0, &"{binaryPath} completion -s {shell}", expected)
944+
execAndCheck(0, &"{binaryPath} completion -s {c}", expected)
945+
execAndCheck(0, &"{binaryPath} completion -s{c}", expected)
946+
for shell in ["powershell", "zsh"]:
947+
test &"{shell} (produces an error)":
948+
let (outp, exitCode) = execCmdEx(&"{binaryPath} completion -s {shell}")
949+
check:
950+
outp.contains(&"invalid value for '-s': '{shell}'")
951+
exitCode == 1
952+
test "the -s option is required":
953+
let (outp, exitCode) = execCmdEx(&"{binaryPath} completion")
954+
check:
955+
outp.contains(&"Please choose a shell. For example: `configlet completion -s bash`")
956+
exitCode == 1
957+
932958
proc main =
933959
const
934960
binaryExt =
@@ -1081,5 +1107,7 @@ proc main =
10811107

10821108
testsForGenerate(binaryPath)
10831109

1110+
testsForCompletion(binaryPath)
1111+
10841112
main()
10851113
{.used.}

0 commit comments

Comments
 (0)