1 module argparse.completer;
2 
3 import argparse;
4 
5 import std.traits: getUDAs;
6 import std.sumtype: SumType;
7 
8 
9 
10 private template defaultCommandName(COMMAND)
11 {
12     static if(getUDAs!(COMMAND, CommandInfo).length > 0)
13         enum defaultCommandName = getUDAs!(COMMAND, CommandInfo)[0].names[0];
14     else
15         enum defaultCommandName = COMMAND.stringof;
16 }
17 
18 
19 package struct Complete(COMMAND)
20 {
21     @(Command("init")
22     .Description("Print initialization script for shell completion.")
23     .ShortDescription("Print initialization script.")
24     )
25     struct Init
26     {
27         @MutuallyExclusive
28         {
29             @(NamedArgument.Description("Provide completion for bash."))
30             bool bash;
31             @(NamedArgument.Description("Provide completion for zsh."))
32             bool zsh;
33             @(NamedArgument.Description("Provide completion for tcsh."))
34             bool tcsh;
35             @(NamedArgument.Description("Provide completion for fish."))
36             bool fish;
37         }
38 
39         @(NamedArgument.Description("Path to completer. Default value: path to this executable."))
40         string completerPath; // path to this binary
41 
42         @(NamedArgument.Description("Command name. Default value: "~defaultCommandName!COMMAND~"."))
43         string commandName = defaultCommandName!COMMAND;   // command to complete
44 
45         void execute(Config config)()
46         {
47             import std.stdio: writeln;
48 
49             if(completerPath.length == 0)
50             {
51                 import std.file: thisExePath;
52                 completerPath = thisExePath();
53             }
54 
55             string commandNameArg;
56             if(commandName != defaultCommandName!COMMAND)
57                 commandNameArg = " --commandName "~commandName;
58 
59             if(bash)
60             {
61                 // According to bash documentation:
62                 //   When the function or command is invoked, the first argument ($1) is the name of the command whose
63                 //   arguments are being completed, the second` argument ($2) is the word being completed, and the third
64                 //   argument ($3) is the word preceding the word being completed on the current command line.
65                 //
66                 // So we add "---" argument to distinguish between the end of actual parameters and those that were added by bash
67 
68                 writeln("# Add this source command into .bashrc:");
69                 writeln("#       source <(", completerPath, " init --bash", commandNameArg, ")");
70                 // 'eval' is used to properly get arguments with spaces. For example, in case of "1 2" argument,
71                 // we will get "1 2" as is, compare to "\"1", "2\"" without 'eval'.
72                 writeln("complete -C 'eval ", completerPath, " --bash -- $COMP_LINE ---' ", commandName);
73             }
74             else if(zsh)
75             {
76                 // We use bash completion for zsh
77                 writeln("# Ensure that you called compinit and bashcompinit like below in your .zshrc:");
78                 writeln("#       autoload -Uz compinit && compinit");
79                 writeln("#       autoload -Uz bashcompinit && bashcompinit");
80                 writeln("# And then add this source command after them into your .zshrc:");
81                 writeln("#       source <(", completerPath, " init --zsh", commandNameArg, ")");
82                 writeln("complete -C 'eval ", completerPath, " --bash -- $COMP_LINE ---' ", commandName);
83             }
84             else if(tcsh)
85                 {
86                     // Comments start with ":" in tsch
87                     writeln(": Add this eval command into .tcshrc:   ;");
88                     writeln(":       eval `", completerPath, " init --tcsh", commandNameArg, "`     ;");
89                     writeln("complete ", commandName, " 'p,*,`", completerPath, " --tcsh -- $COMMAND_LINE`,'");
90                 }
91                 else if(fish)
92                     {
93                         writeln("# Add this source command into ~/.config/fish/config.fish:");
94                         writeln("#       ", completerPath, " init --fish", commandNameArg, " | source");
95                         writeln("complete -c ", commandName, " -a '(COMMAND_LINE=(commandline -p) ", completerPath, " --fish -- (commandline -op))' --no-files");
96                     }
97         }
98     }
99 
100     @(Command("complete")
101     .Description("Print completion.")
102     )
103     struct Complete
104     {
105         @MutuallyExclusive
106         {
107             @(NamedArgument.Description("Provide completion for bash."))
108             bool bash;
109             @(NamedArgument.Description("Provide completion for tcsh."))
110             bool tcsh;
111             @(NamedArgument.Description("Provide completion for fish."))
112             bool fish;
113         }
114 
115         @TrailingArguments
116         string[] args;
117 
118         void execute(Config config)()
119         {
120             import std.process: environment;
121             import std.stdio: writeln;
122             import std.algorithm: each;
123 
124             if(bash)
125             {
126                 // According to bash documentation:
127                 //   When the function or command is invoked, the first argument ($1) is the name of the command whose
128                 //   arguments are being completed, the second` argument ($2) is the word being completed, and the third
129                 //   argument ($3) is the word preceding the word being completed on the current command line.
130                 //
131                 // We don't use these arguments so we just remove those after "---" including itself
132                 while(args.length > 0 && args[$-1] != "---")
133                     args = args[0..$-1];
134 
135                 // Remove "---"
136                 if(args.length > 0 && args[$-1] == "---")
137                     args = args[0..$-1];
138 
139                 // COMP_LINE environment variable contains current command line so if it ends with space ' ' then we
140                 // should provide all available arguments. To do so, we add an empty argument
141                 auto cmdLine = environment.get("COMP_LINE", "");
142                 if(cmdLine.length > 0 && cmdLine[$-1] == ' ')
143                     args ~= "";
144             }
145             else if(tcsh || fish)
146             {
147                 // COMMAND_LINE environment variable contains current command line so if it ends with space ' ' then we
148                 // should provide all available arguments. To do so, we add an empty argument
149                 auto cmdLine = environment.get("COMMAND_LINE", "");
150                 if(cmdLine.length > 0 && cmdLine[$-1] == ' ')
151                     args ~= "";
152             }
153 
154             CLI!(config, COMMAND).completeArgs(args).each!writeln;
155         }
156     }
157 
158     @SubCommands
159     SumType!(Init, Default!Complete) cmd;
160 }