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

devonfw / IDEasy / 12181235589

05 Dec 2024 01:58PM UTC coverage: 66.902% (-0.02%) from 66.917%
12181235589

push

github

web-flow
#508: enabled autocompletion for commandlet options (#833)

2527 of 4130 branches covered (61.19%)

Branch coverage included in aggregate %.

6577 of 9478 relevant lines covered (69.39%)

3.06 hits per line

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

61.57
cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java
1
package com.devonfw.tools.ide.context;
2

3
import java.io.BufferedReader;
4
import java.io.InputStreamReader;
5
import java.net.URL;
6
import java.net.URLConnection;
7
import java.nio.file.Files;
8
import java.nio.file.Path;
9
import java.util.ArrayList;
10
import java.util.HashMap;
11
import java.util.Iterator;
12
import java.util.List;
13
import java.util.Locale;
14
import java.util.Map;
15

16
import com.devonfw.tools.ide.cli.CliAbortException;
17
import com.devonfw.tools.ide.cli.CliArgument;
18
import com.devonfw.tools.ide.cli.CliArguments;
19
import com.devonfw.tools.ide.cli.CliException;
20
import com.devonfw.tools.ide.commandlet.Commandlet;
21
import com.devonfw.tools.ide.commandlet.CommandletManager;
22
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
23
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
24
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
25
import com.devonfw.tools.ide.common.SystemPath;
26
import com.devonfw.tools.ide.completion.CompletionCandidate;
27
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
28
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
29
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
30
import com.devonfw.tools.ide.environment.EnvironmentVariables;
31
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
32
import com.devonfw.tools.ide.environment.IdeSystem;
33
import com.devonfw.tools.ide.environment.IdeSystemImpl;
34
import com.devonfw.tools.ide.git.GitContext;
35
import com.devonfw.tools.ide.git.GitContextImpl;
36
import com.devonfw.tools.ide.git.GitUrl;
37
import com.devonfw.tools.ide.io.FileAccess;
38
import com.devonfw.tools.ide.io.FileAccessImpl;
39
import com.devonfw.tools.ide.log.IdeLogLevel;
40
import com.devonfw.tools.ide.log.IdeLogger;
41
import com.devonfw.tools.ide.log.IdeSubLogger;
42
import com.devonfw.tools.ide.merge.DirectoryMerger;
43
import com.devonfw.tools.ide.network.NetworkProxy;
44
import com.devonfw.tools.ide.os.SystemInfo;
45
import com.devonfw.tools.ide.os.SystemInfoImpl;
46
import com.devonfw.tools.ide.os.WindowsPathSyntax;
47
import com.devonfw.tools.ide.process.ProcessContext;
48
import com.devonfw.tools.ide.process.ProcessContextImpl;
49
import com.devonfw.tools.ide.process.ProcessResult;
50
import com.devonfw.tools.ide.property.Property;
51
import com.devonfw.tools.ide.repo.CustomToolRepository;
52
import com.devonfw.tools.ide.repo.CustomToolRepositoryImpl;
53
import com.devonfw.tools.ide.repo.DefaultToolRepository;
54
import com.devonfw.tools.ide.repo.ToolRepository;
55
import com.devonfw.tools.ide.step.Step;
56
import com.devonfw.tools.ide.step.StepImpl;
57
import com.devonfw.tools.ide.url.model.UrlMetadata;
58
import com.devonfw.tools.ide.validation.ValidationResult;
59
import com.devonfw.tools.ide.validation.ValidationResultValid;
60
import com.devonfw.tools.ide.validation.ValidationState;
61
import com.devonfw.tools.ide.variable.IdeVariables;
62

63
/**
64
 * Abstract base implementation of {@link IdeContext}.
65
 */
