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

devonfw / IDEasy / 19481956452

18 Nov 2025 10:06PM UTC coverage: 69.044% (+0.1%) from 68.905%
19481956452

Pull #1593

github

web-flow
Merge b71c405c9 into 553958662
Pull Request #1593: #1144: #1145: CVE warnings and suggestions

3570 of 5669 branches covered (62.97%)

Branch coverage included in aggregate %.

9308 of 12983 relevant lines covered (71.69%)

3.15 hits per line

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

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

3
import static com.devonfw.tools.ide.variable.IdeVariables.IDE_MIN_VERSION;
4

5
import java.nio.file.Files;
6
import java.nio.file.Path;
7
import java.time.LocalDateTime;
8
import java.util.ArrayList;
9
import java.util.HashMap;
10
import java.util.Iterator;
11
import java.util.List;
12
import java.util.Locale;
13
import java.util.Map;
14
import java.util.Map.Entry;
15
import java.util.Objects;
16
import java.util.function.Predicate;
17

18
import com.devonfw.tools.ide.cli.CliAbortException;
19
import com.devonfw.tools.ide.cli.CliArgument;
20
import com.devonfw.tools.ide.cli.CliArguments;
21
import com.devonfw.tools.ide.cli.CliException;
22
import com.devonfw.tools.ide.commandlet.Commandlet;
23
import com.devonfw.tools.ide.commandlet.CommandletManager;
24
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
25
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
26
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
27
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
28
import com.devonfw.tools.ide.common.SystemPath;
29
import com.devonfw.tools.ide.completion.CompletionCandidate;
30
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
31
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
32
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
33
import com.devonfw.tools.ide.environment.EnvironmentVariables;
34
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
35
import com.devonfw.tools.ide.environment.IdeSystem;
36
import com.devonfw.tools.ide.environment.IdeSystemImpl;
37
import com.devonfw.tools.ide.git.GitContext;
38
import com.devonfw.tools.ide.git.GitContextImpl;
39
import com.devonfw.tools.ide.git.GitUrl;
40
import com.devonfw.tools.ide.io.FileAccess;
41
import com.devonfw.tools.ide.io.FileAccessImpl;
42
import com.devonfw.tools.ide.log.IdeLogArgFormatter;
43
import com.devonfw.tools.ide.log.IdeLogLevel;
44
import com.devonfw.tools.ide.log.IdeLogger;
45
import com.devonfw.tools.ide.log.IdeSubLogger;
46
import com.devonfw.tools.ide.merge.DirectoryMerger;
47
import com.devonfw.tools.ide.migration.IdeMigrator;
48
import com.devonfw.tools.ide.network.NetworkStatus;
49
import com.devonfw.tools.ide.network.NetworkStatusImpl;
50
import com.devonfw.tools.ide.os.SystemInfo;
51
import com.devonfw.tools.ide.os.SystemInfoImpl;
52
import com.devonfw.tools.ide.os.WindowsHelper;
53
import com.devonfw.tools.ide.os.WindowsHelperImpl;
54
import com.devonfw.tools.ide.os.WindowsPathSyntax;
55
import com.devonfw.tools.ide.process.ProcessContext;
56
import com.devonfw.tools.ide.process.ProcessContextImpl;
57
import com.devonfw.tools.ide.process.ProcessResult;
58
import com.devonfw.tools.ide.property.Property;
59
import com.devonfw.tools.ide.step.Step;
60
import com.devonfw.tools.ide.step.StepImpl;
61
import com.devonfw.tools.ide.tool.repository.CustomToolRepository;
62
import com.devonfw.tools.ide.tool.repository.CustomToolRepositoryImpl;
63
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
64
import com.devonfw.tools.ide.tool.repository.MvnRepository;
65
import com.devonfw.tools.ide.tool.repository.NpmRepository;
66
import com.devonfw.tools.ide.tool.repository.ToolRepository;
67
import com.devonfw.tools.ide.url.model.UrlMetadata;
68
import com.devonfw.tools.ide.util.DateTimeUtil;
69
import com.devonfw.tools.ide.util.PrivacyUtil;
70
import com.devonfw.tools.ide.validation.ValidationResult;
71
import com.devonfw.tools.ide.validation.ValidationResultValid;
72
import com.devonfw.tools.ide.validation.ValidationState;
73
import com.devonfw.tools.ide.variable.IdeVariables;
74
import com.devonfw.tools.ide.version.IdeVersion;
75
import com.devonfw.tools.ide.version.VersionIdentifier;
76

77
/**
78
 * Abstract base implementation of {@link IdeContext}.
79
 */
