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

devonfw / IDEasy / 19640865660

24 Nov 2025 04:05PM UTC coverage: 69.024% (+0.1%) from 68.924%
19640865660

push

github

web-flow
#1561: Fix BASH_PATH not used properly (#1577)

Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>

3562 of 5651 branches covered (63.03%)

Branch coverage included in aggregate %.

9262 of 12928 relevant lines covered (71.64%)

3.15 hits per line

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

67.67
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 final IdeStartContextImpl startContext;
89

90
  private Path ideHome;
91

92
  private final Path ideRoot;
93

94
  private Path confPath;
95

96
  protected Path settingsPath;
97

98
  private Path settingsCommitIdPath;
99

100
  protected Path pluginsPath;
101

102
  private Path workspacePath;
103

104
  private String workspaceName;
105

106
  private Path cwd;
107

108
  private Path downloadPath;
109

110
  private Path userHome;
111

112
  private Path userHomeIde;
113

114
  private SystemPath path;
115

116
  private WindowsPathSyntax pathSyntax;
117

118
  private final SystemInfo systemInfo;
119

120
  private EnvironmentVariables variables;
121

122
  private final FileAccess fileAccess;
123

124
  protected CommandletManager commandletManager;
125

126
  protected ToolRepository defaultToolRepository;
127

128
  private CustomToolRepository customToolRepository;
129

130
  private MvnRepository mvnRepository;
131

132
  private NpmRepository npmRepository;
133

134
  private DirectoryMerger workspaceMerger;
135

136
  protected UrlMetadata urlMetadata;
137

138
  protected Path defaultExecutionDirectory;
139

140
  private StepImpl currentStep;
141

142
  private NetworkStatus networkStatus;
143

144
  protected IdeSystem system;
145

146
  private WindowsHelper windowsHelper;
147

148
  private final Map<String, String> privacyMap;
149

150
  private Path bash;
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))) {
6!
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 key = "" + option;
4✔
956
      addMapping(mapping, key, option);
4✔
957
      String numericKey = Integer.toString(i);
3✔
958
      if (numericKey.equals(key)) {
4!
959
        trace("Options should not be numeric: " + key);
×
960
      } else {
961
        addMapping(mapping, numericKey, option);
4✔
962
      }
963
      interaction("Option " + numericKey + ": " + key);
5✔
964
    }
965
    O option = null;
2✔
966
    if (isBatchMode()) {
3!
967
      if (isForceMode()) {
×
968
        option = options[0];
×
969
        interaction("" + option);
×
970
      }
971
    } else {
972
      while (option == null) {
2✔
973
        String answer = readLine();
3✔
974
        option = mapping.get(answer);
4✔
975
        if (option == null) {
2!
976
          warning("Invalid answer: '" + answer + "' - please try again.");
×
977
        }
978
      }
1✔
979
    }
980
    return option;
2✔
981
  }
982

983
  /**
984
   * @return the input from the end-user (e.g. read from the console).
985
   */
986
  protected abstract String readLine();
987

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

990
    O duplicate = mapping.put(key, option);
5✔
991
    if (duplicate != null) {
2!
992
      throw new IllegalArgumentException("Duplicated option " + key);
×
993
    }
994
  }
1✔
995

996
  @Override
997
  public Step getCurrentStep() {
998

999
    return this.currentStep;
×
1000
  }
1001

1002
  @Override
1003
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1004

1005
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1006
    return this.currentStep;
3✔
1007
  }
1008

1009
  /**
1010
   * Internal method to end the running {@link Step}.
1011
   *
1012
   * @param step the current {@link Step} to end.
1013
   */
1014
  public void endStep(StepImpl step) {
1015

1016
    if (step == this.currentStep) {
4!
1017
      this.currentStep = this.currentStep.getParent();
6✔
1018
    } else {
1019
      String currentStepName = "null";
×
1020
      if (this.currentStep != null) {
×
1021
        currentStepName = this.currentStep.getName();
×
1022
      }
1023
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1024
    }
1025
  }