66
public abstract class AbstractIdeContext implements IdeContext {
67

68
  private static final GitUrl IDE_URLS_GIT = new GitUrl("https://github.com/devonfw/ide-urls.git", null);
7✔
69

70
  private final IdeStartContextImpl startContext;
71

72
  private Path ideHome;
73

74
  private final Path ideRoot;
75

76
  private Path confPath;
77

78
  protected Path settingsPath;
79

80
  private Path softwarePath;
81

82
  private Path softwareExtraPath;
83

84
  private Path softwareRepositoryPath;
85

86
  protected Path pluginsPath;
87

88
  private Path workspacePath;
89

90
  private String workspaceName;
91

92
  protected Path urlsPath;
93

94
  private Path tempPath;
95

96
  private Path tempDownloadPath;
97

98
  private Path cwd;
99

100
  private Path downloadPath;
101

102
  private Path toolRepositoryPath;
103

104
  protected Path userHome;
105

106
  private Path userHomeIde;
107

108
  private SystemPath path;
109

110
  private WindowsPathSyntax pathSyntax;
111

112
  private final SystemInfo systemInfo;
113

114
  private EnvironmentVariables variables;
115

116
  private final FileAccess fileAccess;
117

118
  protected CommandletManager commandletManager;
119

120
  protected ToolRepository defaultToolRepository;
121

122
  private CustomToolRepository customToolRepository;
123

124
  private DirectoryMerger workspaceMerger;
125

126
  protected UrlMetadata urlMetadata;
127

128
  protected Path defaultExecutionDirectory;
129

130
  private StepImpl currentStep;
131

132
  protected Boolean online;
133

134
  protected IdeSystem system;
135

136
  private NetworkProxy networkProxy;
137

138
  /**
139
   * The constructor.
140
   *
141
   * @param startContext the {@link IdeLogger}.
142
   * @param workingDirectory the optional {@link Path} to current working directory.
143
   */
144
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
145

146
    super();
2✔
147
    this.startContext = startContext;
3✔
148
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
149
    this.commandletManager = new CommandletManagerImpl(this);
6✔
150
    this.fileAccess = new FileAccessImpl(this);
6✔
151
    String workspace = WORKSPACE_MAIN;
2✔
152
    if (workingDirectory == null) {
2!
153
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
154
    } else {
155
      workingDirectory = workingDirectory.toAbsolutePath();
3✔
156
    }
157
    // detect IDE_HOME and WORKSPACE
158
    Path currentDir = workingDirectory;
2✔
159
    String name1 = "";
2✔
160
    String name2 = "";
2✔
161
    while (currentDir != null) {
2✔
162
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
163
      if (isIdeHome(currentDir)) {
4✔
164
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
165
          workspace = name2;
3✔
166
        }
167
        break;
168
      }
169
      name2 = name1;
2✔
170
      int nameCount = currentDir.getNameCount();
3✔
171
      if (nameCount >= 1) {
3✔
172
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
173
      }
174
      currentDir = currentDir.getParent();
3✔
175
    }
1✔
176

177
    // detection completed, initializing variables
178
    this.ideRoot = findIdeRoot(currentDir);
5✔
179

180
    setCwd(workingDirectory, workspace, currentDir);
5✔
181

182
    if (this.ideRoot == null) {
3✔
183
      this.toolRepositoryPath = null;
3✔
184
      this.urlsPath = null;
3✔
185
      this.tempPath = null;
3✔
186
      this.tempDownloadPath = null;
3✔
187
      this.softwareRepositoryPath = null;
4✔
188
    } else {
189
      Path ideBase = this.ideRoot.resolve(FOLDER_IDE);
5✔
190
      this.toolRepositoryPath = ideBase.resolve("software");
5✔
191
      this.urlsPath = ideBase.resolve("urls");
5✔
192
      this.tempPath = ideBase.resolve("tmp");
5✔
193
      this.tempDownloadPath = this.tempPath.resolve(FOLDER_DOWNLOADS);
6✔
194
      this.softwareRepositoryPath = ideBase.resolve(FOLDER_SOFTWARE);
5✔
195
      if (Files.isDirectory(this.tempPath)) {
7✔
196
        // TODO delete all files older than 1 day here...
197
      } else {
198
        this.fileAccess.mkdirs(this.tempDownloadPath);
5✔
199
      }
200
    }
201

202
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
203
  }
1✔
204

205
  private Path findIdeRoot(Path ideHomePath) {
206

207
    Path ideRootPath = null;
2✔
208
    if (ideHomePath != null) {
2✔
209
      ideRootPath = ideHomePath.getParent();
4✔
210
    } else if (!isTest()) {
3!
211
      ideRootPath = getIdeRootPathFromEnv();
×
212
    }
213
    return ideRootPath;
2✔
214
  }
215

216
  private Path getIdeRootPathFromEnv() {
217
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
218
    if (root != null) {
×
219
      Path rootPath = Path.of(root);
×
220
      if (Files.isDirectory(rootPath)) {
×
221
        return rootPath;
×
222
      }
223
    }
224
    return null;
×
225
  }
226

227
  @Override
