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