80
public abstract class AbstractIdeContext implements IdeContext, IdeLogArgFormatter {
81

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

84
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
85
  public static final String BASH = "bash";
86
  private static final String DEFAULT_WINDOWS_GIT_PATH = "C:\\Program Files\\Git\\bin\\bash.exe";
87

88
  private static final String OPTION_DETAILS_START = "([";
89

90
  private final IdeStartContextImpl startContext;
91

92
  private Path ideHome;
93

94
  private final Path ideRoot;
95

96
  private Path confPath;
97

98
  protected Path settingsPath;
99

100
  private Path settingsCommitIdPath;
101

102
  protected Path pluginsPath;
103

104
  private Path workspacePath;
105

106
  private String workspaceName;
107

108
  private Path cwd;
109

110
  private Path downloadPath;
111

112
  private Path userHome;
113

114
  private Path userHomeIde;
115

116
  private SystemPath path;
117

118
  private WindowsPathSyntax pathSyntax;
119

120
  private final SystemInfo systemInfo;
121

122
  private EnvironmentVariables variables;
123

124
  private final FileAccess fileAccess;
125

126
  protected CommandletManager commandletManager;
127

128
  protected ToolRepository defaultToolRepository;
129

130
  private CustomToolRepository customToolRepository;
131

132
  private MvnRepository mvnRepository;
133

134
  private NpmRepository npmRepository;
135

136
  private DirectoryMerger workspaceMerger;
137

138
  protected UrlMetadata urlMetadata;
139

140
  protected Path defaultExecutionDirectory;
141

142
  private StepImpl currentStep;
143

144
  private NetworkStatus networkStatus;
145

146
  protected IdeSystem system;
147

148
  private WindowsHelper windowsHelper;
149

150
  private final Map<String, String> privacyMap;
151

152
  /**
153
   * The constructor.
154
   *
155
   * @param startContext the {@link IdeLogger}.
156
   * @param workingDirectory the optional {@link Path} to current working directory.
157
   */
158
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
159

160
    super();
2✔
161
    this.startContext = startContext;
3✔
162
    this.startContext.setArgFormatter(this);
4✔
163
    this.privacyMap = new HashMap<>();
5✔
164
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
165
    this.commandletManager = new CommandletManagerImpl(this);
6✔
166
    this.fileAccess = new FileAccessImpl(this);
6✔
167
    String userHomeProperty = getSystem().getProperty("user.home");
5✔
168
    if (userHomeProperty != null) {
2!
169
      this.userHome = Path.of(userHomeProperty);
×
170
    }
171
    if (workingDirectory == null) {
2!
172
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
173
    }
174
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
175
    if (Files.isDirectory(workingDirectory)) {
5✔
176
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
177
    } else {
178
      warning("Current working directory does not exist: {}", workingDirectory);
9✔
179
    }
180
    this.cwd = workingDirectory;
3✔
181
    // detect IDE_HOME and WORKSPACE
182
    String workspace = null;
2✔
183
    Path ideHomeDir = null;
2✔
184
    IdeHomeAndWorkspace ideHomeAndWorkspace = findIdeHome(workingDirectory);
4✔
185
    if (ideHomeAndWorkspace != null) {
2!
186
      ideHomeDir = ideHomeAndWorkspace.home();
3✔
187
      workspace = ideHomeAndWorkspace.workspace();
3✔
188
    }
189

190
    // detection completed, initializing variables
191
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
192

193
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
194

195
    if (this.ideRoot != null) {
3✔
196
      Path tempDownloadPath = getTempDownloadPath();
3✔
197
      if (Files.isDirectory(tempDownloadPath)) {
6✔
198
        // TODO delete all files older than 1 day here...
199
      } else {
200
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
201
      }
202
    }
203

204
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
205
  }
1✔
206

207
  /**
208
   * Searches for the IDE home directory by traversing up the directory tree from the given working directory. This method can be overridden in test contexts to
209
   * add additional validation or boundary checks.
210
   *
211
   * @param workingDirectory the starting directory for the search.
212
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
213
   */
214
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
215

216
    Path currentDir = workingDirectory;
2✔
217
    String name1 = "";
2✔
218
    String name2 = "";
2✔
219
    String workspace = WORKSPACE_MAIN;
2✔
220
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
221

222
    while (currentDir != null) {
2✔
223
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
224
      if (isIdeHome(currentDir)) {
4✔
225
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
226
          workspace = name2;
3✔
227
        }
228
        break;
229
      }
230
      name2 = name1;
2✔
231
      int nameCount = currentDir.getNameCount();
3✔
232
      if (nameCount >= 1) {
3✔
233
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
234
      }
235
      currentDir = currentDir.getParent();
3✔
236
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
237
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
238
        currentDir = null;
×
239
      }
240
    }
1✔
241

242
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
243
  }
244

245
  /**
246
   * @return a new {@link MvnRepository}
247
   */
248
  protected MvnRepository createMvnRepository() {
249
    return new MvnRepository(this);
5✔
250
  }
251

252
  /**
253
   * @return a new {@link NpmRepository}
254
   */
255
  protected NpmRepository createNpmRepository() {
256
    return new NpmRepository(this);
5✔
257
  }
258

259
  private Path findIdeRoot(Path ideHomePath) {
260

261
    Path ideRootPath = null;
2✔
262
    if (ideHomePath != null) {
2✔
263
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
264
      ideRootPath = ideHomePath.getParent();
3✔
265
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
8!
266
        warning(
12✔
267
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
268
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
269
            ideRootPathFromEnv,
270
            ideHomePath.getFileName(), ideRootPath);
6✔
271
      }
272
    } else if (!isTest()) {
4!
273
      ideRootPath = getIdeRootPathFromEnv(true);
×
274
    }
275
    return ideRootPath;
2✔
276
  }
277

278
  /**
279
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
280
   */
281
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
282

283
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
284
    if (root != null) {
×
285
      Path rootPath = Path.of(root);
×
286
      if (Files.isDirectory(rootPath)) {
×
287
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
288
        if (withSanityCheck) {
×
289
          int nameCount = rootPath.getNameCount();
×
290
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
291
          int delta = absoluteNameCount - nameCount;
×
292
          if (delta >= 0) {
×
293
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
294
              String rootName = rootPath.getName(nameIndex).toString();
×
295
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
296
              if (!rootName.equals(absoluteRootName)) {
×
297
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
298
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
299
                break;
×
300
              }
301
            }
302
          } else {
303
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
304
                absoluteRootPath);
305
          }
306
        }
307
        return absoluteRootPath;
×
308
      } else if (withSanityCheck) {
×
309
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
310
      }
311
    }
312
    return null;
×
313
  }
314

315
  @Override
