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

devonfw / IDEasy / 19225950251

10 Nov 2025 08:50AM UTC coverage: 68.879% (+0.02%) from 68.857%
19225950251

push

github

hohwille
#1169: #1583: fixed test on Windows

3488 of 5549 branches covered (62.86%)

Branch coverage included in aggregate %.

9130 of 12770 relevant lines covered (71.5%)

3.14 hits per line

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

64.14
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.MvnRepository;
67
import com.devonfw.tools.ide.tool.repository.NpmRepository;
68
import com.devonfw.tools.ide.tool.repository.ToolRepository;
69
import com.devonfw.tools.ide.url.model.UrlMetadata;
70
import com.devonfw.tools.ide.util.DateTimeUtil;
71
import com.devonfw.tools.ide.util.PrivacyUtil;
72
import com.devonfw.tools.ide.validation.ValidationResult;
73
import com.devonfw.tools.ide.validation.ValidationResultValid;
74
import com.devonfw.tools.ide.validation.ValidationState;
75
import com.devonfw.tools.ide.variable.IdeVariables;
76
import com.devonfw.tools.ide.version.IdeVersion;
77
import com.devonfw.tools.ide.version.VersionIdentifier;
78

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

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

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

89
  private final IdeStartContextImpl startContext;
90

91
  private Path ideHome;
92

93
  private final Path ideRoot;
94

95
  private Path confPath;
96

97
  protected Path settingsPath;
98

99
  private Path settingsCommitIdPath;
100

101
  protected Path pluginsPath;
102

103
  private Path workspacePath;
104

105
  private String workspaceName;
106

107
  private Path cwd;
108

109
  private Path downloadPath;
110

111
  private Path userHome;
112

113
  private Path userHomeIde;
114

115
  private SystemPath path;
116

117
  private WindowsPathSyntax pathSyntax;
118

119
  private final SystemInfo systemInfo;
120

121
  private EnvironmentVariables variables;
122

123
  private final FileAccess fileAccess;
124

125
  protected CommandletManager commandletManager;
126

127
  protected ToolRepository defaultToolRepository;
128

129
  private CustomToolRepository customToolRepository;
130

131
  private MvnRepository mvnRepository;
132

133
  private NpmRepository npmRepository;
134

135
  private DirectoryMerger workspaceMerger;
136

137
  protected UrlMetadata urlMetadata;
138

139
  protected Path defaultExecutionDirectory;
140

141
  private StepImpl currentStep;
142

143
  protected Boolean online;
144

145
  protected IdeSystem system;
146

147
  private NetworkProxy networkProxy;
148

149
  private WindowsHelper windowsHelper;
150

151
  private final Map<String, String> privacyMap;
152

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

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

191
    // detection completed, initializing variables
192
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
193

194
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
195

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

205
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
206
  }
1✔
207

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

217
    Path currentDir = workingDirectory;
2✔
218
    String name1 = "";
2✔
219
    String name2 = "";
2✔
220
    String workspace = WORKSPACE_MAIN;
2✔
221
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
222

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

243
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
244
  }
245

246
  /**
247
   * @return a new {@link MvnRepository}
248
   */
249
  protected MvnRepository createMvnRepository() {
250
    return new MvnRepository(this);
5✔
251
  }
252

253
  /**
254
   * @return a new {@link NpmRepository}
255
   */
256
  protected NpmRepository createNpmRepository() {
257
    return new NpmRepository(this);
5✔
258
  }
259

