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

devonfw / IDEasy / 17673062909

12 Sep 2025 11:24AM UTC coverage: 68.635% (-0.08%) from 68.712%
17673062909

push

github

web-flow
#1464: added logging to debug the problem, added BASH_PATH variable and PATH fallback (#1474)

3380 of 5391 branches covered (62.7%)

Branch coverage included in aggregate %.

8822 of 12387 relevant lines covered (71.22%)

3.13 hits per line

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

63.52
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.net.URL;
8
import java.net.URLConnection;
9
import java.nio.file.Files;
10
import java.nio.file.Path;
11
import java.time.LocalDateTime;
12
import java.util.ArrayList;
13
import java.util.HashMap;
14
import java.util.Iterator;
15
import java.util.List;
16
import java.util.Locale;
17
import java.util.Map;
18
import java.util.Map.Entry;
19
import java.util.Objects;
20

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

132
  private DirectoryMerger workspaceMerger;
133

134
  protected UrlMetadata urlMetadata;
135

136
  protected Path defaultExecutionDirectory;
137

138
  private StepImpl currentStep;
139

140
  protected Boolean online;
141

142
  protected IdeSystem system;
143

144
  private NetworkProxy networkProxy;
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
    String workspace = WORKSPACE_MAIN;
2✔
170
    if (workingDirectory == null) {
2!
171
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
172
    }
173
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
174
    if (Files.isDirectory(workingDirectory)) {
5✔
175
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
176
    } else {
177
      warning("Current working directory does not exist: {}", workingDirectory);
9✔
178
    }
179
    this.cwd = workingDirectory;
3✔
180
    // detect IDE_HOME and WORKSPACE
181
    Path currentDir = workingDirectory;
2✔
182
    String name1 = "";
2✔
183
    String name2 = "";
2✔
184
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
185
    while (currentDir != null) {
2✔
186
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
187
      if (isIdeHome(currentDir)) {
4✔
188
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
189
          workspace = name2;
3✔
190
        }
191
        break;
192
      }
193
      name2 = name1;
2✔
194
      int nameCount = currentDir.getNameCount();
3✔
195
      if (nameCount >= 1) {
3✔
196
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
197
      }
198
      currentDir = currentDir.getParent();
3✔
199
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
200
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
201
        currentDir = null;
×
202
      }
203
    }
1✔
204

205
    // detection completed, initializing variables
206
    this.ideRoot = findIdeRoot(currentDir);
5✔
207

208
    setCwd(workingDirectory, workspace, currentDir);
5✔
209

210
    if (this.ideRoot != null) {
3✔
211
      Path tempDownloadPath = getTempDownloadPath();
3✔
212
      if (Files.isDirectory(tempDownloadPath)) {
6✔
213
        // TODO delete all files older than 1 day here...
214
      } else {
215
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
216
      }
217
    }
218

219
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
220
    this.mavenRepository = new MavenRepository(this);
6✔
221
  }
1✔
222

223
  private Path findIdeRoot(Path ideHomePath) {
224

225
    Path ideRootPath = null;
2✔
226
    if (ideHomePath != null) {
2✔
227
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
228
      ideRootPath = ideHomePath.getParent();
3✔
229
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
8!
230
        warning(
12✔
231
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
232
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
233
            ideRootPathFromEnv,
234
            ideHomePath.getFileName(), ideRootPath);
6✔
235
      }
236
    } else if (!isTest()) {
4!
237
      ideRootPath = getIdeRootPathFromEnv(true);
×
238
    }
239
    return ideRootPath;
2✔
240
  }
241

242
  /**
243
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
244
   */
245
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
246

247
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
248
    if (root != null) {
×
249
      Path rootPath = Path.of(root);
×
250
      if (Files.isDirectory(rootPath)) {
×
251
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
252
        if (withSanityCheck) {
×
253
          int nameCount = rootPath.getNameCount();
×
254
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
255
          int delta = absoluteNameCount - nameCount;
×
256
          if (delta >= 0) {
×
257
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
258
              String rootName = rootPath.getName(nameIndex).toString();
×
259
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
260
              if (!rootName.equals(absoluteRootName)) {
×
261
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
262
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
263
                break;
×
264
              }
265
            }
266
          } else {
267
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
268
                absoluteRootPath);
269
          }
270
        }
271
        return absoluteRootPath;
×
272
      } else if (withSanityCheck) {
×
273
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
274
      }
275
    }
276
    return null;
×
277
  }
278

279
  @Override
