1
2
3
4
5
6 package dev.metaschema.cli.processor.command;
7
8 import org.apache.commons.cli.CommandLine;
9 import org.apache.commons.cli.Option;
10
11 import java.io.File;
12 import java.io.IOException;
13 import java.io.OutputStreamWriter;
14 import java.io.PrintWriter;
15 import java.io.Writer;
16 import java.nio.charset.StandardCharsets;
17 import java.nio.file.Files;
18 import java.nio.file.Path;
19 import java.util.Collection;
20 import java.util.List;
21 import java.util.Locale;
22
23 import dev.metaschema.cli.processor.CLIProcessor;
24 import dev.metaschema.cli.processor.CallingContext;
25 import dev.metaschema.cli.processor.ExitCode;
26 import dev.metaschema.cli.processor.completion.CompletionScriptGenerator;
27 import dev.metaschema.core.util.ObjectUtils;
28 import edu.umd.cs.findbugs.annotations.NonNull;
29
30
31
32
33
34
35
36 public class ShellCompletionCommand
37 extends AbstractTerminalCommand {
38
39 @NonNull
40 private static final String COMMAND = "shell-completion";
41
42 @NonNull
43 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
44 ExtraArgument.newInstance("shell", true)));
45
46 @NonNull
47 private static final Option TO_OPTION = ObjectUtils.notNull(
48 Option.builder()
49 .longOpt("to")
50 .hasArg()
51 .argName("FILE")
52 .type(File.class)
53 .desc("write completion script to this file instead of stdout")
54 .get());
55
56
57
58
59 public enum Shell {
60
61 BASH("bash"),
62
63 ZSH("zsh");
64
65 @NonNull
66 private final String name;
67
68 Shell(@NonNull String name) {
69 this.name = name;
70 }
71
72
73
74
75
76
77 @NonNull
78 public String getName() {
79 return name;
80 }
81
82
83
84
85
86
87
88
89
90
91 @NonNull
92 public static Shell fromString(@NonNull String value) {
93 String normalized = value.toLowerCase(Locale.ROOT);
94 for (Shell shell : values()) {
95 if (shell.name.equals(normalized)) {
96 return shell;
97 }
98 }
99 throw new IllegalArgumentException(
100 "Unknown shell: " + value + ". Supported shells: bash, zsh");
101 }
102 }
103
104 @Override
105 public String getName() {
106 return COMMAND;
107 }
108
109 @Override
110 public String getDescription() {
111 return "Generate shell completion script for bash or zsh";
112 }
113
114 @SuppressWarnings("null")
115 @Override
116 public Collection<? extends Option> gatherOptions() {
117 return List.of(TO_OPTION);
118 }
119
120 @Override
121 public List<ExtraArgument> getExtraArguments() {
122 return EXTRA_ARGUMENTS;
123 }
124
125 @Override
126 public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
127 return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
128 }
129
130
131
132
133
134
135
136
137
138
139
140 protected void executeCommand(
141 @NonNull CallingContext callingContext,
142 @NonNull CommandLine cmdLine) throws CommandExecutionException {
143
144 List<String> extraArgs = cmdLine.getArgList();
145 if (extraArgs.isEmpty()) {
146 throw new CommandExecutionException(
147 ExitCode.INVALID_ARGUMENTS,
148 "Shell type is required. Supported shells: bash, zsh");
149 }
150
151 Shell shell;
152 try {
153 shell = Shell.fromString(ObjectUtils.notNull(extraArgs.get(0)));
154 } catch (IllegalArgumentException ex) {
155 throw new CommandExecutionException(ExitCode.INVALID_ARGUMENTS, ex.getMessage());
156 }
157
158 CLIProcessor processor = callingContext.getCLIProcessor();
159 CompletionScriptGenerator generator = new CompletionScriptGenerator(
160 processor.getExec(),
161 processor.getTopLevelCommands());
162
163 String script = shell == Shell.BASH
164 ? generator.generateBashCompletion()
165 : generator.generateZshCompletion();
166
167 String outputFile = cmdLine.getOptionValue(TO_OPTION);
168 if (outputFile != null) {
169 writeToFile(outputFile, script);
170 } else {
171 writeToStdout(script);
172 }
173 }
174
175 private static void writeToFile(@NonNull String outputFile, @NonNull String script)
176 throws CommandExecutionException {
177 Path path = resolveAgainstCWD(ObjectUtils.notNull(Path.of(outputFile)));
178 try (Writer writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
179 writer.write(script);
180 } catch (IOException ex) {
181 throw new CommandExecutionException(
182 ExitCode.IO_ERROR,
183 "Failed to write completion script to: " + path,
184 ex);
185 }
186 }
187
188 private static void writeToStdout(@NonNull String script) {
189 PrintWriter writer = new PrintWriter(
190 new OutputStreamWriter(System.out, StandardCharsets.UTF_8), true);
191 writer.print(script);
192 writer.flush();
193 }
194 }