228
  public void setCwd(Path userDir, String workspace, Path ideHome) {
229

230
    this.cwd = userDir;
3✔
231
    this.workspaceName = workspace;
3✔
232
    this.ideHome = ideHome;
3✔
233
    if (ideHome == null) {
2✔
234
      this.workspacePath = null;
3✔
235
      this.confPath = null;
3✔
236
      this.settingsPath = null;
3✔
237
      this.softwarePath = null;
3✔
238
      this.softwareExtraPath = null;
3✔
239
      this.pluginsPath = null;
4✔
240
    } else {
241
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
242
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
243
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
244
      this.softwarePath = this.ideHome.resolve(FOLDER_SOFTWARE);
6✔
245
      this.softwareExtraPath = this.softwarePath.resolve(FOLDER_EXTRA);
6✔
246
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
247
    }
248
    if (isTest()) {
3!
249
      // only for testing...
250
      if (this.ideHome == null) {
3✔
251
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
252
      } else {
253
        this.userHome = this.ideHome.resolve("home");
7✔
254
      }
255
    } else {
256
      this.userHome = Path.of(getSystem().getProperty("user.home"));
×
257
    }
258
    this.userHomeIde = this.userHome.resolve(".ide");
6✔
259
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
260

261
    this.path = computeSystemPath();
4✔
262
    this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
263
  }
1✔
264

265
  private String getMessageIdeHomeFound() {
266

267
    return "IDE environment variables have been set for " + this.ideHome + " in workspace " + this.workspaceName;
6✔
268
  }
269

270
  private String getMessageIdeHomeNotFound() {
271

272
    return "You are not inside an IDE installation: " + this.cwd;
×
273
  }
274

275
  private String getMessageIdeRootNotFound() {
276
    String root = getSystem().getEnv("IDE_ROOT");
×
277
    if (root == null) {
×
278
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
279
    } else {
280
      return "The environment variable IDE_ROOT is pointing to an invalid path " + root + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
×
281
    }
282
  }
283

284
  /**
285
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
286
   */
287
  public boolean isTest() {
288

289
    return false;
×
290
  }
291

292
  protected SystemPath computeSystemPath() {
293

294
    return new SystemPath(this);
×
295
  }
296

297

298
  private boolean isIdeHome(Path dir) {
299

300
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
301
      return false;
2✔
302
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
303
      return false;
×
304
    }
305
    return true;
2✔
306
  }
307

308
  private EnvironmentVariables createVariables() {
309

310
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
311
    AbstractEnvironmentVariables user = extendVariables(system, this.userHomeIde, EnvironmentVariablesType.USER);
7✔
312
    AbstractEnvironmentVariables settings = extendVariables(user, this.settingsPath, EnvironmentVariablesType.SETTINGS);
7✔
313
    // TODO should we keep this workspace properties? Was this feature ever used?
314
    AbstractEnvironmentVariables workspace = extendVariables(settings, this.workspacePath, EnvironmentVariablesType.WORKSPACE);
7✔
315
    AbstractEnvironmentVariables conf = extendVariables(workspace, this.confPath, EnvironmentVariablesType.CONF);
7✔
316
    return conf.resolved();
3✔
317
  }
318

319
  protected AbstractEnvironmentVariables createSystemVariables() {
320

321
    return EnvironmentVariables.ofSystem(this);
3✔
322
  }
323

324
  protected AbstractEnvironmentVariables extendVariables(AbstractEnvironmentVariables envVariables, Path propertiesPath, EnvironmentVariablesType type) {
325

326
    Path propertiesFile = null;
2✔
327
    if (propertiesPath == null) {
2✔
328
      trace("Configuration directory for type {} does not exist.", type);
10✔
329
    } else if (Files.isDirectory(propertiesPath)) {
5✔
330
      propertiesFile = propertiesPath.resolve(EnvironmentVariables.DEFAULT_PROPERTIES);
4✔
331
      boolean legacySupport = (type != EnvironmentVariablesType.USER);
7✔
332
      if (legacySupport && !Files.exists(propertiesFile)) {
7✔
333
        Path legacyFile = propertiesPath.resolve(EnvironmentVariables.LEGACY_PROPERTIES);
4✔
334
        if (Files.exists(legacyFile)) {
5!
335
          propertiesFile = legacyFile;
×
336
        }
337
      }
338
    } else {
1✔
339
      debug("Configuration directory {} does not exist.", propertiesPath);
9✔
340
    }
341
    return envVariables.extend(propertiesFile, type);
5✔
342
  }
