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 }