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

devonfw / IDEasy / 18478663135

13 Oct 2025 09:26PM UTC coverage: 68.146% (-0.3%) from 68.41%
18478663135

Pull #1017

github

web-flow
Merge 577c1cbd9 into b792ba719
Pull Request #1017: #404: enhance logging with custom slf4j bridge

3456 of 5545 branches covered (62.33%)

Branch coverage included in aggregate %.

9057 of 12817 relevant lines covered (70.66%)

3.1 hits per line

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

63.61
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 final MvnRepository mvnRepository;
132

133
  private final 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
  /** Context used for logging */
154
  private static IdeContext loggingContext;
155

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

164
    super();
2✔
165
    this.startContext = startContext;
3✔
166
    this.startContext.setArgFormatter(this);
4✔
167
    this.privacyMap = new HashMap<>();
5✔
168
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
169
    this.commandletManager = new CommandletManagerImpl(this);
6✔
170
    this.fileAccess = new FileAccessImpl(this);
6✔
171
    String userHomeProperty = getSystem().getProperty("user.home");
5✔
172
    if (userHomeProperty != null) {
2!
173
      this.userHome = Path.of(userHomeProperty);
×
174
    }
175
    String workspace = WORKSPACE_MAIN;
2✔
176
    if (workingDirectory == null) {
2!
177
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
178
    }
179
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
180
    if (Files.isDirectory(workingDirectory)) {
5✔
181
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
182
    } else {
183
      warning("Current working directory does not exist: {}", workingDirectory);
9✔
184
    }
185
    this.cwd = workingDirectory;
3✔
186
    // detect IDE_HOME and WORKSPACE
187
    Path currentDir = workingDirectory;
2✔
188
    String name1 = "";
2✔
189
    String name2 = "";
2✔
190
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
191
    while (currentDir != null) {
2✔
192
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
193
      if (isIdeHome(currentDir)) {
4✔
194
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
195
          workspace = name2;
3✔
196
        }
197
        break;
198
      }
199
      name2 = name1;
2✔
200
      int nameCount = currentDir.getNameCount();
3✔
201
      if (nameCount >= 1) {
3✔
202
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
203
      }
204
      currentDir = currentDir.getParent();
3✔
205
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
206
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
207
        currentDir = null;
×
208
      }
209
    }
1✔
210

211
    // detection completed, initializing variables
212
    this.ideRoot = findIdeRoot(currentDir);
5✔
213

214
    setCwd(workingDirectory, workspace, currentDir);
5✔
215

216
    if (this.ideRoot != null) {
3✔
217
      Path tempDownloadPath = getTempDownloadPath();
3✔
218
      if (Files.isDirectory(tempDownloadPath)) {
6✔
219
        // TODO delete all files older than 1 day here...
220
      } else {
221
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
222
      }
223
    }
224

225
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
226
    loggingContext = this;
2✔
227
    this.mvnRepository = new MvnRepository(this);
6✔
228
    this.npmRepository = new NpmRepository(this);
6✔
229
  }
1✔
230

231
  private Path findIdeRoot(Path ideHomePath) {
232

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

250
  /**
251
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
252
   */
253
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
254

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

287
  @Override
288
  public void setCwd(Path userDir, String workspace, Path ideHome) {
289

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

319
  private String getMessageIdeHomeFound() {
320

321
    String wks = this.workspaceName;
3✔
322
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
323
      wks = "*".repeat(wks.length());
×
324
    }
325
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
326
  }
327

328
  private String getMessageNotInsideIdeProject() {
329

330
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
331
  }
332

333
  private String getMessageIdeRootNotFound() {
334

335
    String root = getSystem().getEnv("IDE_ROOT");
5✔
336
    if (root == null) {
2!
337
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
338
    } else {
339
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
340
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
341
    }
342
  }
343

344
  /**
345
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
346
   */
347
  public boolean isTest() {
348

349
    return false;
×
350
  }
351

352
  protected SystemPath computeSystemPath() {
353

354
    return new SystemPath(this);
×
355
  }
356

357
  private boolean isIdeHome(Path dir) {
358

359
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
360
      return false;
2✔
361
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
362
      return false;
×
363
    }
364
    return true;
2✔
365
  }