1✔
1026

1027
  /**
1028
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1029
   *
1030
   * @param arguments the {@link CliArgument}.
1031
   * @return the return code of the execution.
1032
   */
1033
  public int run(CliArguments arguments) {
1034

1035
    CliArgument current = arguments.current();
3✔
1036
    assert (this.currentStep == null);
4!
1037
    boolean supressStepSuccess = false;
2✔
1038
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1039
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1040
    Commandlet cmd = null;
2✔
1041
    ValidationResult result = null;
2✔
1042
    try {
1043
      while (commandletIterator.hasNext()) {
3✔
1044
        cmd = commandletIterator.next();
4✔
1045
        result = applyAndRun(arguments.copy(), cmd);
6✔
1046
        if (result.isValid()) {
3!
1047
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1048
          step.success();
2✔
1049
          return ProcessResult.SUCCESS;
4✔
1050
        }
1051
      }
1052
      this.startContext.activateLogging();
3✔
1053
      verifyIdeMinVersion(false);
3✔
1054
      if (result != null) {
2!
1055
        error(result.getErrorMessage());
×
1056
      }
1057
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1058
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1059
      if (cmd != null) {
2!
1060
        help.commandlet.setValue(cmd);
×
1061
      }
1062
      help.run();
2✔
1063
      return 1;
4✔
1064
    } catch (Throwable t) {
1✔
1065
      this.startContext.activateLogging();
3✔
1066
      step.error(t, true);
4✔
1067
      throw t;
2✔
1068
    } finally {
1069
      step.close();
2✔
1070
      assert (this.currentStep == null);
4!
1071
      step.logSummary(supressStepSuccess);
3✔
1072
    }
1073
  }
1074

1075
  @Override
1076
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1077

1078
    this.startContext.deactivateLogging(threshold);
4✔
1079
    lambda.run();
2✔
1080
    this.startContext.activateLogging();
3✔
1081
  }
1✔
1082

1083
  /**
1084
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1085
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1086
   *     {@link Commandlet} did not match and we have to try a different candidate).
1087
   */
1088
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1089

1090
    IdeLogLevel previousLogLevel = null;
2✔
1091
    cmd.reset();
2✔
1092
    ValidationResult result = apply(arguments, cmd);
5✔
1093
    if (result.isValid()) {
3!
1094
      result = cmd.validate();
3✔
1095
    }
1096
    if (result.isValid()) {
3!
1097
      debug("Running commandlet {}", cmd);
9✔
1098
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1099
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1100
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1101
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1102
      }
1103
      try {
1104
        if (cmd.isProcessableOutput()) {
3!
1105
          if (!debug().isEnabled()) {
×
1106
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1107
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1108
          }
1109
          this.startContext.activateLogging();
×
1110
        } else {
1111
          this.startContext.activateLogging();
3✔
1112
          if (cmd.isIdeHomeRequired()) {
3!
1113
            debug(getMessageIdeHomeFound());
4✔
1114
          }
1115
          Path settingsRepository = getSettingsGitRepository();
3✔
1116
          if (settingsRepository != null) {
2!
1117
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1118
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1119
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1120
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1121
                interaction(
×
1122
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1123

1124
              } else {
1125
                interaction(
×
1126
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1127
              }
1128
            }
1129
          }
1130
        }
1131
        boolean success = ensureLicenseAgreement(cmd);
4✔
1132
        if (!success) {
2!
1133
          return ValidationResultValid.get();
×
1134
        }
1135
        cmd.run();
2✔
1136
      } finally {
1137
        if (previousLogLevel != null) {
2!
1138
          this.startContext.setLogLevel(previousLogLevel);
×
1139
        }
1140
      }
1✔
1141
    } else {
1142
      trace("Commandlet did not match");
×
1143
    }
1144
    return result;
2✔
1145
  }
1146

1147
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1148

1149
    if (isTest()) {
3!
1150
      return true; // ignore for tests
2✔
1151
    }
1152
    getFileAccess().mkdirs(this.userHomeIde);
×
1153
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1154
    if (Files.isRegularFile(licenseAgreement)) {
×
1155
      return true; // success, license already accepted
×
1156
    }
1157
    if (cmd instanceof EnvironmentCommandlet) {
×
1158
      // 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
1159
      // 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
1160
      // printing anything anymore in such case.
1161
      return false;
×
1162
    }
1163
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1164
    if (logLevelInfoDisabled) {
×
1165
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1166
    }
1167
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1168
    if (logLevelInteractionDisabled) {
×
1169
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1170
    }
1171
    StringBuilder sb = new StringBuilder(1180);
×
1172
    sb.append(LOGO).append("""
×
1173
        Welcome to IDEasy!
1174
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1175
        It supports automatic download and installation of arbitrary 3rd party tools.
1176
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1177
        But if explicitly configured, also commercial software that requires an additional license may be used.
1178
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1179
        You are solely responsible for all risks implied by using this software.
1180
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1181
        You will be able to find it online under the following URL:
1182
        """).append(LICENSE_URL);
×
1183
    if (this.ideRoot != null) {
×
1184
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1185
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1186
    }
1187
    info(sb.toString());
×
1188
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1189

1190
    sb.setLength(0);
×
1191
    LocalDateTime now = LocalDateTime.now();
×
1192
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1193
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1194
    try {
1195
      Files.writeString(licenseAgreement, sb);
×
1196
    } catch (Exception e) {
×
1197
      throw new RuntimeException("Failed to save license agreement!", e);
×
1198
    }
×
1199
    if (logLevelInfoDisabled) {
×
1200
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1201
    }
1202
    if (logLevelInteractionDisabled) {
×
1203
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1204
    }
1205
    return true;
×
1206
  }
1207

1208
  @Override
1209
  public void verifyIdeMinVersion(boolean throwException) {
1210
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1211
    if (minVersion == null) {
2✔
1212
      return;
1✔
1213
    }
1214
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1215
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1216
          + "However, this is too old as your project requires at latest version %s\n"
1217
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1218
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1219
      if (throwException) {
2✔
1220
        throw new CliException(message);
5✔
1221
      } else {
1222
        warning(message);
3✔
1223
      }
1224
    }
1225
  }
1✔
1226

1227
  /**
1228
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1229
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1230
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1231
   */
1232
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1233

1234
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1235
    if (arguments.current().isStart()) {
4✔
1236
      arguments.next();
3✔
1237
    }
1238
    if (includeContextOptions) {
2✔
1239
      ContextCommandlet cc = new ContextCommandlet();
4✔
1240
      for (Property<?> property : cc.getProperties()) {
11✔
1241
        assert (property.isOption());
4!
1242
        property.apply(arguments, this, cc, collector);
7✔
1243
      }
1✔
1244
    }
1245
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1246
    CliArgument current = arguments.current();
3✔
1247
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1248
      collector.add(current.get(), null, null, null);
7✔
1249
    }
1250
    arguments.next();
3✔
1251
    while (commandletIterator.hasNext()) {
3✔
1252
      Commandlet cmd = commandletIterator.next();
4✔
1253
      if (!arguments.current().isEnd()) {
4✔
1254
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1255
      }
1256
    }
1✔
1257
    return collector.getSortedCandidates();
3✔
1258
  }
1259

1260
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1261

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

1318
  /**
1319
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1320
   *     {@link CliArguments#copy() copy} as needed.
1321
   * @param cmd the potential {@link Commandlet} to match.
1322
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1323
   */
1324
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1325

1326
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1327
    CliArgument currentArgument = arguments.current();
3✔
1328
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1329
    Property<?> property = null;
