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

devonfw / IDEasy / 22203456564

19 Feb 2026 10:51PM UTC coverage: 70.112% (-0.4%) from 70.474%
22203456564

Pull #1710

github

web-flow
Merge 97cf467e5 into 379acdc9d
Pull Request #1710: #404: allow logging via SLF4J

4065 of 6386 branches covered (63.65%)

Branch coverage included in aggregate %.

10580 of 14502 relevant lines covered (72.96%)

3.16 hits per line

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

67.22
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.custom.CustomToolRepository;
62
import com.devonfw.tools.ide.tool.custom.CustomToolRepositoryImpl;
63
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
64
import com.devonfw.tools.ide.tool.npm.NpmRepository;
65
import com.devonfw.tools.ide.tool.pip.PipRepository;
66
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
67
import com.devonfw.tools.ide.tool.repository.ToolRepository;
68
import com.devonfw.tools.ide.url.model.UrlMetadata;
69
import com.devonfw.tools.ide.util.DateTimeUtil;
70
import com.devonfw.tools.ide.util.PrivacyUtil;
71
import com.devonfw.tools.ide.validation.ValidationResult;
72
import com.devonfw.tools.ide.validation.ValidationResultValid;
73
import com.devonfw.tools.ide.validation.ValidationState;
74
import com.devonfw.tools.ide.variable.IdeVariables;
75
import com.devonfw.tools.ide.version.IdeVersion;
76
import com.devonfw.tools.ide.version.VersionIdentifier;
77

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

83
  /** The default shell bash (Bourne Again SHell). */
84
  public static final String BASH = "bash";
85

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

88
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
89

90
  private static final String DEFAULT_WINDOWS_GIT_PATH = "C:\\Program Files\\Git\\bin\\bash.exe";
91

92
  private static final String OPTION_DETAILS_START = "([";
93

94
  private final IdeStartContextImpl startContext;
95

96
  private Path ideHome;
97

98
  private final Path ideRoot;
99

100
  private Path confPath;
101

102
  protected Path settingsPath;
103

104
  private Path settingsCommitIdPath;
105

106
  protected Path pluginsPath;
107

108
  private Path workspacePath;
109

110
  private String workspaceName;
111

112
  private Path cwd;
113

114
  private Path downloadPath;
115

116
  private Path userHome;
117

118
  private Path userHomeIde;
119

120
  private SystemPath path;
121

122
  private WindowsPathSyntax pathSyntax;
123

124
  private final SystemInfo systemInfo;
125

126
  private EnvironmentVariables variables;
127

128
  private final FileAccess fileAccess;
129

130
  protected CommandletManager commandletManager;
131

132
  protected ToolRepository defaultToolRepository;
133

134
  private CustomToolRepository customToolRepository;
135

136
  private MvnRepository mvnRepository;
137

138
  private NpmRepository npmRepository;
139

140
  private PipRepository pipRepository;
141

142
  private DirectoryMerger workspaceMerger;
143

144
  protected UrlMetadata urlMetadata;
145

146
  protected Path defaultExecutionDirectory;
147

148
  private StepImpl currentStep;
149

150
  private NetworkStatus networkStatus;
151

152
  protected IdeSystem system;
153

154
  private WindowsHelper windowsHelper;
155

156
  private final Map<String, String> privacyMap;
157

158
  private Path bash;
159

160
  /**
161
   * The constructor.
162
   *
163
   * @param startContext the {@link IdeLogger}.
164
   * @param workingDirectory the optional {@link Path} to current working directory.
165
   */
166
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
167

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

198
    // detection completed, initializing variables
199
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
200

201
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
202

203
    if (this.ideRoot != null) {
3✔
204
      Path tempDownloadPath = getTempDownloadPath();
3✔
205
      if (Files.isDirectory(tempDownloadPath)) {
6✔
206
        // TODO delete all files older than 1 day here...
207
      } else {
208
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
209
      }
210
    }
211
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
212
  }
1✔
213

214
  /**
215
   * 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
216
   * add additional validation or boundary checks.
217
   *
218
   * @param workingDirectory the starting directory for the search.
219
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
220
   */
221
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
222

223
    Path currentDir = workingDirectory;
2✔
224
    String name1 = "";
2✔
225
    String name2 = "";
2✔
226
    String workspace = WORKSPACE_MAIN;
2✔
227
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
228

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

249
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
250
  }
251

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

259
  /**
260
   * @return a new {@link NpmRepository}
261
   */
262
  protected NpmRepository createNpmRepository() {
263
    return new NpmRepository(this);
×
264
  }
265

266
  /**
267
   * @return a new {@link PipRepository}
268
   */
269
  protected PipRepository createPipRepository() {
270
    return new PipRepository(this);
×
271
  }
272

273
  private Path findIdeRoot(Path ideHomePath) {
274

275
    Path ideRootPath = null;
2✔
276
    if (ideHomePath != null) {
2✔
277
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
278
      ideRootPath = ideHomePath.getParent();
3✔
279
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
2!
280
        warning(
×
281
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
282
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
283
            ideRootPathFromEnv,
284
            ideHomePath.getFileName(), ideRootPath);
×
285
      }
286
    } else if (!isTest()) {
4!
287
      ideRootPath = getIdeRootPathFromEnv(true);
×
288
    }
289
    return ideRootPath;