366

367
  private EnvironmentVariables createVariables() {
368

369
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
370
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
371
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
372
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
373
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
374
    return conf.resolved();
3✔
375
  }
376

377
  protected AbstractEnvironmentVariables createSystemVariables() {
378

379
    return EnvironmentVariables.ofSystem(this);
3✔
380
  }
381

382
  @Override
383
  public SystemInfo getSystemInfo() {
384

385
    return this.systemInfo;
3✔
386
  }
387

388
  @Override
389
  public FileAccess getFileAccess() {
390

391
    // currently FileAccess contains download method and requires network proxy to be configured. Maybe download should be moved to its own interface/class
392
    configureNetworkProxy();
2✔
393
    return this.fileAccess;
3✔
394
  }
395

396
  @Override
397
  public CommandletManager getCommandletManager() {
398

399
    return this.commandletManager;
3✔
400
  }
401

402
  @Override
403
  public ToolRepository getDefaultToolRepository() {
404

405
    return this.defaultToolRepository;
3✔
406
  }
407

408
  @Override
409
  public MvnRepository getMvnRepository() {
410

411
    return this.mvnRepository;
3✔
412
  }
413

414
  @Override
415
  public NpmRepository getNpmRepository() {
416

417
    return this.npmRepository;
3✔
418
  }
419

420
  @Override
421
  public CustomToolRepository getCustomToolRepository() {
422

423
    if (this.customToolRepository == null) {
3!
424
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
425
    }
426
    return this.customToolRepository;
3✔
427
  }
428

429
  @Override
430
  public Path getIdeHome() {
431

432
    return this.ideHome;
3✔
433
  }
434

435
  @Override
436
  public String getProjectName() {
437

438
    if (this.ideHome != null) {
3!
439
      return this.ideHome.getFileName().toString();
5✔
440
    }
441
    return "";
×
442
  }
443

444
  @Override
445
  public VersionIdentifier getProjectVersion() {
446

447
    if (this.ideHome != null) {
3!
448
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
449
      if (Files.exists(versionFile)) {
5✔
450
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
451
        return VersionIdentifier.of(version);
3✔
452
      }
453
    }
454
    return IdeMigrator.START_VERSION;
2✔
455
  }
456

457
  @Override
458
  public void setProjectVersion(VersionIdentifier version) {
459

460
    if (this.ideHome == null) {
3!
461
      throw new IllegalStateException("IDE_HOME not available!");
×
462
    }
463
    Objects.requireNonNull(version);
3✔
464
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
465
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
466
  }
1✔
467

468
  @Override
469
  public Path getIdeRoot() {
470

471
    return this.ideRoot;
3✔
472
  }
473

474
  @Override
475
  public Path getIdePath() {
476

477
    Path myIdeRoot = getIdeRoot();
3✔
478
    if (myIdeRoot == null) {
2!
479
      return null;
×
480
    }
481
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
482
  }
483

484
  @Override
485
  public Path getCwd() {
486

487
    return this.cwd;
3✔
488
  }
489

490
  @Override
491
  public Path getTempPath() {
492

493
    Path idePath = getIdePath();
3✔
494
    if (idePath == null) {
2!
495
      return null;
×
496
    }
497
    return idePath.resolve("tmp");
4✔
498
  }
499

500
  @Override
501
  public Path getTempDownloadPath() {
502

503
    Path tmp = getTempPath();
3✔
504
    if (tmp == null) {
2!
505
      return null;
×
506
    }
507
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
508
  }
509

510
  @Override
511
  public Path getUserHome() {
512

513
    return this.userHome;
3✔
514
  }
515

516
  /**
517
   * This method should only be used for tests to mock user home.
518
   *
519
   * @param userHome the new value of {@link #getUserHome()}.
520
   */