280
  public void setCwd(Path userDir, String workspace, Path ideHome) {
281

282
    this.cwd = userDir;
3✔
283
    this.workspaceName = workspace;
3✔
284
    this.ideHome = ideHome;
3✔
285
    if (ideHome == null) {
2✔
286
      this.workspacePath = null;
3✔
287
      this.confPath = null;
3✔
288
      this.settingsPath = null;
3✔
289
      this.pluginsPath = null;
4✔
290
    } else {
291
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
292
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
293
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
294
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
295
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
296
    }
297
    if (isTest()) {
3!
298
      // only for testing...
299
      if (this.ideHome == null) {
3✔
300
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
301
      } else {
302
        this.userHome = this.ideHome.resolve("home");
6✔
303
      }
304
    }
305
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
306
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
307
    resetPrivacyMap();
2✔
308
    this.path = computeSystemPath();
4✔
309
  }
1✔
310

311
  private String getMessageIdeHomeFound() {
312

313
    String wks = this.workspaceName;
3✔
314
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
315
      wks = "*".repeat(wks.length());
×
316
    }
317
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
318
  }
319

320
  private String getMessageNotInsideIdeProject() {
321

322
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
323
  }
324

325
  private String getMessageIdeRootNotFound() {
326

327
    String root = getSystem().getEnv("IDE_ROOT");
5✔
328
    if (root == null) {
2!
329
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
330
    } else {
331
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
332
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
333
    }
334
  }
335

336
  /**
337
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
338
   */
339
  public boolean isTest() {
340

341
    return false;
×
342
  }
343

344
  protected SystemPath computeSystemPath() {
345

346
    return new SystemPath(this);
×
347
  }
348

349
  private boolean isIdeHome(Path dir) {
350

351
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
352
      return false;
2✔
353
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
354
      return false;
×
355
    }
356
    return true;
2✔
357
  }
358

359
  private EnvironmentVariables createVariables() {
360

361
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
362
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
363
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
364
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
365
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
366
    return conf.resolved();
3✔
367
  }
368

369
  protected AbstractEnvironmentVariables createSystemVariables() {
370

371
    return EnvironmentVariables.ofSystem(this);
3✔
372
  }
373

374
  @Override
375
  public SystemInfo getSystemInfo() {
376

377
    return this.systemInfo;
3✔
378
  }
379

380
  @Override
381
  public FileAccess getFileAccess() {
382

383
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
384
    configureNetworkProxy();
2✔
385
    return this.fileAccess;
3✔
386
  }
387

388
  @Override
389
  public CommandletManager getCommandletManager() {
390

391
    return this.commandletManager;
3✔
392
  }
393

394
  @Override
395
  public ToolRepository getDefaultToolRepository() {
396

397
    return this.defaultToolRepository;
3✔
398
  }
399

400
  @Override
401
  public MavenRepository getMavenToolRepository() {
402

403
    return this.mavenRepository;
3✔
404
  }
405

406
  @Override
407
  public CustomToolRepository getCustomToolRepository() {
408

409
    if (this.customToolRepository == null) {
3!
410
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
411
    }
412
    return this.customToolRepository;
3✔
413
  }
414

415
  @Override
416
  public Path getIdeHome() {
417

418
    return this.ideHome;
3✔
419
  }
420

421
  @Override
422
  public String getProjectName() {
423

424
    if (this.ideHome != null) {
3!
425
      return this.ideHome.getFileName().toString();
5✔
426
    }
427
    return "";
×
428
  }
429

430
  @Override
431
  public VersionIdentifier getProjectVersion() {
432

433
    if (this.ideHome != null) {
3!
434
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
435
      if (Files.exists(versionFile)) {
5✔
436
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
437
        return VersionIdentifier.of(version);
3✔
438
      }
439
    }
440
    return IdeMigrator.START_VERSION;
2✔
441
  }
442

443
  @Override
444
  public void setProjectVersion(VersionIdentifier version) {
445

446
    if (this.ideHome == null) {
3!
447
      throw new IllegalStateException("IDE_HOME not available!");
×
448
    }
449
    Objects.requireNonNull(version);
3✔
450
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
451
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
452
  }
1✔
453

454
  @Override
455
  public Path getIdeRoot() {
456

457
    return this.ideRoot;
3✔
458
  }
459

460
  @Override
461
  public Path getIdePath() {
462

463
    Path myIdeRoot = getIdeRoot();
3✔
464
    if (myIdeRoot == null) {
2!
465
      return null;
×
466
    }
467
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
468
  }
469

470
  @Override
471
  public Path getCwd() {
472

473
    return this.cwd;
3✔
474
  }
475

476
  @Override
477
  public Path getTempPath() {
478

479
    Path idePath = getIdePath();
3✔
480
    if (idePath == null) {
2!
481
      return null;
×
482
    }
483
    return idePath.resolve("tmp");
4✔
484
  }