2✔
290
  }
291

292
  /**
293
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
294
   */
295
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
296

297
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
298
    if (root != null) {
×
299
      Path rootPath = Path.of(root);
×
300
      if (Files.isDirectory(rootPath)) {
×
301
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
302
        if (withSanityCheck) {
×
303
          int nameCount = rootPath.getNameCount();
×
304
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
305
          int delta = absoluteNameCount - nameCount;
×
306
          if (delta >= 0) {
×
307
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
308
              String rootName = rootPath.getName(nameIndex).toString();
×
309
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
310
              if (!rootName.equals(absoluteRootName)) {
×
311
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
312
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
313
                break;
×
314
              }
315
            }
316
          } else {
317
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
318
                absoluteRootPath);
319
          }
320
        }
321
        return absoluteRootPath;
×
322
      } else if (withSanityCheck) {
×
323
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
324
      }
325
    }
326
    return null;
×
327
  }
328

329
  @Override
330
  public void setCwd(Path userDir, String workspace, Path ideHome) {
331

332
    this.cwd = userDir;
3✔
333
    this.workspaceName = workspace;
3✔
334
    this.ideHome = ideHome;
3✔
335
    if (ideHome == null) {
2✔
336
      this.workspacePath = null;
3✔
337
      this.confPath = null;
3✔
338
      this.settingsPath = null;
3✔
339
      this.pluginsPath = null;
4✔
340
    } else {
341
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
342
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
343
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
344
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
345
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
346
    }
347
    if (isTest()) {
3!
348
      // only for testing...
349
      if (this.ideHome == null) {
3✔
350
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
351
      } else {
352
        this.userHome = this.ideHome.resolve("home");
6✔
353
      }
354
    }
355
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
356
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
357
    resetPrivacyMap();
2✔
358
    this.path = computeSystemPath();
4✔
359
  }
1✔
360

361
  private String getMessageIdeHomeFound() {
362

363
    String wks = this.workspaceName;
3✔
364
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
365
      wks = "*".repeat(wks.length());
×
366
    }
367
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
368
  }
369

370
  private String getMessageNotInsideIdeProject() {
371

372
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
373
  }
374

375
  private String getMessageIdeRootNotFound() {
376

377
    String root = getSystem().getEnv("IDE_ROOT");
5✔
378
    if (root == null) {
2!
379
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
380
    } else {
381
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
382
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
383
    }
384
  }
385

386
  /**
387
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
388
   */
389
  public boolean isTest() {
390

391
    return false;
×
392
  }
393

394
  protected SystemPath computeSystemPath() {
395

396
    return new SystemPath(this);
×
397
  }
398

399
  /**
400
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
401
   *
402
   * @param dir the directory to check.
403
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
404
   */
405
  protected boolean isIdeHome(Path dir) {
406

407
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
408
      return false;
2✔
409
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
410
      return false;
×
411
    }
412
    return true;
2✔
413
  }
414

415
  private EnvironmentVariables createVariables() {
416

417
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
418
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
419
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
420
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
421
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
422
    return conf.resolved();
3✔
423
  }
424

425
  protected AbstractEnvironmentVariables createSystemVariables() {
426

427
    return EnvironmentVariables.ofSystem(this);
3✔
428
  }
429

430
  @Override
431
  public SystemInfo getSystemInfo() {
432

433
    return this.systemInfo;
3✔
434
  }
435

436
  @Override
437
  public FileAccess getFileAccess() {
438

439
    return this.fileAccess;
3✔
440
  }
441

442
  @Override
443
  public CommandletManager getCommandletManager() {
444

445
    return this.commandletManager;
3✔
446
  }
447

448
  @Override
449
  public ToolRepository getDefaultToolRepository() {
450

451
    return this.defaultToolRepository;
3✔
452
  }
453

454
  @Override
455
  public MvnRepository getMvnRepository() {
456
    if (this.mvnRepository == null) {
3✔
457
      this.mvnRepository = createMvnRepository();
4✔
458
    }
459
    return this.mvnRepository;
3✔
460
  }
461

462
  @Override
463
  public NpmRepository getNpmRepository() {
464
    if (this.npmRepository == null) {
3✔
465
      this.npmRepository = createNpmRepository();
4✔
466
    }
467
    return this.npmRepository;
3✔
468
  }
469

470
  @Override
471
  public PipRepository getPipRepository() {
472
    if (this.pipRepository == null) {
3✔
473
      this.pipRepository = createPipRepository();
4✔
474
    }
475
    return this.pipRepository;
3✔
476
  }
477

478
  @Override
479
  public CustomToolRepository getCustomToolRepository() {
480

481
    if (this.customToolRepository == null) {
3!
482
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
483
    }
484
    return this.customToolRepository;
3✔
485
  }
486

487
  @Override
488
  public Path getIdeHome() {
489

490
    return this.ideHome;
3✔
491
  }
492

493
  @Override
494
  public String getProjectName() {
495

496
    if (this.ideHome != null) {
3!
497
      return this.ideHome.getFileName().toString();
5✔
498
    }
499
    return "";
×
500
  }
501

502
  @Override
503
  public VersionIdentifier getProjectVersion() {
504

505
    if (this.ideHome != null) {
3!
506
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
507
      if (Files.exists(versionFile)) {
5✔
508
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
509
        return VersionIdentifier.of(version);
3✔
510
      }
511
    }
512
    return IdeMigrator.START_VERSION;
2✔
513
  }
514

515
  @Override
516
  public void setProjectVersion(VersionIdentifier version) {
517

518
    if (this.ideHome == null) {
3!
519
      throw new IllegalStateException("IDE_HOME not available!");
×
520
    }
521
    Objects.requireNonNull(version);
3✔
522
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
523
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
524
  }
1✔
525

526
  @Override
527
  public Path getIdeRoot() {
528

529
    return this.ideRoot;
3✔
530
  }
531

532
  @Override
533
  public Path getIdePath() {
534

535
    Path myIdeRoot = getIdeRoot();
3✔
536
    if (myIdeRoot == null) {
2!
537
      return null;
×
538
    }
539
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
540
  }
541

542
  @Override
543
  public Path getCwd() {
544

545
    return this.cwd;
3✔
546
  }
547

548
  @Override
549
  public Path getTempPath() {
550

551
    Path idePath = getIdePath();
3✔
552
    if (idePath == null) {
2!
553
      return null;
×
554
    }
555
    return idePath.resolve("tmp");
4✔
556
  }
557

558
  @Override
559
  public Path getTempDownloadPath() {
560

561
    Path tmp = getTempPath();
3✔
562
    if (tmp == null) {
2!
563
      return null;
×
564
    }
565
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
566
  }
567

568
  @Override
569
  public Path getUserHome() {
570

571
    return this.userHome;
3✔
572
  }
573

574
  /**
575
   * This method should only be used for tests to mock user home.
576
   *
577
   * @param userHome the new value of {@link #getUserHome()}.
578
   */
579
  protected void setUserHome(Path userHome) {
580

581
    this.userHome = userHome;
3✔
582
    resetPrivacyMap();
2✔
583
  }
1✔
584

585
  @Override
586
  public Path getUserHomeIde() {
587

588
    return this.userHomeIde;
3✔
589
  }
590

591
  @Override
592
  public Path getSettingsPath() {
593

594
    return this.settingsPath;
3✔
595
  }
596

597
  @Override
598
  public Path getSettingsGitRepository() {
599

600
    Path settingsPath = getSettingsPath();
3✔
601
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
602
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
603
      error("Settings repository exists but is not a git repository.");
3✔
604
      return null;
2✔
605
    }
606
    return settingsPath;
2✔
607
  }
608

609
  @Override
610
  public boolean isSettingsRepositorySymlinkOrJunction() {
611

612
    Path settingsPath = getSettingsPath();
3✔
613
    if (settingsPath == null) {
2!
614
      return false;
×
615
    }
616
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
617
  }
618

619
  @Override
620
  public Path getSettingsCommitIdPath() {
621

622
    return this.settingsCommitIdPath;
3✔
623
  }
624

625
  @Override
626
  public Path getConfPath() {
627

628
    return this.confPath;
3✔
629
  }
630

631
  @Override
632
  public Path getSoftwarePath() {
633

634
    if (this.ideHome == null) {
3✔
635
      return null;
2✔
636
    }
637
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
638
  }
639

640
  @Override
641
  public Path getSoftwareExtraPath() {
642

643
    Path softwarePath = getSoftwarePath();
3✔
644
    if (softwarePath == null) {
2!
645
      return null;
×
646
    }
647
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
648
  }
649

650
  @Override
651
  public Path getSoftwareRepositoryPath() {
652

653
    Path idePath = getIdePath();
3✔
654
    if (idePath == null) {
2!
655
      return null;
×
656
    }
657
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
658
  }
659

660
  @Override
661
  public Path getPluginsPath() {
662

663
    return this.pluginsPath;
3✔
664
  }
665

666
  @Override
667
  public String getWorkspaceName() {
668

669
    return this.workspaceName;
3✔
670
  }
671

672
  @Override
673
  public Path getWorkspacePath() {
674

675
    return this.workspacePath;
3✔
676
  }
677

678
  @Override
679
  public Path getDownloadPath() {
680

681
    return this.downloadPath;
3✔
682
  }
683

684
  @Override
685
  public Path getUrlsPath() {
686

687
    Path idePath = getIdePath();
3✔
688
    if (idePath == null) {
2!
689
      return null;
×
690
    }
691
    return idePath.resolve(FOLDER_URLS);
4✔
692
  }
693

694
  @Override
695
  public Path getToolRepositoryPath() {
696

697
    Path idePath = getIdePath();
3✔
698
    if (idePath == null) {
2!
699
      return null;
×
700
    }
701
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
702
  }
703

704
  @Override
705
  public SystemPath getPath() {
706

707
    return this.path;
3✔
708
  }
709

710
  @Override
711
  public EnvironmentVariables getVariables() {
712

713
    if (this.variables == null) {
3✔
714
      this.variables = createVariables();
4✔
715
    }
716
    return this.variables;
3✔
717
  }
718

719
  @Override
720
  public UrlMetadata getUrls() {
721

722
    if (this.urlMetadata == null) {
3✔
723
      if (!isTest()) {
3!
724
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
725
      }
726
      this.urlMetadata = new UrlMetadata(this);
6✔
727
    }
728
    return this.urlMetadata;
3✔
729
  }
730

731
  @Override