316
  public void setCwd(Path userDir, String workspace, Path ideHome) {
317

318
    this.cwd = userDir;
3✔
319
    this.workspaceName = workspace;
3✔
320
    this.ideHome = ideHome;
3✔
321
    if (ideHome == null) {
2✔
322
      this.workspacePath = null;
3✔
323
      this.confPath = null;
3✔
324
      this.settingsPath = null;
3✔
325
      this.pluginsPath = null;
4✔
326
    } else {
327
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
328
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
329
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
330
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
331
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
332
    }
333
    if (isTest()) {
3!
334
      // only for testing...
335
      if (this.ideHome == null) {
3✔
336
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
337
      } else {
338
        this.userHome = this.ideHome.resolve("home");
6✔
339
      }
340
    }
341
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
342
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
343
    resetPrivacyMap();
2✔
344
    this.path = computeSystemPath();
4✔
345
  }
1✔
346

347
  private String getMessageIdeHomeFound() {
348

349
    String wks = this.workspaceName;
3✔
350
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
351
      wks = "*".repeat(wks.length());
×
352
    }
353
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
354
  }
355

356
  private String getMessageNotInsideIdeProject() {
357

358
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
359
  }
360

361
  private String getMessageIdeRootNotFound() {
362

363
    String root = getSystem().getEnv("IDE_ROOT");
5✔
364
    if (root == null) {
2!
365
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
366
    } else {
367
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
368
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
369
    }
370
  }
371

372
  /**
373
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
374
   */
375
  public boolean isTest() {
376

377
    return false;
×
378
  }
379

380
  protected SystemPath computeSystemPath() {
381

382
    return new SystemPath(this);
×
383
  }
384

385
  /**
386
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
387
   *
388
   * @param dir the directory to check.
389
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
390
   */
391
  protected boolean isIdeHome(Path dir) {
392

393
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
394
      return false;
2✔
395
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
396
      return false;
×
397
    }
398
    return true;
2✔
399
  }
400

401
  private EnvironmentVariables createVariables() {
402

403
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
404
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
405
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
406
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
407
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
408
    return conf.resolved();
3✔
409
  }
410

411
  protected AbstractEnvironmentVariables createSystemVariables() {
412

413
    return EnvironmentVariables.ofSystem(this);
3✔
414
  }
415

416
  @Override
417
  public SystemInfo getSystemInfo() {
418

419
    return this.systemInfo;
3✔
420
  }
421

422
  @Override
423
  public FileAccess getFileAccess() {
424

425
    return this.fileAccess;
3✔
426
  }
427

428
  @Override
429
  public CommandletManager getCommandletManager() {
430

431
    return this.commandletManager;
3✔
432
  }
433

434
  @Override
435
  public ToolRepository getDefaultToolRepository() {
436

437
    return this.defaultToolRepository;
3✔
438
  }
439

440
  @Override
441
  public MvnRepository getMvnRepository() {
442
    if (this.mvnRepository == null) {
3✔
443
      this.mvnRepository = createMvnRepository();
4✔
444
    }
445
    return this.mvnRepository;
3✔
446
  }
447

448
  @Override
449
  public NpmRepository getNpmRepository() {
450
    if (this.npmRepository == null) {
3✔
451
      this.npmRepository = createNpmRepository();
4✔
452
    }
453
    return this.npmRepository;
3✔
454
  }
455

456
  @Override
457
  public CustomToolRepository getCustomToolRepository() {
458

459
    if (this.customToolRepository == null) {
3!
460
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
461
    }
462
    return this.customToolRepository;
3✔
463
  }
464

465
  @Override
466
  public Path getIdeHome() {
467

468
    return this.ideHome;
3✔
469
  }
470

471
  @Override
472
  public String getProjectName() {
473

474
    if (this.ideHome != null) {
3!
475
      return this.ideHome.getFileName().toString();
5✔
476
    }
477
    return "";
×
478
  }
479

480
  @Override
481
  public VersionIdentifier getProjectVersion() {
482

483
    if (this.ideHome != null) {
3!
484
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
485
      if (Files.exists(versionFile)) {
5✔
486
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
487
        return VersionIdentifier.of(version);
3✔
488
      }
489
    }
490
    return IdeMigrator.START_VERSION;
2✔
491
  }
492

493
  @Override
494
  public void setProjectVersion(VersionIdentifier version) {
495

496
    if (this.ideHome == null) {
3!
497
      throw new IllegalStateException("IDE_HOME not available!");
×
498
    }
499
    Objects.requireNonNull(version);
3✔
500
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
501
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
502
  }
1✔
503

504
  @Override
505
  public Path getIdeRoot() {
506

507
    return this.ideRoot;
3✔
508
  }
509

510
  @Override
511
  public Path getIdePath() {
512

513
    Path myIdeRoot = getIdeRoot();
3✔
514
    if (myIdeRoot == null) {
2!
515
      return null;
×
516
    }
517
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
518
  }
519

520
  @Override
521
  public Path getCwd() {
522

523
    return this.cwd;
3✔
524
  }
525

526
  @Override
527
  public Path getTempPath() {
528

529
    Path idePath = getIdePath();
3✔
530
    if (idePath == null) {
2!
531
      return null;
×
532
    }
533
    return idePath.resolve("tmp");
4✔
534
  }
535

536
  @Override
537
  public Path getTempDownloadPath() {
538

539
    Path tmp = getTempPath();
3✔
540
    if (tmp == null) {
2!
541
      return null;
×
542
    }
543
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
544
  }
545

546
  @Override
547
  public Path getUserHome() {
548

549
    return this.userHome;
3✔
550
  }
551

552
  /**
553
   * This method should only be used for tests to mock user home.
554
   *
555
   * @param userHome the new value of {@link #getUserHome()}.
556
   */
557
  protected void setUserHome(Path userHome) {
558

559
    this.userHome = userHome;
3✔
560
    resetPrivacyMap();
2✔
561
  }
1✔
562

563
  @Override
564
  public Path getUserHomeIde() {
565

566
    return this.userHomeIde;
3✔
567
  }
568

