View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.cling.invoker.mvnsh;
20  
21  import java.nio.file.Path;
22  import java.util.Map;
23  import java.util.concurrent.atomic.AtomicReference;
24  
25  import org.apache.maven.api.cli.InvokerRequest;
26  import org.apache.maven.api.services.Lookup;
27  import org.apache.maven.cling.invoker.LookupContext;
28  import org.apache.maven.cling.invoker.LookupInvoker;
29  import org.apache.maven.cling.utils.CLIReportingUtils;
30  import org.jline.builtins.ConfigurationPath;
31  import org.jline.console.impl.Builtins;
32  import org.jline.console.impl.SimpleSystemRegistryImpl;
33  import org.jline.console.impl.SystemRegistryImpl;
34  import org.jline.keymap.KeyMap;
35  import org.jline.reader.Binding;
36  import org.jline.reader.EndOfFileException;
37  import org.jline.reader.LineReader;
38  import org.jline.reader.LineReaderBuilder;
39  import org.jline.reader.MaskingCallback;
40  import org.jline.reader.Reference;
41  import org.jline.reader.UserInterruptException;
42  import org.jline.reader.impl.DefaultHighlighter;
43  import org.jline.reader.impl.DefaultParser;
44  import org.jline.reader.impl.history.DefaultHistory;
45  import org.jline.terminal.Terminal;
46  import org.jline.utils.AttributedStringBuilder;
47  import org.jline.utils.AttributedStyle;
48  import org.jline.utils.InfoCmp;
49  import org.jline.widget.TailTipWidgets;
50  
51  /**
52   * mvnsh invoker implementation.
53   */
54  public class ShellInvoker extends LookupInvoker<LookupContext> {
55  
56      public ShellInvoker(Lookup protoLookup) {
57          super(protoLookup, null);
58      }
59  
60      @Override
61      protected LookupContext createContext(InvokerRequest invokerRequest) {
62          return new LookupContext(invokerRequest);
63      }
64  
65      public static final int OK = 0; // OK
66      public static final int ERROR = 1; // "generic" error
67  
68      @Override
69      protected int execute(LookupContext context) throws Exception {
70          // set up JLine built-in commands
71          ConfigurationPath configPath = new ConfigurationPath(context.cwd.get(), context.cwd.get());
72          Builtins builtins = new Builtins(context.cwd, configPath, null);
73          builtins.rename(Builtins.Command.TTOP, "top");
74          builtins.alias("zle", "widget");
75          builtins.alias("bindkey", "keymap");
76  
77          ShellCommandRegistryHolder holder = new ShellCommandRegistryHolder();
78          holder.addCommandRegistry(builtins);
79  
80          // gather commands
81          Map<String, ShellCommandRegistryFactory> factories =
82                  context.lookup.lookupMap(ShellCommandRegistryFactory.class);
83          for (Map.Entry<String, ShellCommandRegistryFactory> entry : factories.entrySet()) {
84              holder.addCommandRegistry(entry.getValue().createShellCommandRegistry(context));
85          }
86  
87          DefaultParser parser = new DefaultParser();
88          parser.setRegexCommand("[:]{0,1}[a-zA-Z!]{1,}\\S*"); // change default regex to support shell commands
89  
90          String banner =
91                  """
92  
93                  ░▒▓██████████████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓███████▓▒░  ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░\s
94                  ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░       ░▒▓█▓▒░░▒▓█▓▒░\s
95                  ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░       ░▒▓█▓▒░░▒▓█▓▒░\s
96                  ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓██████▓▒░ ░▒▓████████▓▒░\s
97                  ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░  ░▒▓█▓▓█▓▒░  ░▒▓█▓▒░░▒▓█▓▒░       ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░\s
98                  ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░  ░▒▓█▓▓█▓▒░  ░▒▓█▓▒░░▒▓█▓▒░       ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░\s
99                  ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░   ░▒▓██▓▒░   ░▒▓█▓▒░░▒▓█▓▒░░▒▓███████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░""";
100         context.writer.accept(banner);
101         if (!context.invokerRequest.options().showVersion().orElse(false)) {
102             context.writer.accept(CLIReportingUtils.showVersionMinimal());
103         }
104         context.writer.accept("");
105 
106         try (holder) {
107             SimpleSystemRegistryImpl systemRegistry =
108                     new SimpleSystemRegistryImpl(parser, context.terminal, context.cwd, configPath) {
109                         @Override
110                         public boolean isCommandOrScript(String command) {
111                             return command.startsWith("!") || super.isCommandOrScript(command);
112                         }
113                     };
114             systemRegistry.setCommandRegistries(holder.getCommandRegistries());
115 
116             Path history = context.userDirectory.resolve(".mvnsh_history");
117             LineReader reader = LineReaderBuilder.builder()
118                     .terminal(context.terminal)
119                     .history(new DefaultHistory())
120                     .highlighter(new ReplHighlighter())
121                     .completer(systemRegistry.completer())
122                     .parser(parser)
123                     .variable(LineReader.LIST_MAX, 50) // max tab completion candidates
124                     .variable(LineReader.HISTORY_FILE, history)
125                     .variable(LineReader.OTHERS_GROUP_NAME, "Others")
126                     .variable(LineReader.COMPLETION_STYLE_GROUP, "fg:blue,bold")
127                     .variable("HELP_COLORS", "ti=1;34:co=38:ar=3:op=33:de=90")
128                     .option(LineReader.Option.GROUP_PERSIST, true)
129                     .build();
130             builtins.setLineReader(reader);
131             systemRegistry.setLineReader(reader);
132             new TailTipWidgets(reader, systemRegistry::commandDescription, 5, TailTipWidgets.TipType.COMPLETER);
133             KeyMap<Binding> keyMap = reader.getKeyMaps().get("main");
134             keyMap.bind(new Reference("tailtip-toggle"), KeyMap.alt("s"));
135 
136             // start the shell and process input until the user quits with Ctrl-D
137             AtomicReference<Exception> failure = new AtomicReference<>();
138             while (true) {
139                 try {
140                     failure.set(null);
141                     systemRegistry.cleanUp();
142                     Thread commandThread = new Thread(() -> {
143                         try {
144                             systemRegistry.execute(reader.readLine(
145                                     context.cwd.get().getFileName().toString() + " mvnsh> ",
146                                     null,
147                                     (MaskingCallback) null,
148                                     null));
149                         } catch (Exception e) {
150                             failure.set(e);
151                         }
152                     });
153                     context.terminal.handle(Terminal.Signal.INT, signal -> commandThread.interrupt());
154                     commandThread.start();
155                     commandThread.join();
156                     if (failure.get() != null) {
157                         throw failure.get();
158                     }
159                 } catch (UserInterruptException e) {
160                     // Ignore
161                     // return CANCELED;
162                 } catch (EndOfFileException e) {
163                     return OK;
164                 } catch (SystemRegistryImpl.UnknownCommandException e) {
165                     context.writer.accept(context.invokerRequest
166                             .messageBuilderFactory()
167                             .builder()
168                             .error(e.getMessage())
169                             .build());
170                 } catch (Exception e) {
171                     systemRegistry.trace(e);
172                     context.writer.accept(context.invokerRequest
173                             .messageBuilderFactory()
174                             .builder()
175                             .error("Error: " + e.getMessage())
176                             .build());
177                     if (context.invokerRequest.options().showErrors().orElse(false)) {
178                         e.printStackTrace(context.terminal.writer());
179                     }
180                     return ERROR;
181                 }
182             }
183         }
184     }
185 
186     private static class ReplHighlighter extends DefaultHighlighter {
187         @Override
188         protected void commandStyle(LineReader reader, AttributedStringBuilder sb, boolean enable) {
189             if (enable) {
190                 if (reader.getTerminal().getNumericCapability(InfoCmp.Capability.max_colors) >= 256) {
191                     sb.style(AttributedStyle.DEFAULT.bold().foreground(69));
192                 } else {
193                     sb.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.CYAN));
194                 }
195             } else {
196                 sb.style(AttributedStyle.DEFAULT.boldOff().foregroundOff());
197             }
198         }
199     }
200 }