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

devonfw / IDEasy / 19337626462

13 Nov 2025 03:59PM UTC coverage: 68.927% (+0.05%) from 68.878%
19337626462

push

github

web-flow
#1188: Use WindowsHelper to find bash (#1568)

Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>
Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>
Co-authored-by: jan-vcapgemini <jan-vincent.hoelzle@capgemini.com>

3495 of 5557 branches covered (62.89%)

Branch coverage included in aggregate %.

9158 of 12800 relevant lines covered (71.55%)

3.14 hits per line

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

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

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

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

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

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

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

83
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
84
  public static final String BASH = "bash";
85

86
  private final IdeStartContextImpl startContext;
87

88
  private Path ideHome;
89

90
  private final Path ideRoot;
91

92
  private Path confPath;
93

94
  protected Path settingsPath;
95

96
  private Path settingsCommitIdPath;
97

98
  protected Path pluginsPath;
99

100
  private Path workspacePath;
101

102
  private String workspaceName;
103

104
  private Path cwd;
105

106
  private Path downloadPath;
107

108
  private Path userHome;
109

110
  private Path userHomeIde;
111

112
  private SystemPath path;
113

114
  private WindowsPathSyntax pathSyntax;
115

116
  private final SystemInfo systemInfo;
117

118
  private EnvironmentVariables variables;
119

120
  private final FileAccess fileAccess;
121

122
  protected CommandletManager commandletManager;
123

124
  protected ToolRepository defaultToolRepository;
125

126
  private CustomToolRepository customToolRepository;
127

128
  private MvnRepository mvnRepository;
129

130
  private NpmRepository npmRepository;
131

132
  private DirectoryMerger workspaceMerger;
133

134
  protected UrlMetadata urlMetadata;
135

136
  protected Path defaultExecutionDirectory;
137

138
  private StepImpl currentStep;
139

140
  private NetworkStatus networkStatus;
141

142
  protected IdeSystem system;
143

144
  private WindowsHelper windowsHelper;
145

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

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

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

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

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

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

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

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

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

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

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

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

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

255
  private Path findIdeRoot(Path ideHomePath) {
256

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

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

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

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

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

343
  private String getMessageIdeHomeFound() {
344

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

352
  private String getMessageNotInsideIdeProject() {
353

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

357
  private String getMessageIdeRootNotFound() {
358

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

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

373
    return false;
×
374
  }
375

376
  protected SystemPath computeSystemPath() {
377

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

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

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

397
  private EnvironmentVariables createVariables() {
398

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

407
  protected AbstractEnvironmentVariables createSystemVariables() {
408

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

412
  @Override
413
  public SystemInfo getSystemInfo() {
414

415
    return this.systemInfo;
3✔
416
  }
417

418
  @Override
419
  public FileAccess getFileAccess() {
420

421
    return this.fileAccess;
3✔
422
  }
423

424
  @Override
425
  public CommandletManager getCommandletManager() {
426

427
    return this.commandletManager;
3✔
428
  }
429

430
  @Override
431
  public ToolRepository getDefaultToolRepository() {
432

433
    return this.defaultToolRepository;
3✔
434
  }
435

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

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

452
  @Override
453
  public CustomToolRepository getCustomToolRepository() {
454

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

461
  @Override
462
  public Path getIdeHome() {
463

464
    return this.ideHome;
3✔
465
  }
466

467
  @Override
468
  public String getProjectName() {
469

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

476
  @Override
477
  public VersionIdentifier getProjectVersion() {
478

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

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

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

500
  @Override
501
  public Path getIdeRoot() {
502

503
    return this.ideRoot;
3✔
504
  }
505

506
  @Override
507
  public Path getIdePath() {
508

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

516
  @Override
517
  public Path getCwd() {
518

519
    return this.cwd;
3✔
520
  }
521

522
  @Override
523
  public Path getTempPath() {
524

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

532
  @Override
533
  public Path getTempDownloadPath() {
534

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

542
  @Override
543
  public Path getUserHome() {
544

545
    return this.userHome;
3✔
546
  }
547

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

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

559
  @Override
560
  public Path getUserHomeIde() {
561

562
    return this.userHomeIde;
3✔
563
  }
564

565
  @Override
566
  public Path getSettingsPath() {
567

568
    return this.settingsPath;
3✔
569
  }
570

571
  @Override
572
  public Path getSettingsGitRepository() {
573

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

583
  @Override
584
  public boolean isSettingsRepositorySymlinkOrJunction() {
585

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

593
  @Override
594
  public Path getSettingsCommitIdPath() {
595

596
    return this.settingsCommitIdPath;
3✔
597
  }
598

599
  @Override
600
  public Path getConfPath() {
601

602
    return this.confPath;
3✔
603
  }
604

605
  @Override
606
  public Path getSoftwarePath() {
607

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

614
  @Override
615
  public Path getSoftwareExtraPath() {
616

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

624
  @Override
625
  public Path getSoftwareRepositoryPath() {
626

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

634
  @Override
635
  public Path getPluginsPath() {
636

637
    return this.pluginsPath;
3✔
638
  }
639

640
  @Override
641
  public String getWorkspaceName() {
642

643
    return this.workspaceName;
3✔
644
  }
645

646
  @Override
647
  public Path getWorkspacePath() {
648

649
    return this.workspacePath;
3✔
650
  }
651

652
  @Override
653
  public Path getDownloadPath() {
654

655
    return this.downloadPath;
3✔
656
  }
657

658
  @Override
659
  public Path getUrlsPath() {
660

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

668
  @Override
669
  public Path getToolRepositoryPath() {
670

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

678
  @Override
679
  public SystemPath getPath() {
680

681
    return this.path;
3✔
682
  }
683

684
  @Override
685
  public EnvironmentVariables getVariables() {
686

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

693
  @Override
694
  public UrlMetadata getUrls() {
695

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

705
  @Override
706
  public boolean isQuietMode() {
707

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

711
  @Override
712
  public boolean isBatchMode() {
713

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

717
  @Override
718
  public boolean isForceMode() {
719

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

723
  @Override
724
  public boolean isForcePull() {
725

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

729
  @Override
730
  public boolean isForcePlugins() {
731

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

735
  @Override
736
  public boolean isForceRepositories() {
737

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

741
  @Override
742
  public boolean isOfflineMode() {
743

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

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

752
  @Override
753
  public boolean isSkipUpdatesMode() {
754

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

758
  @Override
759
  public boolean isNoColorsMode() {
760

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

764
  @Override
765
  public NetworkStatus getNetworkStatus() {
766

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

773
  @Override
774
  public Locale getLocale() {
775

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

783
  @Override
784
  public DirectoryMerger getWorkspaceMerger() {
785

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

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

798
    return this.defaultExecutionDirectory;
×
799
  }
800

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

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

811
  @Override
812
  public GitContext getGitContext() {
813

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

817
  @Override
818
  public ProcessContext newProcess() {
819

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

827
  @Override
828
  public IdeSystem getSystem() {
829

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

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

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

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

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

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

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

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

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

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

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

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

911

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

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

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

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

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

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

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

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

992
  @Override
993
  public Step getCurrentStep() {
994

995
    return this.currentStep;
×
996
  }
997

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

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

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

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

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

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

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

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

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

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

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

1143
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1144

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

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

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

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

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

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

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

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

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

1368
  @Override
1369
  public String findBash() {
1370

1371
    String bash = BASH;
2✔
1372
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1373
      bash = findBashOnWindows();
×
1374
      if (bash == null) {
×
1375
        String variable = IdeVariables.BASH_PATH.getName();
×
1376
        bash = getVariables().get(variable);
×
1377
        if (bash == null) {
×
1378
          trace("Bash not found. Trying to search on system PATH.");
×
1379
          variable = IdeVariables.PATH.getName();
×
1380
          Path plainBash = Path.of(BASH);
×
1381
          Path bashPath = getPath().findBinary(plainBash);
×
1382
          bash = bashPath.toAbsolutePath().toString();
×
1383
          if (bash.contains("AppData\\Local\\Microsoft\\WindowsApps")) {
×
1384
            warning("Only found windows fake bash that is not usable!");
×
1385
            bash = null;
×
1386
          }
1387
        }
1388
        if (bash == null) {
×
1389
          info("Could not find bash in Windows registry, using bash from {} as fallback: {}", variable, bash);
×
1390
        }
1391
      }
1392
    }
1393
    return bash;
2✔
1394
  }
1395

1396
  private String findBashOnWindows() {
1397

1398
    // Check if Git Bash exists in the default location
1399
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1400
    if (Files.exists(defaultPath)) {
×
1401
      return defaultPath.toString();
×
1402
    }
1403

1404
    // If not found in the default location, try the registry query
1405
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1406
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1407
    for (String bashVariant : bashVariants) {
×
1408
      trace("Trying to find bash variant: {}", bashVariant);
×
1409
      for (String registryKey : registryKeys) {
×
1410
        trace("Trying to find bash from registry key: {}", registryKey);
×
1411
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1412
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1413

1414
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1415
        if (path != null) {
×
1416
          String bashPath = path + "\\bin\\bash.exe";
×
1417
          debug("Found bash at: {}", bashPath);
×
1418
          return bashPath;
×
1419
        }
1420
      }
1421
    }
1422
    // no bash found
1423
    return null;
×
1424
  }
1425

1426
  @Override
1427
  public WindowsPathSyntax getPathSyntax() {
1428

1429
    return this.pathSyntax;
3✔
1430
  }
1431

1432
  /**
1433
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1434
   */
1435
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1436

1437
    this.pathSyntax = pathSyntax;
3✔
1438
  }
1✔
1439

1440
  /**
1441
   * @return the {@link IdeStartContextImpl}.
1442
   */
1443
  public IdeStartContextImpl getStartContext() {
1444

1445
    return startContext;
3✔
1446
  }
1447

1448
  /**
1449
   * @return the {@link WindowsHelper}.
1450
   */
1451
  public final WindowsHelper getWindowsHelper() {
1452

1453
    if (this.windowsHelper == null) {
3✔
1454
      this.windowsHelper = createWindowsHelper();
4✔
1455
    }
1456
    return this.windowsHelper;
3✔
1457
  }
1458

1459
  /**
1460
   * @return the new {@link WindowsHelper} instance.
1461
   */
1462
  protected WindowsHelper createWindowsHelper() {
1463

1464
    return new WindowsHelperImpl(this);
×
1465
  }
1466

1467
  /**
1468
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1469
   */
1470
  public void reload() {
1471

1472
    this.variables = null;
3✔
1473
    this.customToolRepository = null;
3✔
1474
  }
1✔
1475

1476
  @Override
1477
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1478

1479
    assert (Files.isDirectory(installationPath));
6!
1480
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1481
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1482
  }
1✔
1483

1484
  /**
1485
   * @param home the IDE_HOME directory.
1486
   * @param workspace the name of the active workspace folder.
1487
   */
1488
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1489

1490
  }
1491

1492
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc