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

devonfw / IDEasy / 21754609702

06 Feb 2026 02:45PM UTC coverage: 70.451% (+0.005%) from 70.446%
21754609702

Pull #1705

github

web-flow
Merge dace30f0b into 70a7fbf5d
Pull Request #1705: #1703 allow snapshot usage

4045 of 6328 branches covered (63.92%)

Branch coverage included in aggregate %.

10496 of 14312 relevant lines covered (73.34%)

3.18 hits per line

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

67.19
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
  private static final GitUrl IDE_URLS_GIT = new GitUrl("https://github.com/devonfw/ide-urls.git", null);
7✔
84

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

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

91
  private final IdeStartContextImpl startContext;
92

93
  private Path ideHome;
94

95
  private final Path ideRoot;
96

97
  private Path confPath;
98

99
  protected Path settingsPath;
100

101
  private Path settingsCommitIdPath;
102

103
  protected Path pluginsPath;
104

105
  private Path workspacePath;
106

107
  private String workspaceName;
108

109
  private Path cwd;
110

111
  private Path downloadPath;
112

113
  private Path userHome;
114

115
  private Path userHomeIde;
116

117
  private SystemPath path;
118

119
  private WindowsPathSyntax pathSyntax;
120

121
  private final SystemInfo systemInfo;
122

123
  private EnvironmentVariables variables;
124

125
  private final FileAccess fileAccess;
126

127
  protected CommandletManager commandletManager;
128

129
  protected ToolRepository defaultToolRepository;
130

131
  private CustomToolRepository customToolRepository;
132

133
  private MvnRepository mvnRepository;
134

135
  private NpmRepository npmRepository;
136

137
  private PipRepository pipRepository;
138

139
  private DirectoryMerger workspaceMerger;
140

141
  protected UrlMetadata urlMetadata;
142

143
  protected Path defaultExecutionDirectory;
144

145
  private StepImpl currentStep;
146

147
  private NetworkStatus networkStatus;
148

149
  protected IdeSystem system;
150

151
  private WindowsHelper windowsHelper;
152

153
  private final Map<String, String> privacyMap;
154

155
  private Path bash;
156

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

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

195
    // detection completed, initializing variables
196
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
197

198
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
199

200
    if (this.ideRoot != null) {
3✔
201
      Path tempDownloadPath = getTempDownloadPath();
3✔
202
      if (Files.isDirectory(tempDownloadPath)) {
6✔
203
        // TODO delete all files older than 1 day here...
204
      } else {
205
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
206
      }
207
    }
208

209
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
210
  }
1✔
211

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

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

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

247
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
248
  }
249

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

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

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

271
  private Path findIdeRoot(Path ideHomePath) {
272

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

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

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

327
  @Override
328
  public void setCwd(Path userDir, String workspace, Path ideHome) {
329

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

359
  private String getMessageIdeHomeFound() {
360

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

368
  private String getMessageNotInsideIdeProject() {
369

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

373
  private String getMessageIdeRootNotFound() {
374

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

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

389
    return false;
×
390
  }
391

392
  protected SystemPath computeSystemPath() {
393

394
    return new SystemPath(this);
×
395
  }
396

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

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

413
  private EnvironmentVariables createVariables() {
414

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

423
  protected AbstractEnvironmentVariables createSystemVariables() {
424

425
    return EnvironmentVariables.ofSystem(this);
3✔
426
  }
427

428
  @Override
429
  public SystemInfo getSystemInfo() {
430

431
    return this.systemInfo;
3✔
432
  }
433

434
  @Override
435
  public FileAccess getFileAccess() {
436

437
    return this.fileAccess;
3✔
438
  }
439

440
  @Override
441
  public CommandletManager getCommandletManager() {
442

443
    return this.commandletManager;
3✔
444
  }
445

446
  @Override
447
  public ToolRepository getDefaultToolRepository() {
448

449
    return this.defaultToolRepository;
3✔
450
  }
451

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

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

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

476
  @Override
477
  public CustomToolRepository getCustomToolRepository() {
478

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

485
  @Override
486
  public Path getIdeHome() {
487

488
    return this.ideHome;
3✔
489
  }
490

491
  @Override
492
  public String getProjectName() {
493

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

500
  @Override
501
  public VersionIdentifier getProjectVersion() {
502

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

513
  @Override
514
  public void setProjectVersion(VersionIdentifier version) {
515

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

524
  @Override
525
  public Path getIdeRoot() {
526

527
    return this.ideRoot;
3✔
528
  }
529

530
  @Override
531
  public Path getIdePath() {
532

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

540
  @Override
541
  public Path getCwd() {
542

543
    return this.cwd;
3✔
544
  }
545

546
  @Override
547
  public Path getTempPath() {
548

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

556
  @Override
557
  public Path getTempDownloadPath() {
558

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

566
  @Override
567
  public Path getUserHome() {
568

569
    return this.userHome;
3✔
570
  }
571

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

579
    this.userHome = userHome;
3✔
580
    resetPrivacyMap();
2✔
581
  }
1✔
582

583
  @Override
584
  public Path getUserHomeIde() {
585

586
    return this.userHomeIde;
3✔
587
  }
588

589
  @Override
590
  public Path getSettingsPath() {
591

592
    return this.settingsPath;
3✔
593
  }
594

595
  @Override
596
  public Path getSettingsGitRepository() {
597

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

607
  @Override
608
  public boolean isSettingsRepositorySymlinkOrJunction() {
609

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

617
  @Override
618
  public Path getSettingsCommitIdPath() {
619

620
    return this.settingsCommitIdPath;
3✔
621
  }
622

623
  @Override
624
  public Path getConfPath() {
625

626
    return this.confPath;
3✔
627
  }
628

629
  @Override
630
  public Path getSoftwarePath() {
631

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

638
  @Override
639
  public Path getSoftwareExtraPath() {
640

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

648
  @Override
649
  public Path getSoftwareRepositoryPath() {
650

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

658
  @Override
659
  public Path getPluginsPath() {
660

661
    return this.pluginsPath;
3✔
662
  }
663

664
  @Override
665
  public String getWorkspaceName() {
666

667
    return this.workspaceName;
3✔
668
  }
669

670
  @Override
671
  public Path getWorkspacePath() {
672

673
    return this.workspacePath;
3✔
674
  }
675

676
  @Override
677
  public Path getDownloadPath() {
678

679
    return this.downloadPath;
3✔
680
  }
681

682
  @Override
683
  public Path getUrlsPath() {
684

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

692
  @Override
693
  public Path getToolRepositoryPath() {
694

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

702
  @Override
703
  public SystemPath getPath() {
704

705
    return this.path;
3✔
706
  }
707

708
  @Override
709
  public EnvironmentVariables getVariables() {
710

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

717
  @Override
718
  public UrlMetadata getUrls() {
719

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

729
  @Override
730
  public boolean isQuietMode() {
731

732
    return this.startContext.isQuietMode();
4✔
733
  }
734

735
  @Override
736
  public boolean isBatchMode() {
737

738
    return this.startContext.isBatchMode();
4✔
739
  }
740

741
  @Override
742
  public boolean isForceMode() {
743

744
    return this.startContext.isForceMode();
4✔
745
  }
746

747
  @Override
748
  public boolean isForcePull() {
749

750
    return this.startContext.isForcePull();
4✔
751
  }
752

753
  @Override
754
  public boolean isForcePlugins() {
755

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

759
  @Override
760
  public boolean isForceRepositories() {
761

762
    return this.startContext.isForceRepositories();
4✔
763
  }
764

765
  @Override
766
  public boolean isOfflineMode() {
767

768
    return this.startContext.isOfflineMode();
4✔
769
  }
770

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

776
  @Override
777
  public boolean isSkipUpdatesMode() {
778

779
    return this.startContext.isSkipUpdatesMode();
4✔
780
  }
781

782
  @Override
783
  public boolean isNoColorsMode() {
784

785
    return this.startContext.isNoColorsMode();
×
786
  }
787

788
  @Override
789
  public NetworkStatus getNetworkStatus() {
790

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

797
  @Override
798
  public Locale getLocale() {
799

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

807
  @Override
808
  public DirectoryMerger getWorkspaceMerger() {
809

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

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

822
    return this.defaultExecutionDirectory;
×
823
  }
824

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

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

835
  @Override
836
  public GitContext getGitContext() {
837

838
    return new GitContextImpl(this);
×
839
  }
840

841
  @Override
842
  public ProcessContext newProcess() {
843

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

851
  @Override
852
  public IdeSystem getSystem() {
853

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

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

866
    return new ProcessContextImpl(this);
×
867
  }
868

869
  @Override
870
  public IdeSubLogger level(IdeLogLevel level) {
871

872
    return this.startContext.level(level);
5✔
873
  }
874

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

887
  @Override
888
  public String formatArgument(Object argument) {
889

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

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

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

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

932
    this.privacyMap.clear();
3✔
933
  }
1✔
934

935

936
  @Override
937
  public String askForInput(String message, String defaultValue) {
938

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

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

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

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

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

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

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

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

1034
  @Override
1035
  public Step getCurrentStep() {
1036

1037
    return this.currentStep;
×
1038
  }
1039

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

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

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

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

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

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

1113
  @Override
1114
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1115

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

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

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

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

1185
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1186

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1546
  @Override
1547
  public WindowsPathSyntax getPathSyntax() {
1548

1549
    return this.pathSyntax;
3✔
1550
  }
1551

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

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

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

1565
    return startContext;
3✔
1566
  }
1567

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

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

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

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

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

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

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

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

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

1610
  }
1611

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

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