• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 36534428096

29 Sep 2026 07:03AM UTC coverage: 73.865% (-0.3%) from 74.13%
36534428096

push

github

web-flow
#2293: rework spyder as ide tool commandlet (#2333)

5732 of 8665 branches covered (66.15%)

Branch coverage included in aggregate %.

15101 of 19539 relevant lines covered (77.29%)

3.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

9.25
cli/src/main/java/com/devonfw/tools/ide/commandlet/ShellCommandlet.java
1
package com.devonfw.tools.ide.commandlet;
2

3
import java.io.IOException;
4
import java.nio.file.Path;
5
import java.nio.file.Paths;
6
import java.util.Iterator;
7

8
import org.fusesource.jansi.AnsiConsole;
9
import org.jline.reader.EndOfFileException;
10
import org.jline.reader.LineReader;
11
import org.jline.reader.LineReaderBuilder;
12
import org.jline.reader.MaskingCallback;
13
import org.jline.reader.Parser;
14
import org.jline.reader.UserInterruptException;
15
import org.jline.reader.impl.DefaultParser;
16
import org.jline.terminal.Terminal;
17
import org.jline.terminal.TerminalBuilder;
18
import org.jline.widget.AutosuggestionWidgets;
19
import org.slf4j.Logger;
20
import org.slf4j.LoggerFactory;
21

22
import com.devonfw.tools.ide.cli.CliArgument;
23
import com.devonfw.tools.ide.cli.CliArguments;
24
import com.devonfw.tools.ide.cli.CliException;
25
import com.devonfw.tools.ide.completion.IdeCompleter;
26
import com.devonfw.tools.ide.context.AbstractIdeContext;
27
import com.devonfw.tools.ide.context.IdeContext;
28
import com.devonfw.tools.ide.property.BooleanProperty;
29
import com.devonfw.tools.ide.property.KeywordProperty;
30
import com.devonfw.tools.ide.property.Property;
31

32
/**
33
 * {@link AbstractCommandlet} for internal interactive shell with build-in auto-completion and help.
34
 */
35
public final class ShellCommandlet extends AbstractCommandlet {
36

37
  private static final Logger LOG = LoggerFactory.getLogger(ShellCommandlet.class);
4 ✔
38

39
  private static final int AUTOCOMPLETER_MAX_RESULTS = 50;
40

41
  private static final int RC_EXIT = 987654321;
42

43
  private static final String EXIT_COMMAND = "exit";
44

45
  private static final String IDE_PREFIX = "ide ";
46

47
  /**
48
   * The constructor.
49
   *
50
   * @param context the {@link IdeContext}.
51
   */
52
  public ShellCommandlet(IdeContext context) {
53

54
    super(context);
3 ✔
55
    addKeyword(getName());
4 ✔
56
  }
1 ✔
57

58
  @Override
59
  public String getName() {
60

61
    return "shell";
2 ✔
62
  }
63

64
  @Override
65
  public boolean isIdeHomeRequired() {
66

67
    return false;
2 ✔
68
  }
69

70
  @Override
71
  protected void doRun() {
72

73
    try {
74
      Parser parser = new DefaultParser();
×
75
      try (Terminal terminal = TerminalBuilder.builder().build()) {
×
76
        IdeCompleter completer = new IdeCompleter((AbstractIdeContext) this.context);
×
77

78
        LineReader reader = LineReaderBuilder.builder().terminal(terminal).completer(completer).parser(parser)
×
79
            .variable(LineReader.LIST_MAX, AUTOCOMPLETER_MAX_RESULTS).build();
×
80

81
        // Create autosuggestion widgets
82
        AutosuggestionWidgets autosuggestionWidgets = new AutosuggestionWidgets(reader);
×
83
        // Enable autosuggestions
84
        autosuggestionWidgets.enable();
×
85

86
        // TODO: implement TailTipWidgets, see: https://github.com/devonfw/IDEasy/issues/169
87

88
        String rightPrompt = null;
×
89
        String line;
90

91
        AnsiConsole.systemInstall();
×
92
        while (true) {
93
          try {
94
            String cwdPath = String.valueOf(context.getCwd());
×
95
            String prompt = cwdPath + (cwdPath.length() <= 80 ? "" : System.lineSeparator()) + "$ ";
×
96
            line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, IDE_PREFIX);
×
97
            line = normalizeLine(line);
×
98
            if (EXIT_COMMAND.equals(line)) {
×
99
              return;
×
100
            }
101
            reader.getHistory().add(line);
×
102
            int rc = runCommand(line);
×
103
            if (rc == RC_EXIT) {
×
104
              return;
×
105
            }
106
          } catch (UserInterruptException e) {
×
107
            // Ignore CTRL+C
108
            return;
×
109
          } catch (EndOfFileException e) {
×
110
            // CTRL+D
111
            return;
×
112
          } finally {
113
            AnsiConsole.systemUninstall();
×
114
          }
×
115
        }
116

117
      } catch (IOException e) {
×
118
        throw new RuntimeException(e);
×
119
      }
120
    } catch (CliException e) {
×
121
      throw e;
×
122
    } catch (Exception e) {
×
123
      throw new RuntimeException("Unexpected error during interactive auto-completion", e);
×
124
    }
125
  }
126

127
  /**
128
   * Strips the pre-filled {@link #IDE_PREFIX} from the given raw input line, so that a command entered after leaving the prefix untouched (e.g.
129
   * {@code ide status}) behaves the same as if only {@code status} was entered. This allows the user to remove the prefix via backspace to enter a non-IDEasy
130
   * command (e.g. {@code cd}) without the misleading {@code ide} prefix.
131
   *
132
   * @param rawLine the raw line as read from the {@link LineReader}.
133
   * @return the normalized line ready to be passed to {@link #runCommand(String)}.
134
   */