732
  public boolean isQuietMode() {
733

734
    return this.startContext.isQuietMode();
4✔
735
  }
736

737
  @Override
738
  public boolean isBatchMode() {
739

740
    return this.startContext.isBatchMode();
4✔
741
  }
742

743
  @Override
744
  public boolean isForceMode() {
745

746
    return this.startContext.isForceMode();
4✔
747
  }
748

749
  @Override
750
  public boolean isForcePull() {
751

752
    return this.startContext.isForcePull();
4✔
753
  }
754

755
  @Override
756
  public boolean isForcePlugins() {
757

758
    return this.startContext.isForcePlugins();
4✔
759
  }
760

761
  @Override
762
  public boolean isForceRepositories() {
763

764
    return this.startContext.isForceRepositories();
4✔
765
  }
766

767
  @Override
768
  public boolean isOfflineMode() {
769

770
    return this.startContext.isOfflineMode();
4✔
771
  }
772

773
  @Override
774
  public boolean isPrivacyMode() {
775
    return this.startContext.isPrivacyMode();
4✔
776
  }
777

778
  @Override
779
  public boolean isSkipUpdatesMode() {
780

781
    return this.startContext.isSkipUpdatesMode();
4✔
782
  }
783

784
  @Override
785
  public boolean isNoColorsMode() {
786

787
    return this.startContext.isNoColorsMode();
×
788
  }
789

790
  @Override
791
  public NetworkStatus getNetworkStatus() {
792

793
    if (this.networkStatus == null) {
×
794
      this.networkStatus = new NetworkStatusImpl(this);
×
795
    }
796
    return this.networkStatus;
×
797
  }
798

799
  @Override
800
  public Locale getLocale() {
801

802
    Locale locale = this.startContext.getLocale();
4✔
803
    if (locale == null) {
2✔
804
      locale = Locale.getDefault();
2✔
805
    }
806
    return locale;
2✔
807
  }
808

809
  @Override
810
  public DirectoryMerger getWorkspaceMerger() {
811

812
    if (this.workspaceMerger == null) {
3✔
813
      this.workspaceMerger = new DirectoryMerger(this);
6✔
814
    }
815
    return this.workspaceMerger;
3✔
816
  }
817

818
  /**
819
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
820
   */
821
  @Override
822
  public Path getDefaultExecutionDirectory() {
823

824
    return this.defaultExecutionDirectory;
×
825
  }
826

827
  /**
828
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
829
   */
830
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
831

832
    if (defaultExecutionDirectory != null) {
×
833
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
834
    }
835
  }
×
836

837
  @Override
838
  public GitContext getGitContext() {
839

840
    return new GitContextImpl(this);
×
841
  }
842

843
  @Override
844
  public ProcessContext newProcess() {
845

846
    ProcessContext processContext = createProcessContext();
3✔
847
    if (this.defaultExecutionDirectory != null) {
3!
848
      processContext.directory(this.defaultExecutionDirectory);
×
849
    }
850
    return processContext;
2✔
851
  }
852

853
  @Override
854
  public IdeSystem getSystem() {
855

856
    if (this.system == null) {
×
857
      this.system = new IdeSystemImpl();
×
858
    }
859
    return this.system;
×
860
  }
861

862
  /**
863
   * @return a new instance of {@link ProcessContext}.
864
   * @see #newProcess()
865
   */
866
  protected ProcessContext createProcessContext() {
867

868
    return new ProcessContextImpl(this);
×
869
  }
870

871
  @Override
872
  public IdeSubLogger level(IdeLogLevel level) {
873

874
    return this.startContext.level(level);
5✔
875
  }
876

877
  @Override
878
  public void logIdeHomeAndRootStatus() {
879
    if (this.ideRoot != null) {
3!
880
      success("IDE_ROOT is set to {}", this.ideRoot);
×
881
    }
882
    if (this.ideHome == null) {
3✔
883
      warning(getMessageNotInsideIdeProject());
5✔
884
    } else {
885
      success("IDE_HOME is set to {}", this.ideHome);
10✔
886
    }
887
  }
1✔
888

889
  @Override
890
  public String formatArgument(Object argument) {
891

892
    if (argument == null) {
2✔
893
      return null;
2✔
894
    }
895
    String result = argument.toString();
3✔
896
    if (isPrivacyMode()) {
3✔
897
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
898
        initializePrivacyMap(this.userHome, "~");
×
899
        String projectName = getProjectName();
×
900
        if (!projectName.isEmpty()) {
×
901
          this.privacyMap.put(projectName, "project");
×
902
        }
903
      }
904
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
905
        result = result.replace(entry.getKey(), entry.getValue());
×
906
      }
×
907
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
908
    }
909
    return result;
2✔
910
  }
911

912
  /**
913
   * @param path the sensitive {@link Path} to
914
   * @param replacement the replacement to mask the {@link Path} in log output.
915
   */
916
  protected void initializePrivacyMap(Path path, String replacement) {
917

918
    if (path == null) {
×
919
      return;
×
920
    }
921
    if (this.systemInfo.isWindows()) {
×
922
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
923
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
924
    } else {
925
      this.privacyMap.put(path.toString(), replacement);
×
926
    }
927
  }
×
928

929
  /**
930
   * Resets the privacy map in case fundamental values have changed.
931
   */