485

486
  @Override
487
  public Path getTempDownloadPath() {
488

489
    Path tmp = getTempPath();
3✔
490
    if (tmp == null) {
2!
491
      return null;
×
492
    }
493
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
494
  }
495

496
  @Override
497
  public Path getUserHome() {
498

499
    return this.userHome;
3✔
500
  }
501

502
  /**
503
   * This method should only be used for tests to mock user home.
504
   *
505
   * @param userHome the new value of {@link #getUserHome()}.
506
   */
507
  protected void setUserHome(Path userHome) {
508

509
    this.userHome = userHome;
3✔
510
    resetPrivacyMap();
2✔
511
  }
1✔
512

513
  @Override
514
  public Path getUserHomeIde() {
515

516
    return this.userHomeIde;
3✔
517
  }
518

519
  @Override
520
  public Path getSettingsPath() {
521

522
    return this.settingsPath;
3✔
523
  }
524

525
  @Override
526
  public Path getSettingsGitRepository() {
527

528
    Path settingsPath = getSettingsPath();
3✔
529
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
530
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
531
      error("Settings repository exists but is not a git repository.");
3✔
532
      return null;
2✔
533
    }
534
    return settingsPath;
2✔
535
  }
536

537
  @Override
538
  public boolean isSettingsRepositorySymlinkOrJunction() {
539

540
    Path settingsPath = getSettingsPath();
3✔
541
    if (settingsPath == null) {
2!
542
      return false;
×
543
    }
544
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
545
  }
546

547
  @Override
548
  public Path getSettingsCommitIdPath() {
549

550
    return this.settingsCommitIdPath;
3✔
551
  }
552

553
  @Override
554
  public Path getConfPath() {
555

556
    return this.confPath;
3✔
557
  }
558

559
  @Override
560
  public Path getSoftwarePath() {
561

562
    if (this.ideHome == null) {
3✔
563
      return null;
2✔
564
    }
565
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
566
  }
567

568
  @Override
569
  public Path getSoftwareExtraPath() {
570

571
    Path softwarePath = getSoftwarePath();
3✔
572
    if (softwarePath == null) {
2!
573
      return null;
×
574
    }
575
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
576
  }
577

578
  @Override
579
  public Path getSoftwareRepositoryPath() {
580

581
    Path idePath = getIdePath();
3✔
582
    if (idePath == null) {
2!
583
      return null;
×
584
    }
585
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
586
  }
587

588
  @Override
589
  public Path getPluginsPath() {
590

591
    return this.pluginsPath;
3✔
592
  }
593

594
  @Override
595
  public String getWorkspaceName() {
596

597
    return this.workspaceName;
3✔
598
  }
599

600
  @Override
601
  public Path getWorkspacePath() {
602

603
    return this.workspacePath;
3✔
604
  }
605

606
  @Override
607
  public Path getDownloadPath() {
608

609
    return this.downloadPath;
3✔
610
  }
611

612
  @Override
613
  public Path getUrlsPath() {
614

615
    Path idePath = getIdePath();
3✔
616
    if (idePath == null) {
2!
617
      return null;
×
618
    }
619
    return idePath.resolve(FOLDER_URLS);
4✔
620
  }
621

622
  @Override