569
  @Override
570
  public Path getSettingsPath() {
571

572
    return this.settingsPath;
3✔
573
  }
574

575
  @Override
576
  public Path getSettingsGitRepository() {
577

578
    Path settingsPath = getSettingsPath();
3✔
579
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
580
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
581
      error("Settings repository exists but is not a git repository.");
3✔
582
      return null;
2✔
583
    }
584
    return settingsPath;
2✔
585
  }
586

587
  @Override
588
  public boolean isSettingsRepositorySymlinkOrJunction() {
589

590
    Path settingsPath = getSettingsPath();
3✔
591
    if (settingsPath == null) {
2!
592
      return false;
×
593
    }
594
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
595
  }
596

597
  @Override
598
  public Path getSettingsCommitIdPath() {
599

600
    return this.settingsCommitIdPath;
3✔
601
  }
602

603
  @Override
604
  public Path getConfPath() {
605

606
    return this.confPath;
3✔
607
  }
608

609
  @Override
610
  public Path getSoftwarePath() {
611

612
    if (this.ideHome == null) {
3✔
613
      return null;
2✔
614
    }
615
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
616
  }
617

618
  @Override
619
  public Path getSoftwareExtraPath() {
620

621
    Path softwarePath = getSoftwarePath();
3✔
622
    if (softwarePath == null) {
2!
623
      return null;
×
624
    }
625
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
626
  }
627

628
  @Override
629
  public Path getSoftwareRepositoryPath() {
630

631
    Path idePath = getIdePath();
3✔
632
    if (idePath == null) {
2!
633
      return null;
×
634
    }
635
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
636
  }
637

638
  @Override
639
  public Path getPluginsPath() {
640

641
    return this.pluginsPath;
3✔
642
  }
643

644
  @Override
645
  public String getWorkspaceName() {
646

647
    return this.workspaceName;
3✔
648
  }
649

650
  @Override
651
  public Path getWorkspacePath() {
652

653
    return this.workspacePath;
3✔
654
  }
655

656
  @Override
657
  public Path getDownloadPath() {
658

659
    return this.downloadPath;
3✔
660
  }
661

662
  @Override
663
  public Path getUrlsPath() {
664

665
    Path idePath = getIdePath();
3✔
666
    if (idePath == null) {
2!
667
      return null;
×
668
    }
669
    return idePath.resolve(FOLDER_URLS);
4✔
670
  }
671

672
  @Override
673
  public Path getToolRepositoryPath() {
674

675
    Path idePath = getIdePath();
3✔
676
    if (idePath == null) {
2!
677
      return null;
×
678
    }
679
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
680
  }
681

682
  @Override
683
  public SystemPath getPath() {
684

685
    return this.path;
3✔
686
  }
687

688
  @Override
689
  public EnvironmentVariables getVariables() {
690

691
    if (this.variables == null) {
3✔
692
      this.variables = createVariables();
4✔
693
    }
694
    return this.variables;
3✔
695
  }
696

697
  @Override
698
  public UrlMetadata getUrls() {
699

700
    if (this.urlMetadata == null) {
3✔
701
      if (!isTest()) {
3!
702
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
703
      }
704
      this.urlMetadata = new UrlMetadata(this);
6✔
705
    }
706
    return this.urlMetadata;
3✔
707
  }
708

709
  @Override
710
  public boolean isQuietMode() {
711

712
    return this.startContext.isQuietMode();
4✔
713
  }
714

715
  @Override
716
  public boolean isBatchMode() {
717

718
    return this.startContext.isBatchMode();
4✔
719
  }
720

721
  @Override
722
  public boolean isForceMode() {
723

724
    return this.startContext.isForceMode();
4✔
725
  }
726

727
  @Override
728
  public boolean isForcePull() {
729

730
    return this.startContext.isForcePull();
4✔
731
  }
732

733
  @Override
734
  public boolean isForcePlugins() {
735

736
    return this.startContext.isForcePlugins();
4✔
737
  }
738

739
  @Override
740
  public boolean isForceRepositories() {
741

742
    return this.startContext.isForceRepositories();
4✔
743
  }
744

745
  @Override
746
  public boolean isOfflineMode() {
747

748
    return this.startContext.isOfflineMode();
4✔
749
  }
750

751
  @Override
752
  public boolean isPrivacyMode() {
753
    return this.startContext.isPrivacyMode();
4✔
754
  }
755

756
  @Override
757
  public boolean isSkipUpdatesMode() {
758

759
    return this.startContext.isSkipUpdatesMode();
4✔
760
  }
761

762
  @Override
763
  public boolean isNoColorsMode() {
764

765
    return this.startContext.isNoColorsMode();
×
766
  }
767

768
  @Override
769
  public NetworkStatus getNetworkStatus() {
770

771
    if (this.networkStatus == null) {
×
772
      this.networkStatus = new NetworkStatusImpl(this);
×
773
    }
774
    return this.networkStatus;
×
775
  }
776

777
  @Override
778
  public Locale getLocale() {
779

780
    Locale locale = this.startContext.getLocale();
4✔
781
    if (locale == null) {
2✔
782
      locale = Locale.getDefault();
2✔
783
    }
784
    return locale;
2✔
785
  }
786

787
  @Override
788
  public DirectoryMerger getWorkspaceMerger() {
789

790
    if (this.workspaceMerger == null) {
3✔
791
      this.workspaceMerger = new DirectoryMerger(this);
6✔
792
    }
793
    return this.workspaceMerger;
3✔
794
  }
795

796
  /**
797
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
798
   */
799
  @Override
800
  public Path getDefaultExecutionDirectory() {
801

802
    return this.defaultExecutionDirectory;
×
803
  }
804

805
  /**
806
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
807
   */
808
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
809

810
    if (defaultExecutionDirectory != null) {
×
811
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
812
    }
