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

devonfw / IDEasy / 19363561832

14 Nov 2025 11:44AM UTC coverage: 68.861% (-0.09%) from 68.955%
19363561832

push

github

web-flow
#1586: Validate and use BASH_PATH variable properly (#1587)

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

3496 of 5565 branches covered (62.82%)

Branch coverage included in aggregate %.

9160 of 12814 relevant lines covered (71.48%)

3.14 hits per line

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

63.39
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

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

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

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

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

87
  private final IdeStartContextImpl startContext;
88

89
  private Path ideHome;
90

91
  private final Path ideRoot;
92

93
  private Path confPath;
94

95
  protected Path settingsPath;
96

97
  private Path settingsCommitIdPath;
98

99
  protected Path pluginsPath;
100

101
  private Path workspacePath;
102

103
  private String workspaceName;
104

105
  private Path cwd;
106

107
  private Path downloadPath;
108

109
  private Path userHome;
110

111
  private Path userHomeIde;
112

113
  private SystemPath path;
114

115
  private WindowsPathSyntax pathSyntax;
116

117
  private final SystemInfo systemInfo;
118

119
  private EnvironmentVariables variables;
120

121
  private final FileAccess fileAccess;
122

123
  protected CommandletManager commandletManager;
124

125
  protected ToolRepository defaultToolRepository;
126

127
  private CustomToolRepository customToolRepository;
128

129
  private MvnRepository mvnRepository;
130

131
  private NpmRepository npmRepository;
132

133
  private DirectoryMerger workspaceMerger;
134

135
  protected UrlMetadata urlMetadata;
136

137
  protected Path defaultExecutionDirectory;
138

139
  private StepImpl currentStep;
140

141
  private NetworkStatus networkStatus;
142

143
  protected IdeSystem system;
144

145
  private WindowsHelper windowsHelper;
146

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

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

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

187
    // detection completed, initializing variables
188
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
189

190
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
191

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

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

204
  /**
205
   * 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
206
   * add additional validation or boundary checks.
207
   *
208
   * @param workingDirectory the starting directory for the search.
209
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
210
   */
211
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
212

213
    Path currentDir = workingDirectory;
2✔
214
    String name1 = "";
2✔
215
    String name2 = "";
2✔
216
    String workspace = WORKSPACE_MAIN;
2✔
217
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
218

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

239
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
240
  }
241

242
  /**
243
   * @return a new {@link MvnRepository}
244
   */
245
  protected MvnRepository createMvnRepository() {
246
    return new MvnRepository(this);
5✔
247
  }
248

249
  /**
250
   * @return a new {@link NpmRepository}
251
   */
252
  protected NpmRepository createNpmRepository() {
253
    return new NpmRepository(this);
5✔
254
  }
255