343

344
  @Override
345
  public SystemInfo getSystemInfo() {
346

347
    return this.systemInfo;
3✔
348
  }
349

350
  @Override
351
  public FileAccess getFileAccess() {
352

353
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
354
    configureNetworkProxy();
2✔
355
    return this.fileAccess;
3✔
356
  }
357

358
  @Override
359
  public CommandletManager getCommandletManager() {
360

361
    return this.commandletManager;
3✔
362
  }
363

364
  @Override
365
  public ToolRepository getDefaultToolRepository() {
366

367
    return this.defaultToolRepository;
3✔
368
  }
369

370
  @Override
371
  public CustomToolRepository getCustomToolRepository() {
372

373
    return this.customToolRepository;
3✔
374
  }
375

376
  @Override
377
  public Path getIdeHome() {
378

379
    return this.ideHome;
3✔
380
  }
381

382
  @Override
383
  public String getProjectName() {
384

385
    if (this.ideHome != null) {
3!
386
      return this.ideHome.getFileName().toString();
5✔
387
    }
388
    return "";
×
389
  }
390

391
  @Override
392
  public Path getIdeRoot() {
393

394
    return this.ideRoot;
3✔
395
  }
396

397
  @Override
398
  public Path getCwd() {
399

400
    return this.cwd;
3✔
401
  }
402

403
  @Override
404
  public Path getTempPath() {
405

406
    return this.tempPath;
3✔
407
  }
408

409
  @Override
410
  public Path getTempDownloadPath() {
411

412
    return this.tempDownloadPath;
3✔
413
  }
414

415
  @Override
416
  public Path getUserHome() {
417

418
    return this.userHome;
3✔
419
  }
420

421
  @Override
422
  public Path getUserHomeIde() {
423

424
    return this.userHomeIde;
3✔
425
  }
426

427
  @Override
428
  public Path getSettingsPath() {
429

430
    return this.settingsPath;
3✔
431
  }
432

433
  @Override
434
  public Path getConfPath() {
435

436
    return this.confPath;
3✔
437
  }
438

439
  @Override
440
  public Path getSoftwarePath() {
441

442
    return this.softwarePath;
3✔
443
  }
444

445
  @Override
446
  public Path getSoftwareExtraPath() {
447

448
    return this.softwareExtraPath;
3✔
449
  }
450

451
  @Override
452
  public Path getSoftwareRepositoryPath() {
453

454
    return this.softwareRepositoryPath;
3✔
455
  }
456

457
  @Override
458
  public Path getPluginsPath() {
459

460
    return this.pluginsPath;
3✔
461
  }
462

463
  @Override
464
  public String getWorkspaceName() {
465

466
    return this.workspaceName;
3✔
467
  }
468

469
  @Override
470
  public Path getWorkspacePath() {
471

472
    return this.workspacePath;
3✔
473
  }
474

475
  @Override
476
  public Path getDownloadPath() {
477

478
    return this.downloadPath;
3✔
479
  }
480

481
  @Override
482
  public Path getUrlsPath() {
483

484
    return this.urlsPath;
3✔
485
  }
486

487
  @Override
488
  public Path getToolRepositoryPath() {
489

490
    return this.toolRepositoryPath;
3✔
491
  }
492

493
  @Override
494
  public SystemPath getPath() {
495

496
    return this.path;
3✔
497
  }
498

499
  @Override
500
  public EnvironmentVariables getVariables() {
501

502
    if (this.variables == null) {
3✔
503
      this.variables = createVariables();
4✔
504
    }
505
    return this.variables;
3✔
506
  }
507

508
  @Override
509
  public UrlMetadata getUrls() {
510

511
    if (this.urlMetadata == null) {
3✔
512
      if (!isTest()) {
3!
513
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, this.urlsPath, null);
×
514
      }
515
      this.urlMetadata = new UrlMetadata(this);
6✔
516
    }
517
    return this.urlMetadata;
3✔
518
  }
519

520
  @Override
521
  public boolean isQuietMode() {
522

523
    return this.startContext.isQuietMode();
4✔
524
  }
525

526
  @Override
527
  public boolean isBatchMode() {
528

529
    return this.startContext.isBatchMode();
×
530
  }
531

532
  @Override
533
  public boolean isForceMode() {
534

535
    return this.startContext.isForceMode();
4✔
536
  }
537

538
  @Override