260
  private Path findIdeRoot(Path ideHomePath) {
261

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

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

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

316
  @Override
317
  public void setCwd(Path userDir, String workspace, Path ideHome) {
318

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

348
  private String getMessageIdeHomeFound() {
349

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

357
  private String getMessageNotInsideIdeProject() {
358

359
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
360
  }
361

362
  private String getMessageIdeRootNotFound() {
363

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

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

378
    return false;
×
379
  }
380

381
  protected SystemPath computeSystemPath() {
382

383
    return new SystemPath(this);
×
384
  }
385

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

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

402
  private EnvironmentVariables createVariables() {
403

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

412
  protected AbstractEnvironmentVariables createSystemVariables() {
413

414
    return EnvironmentVariables.ofSystem(this);
3✔
415
  }
416

417
  @Override
418
  public SystemInfo getSystemInfo() {
419

420
    return this.systemInfo;
3✔
421
  }
422

423
  @Override
424
  public FileAccess getFileAccess() {
425

426
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
427
    configureNetworkProxy();
2✔
428
    return this.fileAccess;
3✔
429
  }
430

431
  @Override
432
  public CommandletManager getCommandletManager() {
433

434
    return this.commandletManager;
3✔
435
  }
436

437
  @Override
438
  public ToolRepository getDefaultToolRepository() {
439

440
    return this.defaultToolRepository;
3✔
441
  }
442

443
  @Override
444
  public MvnRepository getMvnRepository() {
445
    if (this.mvnRepository == null) {
3✔
446
      this.mvnRepository = createMvnRepository();
4✔
447
    }
448
    return this.mvnRepository;
3✔
449
  }
450

451
  @Override
452
  public NpmRepository getNpmRepository() {
453
    if (this.npmRepository == null) {
3✔
454
      this.npmRepository = createNpmRepository();
4✔
455
    }
456
    return this.npmRepository;
3✔
457
  }
458

459
  @Override
460
  public CustomToolRepository getCustomToolRepository() {
461

462
    if (this.customToolRepository == null) {
3!
463
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
464
    }
465
    return this.customToolRepository;
3✔
466
  }
467

468
  @Override
469
  public Path getIdeHome() {
470

471
    return this.ideHome;
3✔
472
  }
473

474
  @Override
475
  public String getProjectName() {
476

477
    if (this.ideHome != null) {
3!
478
      return this.ideHome.getFileName().toString();
5✔
479
    }
480
    return "";
×
481
  }
482

483
  @Override
484
  public VersionIdentifier getProjectVersion() {
485

486
    if (this.ideHome != null) {
3!
487
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
488
      if (Files.exists(versionFile)) {
5✔
489
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
490
        return VersionIdentifier.of(version);
3✔
491
      }
492
    }
493
    return IdeMigrator.START_VERSION;
2✔
494
  }
495

496
  @Override
497
  public void setProjectVersion(VersionIdentifier version) {
498

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

507
  @Override
508
  public Path getIdeRoot() {
509

510
    return this.ideRoot;
3✔
511
  }
512

513
  @Override
514
  public Path getIdePath() {
515

516
    Path myIdeRoot = getIdeRoot();
3✔
517
    if (myIdeRoot == null) {
2!
518
      return null;
×
519
    }
520
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
521
  }
522

523
  @Override
524
  public Path getCwd() {
525

526
    return this.cwd;
3✔
527
  }
528

529
  @Override
530
  public Path getTempPath() {
531

532
    Path idePath = getIdePath();
3✔
533
    if (idePath == null) {
2!
534
      return null;
×
535
    }
536
    return idePath.resolve("tmp");
4✔
537
  }
538

539
  @Override
540
  public Path getTempDownloadPath() {
541

542
    Path tmp = getTempPath();
3✔
543
    if (tmp == null) {
2!
544
      return null;
×
545
    }
546
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
547
  }
548

549
  @Override
550
  public Path getUserHome() {
551

552
    return this.userHome;
3✔
553
  }
554

555
  /**
556
   * This method should only be used for tests to mock user home.
557
   *
558
   * @param userHome the new value of {@link #getUserHome()}.
559
   */
560
  protected void setUserHome(Path userHome) {
561

562
    this.userHome = userHome;
3✔
563
    resetPrivacyMap();
2✔
564
  }
1✔
565

566
  @Override
567
  public Path getUserHomeIde() {
568

569
    return this.userHomeIde;
3✔
570
  }
571

572
  @Override
573
  public Path getSettingsPath() {
574

575
    return this.settingsPath;
3✔
576
  }
577

578
  @Override
579
  public Path getSettingsGitRepository() {
580

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

590
  @Override
591
  public boolean isSettingsRepositorySymlinkOrJunction() {
592

593
    Path settingsPath = getSettingsPath();
3✔
594
    if (settingsPath == null) {
2!
595
      return false;
×
596
    }
597
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
598
  }
599

600
  @Override
601
  public Path getSettingsCommitIdPath() {
602

603
    return this.settingsCommitIdPath;
3✔
604
  }
605

606
  @Override
607
  public Path getConfPath() {
608

609
    return this.confPath;
3✔
610
  }
611

612
  @Override
613
  public Path getSoftwarePath() {
614

615
    if (this.ideHome == null) {
3✔
616
      return null;
2✔
617
    }
618
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
619
  }
620

621
  @Override
622
  public Path getSoftwareExtraPath() {
623

624
    Path softwarePath = getSoftwarePath();
3✔
625
    if (softwarePath == null) {
2!
626
      return null;
×
627
    }
628
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
629
  }
630

631
  @Override
632
  public Path getSoftwareRepositoryPath() {
633

634
    Path idePath = getIdePath();
3✔
635
    if (idePath == null) {
2!
636
      return null;
×
637
    }
638
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
639
  }
640

641
  @Override
642
  public Path getPluginsPath() {
643

644
    return this.pluginsPath;
3✔
645
  }
646

647
  @Override
648
  public String getWorkspaceName() {
649

650
    return this.workspaceName;
3✔
651
  }
652

653
  @Override
654
  public Path getWorkspacePath() {
655

656
    return this.workspacePath;
3✔
657
  }
658

659
  @Override
660
  public Path getDownloadPath() {
661

662
    return this.downloadPath;
3✔
663
  }
664

665
  @Override
666
  public Path getUrlsPath() {
667

668
    Path idePath = getIdePath();
3✔
669
    if (idePath == null) {
2!
670
      return null;
×
671
    }
672
    return idePath.resolve(FOLDER_URLS);
4✔
673
  }
674

675
  @Override
676
  public Path getToolRepositoryPath() {
677

678
    Path idePath = getIdePath();
3✔
679
    if (idePath == null) {
2!
680
      return null;
×
681
    }
682
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
683
  }
684

685
  @Override
686
  public SystemPath getPath() {
687

688
    return this.path;
3✔
689
  }
690

691
  @Override
692
  public EnvironmentVariables getVariables() {
693

694
    if (this.variables == null) {
3✔
695
      this.variables = createVariables();
4✔
696
    }
697
    return this.variables;
3✔
698
  }
699

700
  @Override
701
  public UrlMetadata getUrls() {
702

703
    if (this.urlMetadata == null) {
3✔
704
      if (!isTest()) {
3!
705
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
706
      }
707
      this.urlMetadata = new UrlMetadata(this);
6✔
708
    }
709
    return this.urlMetadata;
3✔
710
  }
711

712
  @Override
713
  public boolean isQuietMode() {
714

715
    return this.startContext.isQuietMode();
4✔
716
  }
717

718
  @Override
719
  public boolean isBatchMode() {
720

721
    return this.startContext.isBatchMode();
4✔
722
  }
723

724
  @Override
725
  public boolean isForceMode() {
726

727
    return this.startContext.isForceMode();
4✔
728
  }
729

730
  @Override
731
  public boolean isForcePull() {
732

733
    return this.startContext.isForcePull();
4✔
734
  }
735

736
  @Override
737
  public boolean isForcePlugins() {
738

739
    return this.startContext.isForcePlugins();
4✔
740
  }
741

742
  @Override
743
  public boolean isForceRepositories() {
744

745
    return this.startContext.isForceRepositories();
4✔
746
  }
747

748
  @Override
749
  public boolean isOfflineMode() {
750

751
    return this.startContext.isOfflineMode();
4✔
752
  }
753

754
  @Override
755
  public boolean isPrivacyMode() {
756
    return this.startContext.isPrivacyMode();
4✔
757
  }
758

759
  @Override
760
  public boolean isSkipUpdatesMode() {
761

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

765
  @Override
766
  public boolean isNoColorsMode() {
767

768
    return this.startContext.isNoColorsMode();
×
769
  }
770

771
  @Override
772
  public boolean isOnline() {
773
    // we currently assume we have only a CLI process that runs shortly
774
    // therefore we run this check only once to save resources when this method is called many times
775
    String url = "https://www.github.com";
2✔
776
    return isUrlReachable(url);
4✔
777
  }
778

779
  /**
780
   * This method will be used to test the connection to the given url.
781
   *
782
   * @param url the url to test.
783
   */
784
  protected boolean isUrlReachable(String url) {
785
    if (this.online == null) {
3✔
786
      configureNetworkProxy();
2✔
787
      try {
788
        int timeout = 1000;
2✔
789
        //open a connection to github.com and try to retrieve data
790
        //getContent fails if there is no connection
791
        URLConnection connection = new URL(url).openConnection();
6✔
792
        connection.setConnectTimeout(timeout);
3✔
793
        connection.getContent();
3✔
794
        this.online = Boolean.TRUE;
3✔
795
      } catch (Exception e) {
×
796
        if (debug().isEnabled()) {
×
797
          debug().log(e, "Error when trying to connect to {}", url);
×
798
        }
799
        this.online = Boolean.FALSE;
×
800
      }
1✔
801
    }
802
    return this.online;
4✔
803
  }
804

805
  private void configureNetworkProxy() {
806

807
    if (this.networkProxy == null) {
3✔
808
      this.networkProxy = new NetworkProxy(this);
6✔
809
      this.networkProxy.configure();
3✔
810
    }
811
  }
1✔
812

813
  @Override
814
  public Locale getLocale() {
815

816
    Locale locale = this.startContext.getLocale();
4✔
817
    if (locale == null) {
2✔
818
      locale = Locale.getDefault();
2✔
819
    }
820
    return locale;
2✔
821
  }
822

823
  @Override
824
  public DirectoryMerger getWorkspaceMerger() {
825

826
    if (this.workspaceMerger == null) {
3✔
827
      this.workspaceMerger = new DirectoryMerger(this);
6✔
828
    }
829
    return this.workspaceMerger;
3✔
830
  }
831

832
  /**
833
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
834
   */
835
  @Override
836
  public Path getDefaultExecutionDirectory() {
837

838
    return this.defaultExecutionDirectory;
×
839
  }
840

841
  /**
842
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
843
   */
844
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
845

846
    if (defaultExecutionDirectory != null) {
×
847
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
848
    }
849
  }
×
850

851
  @Override
852
  public GitContext getGitContext() {
853

854
    return new GitContextImpl(this);
×
855
  }
856

857
  @Override
858
  public ProcessContext newProcess() {
859

860
    ProcessContext processContext = createProcessContext();
3✔
861
    if (this.defaultExecutionDirectory != null) {
3!
862
      processContext.directory(this.defaultExecutionDirectory);
×
863
    }
864
    return processContext;
2✔
865
  }
866

867
  @Override
868
  public IdeSystem getSystem() {
869

870
    if (this.system == null) {
×
871
      this.system = new IdeSystemImpl(this);
×
872
    }
873
    return this.system;
×
874
  }
875

876
  /**
877
   * @return a new instance of {@link ProcessContext}.
878
   * @see #newProcess()
879
   */
880
  protected ProcessContext createProcessContext() {
881

882
    return new ProcessContextImpl(this);
5✔
883
  }
884

885
  @Override
886
  public IdeSubLogger level(IdeLogLevel level) {
887

888
    return this.startContext.level(level);
5✔
889
  }
890

891
  @Override
892
  public void logIdeHomeAndRootStatus() {
893
    if (this.ideRoot != null) {
3!
894
      success("IDE_ROOT is set to {}", this.ideRoot);
×
895
    }
896
    if (this.ideHome == null) {
3✔
897
      warning(getMessageNotInsideIdeProject());
5✔
898
    } else {
899
      success("IDE_HOME is set to {}", this.ideHome);
10✔
900
    }
901
  }
1✔
902

903
  @Override
904
  public String formatArgument(Object argument) {
905

906
    if (argument == null) {
2✔
907
      return null;
2✔
908
    }
909
    String result = argument.toString();
3✔
910
    if (isPrivacyMode()) {
3✔
911
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
912
        initializePrivacyMap(this.userHome, "~");
×
913
        String projectName = getProjectName();
×
914
        if (!projectName.isEmpty()) {
×
915
          this.privacyMap.put(projectName, "project");
×
916
        }
917
      }
918
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
919
        result = result.replace(entry.getKey(), entry.getValue());
×
920
      }
×
921
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
922
    }
923
    return result;
2✔
924
  }
925

926
  /**
927
   * @param path the sensitive {@link Path} to
928
   * @param replacement the replacement to mask the {@link Path} in log output.
929
   */
930
  protected void initializePrivacyMap(Path path, String replacement) {
931

932
    if (path == null) {
×
933
      return;
×
934
    }
935
    if (this.systemInfo.isWindows()) {
×
936
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
937
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
938
    } else {
939
      this.privacyMap.put(path.toString(), replacement);
×
940
    }
941
  }
×
942

943
  /**
944
   * Resets the privacy map in case fundamental values have changed.
945
   */
946
  private void resetPrivacyMap() {
947

948
    this.privacyMap.clear();
3✔
949
  }
1✔
950

951

952
  @Override
953
  public String askForInput(String message, String defaultValue) {
954

955
    while (true) {
956
      if (!message.isBlank()) {
3!
957
        interaction(message);
3✔
958
      }
959
      if (isBatchMode()) {
3!
960
        if (isForceMode()) {
×
961
          return defaultValue;
×
962
        } else {
963
          throw new CliAbortException();
×
964
        }
965
      }
966
      String input = readLine().trim();
4✔
967
      if (!input.isEmpty()) {
3!
968
        return input;
2✔
969
      } else {
970
        if (defaultValue != null) {
×
971
          return defaultValue;
×
972
        }
973
      }
974
    }
×
975
  }
976

977
  @SuppressWarnings("unchecked")
978
  @Override
979
  public <O> O question(O[] options, String question, Object... args) {
980

981
    assert (options.length >= 2);
5!
982
    interaction(question, args);
4✔
983
    return displayOptionsAndGetAnswer(options);
4✔
984
  }
985

986
  private <O> O displayOptionsAndGetAnswer(O[] options) {
987
    Map<String, O> mapping = new HashMap<>(options.length);
6✔
988
    int i = 0;
2✔
989
    for (O option : options) {
16✔
990
      i++;
1✔
991
      String key = "" + option;
4✔
992
      addMapping(mapping, key, option);
4✔
993
      String numericKey = Integer.toString(i);
3✔
994
      if (numericKey.equals(key)) {
4!
995
        trace("Options should not be numeric: " + key);
×
996
      } else {
997
        addMapping(mapping, numericKey, option);
4✔
998
      }
999
      interaction("Option " + numericKey + ": " + key);
5✔
1000
    }
1001
    O option = null;
2✔
1002
    if (isBatchMode()) {
3!
1003
      if (isForceMode()) {
×
1004
        option = options[0];
×
1005
        interaction("" + option);
×
1006
      }
1007
    } else {
1008
      while (option == null) {
2✔
1009
        String answer = readLine();
3✔
1010
        option = mapping.get(answer);
4✔
1011
        if (option == null) {
2!
1012
          warning("Invalid answer: '" + answer + "' - please try again.");
×
1013
        }
1014
      }
1✔
1015
    }
1016
    return option;
2✔
1017
  }
1018

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

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

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

1032
  @Override
1033
  public Step getCurrentStep() {
1034

1035
    return this.currentStep;
×
1036
  }
1037

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

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

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

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

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

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

1111
  @Override
1112
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1113

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

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

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

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

1183
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1184

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

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

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

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

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

1296
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1297

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

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

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

1408
  @Override
1409
  public String findBash() {
1410

1411
    String bash = BASH;
2✔
1412
    if (SystemInfoImpl.INSTANCE.isWindows()) {
3!
1413
      bash = findBashOnWindows();
×
1414
      if (bash == null) {
×
1415
        String variable = IdeVariables.BASH_PATH.getName();
×
1416
        bash = getVariables().get(variable);
×
1417
        if (bash == null) {
×
1418
          trace("Bash not found. Trying to search on system PATH.");
×
1419
          variable = IdeVariables.PATH.getName();
×
1420
          Path plainBash = Path.of(BASH);
×
1421
          Path bashPath = getPath().findBinary(plainBash);
×
1422
          bash = bashPath.toAbsolutePath().toString();
×
1423
          if (bash.contains("AppData\\Local\\Microsoft\\WindowsApps")) {
×
1424
            warning("Only found windows fake bash that is not usable!");
×
1425
            bash = null;
×
1426
          }
1427
        }
1428
        if (bash == null) {
×
1429
          info("Could not find bash in Windows registry, using bash from {} as fallback: {}", variable, bash);
×
1430
        }
1431
      }
1432
    }
1433
    return bash;
2✔
1434
  }
1435

1436
  private String findBashOnWindows() {
1437

1438
    // Check if Git Bash exists in the default location
1439
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1440
    if (Files.exists(defaultPath)) {
×
1441
      return defaultPath.toString();
×
1442
    }
1443

1444
    // If not found in the default location, try the registry query
1445
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1446
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1447
    String regQueryResult;
1448
    for (String bashVariant : bashVariants) {
×
1449
      trace("Trying to find bash variant: {}", bashVariant);
×
1450
      for (String registryKey : registryKeys) {
×
1451
        trace("Trying to find bash from registry key: {}", registryKey);
×
1452
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1453
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1454

1455
        try {
1456
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1457
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1458
            StringBuilder output = new StringBuilder();
×
1459
            String line;
1460

1461
            while ((line = reader.readLine()) != null) {
×
1462
              output.append(line);
×
1463
            }
1464

1465
            int exitCode = process.waitFor();
×
1466
            if (exitCode != 0) {
×
1467
              warning("Query to windows registry for finding bash failed with exit code {}", exitCode);
×
1468
              return null;
×
1469
            }
1470

1471
            regQueryResult = output.toString();
×
1472
            trace("Result from windows registry was: {}", regQueryResult);
×
1473
            int index = regQueryResult.indexOf("REG_SZ");
×
1474
            if (index != -1) {
×
1475
              String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1476
              String bashPath = path + "\\bin\\bash.exe";
×
1477
              debug("Found bash at: {}", bashPath);
×
1478
              return bashPath;
×
1479
            }
1480
          }
×
1481
        } catch (Exception e) {
×
1482
          error(e, "Query to windows registry for finding bash failed!");
×
1483
          return null;
×
1484
        }
×
1485
      }
1486
    }
1487
    // no bash found
1488
    return null;
×
1489
  }
1490

1491
  @Override
1492
  public WindowsPathSyntax getPathSyntax() {
1493

1494
    return this.pathSyntax;
3✔
1495
  }
1496

1497
  /**
1498
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1499
   */
1500
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1501

1502
    this.pathSyntax = pathSyntax;
3✔
1503
  }
1✔
1504

1505
  /**
1506
   * @return the {@link IdeStartContextImpl}.
1507
   */
1508
  public IdeStartContextImpl getStartContext() {
1509

1510
    return startContext;
3✔
1511
  }
1512

1513
  /**
1514
   * @return the {@link WindowsHelper}.
1515
   */
1516
  public final WindowsHelper getWindowsHelper() {
1517

1518
    if (this.windowsHelper == null) {
3✔
1519
      this.windowsHelper = createWindowsHelper();
4✔
1520
    }
1521
    return this.windowsHelper;
3✔
1522
  }
1523

1524
  /**
1525
   * @return the new {@link WindowsHelper} instance.
1526
   */
1527
  protected WindowsHelper createWindowsHelper() {
1528

1529
    return new WindowsHelperImpl(this);
×
1530
  }
1531

1532
  /**
1533
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1534
   */
1535
  public void reload() {
1536

1537
    this.variables = null;
3✔
1538
    this.customToolRepository = null;
3✔
1539
  }
1✔
1540

1541
  @Override
1542
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1543

1544
    assert (Files.isDirectory(installationPath));
6!
1545
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1546
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1547
  }
1✔
1548

1549
  /**
1550
   * @param home the IDE_HOME directory.
1551
   * @param workspace the name of the active workspace folder.
1552
   */
1553
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1554

1555
  }
1556

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