813
  }
×
814

815
  @Override
816
  public GitContext getGitContext() {
817

818
    return new GitContextImpl(this);
×
819
  }
820

821
  @Override
822
  public ProcessContext newProcess() {
823

824
    ProcessContext processContext = createProcessContext();
3✔
825
    if (this.defaultExecutionDirectory != null) {
3!
826
      processContext.directory(this.defaultExecutionDirectory);
×
827
    }
828
    return processContext;
2✔
829
  }
830

831
  @Override
832
  public IdeSystem getSystem() {
833

834
    if (this.system == null) {
×
835
      this.system = new IdeSystemImpl(this);
×
836
    }
837
    return this.system;
×
838
  }
839

840
  /**
841
   * @return a new instance of {@link ProcessContext}.
842
   * @see #newProcess()
843
   */
844
  protected ProcessContext createProcessContext() {
845

846
    return new ProcessContextImpl(this);
5✔
847
  }
848

849
  @Override
850
  public IdeSubLogger level(IdeLogLevel level) {
851

852
    return this.startContext.level(level);
5✔
853
  }
854

855
  @Override
856
  public void logIdeHomeAndRootStatus() {
857
    if (this.ideRoot != null) {
3!
858
      success("IDE_ROOT is set to {}", this.ideRoot);
×
859
    }
860
    if (this.ideHome == null) {
3✔
861
      warning(getMessageNotInsideIdeProject());
5✔
862
    } else {
863
      success("IDE_HOME is set to {}", this.ideHome);
10✔
864
    }
865
  }
1✔
866

867
  @Override
868
  public String formatArgument(Object argument) {
869

870
    if (argument == null) {
2✔
871
      return null;
2✔
872
    }
873
    String result = argument.toString();
3✔
874
    if (isPrivacyMode()) {
3✔
875
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
876
        initializePrivacyMap(this.userHome, "~");
×
877
        String projectName = getProjectName();
×
878
        if (!projectName.isEmpty()) {
×
879
          this.privacyMap.put(projectName, "project");
×
880
        }
881
      }
882
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
883
        result = result.replace(entry.getKey(), entry.getValue());
×
884
      }
×
885
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
886
    }
887
    return result;
2✔
888
  }
889

890
  /**
891
   * @param path the sensitive {@link Path} to
892
   * @param replacement the replacement to mask the {@link Path} in log output.
893
   */
894
  protected void initializePrivacyMap(Path path, String replacement) {
895

896
    if (path == null) {
×
897
      return;
×
898
    }
899
    if (this.systemInfo.isWindows()) {
×
900
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
901
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
902
    } else {
903
      this.privacyMap.put(path.toString(), replacement);
×
904
    }
905
  }
×
906

907
  /**
908
   * Resets the privacy map in case fundamental values have changed.
909
   */
910
  private void resetPrivacyMap() {
911

912
    this.privacyMap.clear();
3✔
913
  }
1✔
914

915

916
  @Override
917
  public String askForInput(String message, String defaultValue) {
918

919
    while (true) {
920
      if (!message.isBlank()) {
3!
921
        interaction(message);
3✔
922
      }
923
      if (isBatchMode()) {
3!
924
        if (isForceMode()) {
×
925
          return defaultValue;
×
926
        } else {
927
          throw new CliAbortException();
×
928
        }
929
      }
930
      String input = readLine().trim();
4✔
931
      if (!input.isEmpty()) {
3!
932
        return input;
2✔
933
      } else {
934
        if (defaultValue != null) {
×
935
          return defaultValue;
×
936
        }
937
      }
938
    }
×
939
  }
940

941
  @SuppressWarnings("unchecked")
942
  @Override
943
  public <O> O question(O[] options, String question, Object... args) {
944

945
    assert (options.length >= 2);
5!
946
    interaction(question, args);
4✔
947
    return displayOptionsAndGetAnswer(options);
4✔
948
  }
949

950
  private <O> O displayOptionsAndGetAnswer(O[] options) {
951
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
952
    int i = 0;
2✔
953
    for (O option : options) {
16✔
954
      i++;
1✔
955
      String title = "" + option;
4✔
956
      String key = computeOptionKey(title);
3✔
957
      addMapping(mapping, key, option);
4✔
958
      String numericKey = Integer.toString(i);
3✔
959
      if (numericKey.equals(key)) {
4!
960
        trace("Options should not be numeric: " + key);
×
961
      } else {
962
        addMapping(mapping, numericKey, option);
4✔
963
      }
964
      interaction("Option " + numericKey + ": " + title);
5✔
965
    }
966
    O option = null;
2✔
967
    if (isBatchMode()) {
3!
968
      if (isForceMode()) {
×
969
        option = options[0];
×
970
        interaction("" + option);
×
971
      }
972
    } else {
973
      while (option == null) {
2✔
974
        String answer = readLine();
3✔
975
        option = mapping.get(answer);
4✔
976
        if (option == null) {
2!
977
          warning("Invalid answer: '" + answer + "' - please try again.");
×
978
        }
979
      }
1✔
980
    }
981
    return option;
2✔
982
  }
983

984
  private static String computeOptionKey(String option) {
985
    String key = option;
2✔
986
    int index = -1;
2✔
987
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
988
      int currentIndex = key.indexOf(c);
4✔
989
      if (currentIndex != -1) {
3✔
990
        if ((index == -1) || (currentIndex < index)) {
3!
991
          index = currentIndex;
2✔
992
        }
993
      }
994
    }
995
    if (index > 0) {
2✔
996
      key = key.substring(0, index).trim();
6✔
997
    }
998
    return key;
2✔
999
  }
1000

1001
  /**
1002
   * @return the input from the end-user (e.g. read from the console).
1003
   */
1004
  protected abstract String readLine();
1005

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