539
  public boolean isOfflineMode() {
540

541
    return this.startContext.isOfflineMode();
4✔
542
  }
543

544
  @Override
545
  public boolean isSkipUpdatesMode() {
546
    return this.startContext.isSkipUpdatesMode();
×
547
  }
548

549
  @Override
550
  public boolean isOnline() {
551

552
    if (this.online == null) {
3✔
553
      configureNetworkProxy();
2✔
554
      // we currently assume we have only a CLI process that runs shortly
555
      // therefore we run this check only once to save resources when this method is called many times
556
      try {
557
        int timeout = 1000;
2✔
558
        //open a connection to github.com and try to retrieve data
559
        //getContent fails if there is no connection
560
        URLConnection connection = new URL("https://www.github.com").openConnection();
6✔
561
        connection.setConnectTimeout(timeout);
3✔
562
        connection.getContent();
3✔
563
        this.online = Boolean.TRUE;
3✔
564
      } catch (Exception ignored) {
×
565
        this.online = Boolean.FALSE;
×
566
      }
1✔
567
    }
568
    return this.online.booleanValue();
4✔
569
  }
570

571
  private void configureNetworkProxy() {
572
    if (this.networkProxy == null) {
3✔
573
      this.networkProxy = new NetworkProxy(this);
6✔
574
      this.networkProxy.configure();
3✔
575
    }
576
  }
1✔
577

578
  @Override
579
  public Locale getLocale() {
580

581
    Locale locale = this.startContext.getLocale();
4✔
582
    if (locale == null) {
2!
583
      locale = Locale.getDefault();
×
584
    }
585
    return locale;
2✔
586
  }
587

588
  @Override
589
  public DirectoryMerger getWorkspaceMerger() {
590

591
    if (this.workspaceMerger == null) {
3✔
592
      this.workspaceMerger = new DirectoryMerger(this);
6✔
593
    }
594
    return this.workspaceMerger;
3✔
595
  }
596

597
  /**
598
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
599
   */
600
  @Override
601
  public Path getDefaultExecutionDirectory() {
602

603
    return this.defaultExecutionDirectory;
×
604
  }
605

606
  /**
607
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
608
   */
609
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
610

611
    if (defaultExecutionDirectory != null) {
×
612
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
613
    }
614
  }
×
615

616
  @Override
617
  public GitContext getGitContext() {
618

619
    return new GitContextImpl(this);
×
620
  }
621

622
  @Override
623
  public ProcessContext newProcess() {
624

625
    ProcessContext processContext = createProcessContext();
3✔
626
    if (this.defaultExecutionDirectory != null) {
3!
627
      processContext.directory(this.defaultExecutionDirectory);
×
628
    }
629
    return processContext;
2✔
630
  }
631

632
  @Override
633
  public IdeSystem getSystem() {
634

635
    if (this.system == null) {
×
636
      this.system = new IdeSystemImpl(this);
×
637
    }
638
    return this.system;
×
639
  }
640

641
  /**
642
   * @return a new instance of {@link ProcessContext}.
643
   * @see #newProcess()
644
   */
645
  protected ProcessContext createProcessContext() {
646

647
    return new ProcessContextImpl(this);
×
648
  }
649

650
  @Override
651
  public IdeSubLogger level(IdeLogLevel level) {
652

653
    return this.startContext.level(level);
5✔
654
  }
655

656
  @Override
657
  public void logIdeHomeAndRootStatus() {
658

659
    if (this.ideRoot != null) {
×
660
      success("IDE_ROOT is set to {}", this.ideRoot);
×
661
    }
662
    if (this.ideHome == null) {
×
663
      warning(getMessageIdeHomeNotFound());
×
664
    } else {
665
      success("IDE_HOME is set to {}", this.ideHome);
×
666
    }
667
  }
×
668

669
  @Override
670
  public String askForInput(String message, String defaultValue) {
671

672
    if (!message.isBlank()) {
×
673
      info(message);
×
674
    }
675
    if (isBatchMode()) {
×
676
      if (isForceMode()) {
×
677
        return defaultValue;
×
678
      } else {
679
        throw new CliAbortException();
×
680
      }
681
    }
682
    String input = readLine().trim();
×
683
    return input.isEmpty() ? defaultValue : input;
×
684
  }
685

686
  @Override
687
  public String askForInput(String message) {
688

689
    String input;
690
    do {
691
      info(message);
3✔
692
      input = readLine().trim();
4✔
693
    } while (input.isEmpty());
3!
694

695
    return input;
2✔
696
  }