256
  private Path findIdeRoot(Path ideHomePath) {
257

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

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

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

312
  @Override
313
  public void setCwd(Path userDir, String workspace, Path ideHome) {
314

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

344
  private String getMessageIdeHomeFound() {
345

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

353
  private String getMessageNotInsideIdeProject() {
354

355
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
356
  }
357

358
  private String getMessageIdeRootNotFound() {
359

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

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

374
    return false;
×
375
  }
376

377
  protected SystemPath computeSystemPath() {
378

379
    return new SystemPath(this);
×
380
  }
381

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

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

398
  private EnvironmentVariables createVariables() {
399

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

408
  protected AbstractEnvironmentVariables createSystemVariables() {
409

410
    return EnvironmentVariables.ofSystem(this);
3✔
411
  }
412

413
  @Override
414
  public SystemInfo getSystemInfo() {
415

416
    return this.systemInfo;
3✔
417
  }
418

419
  @Override
420
  public FileAccess getFileAccess() {
421

422
    return this.fileAccess;
3✔
423
  }
424

425
  @Override
426
  public CommandletManager getCommandletManager() {
427

428
    return this.commandletManager;
3✔
429
  }
430

431
  @Override
432
  public ToolRepository getDefaultToolRepository() {
433

434
    return this.defaultToolRepository;
3✔
435
  }
436

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

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

453
  @Override
454
  public CustomToolRepository getCustomToolRepository() {
455

456
    if (this.customToolRepository == null) {
3!
457
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
458
    }
459
    return this.customToolRepository;
3✔
460
  }
461

462
  @Override
463
  public Path getIdeHome() {
464

465
    return this.ideHome;
3✔
466
  }
467

468
  @Override
469
  public String getProjectName() {
470

471
    if (this.ideHome != null) {
3!
472
      return this.ideHome.getFileName().toString();
5✔
473
    }
474
    return "";
×
475
  }
476

477
  @Override
478
  public VersionIdentifier getProjectVersion() {
479

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

490
  @Override
491
  public void setProjectVersion(VersionIdentifier version) {
492

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

501
  @Override
502
  public Path getIdeRoot() {
503

504
    return this.ideRoot;
3✔
505
  }
506

507
  @Override
508
  public Path getIdePath() {
509

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

517
  @Override
518
  public Path getCwd() {
519

520
    return this.cwd;
3✔
521
  }
522

523
  @Override
524
  public Path getTempPath() {
525

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

533
  @Override
534
  public Path getTempDownloadPath() {
535

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

543
  @Override
544
  public Path getUserHome() {
545

546
    return this.userHome;
3✔
547
  }
548

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

556
    this.userHome = userHome;
3✔
557
    resetPrivacyMap();
2✔
558
  }
1✔
559

560
  @Override
561
  public Path getUserHomeIde() {
562

563
    return this.userHomeIde;
3✔
564
  }
565

566
  @Override
567
  public Path getSettingsPath() {
568

569
    return this.settingsPath;
3✔
570
  }
571

572
  @Override
573
  public Path getSettingsGitRepository() {
574

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

584
  @Override
585
  public boolean isSettingsRepositorySymlinkOrJunction() {
586

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

594
  @Override
595
  public Path getSettingsCommitIdPath() {
596

597
    return this.settingsCommitIdPath;
3✔
598
  }
599

600
  @Override
601
  public Path getConfPath() {
602

603
    return this.confPath;
3✔
604
  }
605

606
  @Override
607
  public Path getSoftwarePath() {
608

609
    if (this.ideHome == null) {
3✔
610
      return null;
2✔
611
    }
612
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
613
  }
614

615
  @Override
616
  public Path getSoftwareExtraPath() {
617

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

625
  @Override
626
  public Path getSoftwareRepositoryPath() {
627

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

635
  @Override
636
  public Path getPluginsPath() {
637

638
    return this.pluginsPath;
3✔
639
  }
640

641
  @Override
642
  public String getWorkspaceName() {
643

644
    return this.workspaceName;
3✔
645
  }
646

647
  @Override
648
  public Path getWorkspacePath() {
649

650
    return this.workspacePath;
3✔
651
  }
652

653
  @Override
654
  public Path getDownloadPath() {
655

656
    return this.downloadPath;
3✔
657
  }
658

659
  @Override
660
  public Path getUrlsPath() {
661

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

669
  @Override
670
  public Path getToolRepositoryPath() {
671

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

679
  @Override
680
  public SystemPath getPath() {
681

682
    return this.path;
3✔
683
  }
684

685
  @Override
686
  public EnvironmentVariables getVariables() {
687

688
    if (this.variables == null) {
3✔
689
      this.variables = createVariables();
4✔
690
    }
691
    return this.variables;
3✔
692
  }
693

694
  @Override
695
  public UrlMetadata getUrls() {
696

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

706
  @Override
707
  public boolean isQuietMode() {
708

709
    return this.startContext.isQuietMode();
4✔
710
  }
711

712
  @Override
713
  public boolean isBatchMode() {
714

715
    return this.startContext.isBatchMode();
4✔
716
  }
717

718
  @Override
719
  public boolean isForceMode() {
720

721
    return this.startContext.isForceMode();
4✔
722
  }
723

724
  @Override
725
  public boolean isForcePull() {
726

727
    return this.startContext.isForcePull();
4✔
728
  }
729

730
  @Override
731
  public boolean isForcePlugins() {
732

733
    return this.startContext.isForcePlugins();
4✔
734
  }
735

736
  @Override
737
  public boolean isForceRepositories() {
738

739
    return this.startContext.isForceRepositories();
4✔
740
  }
741

742
  @Override
743
  public boolean isOfflineMode() {
744

745
    return this.startContext.isOfflineMode();
4✔
746
  }
747

748
  @Override
749
  public boolean isPrivacyMode() {
750
    return this.startContext.isPrivacyMode();
4✔
751
  }
752

753
  @Override
754
  public boolean isSkipUpdatesMode() {
755

756
    return this.startContext.isSkipUpdatesMode();
4✔
757
  }
758

759
  @Override
760
  public boolean isNoColorsMode() {
761

762
    return this.startContext.isNoColorsMode();
×
763
  }
764

765
  @Override
766
  public NetworkStatus getNetworkStatus() {
767

768
    if (this.networkStatus == null) {
×
769
      this.networkStatus = new NetworkStatusImpl(this);
×
770
    }
771
    return this.networkStatus;
×
772
  }
773

774
  @Override
775
  public Locale getLocale() {
776

777
    Locale locale = this.startContext.getLocale();
4✔
778
    if (locale == null) {
2✔
779
      locale = Locale.getDefault();
2✔
780
    }
781
    return locale;
2✔
782
  }
783

784
  @Override
785
  public DirectoryMerger getWorkspaceMerger() {
786

787
    if (this.workspaceMerger == null) {
3✔
788
      this.workspaceMerger = new DirectoryMerger(this);
6✔
789
    }
790
    return this.workspaceMerger;
3✔
791
  }
792

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

799
    return this.defaultExecutionDirectory;
×
800
  }
801

802
  /**
803
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
804
   */
805
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
806

807
    if (defaultExecutionDirectory != null) {
×
808
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
809
    }
810
  }
×
811

812
  @Override
813
  public GitContext getGitContext() {
814

815
    return new GitContextImpl(this);
×
816
  }
817

818
  @Override
819
  public ProcessContext newProcess() {
820

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

828
  @Override
829
  public IdeSystem getSystem() {
830

831
    if (this.system == null) {
×
832
      this.system = new IdeSystemImpl(this);
×
833
    }
834
    return this.system;
×
835
  }
836

837
  /**
838
   * @return a new instance of {@link ProcessContext}.
839
   * @see #newProcess()
840
   */
841
  protected ProcessContext createProcessContext() {
842

843
    return new ProcessContextImpl(this);
5✔
844
  }
845

846
  @Override
847
  public IdeSubLogger level(IdeLogLevel level) {
848

849
    return this.startContext.level(level);
5✔
850
  }
851

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

864
  @Override
865
  public String formatArgument(Object argument) {
866

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

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

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

904
  /**
905
   * Resets the privacy map in case fundamental values have changed.
906
   */
907
  private void resetPrivacyMap() {
908

909
    this.privacyMap.clear();
3✔
910
  }
1✔
911

912

913
  @Override
914
  public String askForInput(String message, String defaultValue) {
915

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

938
  @SuppressWarnings("unchecked")
939
  @Override
940
  public <O> O question(O[] options, String question, Object... args) {
941

942
    assert (options.length >= 2);
5!
943
    interaction(question, args);
4✔
944
    return displayOptionsAndGetAnswer(options);
4✔
945
  }
946

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

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

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

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

993
  @Override
994
  public Step getCurrentStep() {
995

996
    return this.currentStep;
×
997
  }
998

999
  @Override
1000
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1001

1002
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1003
    return this.currentStep;
3✔
1004
  }
1005

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

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

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

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

1072
  @Override
1073
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1074

1075
    this.startContext.deactivateLogging(threshold);
4✔
1076
    lambda.run();
2✔
1077
    this.startContext.activateLogging();
3✔
1078
  }
1✔
1079

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

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

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

1144
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1145

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

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

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

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

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

1257
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1258

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

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

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

1369
  @Override
1370
  public String findBash() {
1371

1372
    String bash = BASH;
2✔
1373
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1374
      String variable = IdeVariables.BASH_PATH.getName();
×
1375
      bash = getVariables().get(variable);
×
1376

1377
      if (bash != null) {
×
1378
        Path bashPathVariable = Path.of(bash);
×
1379
        if (Files.exists(bashPathVariable)) {
×
1380
          debug("{} variable was found and points to: {}", IdeVariables.BASH_PATH, bashPathVariable);
×
1381
        } else {
1382
          warning("{} variable was found at: {} but is not pointing to an existing file", IdeVariables.BASH_PATH, bashPathVariable);
×
1383
          bash = null;
×
1384
        }
1385
      } else {
×
1386
        debug("{} variable was not found", IdeVariables.BASH_PATH);
×
1387
      }
1388

1389
      if (bash == null) {
×
1390
        bash = findBashOnWindows();
×
1391
        if (bash == null) {
×
1392
          trace("Bash not found. Trying to search on system PATH.");
×
1393
          variable = IdeVariables.PATH.getName();
×
1394
          if (variable != null) {
×
1395
            Path plainBash = Path.of(BASH);
×
1396
            Path bashPath = getPath().findBinary(plainBash);
×
1397
            bash = bashPath.toAbsolutePath().toString();
×
1398
            if (bash.contains("AppData\\Local\\Microsoft\\WindowsApps")) {
×
1399
              warning("Only found windows fake bash that is not usable!");
×
1400
              bash = null;
×
1401
            }
1402
          } else {
×
1403
            debug("{} was not found", IdeVariables.PATH);
×
1404
          }
1405
        }
1406
        if (bash == null) {
×
1407
          info("Could not find bash in Windows registry, using bash from {} as fallback: {}", variable, bash);
×
1408
        }
1409
      }
1410
    }
1411
    return bash;
2✔
1412
  }
1413

1414
  private String findBashOnWindows() {
1415

1416
    trace("Trying to find bash on Windows");
×
1417
    // Check if Git Bash exists in the default location
1418
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
×
1419
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
×
1420
      trace("Found default path to git on Windows at: {}", getDefaultWindowsGitPath());
×
1421
      return defaultPath.toString();
×
1422
    }
1423

1424
    // If not found in the default location, try the registry query
1425
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1426
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1427
    for (String bashVariant : bashVariants) {
×
1428
      trace("Trying to find bash variant: {}", bashVariant);
×
1429
      for (String registryKey : registryKeys) {
×
1430
        trace("Trying to find bash from registry key: {}", registryKey);
×
1431
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1432
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1433

1434
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1435
        if (path != null) {
×
1436
          String bashPath = path + "\\bin\\bash.exe";
×
1437
          debug("Found bash at: {}", bashPath);
×
1438
          return bashPath;
×
1439
        }
1440
      }
1441
    }
1442
    // no bash found
1443
    return null;
×
1444
  }
1445

1446
  @Override
1447
  public WindowsPathSyntax getPathSyntax() {
1448

1449
    return this.pathSyntax;
3✔
1450
  }
1451

1452
  /**
1453
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1454
   */
1455
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1456

1457
    this.pathSyntax = pathSyntax;
3✔
1458
  }
1✔
1459

1460
  /**
1461
   * @return the {@link IdeStartContextImpl}.
1462
   */
1463
  public IdeStartContextImpl getStartContext() {
1464

1465
    return startContext;
3✔
1466
  }
1467

1468
  /**
1469
   * @return the {@link WindowsHelper}.
1470
   */
1471
  public final WindowsHelper getWindowsHelper() {
1472

1473
    if (this.windowsHelper == null) {
3✔
1474
      this.windowsHelper = createWindowsHelper();
4✔
1475
    }
1476
    return this.windowsHelper;
3✔
1477
  }
1478

1479
  /**
1480
   * @return the new {@link WindowsHelper} instance.
1481
   */
1482
  protected WindowsHelper createWindowsHelper() {
1483

1484
    return new WindowsHelperImpl(this);
×
1485
  }
1486

1487
  /**
1488
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1489
   */
1490
  public void reload() {
1491

1492
    this.variables = null;
3✔
1493
    this.customToolRepository = null;
3✔
1494
  }
1✔
1495

1496
  @Override
1497
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1498

1499
    assert (Files.isDirectory(installationPath));
6!
1500
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1501
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1502
  }
1✔
1503

1504
  /**
1505
   * @param home the IDE_HOME directory.
1506
   * @param workspace the name of the active workspace folder.
1507
   */
1508
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1509

1510
  }
1511

1512
  /**
1513
   * Returns the default git path on Windows. Required to be overwritten in tests.
1514
   *
1515
   * @return default path to git on Windows.
1516
   */
1517
  public String getDefaultWindowsGitPath() {
1518
    return DEFAULT_WINDOWS_GIT_PATH;
×
1519
  }
1520

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

© 2025 Coveralls, Inc