1008
    O duplicate = mapping.put(key, option);
5✔
1009
    if (duplicate != null) {
2!
1010
      throw new IllegalArgumentException("Duplicated option " + key);
×
1011
    }
1012
  }
1✔
1013

1014
  @Override
1015
  public Step getCurrentStep() {
1016

1017
    return this.currentStep;
×
1018
  }
1019

1020
  @Override
1021
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1022

1023
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1024
    return this.currentStep;
3✔
1025
  }
1026

1027
  /**
1028
   * Internal method to end the running {@link Step}.
1029
   *
1030
   * @param step the current {@link Step} to end.
1031
   */
1032
  public void endStep(StepImpl step) {
1033

1034
    if (step == this.currentStep) {
4!
1035
      this.currentStep = this.currentStep.getParent();
6✔
1036
    } else {
1037
      String currentStepName = "null";
×
1038
      if (this.currentStep != null) {
×
1039
        currentStepName = this.currentStep.getName();
×
1040
      }
1041
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1042
    }
1043
  }
1✔
1044

1045
  /**
1046
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1047
   *
1048
   * @param arguments the {@link CliArgument}.
1049
   * @return the return code of the execution.
1050
   */
1051
  public int run(CliArguments arguments) {
1052

1053
    CliArgument current = arguments.current();
3✔
1054
    assert (this.currentStep == null);
4!
1055
    boolean supressStepSuccess = false;
2✔
1056
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1057
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1058
    Commandlet cmd = null;
2✔
1059
    ValidationResult result = null;
2✔
1060
    try {
1061
      while (commandletIterator.hasNext()) {
3✔
1062
        cmd = commandletIterator.next();
4✔
1063
        result = applyAndRun(arguments.copy(), cmd);
6✔
1064
        if (result.isValid()) {
3!
1065
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1066
          step.success();
2✔
1067
          return ProcessResult.SUCCESS;
4✔
1068
        }
1069
      }
1070
      this.startContext.activateLogging();
3✔
1071
      verifyIdeMinVersion(false);
3✔
1072
      if (result != null) {
2!
1073
        error(result.getErrorMessage());
×
1074
      }
1075
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1076
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1077
      if (cmd != null) {
2!
1078
        help.commandlet.setValue(cmd);
×
1079
      }
1080
      help.run();
2✔
1081
      return 1;
4✔
1082
    } catch (Throwable t) {
1✔
1083
      this.startContext.activateLogging();
3✔
1084
      step.error(t, true);
4✔
1085
      throw t;
2✔
1086
    } finally {
1087
      step.close();
2✔
1088
      assert (this.currentStep == null);
4!
1089
      step.logSummary(supressStepSuccess);
3✔
1090
    }
1091
  }
1092

1093
  @Override
1094
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1095

1096
    this.startContext.deactivateLogging(threshold);
4✔
1097
    lambda.run();
2✔
1098
    this.startContext.activateLogging();
3✔
1099
  }
1✔
1100

1101
  /**
1102
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1103
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1104
   *     {@link Commandlet} did not match and we have to try a different candidate).
1105
   */
1106
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1107

1108
    IdeLogLevel previousLogLevel = null;
2✔
1109
    cmd.reset();
2✔
1110
    ValidationResult result = apply(arguments, cmd);
5✔
1111
    if (result.isValid()) {
3!
1112
      result = cmd.validate();
3✔
1113
    }
1114
    if (result.isValid()) {
3!
1115
      debug("Running commandlet {}", cmd);
9✔
1116
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1117
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1118
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1119
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1120
      }
1121
      try {
1122
        if (cmd.isProcessableOutput()) {
3!
1123
          if (!debug().isEnabled()) {
×
1124
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1125
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1126
          }
1127
          this.startContext.activateLogging();
×
1128
        } else {
1129
          this.startContext.activateLogging();
3✔
1130
          if (cmd.isIdeHomeRequired()) {
3!
1131
            debug(getMessageIdeHomeFound());
4✔
1132
          }
1133
          Path settingsRepository = getSettingsGitRepository();
3✔
1134
          if (settingsRepository != null) {
2!
1135
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1136
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1137
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1138
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1139
                interaction(
×
1140
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1141

1142
              } else {
1143
                interaction(
×
1144
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1145
              }
1146
            }
1147
          }
1148
        }
1149
        boolean success = ensureLicenseAgreement(cmd);
4✔
1150
        if (!success) {
2!
1151
          return ValidationResultValid.get();
×
1152
        }
1153
        cmd.run();
2✔
1154
      } finally {
1155
        if (previousLogLevel != null) {
2!
1156
          this.startContext.setLogLevel(previousLogLevel);
×
1157
        }
1158
      }
1✔
1159
    } else {
1160
      trace("Commandlet did not match");
×
1161
    }
1162
    return result;
2✔
1163
  }
1164

1165
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1166

1167
    if (isTest()) {
3!
1168
      return true; // ignore for tests
2✔
1169
    }
1170
    getFileAccess().mkdirs(this.userHomeIde);
×
1171
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1172
    if (Files.isRegularFile(licenseAgreement)) {
×
1173
      return true; // success, license already accepted
×
1174
    }
1175
    if (cmd instanceof EnvironmentCommandlet) {
×
1176
      // if the license was not accepted, "$(ideasy env --bash)" that is written into a variable prevents the user from seeing the question he is asked
1177
      // in such situation the user could not open a bash terminal anymore and gets blocked what would really annoy the user so we exit here without doing or
1178
      // printing anything anymore in such case.
1179
      return false;
×
1180
    }
1181
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1182
    if (logLevelInfoDisabled) {
×
1183
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1184
    }
1185
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1186
    if (logLevelInteractionDisabled) {
×
1187
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1188
    }
1189
    StringBuilder sb = new StringBuilder(1180);