697

698
  @SuppressWarnings("unchecked")
699
  @Override
700
  public <O> O question(String question, O... options) {
701

702
    assert (options.length >= 2);
×
703
    interaction(question);
×
704
    Map<String, O> mapping = new HashMap<>(options.length);
×
705
    int i = 0;
×
706
    for (O option : options) {
×
707
      i++;
×
708
      String key = "" + option;
×
709
      addMapping(mapping, key, option);
×
710
      String numericKey = Integer.toString(i);
×
711
      if (numericKey.equals(key)) {
×
712
        trace("Options should not be numeric: " + key);
×
713
      } else {
714
        addMapping(mapping, numericKey, option);
×
715
      }
716
      interaction("Option " + numericKey + ": " + key);
×
717
    }
718
    O option = null;
×
719
    if (isBatchMode()) {
×
720
      if (isForceMode()) {
×
721
        option = options[0];
×
722
        interaction("" + option);
×
723
      }
724
    } else {
725
      while (option == null) {
×
726
        String answer = readLine();
×
727
        option = mapping.get(answer);
×
728
        if (option == null) {
×
729
          warning("Invalid answer: '" + answer + "' - please try again.");
×
730
        }
731
      }
×
732
    }
733
    return option;
×
734
  }
735

736
  /**
737
   * @return the input from the end-user (e.g. read from the console).
738
   */
739
  protected abstract String readLine();
740

741
  private static <O> void addMapping(Map<String, O> mapping, String key, O option) {
742

743
    O duplicate = mapping.put(key, option);
×
744
    if (duplicate != null) {
×
745
      throw new IllegalArgumentException("Duplicated option " + key);
×
746
    }
747
  }
×
748

749
  @Override
750
  public Step getCurrentStep() {
751

752
    return this.currentStep;
×
753
  }
754

755
  @Override
756
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
757

758
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
759
    return this.currentStep;
3✔
760
  }
761

762
  /**
763
   * Internal method to end the running {@link Step}.
764
   *
765
   * @param step the current {@link Step} to end.
766
   */
767
  public void endStep(StepImpl step) {
768

769
    if (step == this.currentStep) {
4!
770
      this.currentStep = this.currentStep.getParent();
6✔
771
    } else {
772
      String currentStepName = "null";
×
773
      if (this.currentStep != null) {
×
774
        currentStepName = this.currentStep.getName();
×
775
      }
776
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
777
    }
778
  }
1✔
779

780
  /**
781
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
782
   *
783
   * @param arguments the {@link CliArgument}.
784
   * @return the return code of the execution.
785
   */
786
  public int run(CliArguments arguments) {
787

788
    CliArgument current = arguments.current();
3✔
789
    assert (this.currentStep == null);
4!
790
    boolean supressStepSuccess = false;
2✔
791
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
792
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
793
    Commandlet cmd = null;
2✔
794
    ValidationResult result = null;
2✔
795
    try {
796
      while (commandletIterator.hasNext()) {
3!
797
        cmd = commandletIterator.next();
4✔
798
        result = applyAndRun(arguments.copy(), cmd);
6✔
799
        if (result.isValid()) {
3!
800
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
801
          step.success();
2✔
802
          return ProcessResult.SUCCESS;
4✔
803
        }
804
      }
805
      if (result != null) {
×
806
        error(result.getErrorMessage());
×
807
      }
808
      step.error("Invalid arguments: {}", current.getArgs());
×
809

810
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
×
811
      if (cmd != null) {
×
812
        help.commandlet.setValue(cmd);
×
813
      }
814
      help.run();
×
815
      return 1;
×
816
    } catch (Throwable t) {
×
817
      step.error(t, true);
×
818
      throw t;
×
819
    } finally {
820
      step.close();
2✔
821
      assert (this.currentStep == null);
4!
822
      step.logSummary(supressStepSuccess);
3✔
823
    }
824
  }
825

826
  /**
827
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
828
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
829
   *     {@link Commandlet} did not match and we have to try a different candidate).
830
   */
831
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
832

833
    IdeLogLevel previousLogLevel = null;
2✔
834
    cmd.reset();
2✔
835
    ValidationResult result = apply(arguments, cmd);
5✔
836
    if (result.isValid()) {
3!
837
      result = cmd.validate();
3✔
838
    }