932
  private void resetPrivacyMap() {
933

934
    this.privacyMap.clear();
3✔
935
  }
1✔
936

937

938
  @Override
939
  public String askForInput(String message, String defaultValue) {
940

941
    while (true) {
942
      if (!message.isBlank()) {
3!
943
        interaction(message);
3✔
944
      }
945
      if (isBatchMode()) {
3!
946
        if (isForceMode()) {
×
947
          return defaultValue;
×
948
        } else {
949
          throw new CliAbortException();
×
950
        }
951
      }
952
      String input = readLine().trim();
4✔
953
      if (!input.isEmpty()) {
3!
954
        return input;
2✔
955
      } else {
956
        if (defaultValue != null) {
×
957
          return defaultValue;
×
958
        }
959
      }
960
    }
×
961
  }
962

963
  @SuppressWarnings("unchecked")
964
  @Override
965
  public <O> O question(O[] options, String question, Object... args) {
966

967
    assert (options.length > 0);
4!
968
    interaction(question, args);
4✔
969
    return displayOptionsAndGetAnswer(options);
4✔
970
  }
971

972
  private <O> O displayOptionsAndGetAnswer(O[] options) {
973
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
974
    int i = 0;
2✔
975
    for (O option : options) {
16✔
976
      i++;
1✔
977
      String title = "" + option;
4✔
978
      String key = computeOptionKey(title);
3✔
979
      addMapping(mapping, key, option);
4✔
980
      String numericKey = Integer.toString(i);
3✔
981
      if (numericKey.equals(key)) {
4!
982
        trace("Options should not be numeric: " + key);
×
983
      } else {
984
        addMapping(mapping, numericKey, option);
4✔
985
      }
986
      interaction("Option " + numericKey + ": " + title);
5✔
987
    }
988
    O option = null;
2✔
989
    if (isBatchMode()) {
3!
990
      if (isForceMode()) {
×
991
        option = options[0];
×
992
        interaction("" + option);
×
993
      }
994
    } else {
995
      while (option == null) {
2✔
996
        String answer = readLine();
3✔
997
        option = mapping.get(answer);
4✔
998
        if (option == null) {
2!
999
          warning("Invalid answer: '" + answer + "' - please try again.");
×
1000
        }
1001
      }
1✔
1002
    }
1003
    return option;
2✔
1004
  }
1005

1006
  private static String computeOptionKey(String option) {
1007
    String key = option;
2✔
1008
    int index = -1;
2✔
1009
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1010
      int currentIndex = key.indexOf(c);
4✔
1011
      if (currentIndex != -1) {
3✔
1012
        if ((index == -1) || (currentIndex < index)) {
3!
1013
          index = currentIndex;
2✔
1014
        }
1015
      }
1016
    }
1017
    if (index > 0) {
2✔
1018
      key = key.substring(0, index).trim();
6✔
1019
    }
1020
    return key;
2✔
1021
  }
1022

1023
  /**
1024
   * @return the input from the end-user (e.g. read from the console).
1025
   */
1026
  protected abstract String readLine();
1027

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

1030
    O duplicate = mapping.put(key, option);
5✔
1031
    if (duplicate != null) {
2!
1032
      throw new IllegalArgumentException("Duplicated option " + key);
×
1033
    }
1034
  }
1✔
1035

1036
  @Override
1037
  public Step getCurrentStep() {
1038

1039
    return this.currentStep;
×
1040
  }
1041

1042
  @Override
1043
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1044

1045
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1046
    return this.currentStep;
3✔
1047
  }
1048

1049
  /**
1050
   * Internal method to end the running {@link Step}.
1051
   *
1052
   * @param step the current {@link Step} to end.
1053
   */
1054
  public void endStep(StepImpl step) {
1055

1056
    if (step == this.currentStep) {
4!
1057
      this.currentStep = this.currentStep.getParent();
6✔
1058
    } else {
1059
      String currentStepName = "null";
×
1060
      if (this.currentStep != null) {
×
1061
        currentStepName = this.currentStep.getName();
×
1062
      }
1063
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1064
    }
1065
  }
1✔
1066

1067
  /**
1068
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1069
   *
1070
   * @param arguments the {@link CliArgument}.
1071
   * @return the return code of the execution.
1072
   */
1073
  public int run(CliArguments arguments) {
1074

1075
    CliArgument current = arguments.current();
3✔
1076
    assert (this.currentStep == null);
4!
1077
    boolean supressStepSuccess = false;
2✔
1078
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1079
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1080
    Commandlet cmd = null;
2✔
1081
    ValidationResult result = null;
2✔
1082
    try {
1083
      while (commandletIterator.hasNext()) {
3✔
1084
        cmd = commandletIterator.next();
4✔
1085
        result = applyAndRun(arguments.copy(), cmd);
6✔
1086
        if (result.isValid()) {
3!
1087
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1088
          step.success();
2✔
1089
          return ProcessResult.SUCCESS;
4✔
1090
        }
1091
      }
1092
      this.startContext.activateLogging();
3✔
1093
      verifyIdeMinVersion(false);
3✔
1094
      if (result != null) {
2!
1095
        error(result.getErrorMessage());
×
1096
      }
1097
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1098
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1099
      if (cmd != null) {
2!
1100
        help.commandlet.setValue(cmd);
×
1101
      }
1102
      help.run();
2✔
1103
      return 1;
4✔
1104
    } catch (Throwable t) {
1✔
1105
      this.startContext.activateLogging();
3✔
1106
      step.error(t, true);
4✔
1107
      throw t;
2✔
1108
    } finally {
1109
      step.close();
2✔
1110
      assert (this.currentStep == null);
4!
1111
      step.logSummary(supressStepSuccess);
3✔
1112
    }
1113
  }