521
  protected void setUserHome(Path userHome) {
522

523
    this.userHome = userHome;
3✔
524
    resetPrivacyMap();
2✔
525
  }
1✔
526

527
  @Override
528
  public Path getUserHomeIde() {
529

530
    return this.userHomeIde;
3✔
531
  }
532

533
  @Override
534
  public Path getSettingsPath() {
535

536
    return this.settingsPath;
3✔
537
  }
538

539
  @Override
540
  public Path getSettingsGitRepository() {
541

542
    Path settingsPath = getSettingsPath();
3✔
543
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
544
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
545
      error("Settings repository exists but is not a git repository.");
3✔
546
      return null;
2✔
547
    }
548
    return settingsPath;
2✔
549
  }
550

551
  @Override
552
  public boolean isSettingsRepositorySymlinkOrJunction() {
553

554
    Path settingsPath = getSettingsPath();
3✔
555
    if (settingsPath == null) {
2!
556
      return false;
×
557
    }
558
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
559
  }
560

561
  @Override
562
  public Path getSettingsCommitIdPath() {
563

564
    return this.settingsCommitIdPath;
3✔
565
  }
566

567
  @Override
568
  public Path getConfPath() {
569

570
    return this.confPath;
3✔
571
  }
572

573
  @Override
574
  public Path getSoftwarePath() {
575

576
    if (this.ideHome == null) {
3✔
577
      return null;
2✔
578
    }
579
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
580
  }
581

582
  @Override
583
  public Path getSoftwareExtraPath() {
584

585
    Path softwarePath = getSoftwarePath();
3✔
586
    if (softwarePath == null) {
2!
587
      return null;
×
588
    }
589
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
590
  }
591

592
  @Override
593
  public Path getSoftwareRepositoryPath() {
594

595
    Path idePath = getIdePath();
3✔
596
    if (idePath == null) {
2!
597
      return null;
×
598
    }
599
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
600
  }
601

602
  @Override
603
  public Path getPluginsPath() {
604

605
    return this.pluginsPath;
3✔
606
  }
607

608
  @Override
609
  public String getWorkspaceName() {
610

611
    return this.workspaceName;
3✔
612
  }
613

614
  @Override
615
  public Path getWorkspacePath() {
616

617
    return this.workspacePath;
3✔
618
  }
619

620
  @Override
621
  public Path getDownloadPath() {
622

623
    return this.downloadPath;
3✔
624
  }
625

626
  @Override