623
  public Path getToolRepositoryPath() {
624

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

632
  @Override
633
  public SystemPath getPath() {
634

635
    return this.path;
3✔
636
  }
637

638
  @Override
639
  public EnvironmentVariables getVariables() {
640

641
    if (this.variables == null) {
3✔
642
      this.variables = createVariables();
4✔
643
    }
644
    return this.variables;
3✔
645
  }
646

647
  @Override
648
  public UrlMetadata getUrls() {
649

650
    if (this.urlMetadata == null) {
3✔
651
      if (!isTest()) {
3!
652
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
653
      }
654
      this.urlMetadata = new UrlMetadata(this);
6✔
655
    }
656
    return this.urlMetadata;
3✔
657
  }
658

659
  @Override
660
  public boolean isQuietMode() {
661

662
    return this.startContext.isQuietMode();
4✔
663
  }
664

665
  @Override
666
  public boolean isBatchMode() {
667

668
    return this.startContext.isBatchMode();
4✔
669
  }
670

671
  @Override
672
  public boolean isForceMode() {
673

674
    return this.startContext.isForceMode();
4✔
675
  }
676

677
  @Override
678
  public boolean isForcePull() {
679

680
    return this.startContext.isForcePull();
4✔
681
  }
682

683
  @Override
684
  public boolean isForcePlugins() {
685

686
    return this.startContext.isForcePlugins();
4✔
687
  }
688

689
  @Override
690
  public boolean isForceRepositories() {
691

692
    return this.startContext.isForceRepositories();
4✔
693
  }
694

695
  @Override
696
  public boolean isOfflineMode() {
697

698
    return this.startContext.isOfflineMode();
4✔
699
  }
700

701
  @Override
702
  public boolean isPrivacyMode() {
703
    return this.startContext.isPrivacyMode();
4✔
704
  }
705

706
  @Override
707
  public boolean isSkipUpdatesMode() {
708

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

712
  @Override
713
  public boolean isOnline() {
714

715
    if (this.online == null) {
3✔
716
      configureNetworkProxy();
2✔
717
      // we currently assume we have only a CLI process that runs shortly
718
      // therefore we run this check only once to save resources when this method is called many times
719
      String url = "https://www.github.com";
2✔
720
      try {
721
        int timeout = 1000;
2✔
722
        //open a connection to github.com and try to retrieve data
723
        //getContent fails if there is no connection
724
        URLConnection connection = new URL(url).openConnection();
6✔
725
        connection.setConnectTimeout(timeout);
3✔
726
        connection.getContent();
3✔
727
        this.online = Boolean.TRUE;
3✔
728
      } catch (Exception e) {
×
729
        if (debug().isEnabled()) {
×
730
          debug().log(e, "Error when trying to connect to {}", url);
×
731
        }
732
        this.online = Boolean.FALSE;
×
733
      }
1✔
734
    }
735
    return this.online.booleanValue();
4✔
736
  }
737

738
  private void configureNetworkProxy() {
739

740
    if (this.networkProxy == null) {
3✔
741
      this.networkProxy = new NetworkProxy(this);
6✔
742
      this.networkProxy.configure();
3✔
743
    }
744
  }
1✔
745

746
  @Override
747
  public Locale getLocale() {
748

749
    Locale locale = this.startContext.getLocale();
4✔
750
    if (locale == null) {
2✔
751
      locale = Locale.getDefault();
2✔
752
    }
753
    return locale;
2✔
754
  }
755

756
  @Override
757
  public DirectoryMerger getWorkspaceMerger() {
758

759
    if (this.workspaceMerger == null) {
3✔
760
      this.workspaceMerger = new DirectoryMerger(this);
6✔
761
    }
762
    return this.workspaceMerger;
3✔
763
  }
764

765
  /**
766
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
767
   */
768
  @Override
769
  public Path getDefaultExecutionDirectory() {
770

771
    return this.defaultExecutionDirectory;
×
772
  }
773

774
  /**
775
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
776
   */
777
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
778

779
    if (defaultExecutionDirectory != null) {
×
780
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
781
    }
782
  }
×
783

784
  @Override
785
  public GitContext getGitContext() {
786

787
    return new GitContextImpl(this);
×
788
  }
789

790
  @Override
791
  public ProcessContext newProcess() {
792

793
    ProcessContext processContext = createProcessContext();
3✔
794
    if (this.defaultExecutionDirectory != null) {
3!
795
      processContext.directory(this.defaultExecutionDirectory);
×
796
    }
797
    return processContext;
2✔
798
  }
799

800
  @Override
801
  public IdeSystem getSystem() {
802

803
    if (this.system == null) {
×
804
      this.system = new IdeSystemImpl(this);
×
805
    }
806
    return this.system;
×
807
  }
808

809
  /**
810
   * @return a new instance of {@link ProcessContext}.
811
   * @see #newProcess()
812
   */
813
  protected ProcessContext createProcessContext() {
814

815
    return new ProcessContextImpl(this);
5✔
816
  }
817

818
  @Override
819
  public IdeSubLogger level(IdeLogLevel level) {
820

821
    return this.startContext.level(level);
5✔
822
  }
823

824
  @Override
825
  public void logIdeHomeAndRootStatus() {
826
    if (this.ideRoot != null) {
3!
827
      success("IDE_ROOT is set to {}", this.ideRoot);
×
828
    }
829
    if (this.ideHome == null) {
3✔
830
      warning(getMessageNotInsideIdeProject());
5✔
831
    } else {
832
      success("IDE_HOME is set to {}", this.ideHome);
10✔
833
    }
834
  }
1✔
835

836
  @Override
837
  public String formatArgument(Object argument) {
838

839
    if (argument == null) {
2✔
840
      return null;
2✔
841
    }
842
    String result = argument.toString();
3✔
843
    if (isPrivacyMode()) {
3✔
844
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
845
        initializePrivacyMap(this.userHome, "~");
×
846
        String projectName = getProjectName();
×
847
        if (!projectName.isEmpty()) {
×
848
          this.privacyMap.put(projectName, "project");
×
849
        }
850
      }
851
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
852
        result = result.replace(entry.getKey(), entry.getValue());
×
853
      }
×
854
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
855
    }
856
    return result;
2✔
857
  }
858

859
  /**
860
   * @param path the sensitive {@link Path} to
861
   * @param replacement the replacement to mask the {@link Path} in log output.
862
   */
863
  protected void initializePrivacyMap(Path path, String replacement) {
864

865
    if (path == null) {
×
866
      return;
×
867
    }
868
    if (this.systemInfo.isWindows()) {
×
869
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
870
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
871
    } else {
872
      this.privacyMap.put(path.toString(), replacement);
×
873
    }
874
  }
×
875

876
  /**
877
   * Resets the privacy map in case fundamental values have changed.
878
   */
879
  private void resetPrivacyMap() {
880

881
    this.privacyMap.clear();
3✔
882
  }
1✔
883

884

885
  @Override
886
  public String askForInput(String message, String defaultValue) {
887

888
    while (true) {
889
      if (!message.isBlank()) {
3!
890
        interaction(message);
3✔
891
      }
892
      if (isBatchMode()) {
3!
893
        if (isForceMode()) {
×
894
          return defaultValue;
×
895
        } else {
896
          throw new CliAbortException();
×
897
        }
898
      }
899
      String input = readLine().trim();
4✔
900
      if (!input.isEmpty()) {
3!
901
        return input;
2✔
902
      } else {
903
        if (defaultValue != null) {
×
904
          return defaultValue;
×
905
        }
906
      }
907
    }
×
908
  }
909

910
  @SuppressWarnings("unchecked")
911
  @Override
912
  public <O> O question(O[] options, String question, Object... args) {
913

914
    assert (options.length >= 2);
5!
915
    interaction(question, args);
4✔
916
    return displayOptionsAndGetAnswer(options);
4✔
917
  }
918

919
  private <O> O displayOptionsAndGetAnswer(O[] options) {
920
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
921
    int i = 0;
2✔
922
    for (O option : options) {
16✔
923
      i++;
1✔
924
      String key = "" + option;
4✔
925
      addMapping(mapping, key, option);
4✔
926
      String numericKey = Integer.toString(i);
3✔
927
      if (numericKey.equals(key)) {
4!
928
        trace("Options should not be numeric: " + key);
×
929
      } else {
930
        addMapping(mapping, numericKey, option);
4✔
931
      }
932
      interaction("Option " + numericKey + ": " + key);
5✔
933
    }
934
    O option = null;
2✔
935
    if (isBatchMode()) {
3!
936
      if (isForceMode()) {
×
937
        option = options[0];
×
938
        interaction("" + option);
×
939
      }
940
    } else {
941
      while (option == null) {
2✔
942
        String answer = readLine();
3✔
943
        option = mapping.get(answer);
4✔
944
        if (option == null) {
2!
945
          warning("Invalid answer: '" + answer + "' - please try again.");
×
946
        }
947
      }
1✔
948
    }
949
    return option;
2✔
950
  }
951

952
  /**
953
   * @return the input from the end-user (e.g. read from the console).
954
   */
955
  protected abstract String readLine();
956

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

959
    O duplicate = mapping.put(key, option);
5✔
960
    if (duplicate != null) {
2!
961
      throw new IllegalArgumentException("Duplicated option " + key);
×
962
    }
963
  }
1✔
964

965
  @Override
966
  public Step getCurrentStep() {
967

968
    return this.currentStep;
×
969
  }
970

971
  @Override
972
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
973

974
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
975
    return this.currentStep;
3✔
976
  }
977

978
  /**
979
   * Internal method to end the running {@link Step}.
980
   *
981
   * @param step the current {@link Step} to end.
982
   */
983
  public void endStep(StepImpl step) {
984

985
    if (step == this.currentStep) {
4!
986
      this.currentStep = this.currentStep.getParent();
6✔
987
    } else {
988
      String currentStepName = "null";
×
989
      if (this.currentStep != null) {
×
990
        currentStepName = this.currentStep.getName();
×
991
      }
992
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
993
    }
994
  }
1✔
995

996
  /**
997
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
998
   *
999
   * @param arguments the {@link CliArgument}.
1000
   * @return the return code of the execution.
1001
   */
1002
  public int run(CliArguments arguments) {
1003

1004
    CliArgument current = arguments.current();
3✔
1005
    assert (this.currentStep == null);
4!
1006
    boolean supressStepSuccess = false;
2✔
1007
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1008
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1009
    Commandlet cmd = null;
2✔
1010
    ValidationResult result = null;
2✔
1011
    try {
1012
      while (commandletIterator.hasNext()) {
3✔
1013
        cmd = commandletIterator.next();
4✔
1014
        result = applyAndRun(arguments.copy(), cmd);
6✔
1015
        if (result.isValid()) {
3!
1016
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1017
          step.success();
2✔
1018
          return ProcessResult.SUCCESS;
4✔
1019
        }
1020
      }
1021
      this.startContext.activateLogging();
3✔
1022
      verifyIdeMinVersion(false);
3✔
1023
      if (result != null) {
2!
1024
        error(result.getErrorMessage());
×
1025
      }
1026
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1027
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1028
      if (cmd != null) {
2!
1029
        help.commandlet.setValue(cmd);
×
1030
      }
1031
      help.run();
2✔
1032
      return 1;
4✔
1033
    } catch (Throwable t) {
1✔
1034
      this.startContext.activateLogging();
3✔
1035
      step.error(t, true);
4✔
1036
      throw t;
2✔
1037
    } finally {
1038
      step.close();
2✔
1039
      assert (this.currentStep == null);
4!
1040
      step.logSummary(supressStepSuccess);
3✔
1041
    }
1042
  }
1043

1044
  @Override
1045
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1046

1047
    this.startContext.deactivateLogging(threshold);
4✔
1048
    lambda.run();
2✔
1049
    this.startContext.activateLogging();
3✔
1050
  }
1✔
1051

1052
  /**
1053
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1054
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1055
   *     {@link Commandlet} did not match and we have to try a different candidate).
1056
   */
1057
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1058

1059
    IdeLogLevel previousLogLevel = null;
2✔
1060
    cmd.reset();
2✔
1061
    ValidationResult result = apply(arguments, cmd);
5✔
1062
    if (result.isValid()) {
3!
1063
      result = cmd.validate();
3✔
1064
    }
1065
    if (result.isValid()) {
3!
1066
      debug("Running commandlet {}", cmd);
9✔
1067
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1068
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1069
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1070
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1071
      }
1072
      try {
1073
        if (cmd.isProcessableOutput()) {
3!
1074
          if (!debug().isEnabled()) {
×
1075
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1076
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1077
          }
1078
          this.startContext.activateLogging();
×
1079
        } else {
1080
          this.startContext.activateLogging();
3✔
1081
          if (cmd.isIdeHomeRequired()) {
3!
1082
            debug(getMessageIdeHomeFound());
4✔
1083
          }
1084
          Path settingsRepository = getSettingsGitRepository();
3✔
1085
          if (settingsRepository != null) {
2!
1086
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1087
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1088
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1089
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1090
                interaction(
×
1091
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1092

1093
              } else {
1094
                interaction(
×
1095
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1096
              }
1097
            }
1098
          }
1099
        }
1100
        boolean success = ensureLicenseAgreement(cmd);
4✔
1101
        if (!success) {
2!
1102
          return ValidationResultValid.get();
×
1103
        }
1104
        cmd.run();
2✔
1105
      } finally {
1106
        if (previousLogLevel != null) {
2!
1107
          this.startContext.setLogLevel(previousLogLevel);
×
1108
        }
1109
      }
1✔
1110
    } else {
1111
      trace("Commandlet did not match");
×
1112
    }
1113
    return result;
2✔
1114
  }
1115

1116
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1117

1118
    if (isTest()) {
3!
1119
      return true; // ignore for tests
2✔
1120
    }
1121
    getFileAccess().mkdirs(this.userHomeIde);
×
1122
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1123
    if (Files.isRegularFile(licenseAgreement)) {
×
1124
      return true; // success, license already accepted
×
1125
    }
1126
    if (cmd instanceof EnvironmentCommandlet) {
×
1127
      // 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
1128
      // 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
1129
      // printing anything anymore in such case.
1130
      return false;
×
1131
    }
1132
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1133
    if (logLevelInfoDisabled) {
×
1134
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1135
    }
1136
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1137
    if (logLevelInteractionDisabled) {
×
1138
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1139
    }
1140
    StringBuilder sb = new StringBuilder(1180);
×
1141
    sb.append(LOGO).append("""
×
1142
        Welcome to IDEasy!
1143
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1144
        It supports automatic download and installation of arbitrary 3rd party tools.
1145
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1146
        But if explicitly configured, also commercial software that requires an additional license may be used.
1147
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1148
        You are solely responsible for all risks implied by using this software.
1149
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1150
        You will be able to find it online under the following URL:
1151
        """).append(LICENSE_URL);
×
1152
    if (this.ideRoot != null) {
×
1153
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1154
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1155
    }
1156
    info(sb.toString());
×
1157
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1158

1159
    sb.setLength(0);
×
1160
    LocalDateTime now = LocalDateTime.now();
×
1161
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1162
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1163
    try {
1164
      Files.writeString(licenseAgreement, sb);
×
1165
    } catch (Exception e) {
×
1166
      throw new RuntimeException("Failed to save license agreement!", e);
×
1167
    }
×
1168
    if (logLevelInfoDisabled) {
×
1169
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1170
    }
1171
    if (logLevelInteractionDisabled) {
×
1172
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1173
    }
1174
    return true;
×
1175
  }
1176

1177
  @Override
1178
  public void verifyIdeMinVersion(boolean throwException) {
1179
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1180
    if (minVersion == null) {
2✔
1181
      return;
1✔
1182
    }
1183
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1184
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1185
          + "However, this is too old as your project requires at latest version %s\n"
1186
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1187
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1188
      if (throwException) {
2✔
1189
        throw new CliException(message);
5✔
1190
      } else {
1191
        warning(message);
3✔
1192
      }
1193
    }
1194
  }
1✔
1195

1196
  /**
1197
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1198
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1199
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1200
   */
1201
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1202

1203
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1204
    if (arguments.current().isStart()) {
4✔
1205
      arguments.next();
3✔
1206
    }
1207
    if (includeContextOptions) {
2✔
1208
      ContextCommandlet cc = new ContextCommandlet();
4✔
1209
      for (Property<?> property : cc.getProperties()) {
11✔
1210
        assert (property.isOption());
4!
1211
        property.apply(arguments, this, cc, collector);
7✔
1212
      }
1✔
1213
    }
1214
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1215
    CliArgument current = arguments.current();
3✔
1216
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1217
      collector.add(current.get(), null, null, null);
7✔
1218
    }
1219
    arguments.next();
3✔
1220
    while (commandletIterator.hasNext()) {
3✔
1221
      Commandlet cmd = commandletIterator.next();
4✔
1222
      if (!arguments.current().isEnd()) {
4✔
1223
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1224
      }
1225
    }
1✔
1226
    return collector.getSortedCandidates();
3✔
1227
  }
1228

1229
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1230

1231
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1232
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1233
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1234
    List<Property<?>> properties = cmd.getProperties();
3✔
1235
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1236
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1237
    for (Property<?> property : properties) {
10✔
1238
      if (property.isOption()) {
3✔
1239
        optionProperties.add(property);
4✔
1240
      }
1241
    }
1✔
1242
    CliArgument currentArgument = arguments.current();
3✔
1243
    while (!currentArgument.isEnd()) {
3✔
1244
      trace("Trying to match argument '{}'", currentArgument);
9✔
1245
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1246
        if (currentArgument.isCompletion()) {
3✔
1247
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1248
          while (optionIterator.hasNext()) {
3✔
1249
            Property<?> option = optionIterator.next();
4✔
1250
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1251
            if (success) {
2✔
1252
              optionIterator.remove();
2✔
1253
              arguments.next();
3✔
1254
            }
1255
          }
1✔
1256
        } else {
1✔
1257
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1258
          if (option != null) {
2✔
1259
            arguments.next();
3✔
1260
            boolean removed = optionProperties.remove(option);
4✔
1261
            if (!removed) {
2!
1262
              option = null;
×
1263
            }
1264
          }
1265
          if (option == null) {
2✔
1266
            trace("No such option was found.");
3✔
1267
            return;
1✔
1268
          }
1269
        }
1✔
1270
      } else {
1271
        if (valueIterator.hasNext()) {
3✔
1272
          Property<?> valueProperty = valueIterator.next();
4✔
1273
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1274
          if (!success) {
2✔
1275
            trace("Completion cannot match any further.");
3✔
1276
            return;
1✔
1277
          }
1278
        } else {
1✔
1279
          trace("No value left for completion.");
3✔
1280
          return;
1✔
1281
        }
1282
      }
1283
      currentArgument = arguments.current();
4✔
1284
    }
1285
  }
1✔
1286

1287
  /**
1288
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1289
   *     {@link CliArguments#copy() copy} as needed.
1290
   * @param cmd the potential {@link Commandlet} to match.
1291
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1292
   */
1293
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1294

1295
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1296
    CliArgument currentArgument = arguments.current();
3✔
1297
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1298
    Property<?> property = null;
2✔
1299
    if (propertyIterator.hasNext()) {
3!
1300
      property = propertyIterator.next();
4✔
1301
    }