1114

1115
  @Override
1116
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1117

1118
    this.startContext.deactivateLogging(threshold);
4✔
1119
    lambda.run();
2✔
1120
    this.startContext.activateLogging();
3✔
1121
  }
1✔
1122

1123
  /**
1124
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1125
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1126
   *     {@link Commandlet} did not match and we have to try a different candidate).
1127
   */
1128
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1129

1130
    IdeLogLevel previousLogLevel = null;
2✔
1131
    cmd.reset();
2✔
1132
    ValidationResult result = apply(arguments, cmd);
5✔
1133
    if (result.isValid()) {
3!
1134
      result = cmd.validate();
3✔
1135
    }
1136
    if (result.isValid()) {
3!
1137
      debug("Running commandlet {}", cmd);
9✔
1138
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1139
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1140
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1141
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1142
      }
1143
      try {
1144
        if (cmd.isProcessableOutput()) {
3!
1145
          if (!debug().isEnabled()) {
×
1146
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1147
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1148
          }
1149
          this.startContext.activateLogging();
×
1150
        } else {
1151
          this.startContext.activateLogging();
3✔
1152
          if (cmd.isIdeHomeRequired()) {
3!
1153
            debug(getMessageIdeHomeFound());
4✔
1154
          }
1155
          Path settingsRepository = getSettingsGitRepository();
3✔
1156
          if (settingsRepository != null) {
2!
1157
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1158
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1159
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1160
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1161
                interaction(
×
1162
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1163

1164
              } else {
1165
                interaction(
×
1166
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1167
              }
1168
            }
1169
          }
1170
        }
1171
        boolean success = ensureLicenseAgreement(cmd);
4✔
1172
        if (!success) {
2!
1173
          return ValidationResultValid.get();
×
1174
        }
1175
        cmd.run();
2✔
1176
      } finally {
1177
        if (previousLogLevel != null) {
2!
1178
          this.startContext.setLogLevel(previousLogLevel);
×
1179
        }
1180
      }
1✔
1181
    } else {
1182
      trace("Commandlet did not match");
×
1183
    }
1184
    return result;
2✔
1185
  }
1186

1187
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1188

1189
    if (isTest()) {
3!
1190
      return true; // ignore for tests
2✔
1191
    }
1192
    getFileAccess().mkdirs(this.userHomeIde);
×
1193
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1194
    if (Files.isRegularFile(licenseAgreement)) {
×
1195
      return true; // success, license already accepted
×
1196
    }
1197
    if (cmd instanceof EnvironmentCommandlet) {
×
1198
      // 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
1199
      // 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
1200
      // printing anything anymore in such case.
1201
      return false;
×
1202
    }
1203
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1204
    if (logLevelInfoDisabled) {
×
1205
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1206
    }
1207
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1208
    if (logLevelInteractionDisabled) {
×
1209
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1210
    }
1211
    StringBuilder sb = new StringBuilder(1180);
×
1212
    sb.append(LOGO).append("""
×
1213
        Welcome to IDEasy!
1214
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1215
        It supports automatic download and installation of arbitrary 3rd party tools.
1216
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1217
        But if explicitly configured, also commercial software that requires an additional license may be used.
1218
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1219
        You are solely responsible for all risks implied by using this software.
1220
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1221
        You will be able to find it online under the following URL:
1222
        """).append(LICENSE_URL);
×
1223
    if (this.ideRoot != null) {
×
1224
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1225
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1226
    }
1227
    info(sb.toString());
×
1228
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1229

1230
    sb.setLength(0);
×
1231
    LocalDateTime now = LocalDateTime.now();
×
1232
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1233
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1234
    try {
1235
      Files.writeString(licenseAgreement, sb);
×
1236
    } catch (Exception e) {
×
1237
      throw new RuntimeException("Failed to save license agreement!", e);
×
1238
    }
×
1239
    if (logLevelInfoDisabled) {
×
1240
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1241
    }
1242
    if (logLevelInteractionDisabled) {
×
1243
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1244
    }
1245
    return true;
×
1246
  }
1247

1248
  @Override
1249
  public void verifyIdeMinVersion(boolean throwException) {
1250
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1251
    if (minVersion == null) {
2✔
1252
      return;
1✔
1253
    }
1254
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1255
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1256
          + "However, this is too old as your project requires at latest version %s\n"
1257
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1258
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1259
      if (throwException) {
2✔
1260
        throw new CliException(message);
5✔
1261
      } else {
1262
        warning(message);
3✔
1263
      }
1264
    }
1265
  }
1✔
1266

1267
  /**
1268
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1269
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1270
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1271
   */
1272
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1273

1274
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1275
    if (arguments.current().isStart()) {
4✔
1276
      arguments.next();
3✔
1277
    }
1278
    if (includeContextOptions) {
2✔
1279
      ContextCommandlet cc = new ContextCommandlet();
4✔
1280
      for (Property<?> property : cc.getProperties()) {
11✔
1281
        assert (property.isOption());
4!
1282
        property.apply(arguments, this, cc, collector);
7✔
1283
      }
1✔
1284
    }