×
1190
    sb.append(LOGO).append("""
×
1191
        Welcome to IDEasy!
1192
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1193
        It supports automatic download and installation of arbitrary 3rd party tools.
1194
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1195
        But if explicitly configured, also commercial software that requires an additional license may be used.
1196
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1197
        You are solely responsible for all risks implied by using this software.
1198
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1199
        You will be able to find it online under the following URL:
1200
        """).append(LICENSE_URL);
×
1201
    if (this.ideRoot != null) {
×
1202
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1203
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1204
    }
1205
    info(sb.toString());
×
1206
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1207

1208
    sb.setLength(0);
×
1209
    LocalDateTime now = LocalDateTime.now();
×
1210
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1211
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1212
    try {
1213
      Files.writeString(licenseAgreement, sb);
×
1214
    } catch (Exception e) {
×
1215
      throw new RuntimeException("Failed to save license agreement!", e);
×
1216
    }
×
1217
    if (logLevelInfoDisabled) {
×
1218
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1219
    }
1220
    if (logLevelInteractionDisabled) {
×
1221
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1222
    }
1223
    return true;
×
1224
  }
1225

1226
  @Override
1227
  public void verifyIdeMinVersion(boolean throwException) {
1228
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1229
    if (minVersion == null) {
2✔
1230
      return;
1✔
1231
    }
1232
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1233
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1234
          + "However, this is too old as your project requires at latest version %s\n"
1235
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1236
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1237
      if (throwException) {
2✔
1238
        throw new CliException(message);
5✔
1239
      } else {
1240
        warning(message);
3✔
1241
      }
1242
    }
1243
  }
1✔
1244

1245
  /**
1246
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1247
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1248
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1249
   */
1250
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1251

1252
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1253
    if (arguments.current().isStart()) {
4✔
1254
      arguments.next();
3✔
1255
    }
1256
    if (includeContextOptions) {
2✔
1257
      ContextCommandlet cc = new ContextCommandlet();
4✔
1258
      for (Property<?> property : cc.getProperties()) {
11✔
1259
        assert (property.isOption());
4!
1260
        property.apply(arguments, this, cc, collector);
7✔
1261
      }
1✔
1262
    }
1263
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1264
    CliArgument current = arguments.current();
3✔
1265
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1266
      collector.add(current.get(), null, null, null);
7✔
1267
    }
1268
    arguments.next();
3✔
1269
    while (commandletIterator.hasNext()) {
3✔
1270
      Commandlet cmd = commandletIterator.next();
4✔
1271
      if (!arguments.current().isEnd()) {
4✔
1272
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1273
      }
1274
    }
1✔
1275
    return collector.getSortedCandidates();
3✔
1276
  }
1277

1278
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1279

1280
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1281
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1282
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1283
    List<Property<?>> properties = cmd.getProperties();
3✔
1284
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1285
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1286
    for (Property<?> property : properties) {
10✔
1287
      if (property.isOption()) {
3✔
1288
        optionProperties.add(property);
4✔
1289
      }
1290
    }
1✔
1291
    CliArgument currentArgument = arguments.current();
3✔
1292
    while (!currentArgument.isEnd()) {
3✔
1293
      trace("Trying to match argument '{}'", currentArgument);
9✔
1294
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1295
        if (currentArgument.isCompletion()) {
3✔
1296
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1297
          while (optionIterator.hasNext()) {
3✔
1298
            Property<?> option = optionIterator.next();
4✔
1299
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1300
            if (success) {
2✔
1301
              optionIterator.remove();
2✔
1302
              arguments.next();
3✔
1303
            }
1304
          }
1✔
1305
        } else {
1✔
1306
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1307
          if (option != null) {
2✔
1308
            arguments.next();
3✔
1309
            boolean removed = optionProperties.remove(option);
4✔
1310
            if (!removed) {
2!
1311
              option = null;
×
1312
            }
1313
          }
1314
          if (option == null) {
2✔
1315
            trace("No such option was found.");
3✔
1316
            return;
1✔
1317
          }
1318
        }
1✔
1319
      } else {
1320
        if (valueIterator.hasNext()) {
3✔
1321
          Property<?> valueProperty = valueIterator.next();
4✔
1322
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1323
          if (!success) {
2✔
1324
            trace("Completion cannot match any further.");
3✔
1325
            return;
1✔
1326
          }
1327
        } else {
1✔
1328
          trace("No value left for completion.");
3✔
1329
          return;
1✔
1330
        }
1331
      }
1332
      currentArgument = arguments.current();
4✔
1333
    }
1334
  }
1✔
1335

1336
  /**
1337
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1338
   *     {@link CliArguments#copy() copy} as needed.
1339
   * @param cmd the potential {@link Commandlet} to match.
1340
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1341
   */
1342
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1343

1344
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1345
    CliArgument currentArgument = arguments.current();
3✔
1346
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1347
    Property<?> property = null;
2✔
1348
    if (propertyIterator.hasNext()) {
3!
1349
      property = propertyIterator.next();
4✔
1350
    }
1351
    while (!currentArgument.isEnd()) {
3✔
1352
      trace("Trying to match argument '{}'", currentArgument);
9✔
1353
      Property<?> currentProperty = property;
2✔
1354
      if (!arguments.isEndOptions()) {
3!
1355
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1356
        if (option != null) {
2!
1357
          currentProperty = option;
×
1358
        }
1359
      }
1360
      if (currentProperty == null) {
2!
1361
        trace("No option or next value found");
×
1362
        ValidationState state = new ValidationState(null);
×
1363
        state.addErrorMessage("No matching property found");
×
1364
        return state;
×
1365
      }
1366
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1367
      if (currentProperty == property) {
3!
1368
        if (!property.isMultiValued()) {
3✔
1369
          if (propertyIterator.hasNext()) {
3✔
1370
            property = propertyIterator.next();
5✔
1371
          } else {
1372
            property = null;
2✔
1373
          }
1374
        }
1375
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1376
          arguments.stopSplitShortOptions();
2✔
1377
        }
1378
      }