627
  public Path getUrlsPath() {
628

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

636
  @Override
637
  public Path getToolRepositoryPath() {
638

639
    Path idePath = getIdePath();
3✔
640
    if (idePath == null) {
2!
641
      return null;
×
642
    }
643
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
644
  }
645

646
  @Override
647
  public SystemPath getPath() {
648

649
    return this.path;
3✔
650
  }
651

652
  @Override
653
  public EnvironmentVariables getVariables() {
654

655
    if (this.variables == null) {
3✔
656
      this.variables = createVariables();
4✔
657
    }
658
    return this.variables;
3✔
659
  }
660

661
  @Override
662
  public UrlMetadata getUrls() {
663

664
    if (this.urlMetadata == null) {
3✔
665
      if (!isTest()) {
3!
666
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
667
      }
668
      this.urlMetadata = new UrlMetadata(this);
6✔
669
    }
670
    return this.urlMetadata;
3✔
671
  }
672

673
  @Override
674
  public boolean isQuietMode() {
675

676
    return this.startContext.isQuietMode();
4✔
677
  }
678

679
  @Override
680
  public boolean isBatchMode() {
681

682
    return this.startContext.isBatchMode();
4✔
683
  }
684

685
  @Override
686
  public boolean isForceMode() {
687

688
    return this.startContext.isForceMode();
4✔
689
  }
690

691
  @Override
692
  public boolean isForcePull() {
693

694
    return this.startContext.isForcePull();
4✔
695
  }
696

697
  @Override
698
  public boolean isForcePlugins() {
699

700
    return this.startContext.isForcePlugins();
4✔
701
  }
702

703
  @Override
704
  public boolean isForceRepositories() {
705

706
    return this.startContext.isForceRepositories();
4✔
707
  }
708

709
  @Override
710
  public boolean isOfflineMode() {
711

712
    return this.startContext.isOfflineMode();
4✔
713
  }
714

715
  @Override
716
  public boolean isPrivacyMode() {
717
    return this.startContext.isPrivacyMode();
4✔
718
  }
719

720
  @Override
721
  public boolean isSkipUpdatesMode() {
722

723
    return this.startContext.isSkipUpdatesMode();
4✔
724
  }
725

726
  @Override
727
  public boolean isNoColorsMode() {
728

729
    return this.startContext.isNoColorsMode();
×
730
  }
731

732
  @Override
733
  public boolean isOnline() {
734

735
    if (this.online == null) {
3✔
736
      configureNetworkProxy();
2✔
737
      // we currently assume we have only a CLI process that runs shortly
738
      // therefore we run this check only once to save resources when this method is called many times
739
      String url = "https://www.github.com";
2✔
740
      try {
741
        int timeout = 1000;
2✔
742
        //open a connection to github.com and try to retrieve data
743
        //getContent fails if there is no connection
744
        URLConnection connection = new URL(url).openConnection();
6✔
745
        connection.setConnectTimeout(timeout);
3✔
746
        connection.getContent();
3✔
747
        this.online = Boolean.TRUE;
3✔
748
      } catch (Exception e) {
×
749
        if (debug().isEnabled()) {
×
750
          debug().log(e, "Error when trying to connect to {}", url);
×
751
        }
752
        this.online = Boolean.FALSE;
×
753
      }
1✔
754
    }
755
    return this.online.booleanValue();
4✔
756
  }
757

758
  private void configureNetworkProxy() {
759

760
    if (this.networkProxy == null) {
3✔
761
      this.networkProxy = new NetworkProxy(this);
6✔
762
      this.networkProxy.configure();
3✔
763
    }
764
  }
1✔
765

766
  @Override
767
  public Locale getLocale() {
768

769
    Locale locale = this.startContext.getLocale();
4✔
770
    if (locale == null) {
2✔
771
      locale = Locale.getDefault();
2✔
772
    }
773
    return locale;
2✔
774
  }
775

776
  @Override
777
  public DirectoryMerger getWorkspaceMerger() {
778

779
    if (this.workspaceMerger == null) {
3✔
780
      this.workspaceMerger = new DirectoryMerger(this);
6✔
781
    }
782
    return this.workspaceMerger;
3✔
783
  }
784

785
  /**
786
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
787
   */
788
  @Override
789
  public Path getDefaultExecutionDirectory() {
790

791
    return this.defaultExecutionDirectory;
×
792
  }
793

794
  /**
795
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
796
   */
797
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
798

799
    if (defaultExecutionDirectory != null) {
×
800
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
801
    }
802
  }
×
803

804
  @Override
805
  public GitContext getGitContext() {
806

807
    return new GitContextImpl(this);
×
808
  }
809

810
  @Override
811
  public ProcessContext newProcess() {
812

813
    ProcessContext processContext = createProcessContext();
3✔
814
    if (this.defaultExecutionDirectory != null) {
3!
815
      processContext.directory(this.defaultExecutionDirectory);
×
816
    }
817
    return processContext;
2✔
818
  }
819

820
  @Override
821
  public IdeSystem getSystem() {
822

823
    if (this.system == null) {
×
824
      this.system = new IdeSystemImpl(this);
×
825
    }
826
    return this.system;
×
827
  }
828

829
  /**
830
   * @return a new instance of {@link ProcessContext}.
831
   * @see #newProcess()
832
   */
833
  protected ProcessContext createProcessContext() {
834

835
    return new ProcessContextImpl(this);
5✔
836
  }
837

838
  @Override
839
  public IdeSubLogger level(IdeLogLevel level) {
840

841
    return this.startContext.level(level);
5✔
842
  }
843

844
  @Override