135
  static String normalizeLine(String rawLine) {
136

137
    String line = rawLine.trim();
3 ✔
138
    if (line.equals(IDE_PREFIX.trim())) {
5 ✔
139
      return "";
2 ✔
140
    } else if (line.startsWith(IDE_PREFIX)) {
4 ✔
141
      return line.substring(IDE_PREFIX.length()).trim();
6 ✔
142
    }
143
    return line;
2 ✔
144
  }
145

146
  /**
147
   * Converts String of arguments to array and runs the command
148
   *
149
   * @param args String of arguments
150
   * @return status code
151
   */
152
  private int runCommand(String args) {
153

154
    if (EXIT_COMMAND.equals(args) || "quit".equals(args)) {
×
155
      return RC_EXIT;
×
156
    }
157
    String[] arguments = args.split(" ", 0);
×
158
    CliArguments cliArgs = new CliArguments(arguments);
×
159
    cliArgs.next();
×
160

161
    if ("cd".equals(arguments[0])) {
×
162
      return changeDirectory(cliArgs);
×
163
    }
164

165
    return ((AbstractIdeContext) this.context).run(cliArgs);
×
166
  }
167

168
  private int changeDirectory(CliArguments cliArgs) {
169
    if (!cliArgs.hasNext()) {
×
170
      Path homeDir = this.context.getUserHome();
×
171
      context.setCwd(homeDir, context.getWorkspaceName(), context.getIdeHome());
×
172
      return 0;
×
173
    }
174

175
    String targetDir = String.valueOf(cliArgs.next());
×
176
    Path path = Paths.get(targetDir);
×
177

178
    // If the given path is relative, resolve it relative to the current directory
179
    if (!path.isAbsolute()) {
×
180
      path = context.getCwd().resolve(targetDir).normalize();
×
181
    }
182

183
    if (context.getFileAccess().isExpectedFolder(path)) {
×
184
      context.setCwd(path, context.getWorkspaceName(), context.getIdeHome());
×
185
      return 0;
×
186
    } else {
187
      return 1;
×
188
    }
189
  }
190

191
  /**
192
   * @param argument the current {@link CliArgument} (position) to match.
193
   * @param commandlet the potential {@link AbstractCommandlet} to match.
194
   * @return {@code true} if the given {@link AbstractCommandlet} matches to the given {@link CliArgument}(s) and those have been applied (set in the
195
   *     {@link AbstractCommandlet} and {@link AbstractCommandlet#validate() validated}), {@code false} otherwise (the {@link AbstractCommandlet} did not match
196
   *     and we have to try a different candidate).
197
   */
198
  private boolean apply(CliArgument argument, AbstractCommandlet commandlet) {
199

200
    LOG.trace("Trying to match arguments to commandlet {}", commandlet.getName());
×
201
    CliArgument currentArgument = argument;
×
202
    Iterator<Property<?>> valueIterator = commandlet.getValues().iterator();
×
203
    Property<?> currentProperty = null;
×
204
    boolean endOpts = false;
×
205
    while (!currentArgument.isEnd()) {
×
206
      if (currentArgument.isEndOptions()) {
×
207
        endOpts = true;
×
208
      } else {
209
        String arg = currentArgument.get();
×
210
        LOG.trace("Trying to match argument '{}'", currentArgument);
×
211
        if ((currentProperty != null) && (currentProperty.isExpectValue())) {
×
212
          currentProperty.setValueAsString(arg, this.context);
×
213
          if (!currentProperty.isMultiValued()) {
×
214
            currentProperty = null;
×
215
          }
216
        } else {
217
          Property<?> property = null;
×
218
          if (!endOpts) {
×
219
            property = commandlet.getOption(currentArgument.getKey());
×
220
          }
221
          if (property == null) {
×
222
            if (!valueIterator.hasNext()) {
×
223
              LOG.trace("No option or next value found");
×
224
              return false;
×
225
            }
226
            currentProperty = valueIterator.next();
×
227
            LOG.trace("Next value candidate is {}", currentProperty);
×
228
            if (currentProperty instanceof KeywordProperty keyword) {
×
229
              if (keyword.matches(arg)) {
×
230
                keyword.setValue(Boolean.TRUE);
×
231
                LOG.trace("Keyword matched");
×
232
              } else {
233
                LOG.trace("Missing keyword");
×
234
                return false;
×
235
              }
236
            } else {
237
              boolean success = currentProperty.assignValueAsString(arg, this.context, commandlet);
×
238
              if (!success && currentProperty.isRequired()) {
×
239
                return false;
×
240
              }
241
            }
242
            if ((currentProperty != null) && !currentProperty.isMultiValued()) {
×
243
              currentProperty = null;
×
244
            }
245
          } else {
246
            LOG.trace("Found option by name");
×
247
            String value = currentArgument.getValue();
×
248
            if (value != null) {
×
249
              property.setValueAsString(value, this.context);
×
250
            } else if (property instanceof BooleanProperty) {
×
251
              ((BooleanProperty) property).setValue(Boolean.TRUE);
×
252
            } else {
253
              currentProperty = property;
×
254
              if (property.isEndOptions()) {
×
255
                endOpts = true;
×
256
              }
257
              throw new UnsupportedOperationException("not implemented");
×
258
            }
259
          }
260
        }
261
      }
262
      currentArgument = currentArgument.getNext(!endOpts);
×
263
    }
264
    return commandlet.validate().isValid();
×
265
  }
266
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc