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                  ░▒▓██████████████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓███████▓▒░  ░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░\s
95                  ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░       ░▒▓█▓▒░░▒▓█▓▒░\s
96                  ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░       ░▒▓█▓▒░░▒▓█▓▒░\s
97                  ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓██████▓▒░ ░▒▓████████▓▒░\s
98                  ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░  ░▒▓█▓▓█▓▒░  ░▒▓█▓▒░░▒▓█▓▒░       ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░\s
99                  ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░  ░▒▓█▓▓█▓▒░  ░▒▓█▓▒░░▒▓█▓▒░       ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░\s
100                 ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░   ░▒▓██▓▒░   ░▒▓█▓▒░░▒▓█▓▒░░▒▓███████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░""";
101         context.writer.accept(banner);
102         if (!context.options().showVersion().orElse(false)) {
103             context.writer.accept(CLIReportingUtils.showVersionMinimal());
104         }
105         context.writer.accept("");
106 
107         try (holder) {
108             SimpleSystemRegistryImpl systemRegistry =
109                     new SimpleSystemRegistryImpl(parser, context.terminal, context.cwd, configPath) {
110                         @Override
111                         public boolean isCommandOrScript(String command) {
112                             return command.startsWith("!") || super.isCommandOrScript(command);
113                         }
114                     };
115             systemRegistry.setCommandRegistries(holder.getCommandRegistries());
116 
117             Path history = context.userDirectory.resolve(".mvnsh_history");
118             LineReader reader = LineReaderBuilder.builder()
119                     .terminal(context.terminal)
120                     .history(new DefaultHistory())
121                     .highlighter(new ReplHighlighter())
122                     .completer(systemRegistry.completer())
123                     .parser(parser)
124                     .variable(LineReader.LIST_MAX, 50) // max tab completion candidates
125                     .variable(LineReader.HISTORY_FILE, history)
126                     .variable(LineReader.OTHERS_GROUP_NAME, "Others")
127                     .variable(LineReader.COMPLETION_STYLE_GROUP, "fg:blue,bold")
128                     .variable("HELP_COLORS", "ti=1;34:co=38:ar=3:op=33:de=90")
129                     .option(LineReader.Option.GROUP_PERSIST, true)
130                     .build();
131             builtins.setLineReader(reader);
132             systemRegistry.setLineReader(reader);
133             new TailTipWidgets(reader, systemRegistry::commandDescription, 5, TailTipWidgets.TipType.COMPLETER);
134             KeyMap<Binding> keyMap = reader.getKeyMaps().get("main");
135             keyMap.bind(new Reference("tailtip-toggle"), KeyMap.alt("s"));
136 
137             // start the shell and process input until the user quits with Ctrl-D
138             AtomicReference<Exception> failure = new AtomicReference<>();
139             while (true) {
140                 try {
141                     failure.set(null);
142                     systemRegistry.cleanUp();
143                     Thread commandThread = new Thread(() -> {
144                         try {
145                             systemRegistry.execute(reader.readLine(
146                                     context.cwd.get().getFileName().toString() + " mvnsh> ",
147                                     null,
148                                     (MaskingCallback) null,
149                                     null));
150                         } catch (Exception e) {
151                             failure.set(e);
152                         }
153                     });
154                     context.terminal.handle(Terminal.Signal.INT, signal -> commandThread.interrupt());
155                     commandThread.start();
156                     commandThread.join();
157                     if (failure.get() != null) {
158                         throw failure.get();
159                     }
160                 } catch (UserInterruptException e) {
161                     // Ignore
162                     // return CANCELED;
163                 } catch (EndOfFileException e) {
164                     return OK;
165                 } catch (SystemRegistryImpl.UnknownCommandException e) {
166                     context.writer.accept(context.invokerRequest
167                             .messageBuilderFactory()
168                             .builder()
169                             .error(e.getMessage())
170                             .build());
171                 } catch (Exception e) {
172                     systemRegistry.trace(e);
173                     context.writer.accept(context.invokerRequest
174                             .messageBuilderFactory()
175                             .builder()
176                             .error("Error: " + e.getMessage())
177                             .build());
178                     if (context.options().showErrors().orElse(false)) {
179                         e.printStackTrace(context.terminal.writer());
180                     }
181                     return ERROR;
182                 }
183             }
184         }
185     }
186 
187     private static class ReplHighlighter extends DefaultHighlighter {
188         @Override
189         protected void commandStyle(LineReader reader, AttributedStringBuilder sb, boolean enable) {
190             if (enable) {
191                 if (reader.getTerminal().getNumericCapability(InfoCmp.Capability.max_colors) >= 256) {
192                     sb.style(AttributedStyle.DEFAULT.bold().foreground(69));
193                 } else {
194                     sb.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.CYAN));
195                 }
196             } else {
197                 sb.style(AttributedStyle.DEFAULT.boldOff().foregroundOff());
198             }
199         }
200     }
201 }