1379
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1380
      if (!matches) {
2!
1381
        ValidationState state = new ValidationState(null);
×
1382
        state.addErrorMessage("No matching property found");
×
1383
        return state;
×
1384
      }
1385
      currentArgument = arguments.current();
3✔
1386
    }
1✔
1387
    return ValidationResultValid.get();
2✔
1388
  }
1389

1390
  @Override
1391
  public String findBash() {
1392

1393
    String bash = BASH;
2✔
1394
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1395
      String bashPathVariableName = IdeVariables.BASH_PATH.getName();
×
1396
      bash = getVariables().get(bashPathVariableName);
×
1397

1398
      if (bash != null) {
×
1399
        Path bashPathVariable = Path.of(bash);
×
1400
        if (Files.exists(bashPathVariable)) {
×
1401
          debug("{} variable was found and points to: {}", bashPathVariableName, bashPathVariable);
×
1402
        } else {
1403
          warning("{} variable was found at: {} but is not pointing to an existing file", bashPathVariableName, bashPathVariable);
×
1404
          bash = null;
×
1405
        }
1406
      } else {
×
1407
        debug("{} variable was not found", bashPathVariableName);
×
1408
      }
1409

1410
      if (bash == null) {
×
1411
        bash = findBashOnWindows();
×
1412
        if (bash == null) {
×
1413
          trace("Bash not found. Trying to search on system PATH.");
×
1414
          String pathVariableName = IdeVariables.PATH.getName();
×
1415
          if (pathVariableName != null) {
×
1416
            Path plainBash = Path.of(BASH);
×
1417
            Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
×
1418
                "\\windows\\system32");
1419
            Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
×
1420
            bash = bashPath.toAbsolutePath().toString();
×
1421
            if (bashPath.equals(plainBash)) {
×
1422
              warning("Could not find any usable bash on your PATH!");
×
1423
              bash = null;
×
1424
            }
1425
          } else {
×
1426
            debug("{} was not found", pathVariableName);
×
1427
          }
1428
        }
1429
        if (bash == null) {
×
1430
          info("Could not find bash in Windows registry, using bash from {} as fallback: {}", bashPathVariableName, bash);
×
1431
        }
1432
      }
1433
    }
1434
    return bash;
2✔
1435
  }
1436

1437
  /**
1438
   * @param path the path to check.
1439
   * @param toIgnore the String sequence which needs to be checked and ignored.
1440
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1441
   */
1442
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1443
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
×
1444
    return !s.contains(toIgnore);
×
1445
  }
1446

1447
  private String findBashOnWindows() {
1448

1449
    trace("Trying to find bash on Windows");
×
1450
    // Check if Git Bash exists in the default location
1451
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
×
1452
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
×
1453
      trace("Found default path to git on Windows at: {}", getDefaultWindowsGitPath());
×
1454
      return defaultPath.toString();
×
1455
    }
1456

1457
    // If not found in the default location, try the registry query
1458
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1459
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1460
    for (String bashVariant : bashVariants) {
×
1461
      trace("Trying to find bash variant: {}", bashVariant);
×
1462
      for (String registryKey : registryKeys) {
×
1463
        trace("Trying to find bash from registry key: {}", registryKey);
×
1464
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1465
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1466

1467
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1468
        if (path != null) {
×
1469
          String bashPath = path + "\\bin\\bash.exe";
×
1470
          debug("Found bash at: {}", bashPath);
×
1471
          return bashPath;
×
1472
        }
1473
      }
1474
    }
1475
    // no bash found
1476
    return null;
×
1477
  }
1478

1479
  @Override
1480
  public WindowsPathSyntax getPathSyntax() {
1481

1482
    return this.pathSyntax;
3✔
1483
  }
1484

1485
  /**
1486
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1487
   */
1488
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1489

1490
    this.pathSyntax = pathSyntax;
3✔
1491
  }
1✔
1492

1493
  /**
1494
   * @return the {@link IdeStartContextImpl}.
1495
   */
1496
  public IdeStartContextImpl getStartContext() {
1497

1498
    return startContext;
3✔
1499
  }
1500

1501
  /**
1502
   * @return the {@link WindowsHelper}.
1503
   */
1504
  public final WindowsHelper getWindowsHelper() {
1505

1506
    if (this.windowsHelper == null) {
3✔
1507
      this.windowsHelper = createWindowsHelper();
4✔
1508
    }
1509
    return this.windowsHelper;
3✔
1510
  }
1511

1512
  /**
1513
   * @return the new {@link WindowsHelper} instance.
1514
   */
1515
  protected WindowsHelper createWindowsHelper() {
1516

1517
    return new WindowsHelperImpl(this);
×
1518
  }
1519

1520
  /**
1521
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1522
   */
1523
  public void reload() {
1524

1525
    this.variables = null;
3✔
1526
    this.customToolRepository = null;
3✔
1527
  }
1✔
1528

1529
  @Override
1530
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1531

1532
    assert (Files.isDirectory(installationPath));
6!
1533
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1534
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1535
  }
1✔
1536

1537
  /**
1538
   * @param home the IDE_HOME directory.
1539
   * @param workspace the name of the active workspace folder.
1540
   */
1541
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1542

1543
  }
1544

1545
  /**
1546
   * Returns the default git path on Windows. Required to be overwritten in tests.
1547
   *
1548
   * @return default path to git on Windows.
1549
   */
1550
  public String getDefaultWindowsGitPath() {
1551
    return DEFAULT_WINDOWS_GIT_PATH;
×
1552
  }
1553

1554
}
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