839
    if (result.isValid()) {
3!
840
      debug("Running commandlet {}", cmd);
9✔
841
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
842
        throw new CliException(getMessageIdeHomeNotFound(), ProcessResult.NO_IDE_HOME);
×
843
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
844
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
×
845
      }
846
      try {
847
        if (cmd.isProcessableOutput()) {
3!
848
          if (!debug().isEnabled()) {
×
849
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
850
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
851
          }
852
          this.startContext.activateLogging();
×
853
        } else {
854
          this.startContext.activateLogging();
3✔
855
          verifyIdeRoot();
2✔
856
          if (cmd.isIdeHomeRequired()) {
3!
857
            debug(getMessageIdeHomeFound());
4✔
858
          }
859
          if (this.settingsPath != null) {
3!
860
            if (getGitContext().isRepositoryUpdateAvailable(this.settingsPath) ||
7!
861
                (getGitContext().fetchIfNeeded(this.settingsPath) && getGitContext().isRepositoryUpdateAvailable(this.settingsPath))) {
5!
862
              interaction("Updates are available for the settings repository. If you want to pull the latest changes, call ide update.");
×
863
            }
864
          }
865
        }
866
        cmd.run();
2✔
867
      } finally {
868
        if (previousLogLevel != null) {
2!
869
          this.startContext.setLogLevel(previousLogLevel);
×
870
        }
871
      }
1✔
872
    } else {
873
      trace("Commandlet did not match");
×
874
    }
875
    return result;
2✔
876
  }
877

878
  private void verifyIdeRoot() {
879
    if (!isTest()) {
3!
880
      if (this.ideRoot == null) {
×
881
        warning("Variable IDE_ROOT is undefined. Please check your installation or run setup script again.");
×
882
      } else if (this.ideHome != null) {
×
883
        Path ideRootPath = getIdeRootPathFromEnv();
×
884
        if (!this.ideRoot.equals(ideRootPath)) {
×
885
          warning("Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.", ideRootPath,
×
886
              this.ideHome.getFileName(), this.ideRoot);
×
887
        }
888
      }
889
    }
890
  }
1✔
891

892
  /**
893
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
894
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
895
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
896
   */
897
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
898
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
899
    if (arguments.current().isStart()) {
4✔
900
      arguments.next();
3✔
901
    }
902
    if (includeContextOptions) {
2✔
903
      ContextCommandlet cc = new ContextCommandlet();
4✔
904
      for (Property<?> property : cc.getProperties()) {
11✔
905
        assert (property.isOption());
4!
906
        property.apply(arguments, this, cc, collector);
7✔
907
      }
1✔
908
    }
909
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
910
    CliArgument current = arguments.current();
3✔
911
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
912
      collector.add(current.get(), null, null, null);
7✔
913
    }
914
    arguments.next();
3✔
915
    while (commandletIterator.hasNext()) {
3✔
916
      Commandlet cmd = commandletIterator.next();
4✔
917
      if (!arguments.current().isEnd()) {
4✔
918
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
919
      }
920
    }
1✔
921
    return collector.getSortedCandidates();
3✔
922
  }
923

924
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
925
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
926
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
927
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
928
    List<Property<?>> properties = cmd.getProperties();
3✔
929
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
930
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
931
    for (Property<?> property : properties) {
10✔
932
      if (property.isOption()) {
3✔
933
        optionProperties.add(property);
4✔
934
      }
935
    }
1✔
936
    CliArgument currentArgument = arguments.current();
3✔
937
    while (!currentArgument.isEnd()) {
3✔
938
      trace("Trying to match argument '{}'", currentArgument);
9✔
939
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
940
        if (currentArgument.isCompletion()) {
3✔
941
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
942
          while (optionIterator.hasNext()) {
3✔
943
            Property<?> option = optionIterator.next();
4✔
944
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
945
            if (success) {
2✔
946
              optionIterator.remove();
2✔
947
              arguments.next();
3✔
948
            }
949
          }
1✔
950
        } else {
1✔
951
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
952
          if (option != null) {
2✔
953
            arguments.next();
3✔
954
            boolean removed = optionProperties.remove(option);
4✔
955
            if (!removed) {
2!
956
              option = null;
×
957
            }
958
          }
959
          if (option == null) {
2✔
960
            trace("No such option was found.");
3✔
961
            return;
1✔
962
          }
963
        }
1✔
964
      } else {
965
        if (valueIterator.hasNext()) {
3✔
966
          Property<?> valueProperty = valueIterator.next();
4✔
967
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
968
          if (!success) {
2!
969
            trace("Completion cannot match any further.");
×
970
            return;
×
971
          }
972
        } else {
1✔
973
          trace("No value left for completion.");
3✔
974
          return;
1✔
975
        }
976
      }