1285
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1286
    CliArgument current = arguments.current();
3✔
1287
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1288
      collector.add(current.get(), null, null, null);
7✔
1289
    }
1290
    arguments.next();
3✔
1291
    while (commandletIterator.hasNext()) {
3✔
1292
      Commandlet cmd = commandletIterator.next();
4✔
1293
      if (!arguments.current().isEnd()) {
4✔
1294
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1295
      }
1296
    }
1✔
1297
    return collector.getSortedCandidates();
3✔
1298
  }
1299

1300
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1301

1302
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1303
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1304
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1305
    List<Property<?>> properties = cmd.getProperties();
3✔
1306
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1307
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1308
    for (Property<?> property : properties) {
10✔
1309
      if (property.isOption()) {
3✔
1310
        optionProperties.add(property);
4✔
1311
      }
1312
    }
1✔
1313
    CliArgument currentArgument = arguments.current();
3✔
1314
    while (!currentArgument.isEnd()) {
3✔
1315
      trace("Trying to match argument '{}'", currentArgument);
9✔
1316
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1317
        if (currentArgument.isCompletion()) {
3✔
1318
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1319
          while (optionIterator.hasNext()) {
3✔
1320
            Property<?> option = optionIterator.next();
4✔
1321
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1322
            if (success) {
2✔
1323
              optionIterator.remove();
2✔
1324
              arguments.next();
3✔
1325
            }
1326
          }
1✔
1327
        } else {
1✔
1328
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1329
          if (option != null) {
2✔
1330
            arguments.next();
3✔
1331
            boolean removed = optionProperties.remove(option);
4✔
1332
            if (!removed) {
2!
1333
              option = null;
×
1334
            }
1335
          }
1336
          if (option == null) {
2✔
1337
            trace("No such option was found.");
3✔
1338
            return;
1✔
1339
          }
1340
        }
1✔
1341
      } else {
1342
        if (valueIterator.hasNext()) {
3✔
1343
          Property<?> valueProperty = valueIterator.next();
4✔
1344
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1345
          if (!success) {
2✔
1346
            trace("Completion cannot match any further.");
3✔
1347
            return;
1✔
1348
          }
1349
        } else {
1✔
1350
          trace("No value left for completion.");
3✔
1351
          return;
1✔
1352
        }
1353
      }
1354
      currentArgument = arguments.current();
4✔
1355
    }
1356
  }
1✔
1357

1358
  /**
1359
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1360
   *     {@link CliArguments#copy() copy} as needed.
1361
   * @param cmd the potential {@link Commandlet} to match.
1362
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1363
   */
1364
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1365

1366
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1367
    CliArgument currentArgument = arguments.current();
3✔
1368
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1369
    Property<?> property = null;
2✔
1370
    if (propertyIterator.hasNext()) {
3!
1371
      property = propertyIterator.next();
4✔
1372
    }
1373
    while (!currentArgument.isEnd()) {
3✔
1374
      trace("Trying to match argument '{}'", currentArgument);
9✔
1375
      Property<?> currentProperty = property;
2✔
1376
      if (!arguments.isEndOptions()) {
3!
1377
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1378
        if (option != null) {
2!
1379
          currentProperty = option;
×
1380
        }
1381
      }
1382
      if (currentProperty == null) {
2!
1383
        trace("No option or next value found");
×
1384
        ValidationState state = new ValidationState(null);
×
1385
        state.addErrorMessage("No matching property found");
×
1386
        return state;
×
1387
      }
1388
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1389
      if (currentProperty == property) {
3!
1390
        if (!property.isMultiValued()) {
3✔
1391
          if (propertyIterator.hasNext()) {
3✔
1392
            property = propertyIterator.next();
5✔
1393
          } else {
1394
            property = null;
2✔
1395
          }
1396
        }
1397
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1398
          arguments.stopSplitShortOptions();
2✔
1399
        }
1400
      }
1401
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1402
      if (!matches) {
2!
1403
        ValidationState state = new ValidationState(null);
×
1404
        state.addErrorMessage("No matching property found");
×
1405
        return state;
×
1406
      }
1407
      currentArgument = arguments.current();
3✔
1408
    }
1✔
1409
    return ValidationResultValid.get();
2✔
1410
  }
1411

1412
  @Override
1413
  public Path findBash() {
1414
    if (this.bash != null) {
3✔
1415
      return this.bash;
3✔
1416
    }
1417
    Path bashPath = findBashOnBashPath();
3✔
1418
    if (bashPath == null) {
2✔
1419
      bashPath = findBashInPath();
3✔
1420
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1421
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1422
        if (bashPath == null) {
2!
1423
          bashPath = findBashInWindowsRegistry();
3✔
1424
        }
1425
      }
1426
    }
1427
    if (bashPath == null) {
2✔
1428
      error("No bash executable could be found on your system.");
4✔
1429
    } else {
1430
      this.bash = bashPath;
3✔
1431
    }
1432
    return bashPath;
2✔
1433
  }
1434

1435
  private Path findBashOnBashPath() {
1436
    trace("Trying to find BASH_PATH environment variable.");
3✔
1437
    Path bash;
1438
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1439
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1440
    if (bashVariable != null) {
2✔
1441
      bash = Path.of(bashVariable);
5✔
1442
      if (Files.exists(bash)) {
5✔
1443
        debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
13✔
1444
        return bash;
2✔
1445
      } else {
1446
        error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
13✔
1447
        return null;
2✔
1448
      }
1449
    } else {
1450
      debug("{} environment variable was not found", bashPathVariableName);
9✔
1451
      return null;
2✔
1452
    }
1453
  }
1454

1455
  /**
1456
   * @param path the path to check.
1457
   * @param toIgnore the String sequence which needs to be checked and ignored.
1458
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1459
   */
1460
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1461
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1462
    return !s.contains(toIgnore);
7!
1463
  }
1464

1465
  /**
1466
   * Tries to find the bash.exe within the PATH environment variable.
1467
   *
1468
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1469
   */
1470
  private Path findBashInPath() {
1471
    trace("Trying to find bash in PATH environment variable.");
3✔
1472
    Path bash;
1473
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1474
    if (pathVariableName != null) {
2!
1475
      Path plainBash = Path.of(BASH);
5✔
1476
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1477
          "\\windows\\system32");
1478
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1479
      bash = bashPath.toAbsolutePath();
3✔
1480
      if (bashPath.equals(plainBash)) {
4✔
1481
        warning("No usable bash executable was found in your PATH environment variable!");
3✔
1482
        bash = null;
3✔
1483
      } else {
1484
        if (Files.exists(bashPath)) {
5!
1485
          debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
10✔
1486
        } else {
1487
          bash = null;
×
1488
          error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1489
        }
1490
      }
1491
    } else {
1✔
1492
      bash = null;
×
1493
      // this should never happen...
1494
      error("PATH environment variable was not found");
×
1495
    }
1496
    return bash;
2✔
1497
  }
1498

1499
  /**
1500
   * Tries to find the bash.exe within the Windows registry.
1501
   *
1502
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1503
   */
1504
  protected Path findBashInWindowsRegistry() {
1505
    trace("Trying to find bash in Windows registry");
×
1506
    // If not found in the default location, try the registry query
1507
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1508
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1509
    for (String bashVariant : bashVariants) {
×
1510
      trace("Trying to find bash variant: {}", bashVariant);
×
1511
      for (String registryKey : registryKeys) {
×
1512
        trace("Trying to find bash from registry key: {}", registryKey);
×
1513
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1514
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1515

1516
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1517
        if (path != null) {
×
1518
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1519
          if (Files.exists(bashPath)) {
×
1520
            debug("Found bash at: {}", bashPath);
×
1521
            return bashPath;
×
1522
          } else {
1523
            error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1524
            return null;
×
1525
          }
1526
        } else {
1527
          info("No bash executable could be found in the Windows registry.");
×
1528
        }
1529
      }
1530
    }
1531
    // no bash found
1532
    return null;
×
1533
  }
1534

1535
  private Path findBashOnWindowsDefaultGitPath() {
1536
    // Check if Git Bash exists in the default location
1537
    trace("Trying to find bash on the Windows default git path.");
3✔
1538
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1539
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1540
      trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1541
      return defaultPath;
×
1542
    }
1543
    debug("No bash was found on the Windows default git path.");
3✔
1544
    return null;
2✔
1545
  }
1546

1547
  @Override
1548
  public WindowsPathSyntax getPathSyntax() {
1549

1550
    return this.pathSyntax;
3✔
1551
  }
1552

1553
  /**
1554
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1555
   */
1556
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1557

1558
    this.pathSyntax = pathSyntax;
3✔
1559
  }
1✔
1560

1561
  /**
1562
   * @return the {@link IdeStartContextImpl}.
1563
   */
1564
  public IdeStartContextImpl getStartContext() {
1565

1566
    return startContext;
3✔
1567
  }
1568

1569
  /**
1570
   * @return the {@link WindowsHelper}.
1571
   */
1572
  public final WindowsHelper getWindowsHelper() {
1573

1574
    if (this.windowsHelper == null) {
3✔
1575
      this.windowsHelper = createWindowsHelper();
4✔
1576
    }
1577
    return this.windowsHelper;
3✔
1578
  }
1579

1580
  /**
1581
   * @return the new {@link WindowsHelper} instance.
1582
   */
1583
  protected WindowsHelper createWindowsHelper() {
1584

1585
    return new WindowsHelperImpl(this);
×
1586
  }
1587

1588
  /**
1589
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1590
   */
1591
  public void reload() {
1592

1593
    this.variables = null;
3✔
1594
    this.customToolRepository = null;
3✔
1595
  }
1✔
1596

1597
  @Override
1598
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1599

1600
    assert (Files.isDirectory(installationPath));
6!
1601
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1602
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1603
  }
1✔
1604

1605
  /*
1606
   * @param home the IDE_HOME directory.
1607
   * @param workspace the name of the active workspace folder.
1608
   */
1609
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1610

1611
  }
1612

1613
  /**
1614
   * Returns the default git path on Windows. Required to be overwritten in tests.
1615
   *
1616
   * @return default path to git on Windows.
1617
   */
1618
  public String getDefaultWindowsGitPath() {
1619
    return DEFAULT_WINDOWS_GIT_PATH;
×
1620
  }
1621

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