1302
    while (!currentArgument.isEnd()) {
3✔
1303
      trace("Trying to match argument '{}'", currentArgument);
9✔
1304
      Property<?> currentProperty = property;
2✔
1305
      if (!arguments.isEndOptions()) {
3!
1306
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1307
        if (option != null) {
2!
1308
          currentProperty = option;
×
1309
        }
1310
      }
1311
      if (currentProperty == null) {
2!
1312
        trace("No option or next value found");
×
1313
        ValidationState state = new ValidationState(null);
×
1314
        state.addErrorMessage("No matching property found");
×
1315
        return state;
×
1316
      }
1317
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1318
      if (currentProperty == property) {
3!
1319
        if (!property.isMultiValued()) {
3✔
1320
          if (propertyIterator.hasNext()) {
3✔
1321
            property = propertyIterator.next();
5✔
1322
          } else {
1323
            property = null;
2✔
1324
          }
1325
        }
1326
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1327
          arguments.stopSplitShortOptions();
2✔
1328
        }
1329
      }
1330
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1331
      if (!matches) {
2!
1332
        ValidationState state = new ValidationState(null);
×
1333
        state.addErrorMessage("No matching property found");
×
1334
        return state;
×
1335
      }
1336
      currentArgument = arguments.current();
3✔
1337
    }
1✔
1338
    return ValidationResultValid.get();
2✔
1339
  }
1340

1341
  @Override
1342
  public String findBash() {
1343

1344
    String bash = BASH;
2✔
1345
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1346
      bash = findBashOnWindows();
×
1347
      if (bash == null) {
×
1348
        String variable = IdeVariables.BASH_PATH.getName();
×
1349
        bash = getVariables().get(variable);
×
1350
        if (bash == null) {
×
1351
          trace("Bash not found. Trying to search on system PATH.");
×
1352
          variable = IdeVariables.PATH.getName();
×
1353
          Path plainBash = Path.of(BASH);
×
1354
          Path bashPath = getPath().findBinary(plainBash);
×
1355
          bash = bashPath.toAbsolutePath().toString();
×
1356
          if (bash.contains("AppData\\Local\\Microsoft\\WindowsApps")) {
×
1357
            warning("Only found windows fake bash that is not usable!");
×
1358
            bash = null;
×
1359
          }
1360
        }
1361
        if (bash == null) {
×
1362
          info("Could not find bash in Windows registry, using bash from {} as fallback: {}", variable, bash);
×
1363
        }
1364
      }
1365
    }
1366
    return bash;
2✔
1367
  }
1368

1369
  private String findBashOnWindows() {
1370

1371
    // Check if Git Bash exists in the default location
1372
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1373
    if (Files.exists(defaultPath)) {
×
1374
      return defaultPath.toString();
×
1375
    }
1376

1377
    // If not found in the default location, try the registry query
1378
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1379
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1380
    String regQueryResult;
1381
    for (String bashVariant : bashVariants) {
×
1382
      trace("Trying to find bash variant: {}", bashVariant);
×
1383
      for (String registryKey : registryKeys) {
×
1384
        trace("Trying to find bash from registry key: {}", registryKey);
×
1385
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1386
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1387

1388
        try {
1389
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1390
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1391
            StringBuilder output = new StringBuilder();
×
1392
            String line;
1393

1394
            while ((line = reader.readLine()) != null) {
×
1395
              output.append(line);
×
1396
            }
1397

1398
            int exitCode = process.waitFor();
×
1399
            if (exitCode != 0) {
×
1400
              warning("Query to windows registry for finding bash failed with exit code {}", exitCode);
×
1401
              return null;
×
1402
            }
1403

1404
            regQueryResult = output.toString();
×
1405
            trace("Result from windows registry was: {}", regQueryResult);
×
1406
            int index = regQueryResult.indexOf("REG_SZ");
×
1407
            if (index != -1) {
×
1408
              String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1409
              String bashPath = path + "\\bin\\bash.exe";
×
1410
              debug("Found bash at: {}", bashPath);
×
1411
              return bashPath;
×
1412
            }
1413
          }
×
1414
        } catch (Exception e) {
×
1415
          error(e, "Query to windows registry for finding bash failed!");
×
1416
          return null;
×
1417
        }
×
1418
      }
1419
    }
1420
    // no bash found
1421
    return null;
×
1422
  }
1423

1424
  @Override
1425
  public WindowsPathSyntax getPathSyntax() {
1426

1427
    return this.pathSyntax;
3✔
1428
  }
1429

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

1435
    this.pathSyntax = pathSyntax;
3✔
1436
  }
1✔
1437

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

1443
    return startContext;
3✔
1444
  }
1445

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

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

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

1462
    return new WindowsHelperImpl(this);
×
1463
  }
1464

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

1470
    this.variables = null;
3✔
1471
    this.customToolRepository = null;
3✔
1472
  }
1✔
1473

1474
  @Override
1475
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1476

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

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

© 2026 Coveralls, Inc