845
  public void logIdeHomeAndRootStatus() {
846
    if (this.ideRoot != null) {
3!
847
      success("IDE_ROOT is set to {}", this.ideRoot);
×
848
    }
849
    if (this.ideHome == null) {
3✔
850
      warning(getMessageNotInsideIdeProject());
5✔
851
    } else {
852
      success("IDE_HOME is set to {}", this.ideHome);
10✔
853
    }
854
  }
1✔
855

856
  @Override
857
  public String formatArgument(Object argument) {
858

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

879
  /**
880
   * @param path the sensitive {@link Path} to
881
   * @param replacement the replacement to mask the {@link Path} in log output.
882
   */
883
  protected void initializePrivacyMap(Path path, String replacement) {
884

885
    if (path == null) {
×
886
      return;
×
887
    }
888
    if (this.systemInfo.isWindows()) {
×
889
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
890
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
891
    } else {
892
      this.privacyMap.put(path.toString(), replacement);
×
893
    }
894
  }
×
895

896
  /**
897
   * Resets the privacy map in case fundamental values have changed.
898
   */
899
  private void resetPrivacyMap() {
900

901
    this.privacyMap.clear();
3✔
902
  }
1✔
903

904

905
  @Override
906
  public String askForInput(String message, String defaultValue) {
907

908
    while (true) {
909
      if (!message.isBlank()) {
3!
910
        interaction(message);
3✔
911
      }
912
      if (isBatchMode()) {
3!
913
        if (isForceMode()) {
×
914
          return defaultValue;
×
915
        } else {
916
          throw new CliAbortException();
×
917
        }
918
      }
919
      String input = readLine().trim();
4✔
920
      if (!input.isEmpty()) {
3!
921
        return input;
2✔
922
      } else {
923
        if (defaultValue != null) {
×
924
          return defaultValue;
×
925
        }
926
      }
927
    }
×
928
  }
929

930
  @SuppressWarnings("unchecked")
931
  @Override
932
  public <O> O question(O[] options, String question, Object... args) {
933

934
    assert (options.length >= 2);
5!
935
    interaction(question, args);
4✔
936
    return displayOptionsAndGetAnswer(options);
4✔
937
  }
938

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

972
  /**
973
   * @return the input from the end-user (e.g. read from the console).
974
   */
975
  protected abstract String readLine();
976

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

979
    O duplicate = mapping.put(key, option);
5✔
980
    if (duplicate != null) {
2!
981
      throw new IllegalArgumentException("Duplicated option " + key);
×
982
    }
983
  }
1✔
984

985
  @Override
986
  public Step getCurrentStep() {
987

988
    return this.currentStep;
×
989
  }
990

991
  @Override
992
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
993

994
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
995
    return this.currentStep;
3✔
996
  }
997

998
  /**
999
   * Internal method to end the running {@link Step}.
1000
   *
1001
   * @param step the current {@link Step} to end.
1002
   */
1003
  public void endStep(StepImpl step) {
1004

1005
    if (step == this.currentStep) {
4!
1006
      this.currentStep = this.currentStep.getParent();
6✔
1007
    } else {
1008
      String currentStepName = "null";
×
1009
      if (this.currentStep != null) {
×
1010
        currentStepName = this.currentStep.getName();
×
1011
      }
1012
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1013
    }
1014
  }
1✔
1015

1016
  /**
1017
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1018
   *
1019
   * @param arguments the {@link CliArgument}.
1020
   * @return the return code of the execution.
1021
   */
1022
  public int run(CliArguments arguments) {
1023

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

1064
  @Override
1065
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1066

1067
    this.startContext.deactivateLogging(threshold);
4✔
1068
    lambda.run();
2✔
1069
    this.startContext.activateLogging();
3✔
1070
  }
1✔
1071

1072
  /**
1073
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1074
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1075
   *     {@link Commandlet} did not match and we have to try a different candidate).
1076
   */
1077
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1078

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

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

1136
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1137

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

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

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

1216
  /**
1217
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1218
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1219
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1220
   */
1221
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1222

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

1249
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1250

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

1307
  /**
1308
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1309
   *     {@link CliArguments#copy() copy} as needed.
1310
   * @param cmd the potential {@link Commandlet} to match.
1311
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1312
   */
1313
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1314

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

1361
  @Override
1362
  public String findBash() {
1363

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

1389
  private String findBashOnWindows() {
1390

1391
    // Check if Git Bash exists in the default location
1392
    Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
×
1393
    if (Files.exists(defaultPath)) {
×
1394
      return defaultPath.toString();
×
1395
    }
1396

1397
    // If not found in the default location, try the registry query
1398
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1399
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1400
    String regQueryResult;
1401
    for (String bashVariant : bashVariants) {
×
1402
      trace("Trying to find bash variant: {}", bashVariant);
×
1403
      for (String registryKey : registryKeys) {
×
1404
        trace("Trying to find bash from registry key: {}", registryKey);
×
1405
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1406
        String command = "reg query " + registryKey + "\\Software\\" + bashVariant + "  /v " + toolValueName + " 2>nul";
×
1407

1408
        try {
1409
          Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
×
1410
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
×
1411
            StringBuilder output = new StringBuilder();
×
1412
            String line;
1413

1414
            while ((line = reader.readLine()) != null) {
×
1415
              output.append(line);
×
1416
            }
1417

1418
            int exitCode = process.waitFor();
×
1419
            if (exitCode != 0) {
×
1420
              warning("Query to windows registry for finding bash failed with exit code {}", exitCode);
×
1421
              return null;
×
1422
            }
1423

1424
            regQueryResult = output.toString();
×
1425
            trace("Result from windows registry was: {}", regQueryResult);
×
1426
            int index = regQueryResult.indexOf("REG_SZ");
×
1427
            if (index != -1) {
×
1428
              String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
×
1429
              String bashPath = path + "\\bin\\bash.exe";
×
1430
              debug("Found bash at: {}", bashPath);
×
1431
              return bashPath;
×
1432
            }
1433
          }
×
1434
        } catch (Exception e) {
×
1435
          error(e, "Query to windows registry for finding bash failed!");
×
1436
          return null;
×
1437
        }
×
1438
      }
1439
    }
1440
    // no bash found
1441
    return null;
×
1442
  }
1443

1444
  @Override
1445
  public WindowsPathSyntax getPathSyntax() {
1446

1447
    return this.pathSyntax;
3✔
1448
  }
1449

1450
  /**
1451
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1452
   */
1453
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1454

1455
    this.pathSyntax = pathSyntax;
3✔
1456
  }
1✔
1457

1458
  /**
1459
   * @return the {@link IdeStartContextImpl}.
1460
   */
1461
  public IdeStartContextImpl getStartContext() {
1462

1463
    return startContext;
3✔
1464
  }
1465

1466
  /**
1467
   * @return the {@link WindowsHelper}.
1468
   */
1469
  public final WindowsHelper getWindowsHelper() {
1470

1471
    if (this.windowsHelper == null) {
3✔
1472
      this.windowsHelper = createWindowsHelper();
4✔
1473
    }
1474
    return this.windowsHelper;
3✔
1475
  }
1476

1477
  /**
1478
   * @return the new {@link WindowsHelper} instance.
1479
   */
1480
  protected WindowsHelper createWindowsHelper() {
1481

1482
    return new WindowsHelperImpl(this);
×
1483
  }
1484

1485
  /**
1486
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1487
   */
1488
  public void reload() {
1489

1490
    this.variables = null;
3✔
1491
    this.customToolRepository = null;
3✔
1492
  }
1✔
1493

1494
  @Override
1495
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1496

1497
    assert (Files.isDirectory(installationPath));
6!
1498
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1499
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1500
  }
1✔
1501

1502
  /**
1503
   * Gets the logging context.
1504
   *
1505
   * @return {@link IdeContext}.
1506
   */
1507
  public static IdeContext getLoggingContext() {
1508

1509
    return loggingContext;
2✔
1510
  }
1511

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