977
      currentArgument = arguments.current();
4✔
978
    }
979
  }
1✔
980

981

982
  /**
983
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
984
   *     {@link CliArguments#copy() copy} as needed.
985
   * @param cmd the potential {@link Commandlet} to match.
986
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
987
   */
988
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
989

990
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
991
    CliArgument currentArgument = arguments.current();
3✔
992
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
993
    Property<?> property = null;
2✔
994
    if (propertyIterator.hasNext()) {
3!
995
      property = propertyIterator.next();
4✔
996
    }
997
    while (!currentArgument.isEnd()) {
3✔
998
      trace("Trying to match argument '{}'", currentArgument);
9✔
999
      Property<?> currentProperty = property;
2✔
1000
      if (!arguments.isEndOptions()) {
3!
1001
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1002
        if (option != null) {
2!
1003
          currentProperty = option;
×
1004
        }
1005
      }
1006
      if (currentProperty == null) {
2!
1007
        trace("No option or next value found");
×
1008
        ValidationState state = new ValidationState(null);
×
1009
        state.addErrorMessage("No matching property found");
×
1010
        return state;
×
1011
      }
1012
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1013
      if (currentProperty == property) {
3!
1014
        if (!property.isMultiValued()) {
3✔
1015
          if (propertyIterator.hasNext()) {
3!
1016
            property = propertyIterator.next();
5✔
1017
          } else {
1018
            property = null;
×
1019
          }
1020
        }
1021
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1022
          arguments.stopSplitShortOptions();
2✔
1023
        }
1024
      }
1025
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1026
      if (!matches && currentArgument.isCompletion()) {
2!
1027
        ValidationState state = new ValidationState(null);
×
1028
        state.addErrorMessage("No matching property found");
×
1029
        return state;
×
1030
      }
1031
      currentArgument = arguments.current();
3✔
1032
    }
1✔
1033
    return ValidationResultValid.get();
2✔
1034
  }
1035

1036
  @Override
1037
  public String findBash() {
1038

1039
    String bash = "bash";
2✔
1040
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1041
      bash = findBashOnWindows();
×
1042
    }
1043

1044
    return bash;
2✔
1045
  }
1046

1047
  private String findBashOnWindows() {
1048

1049
    // Check if Git Bash exists in the default location
1050
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1051
    if (Files.exists(defaultPath)) {
×
1052
      return defaultPath.toString();
×
1053
    }
1054

1055
    // If not found in the default location, try the registry query
1056
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1057
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1058
    String regQueryResult;
1059
    for (String bashVariant : bashVariants) {
×
1060
      for (String registryKey : registryKeys) {
×
1061
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1062
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1063

1064
        try {
1065
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1066
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1067
            StringBuilder output = new StringBuilder();
×
1068
            String line;
1069

1070
            while ((line = reader.readLine()) != null) {
×
1071
              output.append(line);
×
1072
            }
1073

1074
            int exitCode = process.waitFor();
×
1075
            if (exitCode != 0) {
×
1076
              return null;
×
1077
            }
1078

1079
            regQueryResult = output.toString();
×
1080
            if (regQueryResult != null) {
×
1081
              int index = regQueryResult.indexOf("REG_SZ");
×
1082
              if (index != -1) {
×
1083
                String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1084
                return path + "\\bin\\bash.exe";
×
1085
              }
1086
            }
1087

1088
          }
×
1089
        } catch (Exception e) {
×
1090
          return null;
×
1091
        }
×
1092
      }
1093
    }
1094
    // no bash found
1095
    return null;
×
1096
  }
1097

1098
  @Override
1099
  public WindowsPathSyntax getPathSyntax() {
1100
    return this.pathSyntax;
3✔
1101
  }
1102

1103
  /**
1104
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1105
   */
1106
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1107

1108
    this.pathSyntax = pathSyntax;
3✔
1109
  }
1✔
1110

1111
  /**
1112
   * @return the {@link IdeStartContextImpl}.
1113
   */
1114
  public IdeStartContextImpl getStartContext() {
1115

1116
    return startContext;
3✔
1117
  }
1118

1119
  /**
1120
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1121
   */
1122
  public void reload() {
1123
    this.variables = null;
3✔
1124
  }
1✔
1125
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc