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

devonfw / IDEasy / 19263429939

11 Nov 2025 10:58AM UTC coverage: 68.878% (-0.03%) from 68.904%
19263429939

push

github

web-flow
#1551: add NetworkStatus and improve status commandlet (#1557)

Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>

3496 of 5559 branches covered (62.89%)

Branch coverage included in aggregate %.

9159 of 12814 relevant lines covered (71.48%)

3.14 hits per line

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

63.46
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.io.BufferedReader;
6
import java.io.InputStreamReader;
7
import java.nio.file.Files;
8
import java.nio.file.Path;
9
import java.time.LocalDateTime;
10
import java.util.ArrayList;
11
import java.util.HashMap;
12
import java.util.Iterator;
13
import java.util.List;
14
import java.util.Locale;
15
import java.util.Map;
16
import java.util.Map.Entry;
17
import java.util.Objects;
18

19
import com.devonfw.tools.ide.cli.CliAbortException;
20
import com.devonfw.tools.ide.cli.CliArgument;
21
import com.devonfw.tools.ide.cli.CliArguments;
22
import com.devonfw.tools.ide.cli.CliException;
23
import com.devonfw.tools.ide.commandlet.Commandlet;
24
import com.devonfw.tools.ide.commandlet.CommandletManager;
25
import com.devonfw.tools.ide.commandlet.CommandletManagerImpl;
26
import com.devonfw.tools.ide.commandlet.ContextCommandlet;
27
import com.devonfw.tools.ide.commandlet.EnvironmentCommandlet;
28
import com.devonfw.tools.ide.commandlet.HelpCommandlet;
29
import com.devonfw.tools.ide.common.SystemPath;
30
import com.devonfw.tools.ide.completion.CompletionCandidate;
31
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
32
import com.devonfw.tools.ide.completion.CompletionCandidateCollectorDefault;
33
import com.devonfw.tools.ide.environment.AbstractEnvironmentVariables;
34
import com.devonfw.tools.ide.environment.EnvironmentVariables;
35
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
36
import com.devonfw.tools.ide.environment.IdeSystem;
37
import com.devonfw.tools.ide.environment.IdeSystemImpl;
38
import com.devonfw.tools.ide.git.GitContext;
39
import com.devonfw.tools.ide.git.GitContextImpl;
40
import com.devonfw.tools.ide.git.GitUrl;
41
import com.devonfw.tools.ide.io.FileAccess;
42
import com.devonfw.tools.ide.io.FileAccessImpl;
43
import com.devonfw.tools.ide.log.IdeLogArgFormatter;
44
import com.devonfw.tools.ide.log.IdeLogLevel;
45
import com.devonfw.tools.ide.log.IdeLogger;
46
import com.devonfw.tools.ide.log.IdeSubLogger;
47
import com.devonfw.tools.ide.merge.DirectoryMerger;
48
import com.devonfw.tools.ide.migration.IdeMigrator;
49
import com.devonfw.tools.ide.network.NetworkStatus;
50
import com.devonfw.tools.ide.network.NetworkStatusImpl;
51
import com.devonfw.tools.ide.os.SystemInfo;
52
import com.devonfw.tools.ide.os.SystemInfoImpl;
53
import com.devonfw.tools.ide.os.WindowsHelper;
54
import com.devonfw.tools.ide.os.WindowsHelperImpl;
55
import com.devonfw.tools.ide.os.WindowsPathSyntax;
56
import com.devonfw.tools.ide.process.ProcessContext;
57
import com.devonfw.tools.ide.process.ProcessContextImpl;
58
import com.devonfw.tools.ide.process.ProcessResult;
59
import com.devonfw.tools.ide.property.Property;
60
import com.devonfw.tools.ide.step.Step;
61
import com.devonfw.tools.ide.step.StepImpl;
62
import com.devonfw.tools.ide.tool.repository.CustomToolRepository;
63
import com.devonfw.tools.ide.tool.repository.CustomToolRepositoryImpl;
64
import com.devonfw.tools.ide.tool.repository.DefaultToolRepository;
65
import com.devonfw.tools.ide.tool.repository.MvnRepository;
66
import com.devonfw.tools.ide.tool.repository.NpmRepository;
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

88
  private final IdeStartContextImpl startContext;
89

90
  private Path ideHome;
91

92
  private final Path ideRoot;
93

94
  private Path confPath;
95

96
  protected Path settingsPath;
97

98
  private Path settingsCommitIdPath;
99

100
  protected Path pluginsPath;
101

102
  private Path workspacePath;
103

104
  private String workspaceName;
105

106
  private Path cwd;
107

108
  private Path downloadPath;
109

110
  private Path userHome;
111

112
  private Path userHomeIde;
113

114
  private SystemPath path;
115

116
  private WindowsPathSyntax pathSyntax;
117

118
  private final SystemInfo systemInfo;
119

120
  private EnvironmentVariables variables;
121

122
  private final FileAccess fileAccess;
123

124
  protected CommandletManager commandletManager;
125

126
  protected ToolRepository defaultToolRepository;
127

128
  private CustomToolRepository customToolRepository;
129

130
  private MvnRepository mvnRepository;
131

132
  private NpmRepository npmRepository;
133

134
  private DirectoryMerger workspaceMerger;
135

136
  protected UrlMetadata urlMetadata;
137

138
  protected Path defaultExecutionDirectory;
139

140
  private StepImpl currentStep;
141

142
  private NetworkStatus networkStatus;
143

144
  protected IdeSystem system;
145

146
  private WindowsHelper windowsHelper;
147

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

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

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

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

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

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

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

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

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

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

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

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

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

257
  private Path findIdeRoot(Path ideHomePath) {
258

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

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

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

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

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

345
  private String getMessageIdeHomeFound() {
346

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

354
  private String getMessageNotInsideIdeProject() {
355

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

359
  private String getMessageIdeRootNotFound() {
360

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

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

375
    return false;
×
376
  }
377

378
  protected SystemPath computeSystemPath() {
379

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

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

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

399
  private EnvironmentVariables createVariables() {
400

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

409
  protected AbstractEnvironmentVariables createSystemVariables() {
410

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

414
  @Override
415
  public SystemInfo getSystemInfo() {
416

417
    return this.systemInfo;
3✔
418
  }
419

420
  @Override
421
  public FileAccess getFileAccess() {
422

423
    return this.fileAccess;
3✔
424
  }
425

426
  @Override
427
  public CommandletManager getCommandletManager() {
428

429
    return this.commandletManager;
3✔
430
  }
431

432
  @Override
433
  public ToolRepository getDefaultToolRepository() {
434

435
    return this.defaultToolRepository;
3✔
436
  }
437

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

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

454
  @Override
455
  public CustomToolRepository getCustomToolRepository() {
456

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

463
  @Override
464
  public Path getIdeHome() {
465

466
    return this.ideHome;
3✔
467
  }
468

469
  @Override
470
  public String getProjectName() {
471

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

478
  @Override
479
  public VersionIdentifier getProjectVersion() {
480

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

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

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

502
  @Override
503
  public Path getIdeRoot() {
504

505
    return this.ideRoot;
3✔
506
  }
507

508
  @Override
509
  public Path getIdePath() {
510

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

518
  @Override
519
  public Path getCwd() {
520

521
    return this.cwd;
3✔
522
  }
523

524
  @Override
525
  public Path getTempPath() {
526

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

534
  @Override
535
  public Path getTempDownloadPath() {
536

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

544
  @Override
545
  public Path getUserHome() {
546

547
    return this.userHome;
3✔
548
  }
549

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

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

561
  @Override
562
  public Path getUserHomeIde() {
563

564
    return this.userHomeIde;
3✔
565
  }
566

567
  @Override
568
  public Path getSettingsPath() {
569

570
    return this.settingsPath;
3✔
571
  }
572

573
  @Override
574
  public Path getSettingsGitRepository() {
575

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

585
  @Override
586
  public boolean isSettingsRepositorySymlinkOrJunction() {
587

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

595
  @Override
596
  public Path getSettingsCommitIdPath() {
597

598
    return this.settingsCommitIdPath;
3✔
599
  }
600

601
  @Override
602
  public Path getConfPath() {
603

604
    return this.confPath;
3✔
605
  }
606

607
  @Override
608
  public Path getSoftwarePath() {
609

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

616
  @Override
617
  public Path getSoftwareExtraPath() {
618

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

626
  @Override
627
  public Path getSoftwareRepositoryPath() {
628

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

636
  @Override
637
  public Path getPluginsPath() {
638

639
    return this.pluginsPath;
3✔
640
  }
641

642
  @Override
643
  public String getWorkspaceName() {
644

645
    return this.workspaceName;
3✔
646
  }
647

648
  @Override
649
  public Path getWorkspacePath() {
650

651
    return this.workspacePath;
3✔
652
  }
653

654
  @Override
655
  public Path getDownloadPath() {
656

657
    return this.downloadPath;
3✔
658
  }
659

660
  @Override
661
  public Path getUrlsPath() {
662

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

670
  @Override
671
  public Path getToolRepositoryPath() {
672

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

680
  @Override
681
  public SystemPath getPath() {
682

683
    return this.path;
3✔
684
  }
685

686
  @Override
687
  public EnvironmentVariables getVariables() {
688

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

695
  @Override
696
  public UrlMetadata getUrls() {
697

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

707
  @Override
708
  public boolean isQuietMode() {
709

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

713
  @Override
714
  public boolean isBatchMode() {
715

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

719
  @Override
720
  public boolean isForceMode() {
721

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

725
  @Override
726
  public boolean isForcePull() {
727

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

731
  @Override
732
  public boolean isForcePlugins() {
733

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

737
  @Override
738
  public boolean isForceRepositories() {
739

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

743
  @Override
744
  public boolean isOfflineMode() {
745

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

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

754
  @Override
755
  public boolean isSkipUpdatesMode() {
756

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

760
  @Override
761
  public boolean isNoColorsMode() {
762

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

766
  @Override
767
  public NetworkStatus getNetworkStatus() {
768

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

775
  @Override
776
  public Locale getLocale() {
777

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

785
  @Override
786
  public DirectoryMerger getWorkspaceMerger() {
787

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

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

800
    return this.defaultExecutionDirectory;
×
801
  }
802

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

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

813
  @Override
814
  public GitContext getGitContext() {
815

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

819
  @Override
820
  public ProcessContext newProcess() {
821

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

829
  @Override
830
  public IdeSystem getSystem() {
831

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

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

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

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

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

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

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

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

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

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

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

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

913

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

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

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

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

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

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

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

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

994
  @Override
995
  public Step getCurrentStep() {
996

997
    return this.currentStep;
×
998
  }
999

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

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

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

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

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

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

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

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

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

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

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

1145
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1146

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

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

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

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

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

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

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

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

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

1370
  @Override
1371
  public String findBash() {
1372

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

1398
  private String findBashOnWindows() {
1399

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

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

1417
        try {
1418
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1419
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1420
            StringBuilder output = new StringBuilder();
×
1421
            String line;
1422

1423
            while ((line = reader.readLine()) != null) {
×
1424
              output.append(line);
×
1425
            }
1426

1427
            int exitCode = process.waitFor();
×
1428
            if (exitCode != 0) {
×
1429
              warning("Query to windows registry for finding bash failed with exit code {}", exitCode);
×
1430
              return null;
×
1431
            }
1432

1433
            regQueryResult = output.toString();
×
1434
            trace("Result from windows registry was: {}", regQueryResult);
×
1435
            int index = regQueryResult.indexOf("REG_SZ");
×
1436
            if (index != -1) {
×
1437
              String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1438
              String bashPath = path + "\\bin\\bash.exe";
×
1439
              debug("Found bash at: {}", bashPath);
×
1440
              return bashPath;
×
1441
            }
1442
          }
×
1443
        } catch (Exception e) {
×
1444
          error(e, "Query to windows registry for finding bash failed!");
×
1445
          return null;
×
1446
        }
×
1447
      }
1448
    }
1449
    // no bash found
1450
    return null;
×
1451
  }
1452

1453
  @Override
1454
  public WindowsPathSyntax getPathSyntax() {
1455

1456
    return this.pathSyntax;
3✔
1457
  }
1458

1459
  /**
1460
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1461
   */
1462
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1463

1464
    this.pathSyntax = pathSyntax;
3✔
1465
  }
1✔
1466

1467
  /**
1468
   * @return the {@link IdeStartContextImpl}.
1469
   */
1470
  public IdeStartContextImpl getStartContext() {
1471

1472
    return startContext;
3✔
1473
  }
1474

1475
  /**
1476
   * @return the {@link WindowsHelper}.
1477
   */
1478
  public final WindowsHelper getWindowsHelper() {
1479

1480
    if (this.windowsHelper == null) {
3✔
1481
      this.windowsHelper = createWindowsHelper();
4✔
1482
    }
1483
    return this.windowsHelper;
3✔
1484
  }
1485

1486
  /**
1487
   * @return the new {@link WindowsHelper} instance.
1488
   */
1489
  protected WindowsHelper createWindowsHelper() {
1490

1491
    return new WindowsHelperImpl(this);
×
1492
  }
1493

1494
  /**
1495
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1496
   */
1497
  public void reload() {
1498

1499
    this.variables = null;
3✔
1500
    this.customToolRepository = null;
3✔
1501
  }
1✔
1502

1503
  @Override
1504
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1505

1506
    assert (Files.isDirectory(installationPath));
6!
1507
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1508
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1509
  }
1✔
1510

1511
  /**
1512
   * @param home the IDE_HOME directory.
1513
   * @param workspace the name of the active workspace folder.
1514
   */
1515
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1516

1517
  }
1518

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