2✔
1330
    if (propertyIterator.hasNext()) {
3!
1331
      property = propertyIterator.next();
4✔
1332
    }
1333
    while (!currentArgument.isEnd()) {
3✔
1334
      trace("Trying to match argument '{}'", currentArgument);
9✔
1335
      Property<?> currentProperty = property;
2✔
1336
      if (!arguments.isEndOptions()) {
3!
1337
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1338
        if (option != null) {
2!
1339
          currentProperty = option;
×
1340
        }
1341
      }
1342
      if (currentProperty == null) {
2!
1343
        trace("No option or next value found");
×
1344
        ValidationState state = new ValidationState(null);
×
1345
        state.addErrorMessage("No matching property found");
×
1346
        return state;
×
1347
      }
1348
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1349
      if (currentProperty == property) {
3!
1350
        if (!property.isMultiValued()) {
3✔
1351
          if (propertyIterator.hasNext()) {
3✔
1352
            property = propertyIterator.next();
5✔
1353
          } else {
1354
            property = null;
2✔
1355
          }
1356
        }
1357
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1358
          arguments.stopSplitShortOptions();
2✔
1359
        }
1360
      }
1361
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1362
      if (!matches) {
2!
1363
        ValidationState state = new ValidationState(null);
×
1364
        state.addErrorMessage("No matching property found");
×
1365
        return state;
×
1366
      }
1367
      currentArgument = arguments.current();
3✔
1368
    }
1✔
1369
    return ValidationResultValid.get();
2✔
1370
  }
1371

1372
  @Override
1373
  public Path findBash() {
1374
    if (this.bash != null) {
3✔
1375
      return this.bash;
3✔
1376
    }
1377
    Path bashPath = findBashOnBashPath();
3✔
1378
    if (bashPath == null) {
2✔
1379
      bashPath = findBashInPath();
3✔
1380
      if (bashPath == null && getSystemInfo().isWindows()) {
6!
1381
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1382
        if (bashPath == null) {
2!
1383
          bashPath = findBashInWindowsRegistry();
3✔
1384
        }
1385
      }
1386
    }
1387
    if (bashPath == null) {
2✔
1388
      error("No bash executable could be found on your system.");
4✔
1389
    } else {
1390
      this.bash = bashPath;
3✔
1391
    }
1392
    return bashPath;
2✔
1393
  }
1394

1395
  private Path findBashOnBashPath() {
1396
    trace("Trying to find BASH_PATH environment variable.");
3✔
1397
    Path bash;
1398
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1399
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1400
    if (bashVariable != null) {
2✔
1401
      bash = Path.of(bashVariable);
5✔
1402
      if (Files.exists(bash)) {
5✔
1403
        debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
13✔
1404
        return bash;
2✔
1405
      } else {
1406
        error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
13✔
1407
        return null;
2✔
1408
      }
1409
    } else {
1410
      debug("{} environment variable was not found", bashPathVariableName);
9✔
1411
      return null;
2✔
1412
    }
1413
  }
1414

1415
  /**
1416
   * @param path the path to check.
1417
   * @param toIgnore the String sequence which needs to be checked and ignored.
1418
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1419
   */
1420
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1421
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1422
    return !s.contains(toIgnore);
7!
1423
  }
1424

1425
  /**
1426
   * Tries to find the bash.exe within the PATH environment variable.
1427
   *
1428
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1429
   */
1430
  private Path findBashInPath() {
1431
    trace("Trying to find bash in PATH environment variable.");
3✔
1432
    Path bash;
1433
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1434
    if (pathVariableName != null) {
2!
1435
      Path plainBash = Path.of(BASH);
5✔
1436
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1437
          "\\windows\\system32");
1438
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1439
      bash = bashPath.toAbsolutePath();
3✔
1440
      if (bashPath.equals(plainBash)) {
4✔
1441
        warning("No usable bash executable was found in your PATH environment variable!");
3✔
1442
        bash = null;
3✔
1443
      } else {
1444
        if (Files.exists(bashPath)) {
5!
1445
          debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
10✔
1446
        } else {
1447
          bash = null;
×
1448
          error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1449
        }
1450
      }
1451
    } else {
1✔
1452
      bash = null;
×
1453
      // this should never happen...
1454
      error("PATH environment variable was not found");
×
1455
    }
1456
    return bash;
2✔
1457
  }
1458

1459
  /**
1460
   * Tries to find the bash.exe within the Windows registry.
1461
   *
1462
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1463
   */
1464
  protected Path findBashInWindowsRegistry() {
1465
    trace("Trying to find bash in Windows registry");
×
1466
    // If not found in the default location, try the registry query
1467
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1468
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1469
    for (String bashVariant : bashVariants) {
×
1470
      trace("Trying to find bash variant: {}", bashVariant);
×
1471
      for (String registryKey : registryKeys) {
×
1472
        trace("Trying to find bash from registry key: {}", registryKey);
×
1473
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1474
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1475

1476
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1477
        if (path != null) {
×
1478
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1479
          if (Files.exists(bashPath)) {
×
1480
            debug("Found bash at: {}", bashPath);
×
1481
            return bashPath;
×
1482
          } else {
1483
            error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1484
            return null;
×
1485
          }
1486
        } else {
1487
          info("No bash executable could be found in the Windows registry.");
×
1488
        }
1489
      }
1490
    }
1491
    // no bash found
1492
    return null;
×
1493
  }
1494

1495
  private Path findBashOnWindowsDefaultGitPath() {
1496
    // Check if Git Bash exists in the default location
1497
    trace("Trying to find bash on the Windows default git path.");
3✔
1498
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1499
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1500
      trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1501
      return defaultPath;
×
1502
    }
1503
    debug("No bash was found on the Windows default git path.");
3✔
1504
    return null;
2✔
1505
  }
1506

1507
  @Override
1508
  public WindowsPathSyntax getPathSyntax() {
1509

1510
    return this.pathSyntax;
3✔
1511
  }
1512

1513
  /**
1514
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1515
   */
1516
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1517

1518
    this.pathSyntax = pathSyntax;
3✔
1519
  }
1✔
1520

1521
  /**
1522
   * @return the {@link IdeStartContextImpl}.
1523
   */
1524
  public IdeStartContextImpl getStartContext() {
1525

1526
    return startContext;
3✔
1527
  }
1528

1529
  /**
1530
   * @return the {@link WindowsHelper}.
1531
   */
1532
  public final WindowsHelper getWindowsHelper() {
1533

1534
    if (this.windowsHelper == null) {
3✔
1535
      this.windowsHelper = createWindowsHelper();
4✔
1536
    }
1537
    return this.windowsHelper;
3✔
1538
  }
1539

1540
  /**
1541
   * @return the new {@link WindowsHelper} instance.
1542
   */
1543
  protected WindowsHelper createWindowsHelper() {
1544

1545
    return new WindowsHelperImpl(this);
×
1546
  }
1547

1548
  /**
1549
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1550
   */
1551
  public void reload() {
1552

1553
    this.variables = null;
3✔
1554
    this.customToolRepository = null;
3✔
1555
  }
1✔
1556

1557
  @Override
1558
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1559

1560
    assert (Files.isDirectory(installationPath));
6!
1561
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1562
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1563
  }
1✔
1564

1565
  /**
1566
   * @param home the IDE_HOME directory.
1567
   * @param workspace the name of the active workspace folder.
1568
   */
1569
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1570

1571
  }
1572

1573
  /**
1574
   * Returns the default git path on Windows. Required to be overwritten in tests.
1575
   *
1576
   * @return default path to git on Windows.
1577
   */
1578
  public String getDefaultWindowsGitPath() {
1579
    return DEFAULT_WINDOWS_GIT_PATH;
×
1580
  }
1581

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