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

devonfw / IDEasy / 22264313095

21 Feb 2026 09:03PM UTC coverage: 70.645% (+0.2%) from 70.474%
22264313095

Pull #1710

github

web-flow
Merge eff4c822e into 379acdc9d
Pull Request #1710: #404: allow logging via SLF4J

4121 of 6440 branches covered (63.99%)

Branch coverage included in aggregate %.

10696 of 14534 relevant lines covered (73.59%)

3.13 hits per line

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

66.64
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.ByteArrayInputStream;
6
import java.io.ByteArrayOutputStream;
7
import java.io.IOException;
8
import java.nio.file.Files;
9
import java.nio.file.Path;
10
import java.time.LocalDateTime;
11
import java.util.ArrayList;
12
import java.util.HashMap;
13
import java.util.Iterator;
14
import java.util.List;
15
import java.util.Locale;
16
import java.util.Map;
17
import java.util.Map.Entry;
18
import java.util.Objects;
19
import java.util.Properties;
20
import java.util.function.Predicate;
21
import java.util.logging.LogManager;
22

23
import org.slf4j.Logger;
24
import org.slf4j.LoggerFactory;
25

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

87
/**
88
 * Abstract base implementation of {@link IdeContext}.
89
 */
90
public abstract class AbstractIdeContext implements IdeContext, IdeLogArgFormatter {
91

92
  private static final Logger LOG = LoggerFactory.getLogger(AbstractIdeContext.class);
3✔
93

94
  /** The default shell bash (Bourne Again SHell). */
95
  public static final String BASH = "bash";
96

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

99
  private static final String LICENSE_URL = "https://github.com/devonfw/IDEasy/blob/main/documentation/LICENSE.adoc";
100

101
  private static final String DEFAULT_WINDOWS_GIT_PATH = "C:\\Program Files\\Git\\bin\\bash.exe";
102

103
  private static final String OPTION_DETAILS_START = "([";
104

105
  private final IdeStartContextImpl startContext;
106

107
  private Path ideHome;
108

109
  private final Path ideRoot;
110

111
  private Path confPath;
112

113
  protected Path settingsPath;
114

115
  private Path settingsCommitIdPath;
116

117
  protected Path pluginsPath;
118

119
  private Path workspacePath;
120

121
  private String workspaceName;
122

123
  private Path cwd;
124

125
  private Path downloadPath;
126

127
  private Path userHome;
128

129
  private Path userHomeIde;
130

131
  private SystemPath path;
132

133
  private WindowsPathSyntax pathSyntax;
134

135
  private final SystemInfo systemInfo;
136

137
  private EnvironmentVariables variables;
138

139
  private final FileAccess fileAccess;
140

141
  protected CommandletManager commandletManager;
142

143
  protected ToolRepository defaultToolRepository;
144

145
  private CustomToolRepository customToolRepository;
146

147
  private MvnRepository mvnRepository;
148

149
  private NpmRepository npmRepository;
150

151
  private PipRepository pipRepository;
152

153
  private DirectoryMerger workspaceMerger;
154

155
  protected UrlMetadata urlMetadata;
156

157
  protected Path defaultExecutionDirectory;
158

159
  private StepImpl currentStep;
160

161
  private NetworkStatus networkStatus;
162

163
  protected IdeSystem system;
164

165
  private WindowsHelper windowsHelper;
166

167
  private final Map<String, String> privacyMap;
168

169
  private Path bash;
170

171
  private boolean julConfigured;
172

173
  private final IdeSubLogger[] loggers;
174

175
  /**
176
   * The constructor.
177
   *
178
   * @param startContext the {@link IdeLogger}.
179
   * @param workingDirectory the optional {@link Path} to current working directory.
180
   */
181
  public AbstractIdeContext(IdeStartContextImpl startContext, Path workingDirectory) {
182

183
    super();
2✔
184
    // init sub loggers
185
    IdeLogLevel[] levels = IdeLogLevel.values();
2✔
186
    this.loggers = new IdeSubLogger[levels.length];
5✔
187
    for (IdeLogLevel level : levels) {
16✔
188
      this.loggers[level.ordinal()] = new IdeSubLoggerAdapter(level, LOG);
10✔
189
    }
190

191
    this.startContext = startContext;
3✔
192
    this.startContext.setArgFormatter(this);
4✔
193
    this.privacyMap = new HashMap<>();
5✔
194
    this.systemInfo = SystemInfoImpl.INSTANCE;
3✔
195
    this.commandletManager = new CommandletManagerImpl(this);
6✔
196
    this.fileAccess = new FileAccessImpl(this);
6✔
197
    String userHomeProperty = getSystem().getProperty("user.home");
5✔
198
    if (userHomeProperty != null) {
2!
199
      this.userHome = Path.of(userHomeProperty);
×
200
    }
201
    if (workingDirectory == null) {
2!
202
      workingDirectory = Path.of(System.getProperty("user.dir"));
×
203
    }
204
    workingDirectory = workingDirectory.toAbsolutePath();
3✔
205
    if (Files.isDirectory(workingDirectory)) {
5✔
206
      workingDirectory = this.fileAccess.toCanonicalPath(workingDirectory);
6✔
207
    } else {
208
      warning("Current working directory does not exist: {}", workingDirectory);
9✔
209
    }
210
    this.cwd = workingDirectory;
3✔
211
    // detect IDE_HOME and WORKSPACE
212
    String workspace = null;
2✔
213
    Path ideHomeDir = null;
2✔
214
    IdeHomeAndWorkspace ideHomeAndWorkspace = findIdeHome(workingDirectory);
4✔
215
    if (ideHomeAndWorkspace != null) {
2!
216
      ideHomeDir = ideHomeAndWorkspace.home();
3✔
217
      workspace = ideHomeAndWorkspace.workspace();
3✔
218
    }
219

220
    // detection completed, initializing variables
221
    this.ideRoot = findIdeRoot(ideHomeDir);
5✔
222

223
    setCwd(workingDirectory, workspace, ideHomeDir);
5✔
224

225
    if (this.ideRoot != null) {
3✔
226
      Path tempDownloadPath = getTempDownloadPath();
3✔
227
      if (Files.isDirectory(tempDownloadPath)) {
6✔
228
        // TODO delete all files older than 1 day here...
229
      } else {
230
        this.fileAccess.mkdirs(tempDownloadPath);
4✔
231
      }
232
    }
233
    this.defaultToolRepository = new DefaultToolRepository(this);
6✔
234
  }
1✔
235

236
  /**
237
   * 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
238
   * add additional validation or boundary checks.
239
   *
240
   * @param workingDirectory the starting directory for the search.
241
   * @return an instance of {@link IdeHomeAndWorkspace} where the IDE_HOME was found or {@code null} if not found.
242
   */
243
  protected IdeHomeAndWorkspace findIdeHome(Path workingDirectory) {
244

245
    Path currentDir = workingDirectory;
2✔
246
    String name1 = "";
2✔
247
    String name2 = "";
2✔
248
    String workspace = WORKSPACE_MAIN;
2✔
249
    Path ideRootPath = getIdeRootPathFromEnv(false);
4✔
250

251
    while (currentDir != null) {
2✔
252
      trace("Looking for IDE_HOME in {}", currentDir);
9✔
253
      if (isIdeHome(currentDir)) {
4✔
254
        if (FOLDER_WORKSPACES.equals(name1) && !name2.isEmpty()) {
7✔
255
          workspace = name2;
3✔
256
        }
257
        break;
258
      }
259
      name2 = name1;
2✔
260
      int nameCount = currentDir.getNameCount();
3✔
261
      if (nameCount >= 1) {
3✔
262
        name1 = currentDir.getName(nameCount - 1).toString();
7✔
263
      }
264
      currentDir = currentDir.getParent();
3✔
265
      if ((ideRootPath != null) && (ideRootPath.equals(currentDir))) {
2!
266
        // prevent that during tests we traverse to the real IDE project of IDEasy developer
267
        currentDir = null;
×
268
      }
269
    }
1✔
270

271
    return new IdeHomeAndWorkspace(currentDir, workspace);
6✔
272
  }
273

274
  /**
275
   * @return a new {@link MvnRepository}
276
   */
277
  protected MvnRepository createMvnRepository() {
278
    return new MvnRepository(this);
5✔
279
  }
280

281
  /**
282
   * @return a new {@link NpmRepository}
283
   */
284
  protected NpmRepository createNpmRepository() {
285
    return new NpmRepository(this);
×
286
  }
287

288
  /**
289
   * @return a new {@link PipRepository}
290
   */
291
  protected PipRepository createPipRepository() {
292
    return new PipRepository(this);
×
293
  }
294

295
  private Path findIdeRoot(Path ideHomePath) {
296

297
    Path ideRootPath = null;
2✔
298
    if (ideHomePath != null) {
2✔
299
      Path ideRootPathFromEnv = getIdeRootPathFromEnv(true);
4✔
300
      ideRootPath = ideHomePath.getParent();
3✔
301
      if ((ideRootPathFromEnv != null) && !ideRootPath.toString().equals(ideRootPathFromEnv.toString())) {
2!
302
        warning(
×
303
            "Variable IDE_ROOT is set to '{}' but for your project '{}' the path '{}' would have been expected.\n"
304
                + "Please check your 'user.dir' or working directory setting and make sure that it matches your IDE_ROOT variable.",
305
            ideRootPathFromEnv,
306
            ideHomePath.getFileName(), ideRootPath);
×
307
      }
308
    } else if (!isTest()) {
4!
309
      ideRootPath = getIdeRootPathFromEnv(true);
×
310
    }
311
    return ideRootPath;
2✔
312
  }
313

314
  /**
315
   * @return the {@link #getIdeRoot() IDE_ROOT} from the system environment.
316
   */
317
  protected Path getIdeRootPathFromEnv(boolean withSanityCheck) {
318

319
    String root = getSystem().getEnv(IdeVariables.IDE_ROOT.getName());
×
320
    if (root != null) {
×
321
      Path rootPath = Path.of(root);
×
322
      if (Files.isDirectory(rootPath)) {
×
323
        Path absoluteRootPath = getFileAccess().toCanonicalPath(rootPath);
×
324
        if (withSanityCheck) {
×
325
          int nameCount = rootPath.getNameCount();
×
326
          int absoluteNameCount = absoluteRootPath.getNameCount();
×
327
          int delta = absoluteNameCount - nameCount;
×
328
          if (delta >= 0) {
×
329
            for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) {
×
330
              String rootName = rootPath.getName(nameIndex).toString();
×
331
              String absoluteRootName = absoluteRootPath.getName(nameIndex + delta).toString();
×
332
              if (!rootName.equals(absoluteRootName)) {
×
333
                warning("IDE_ROOT is set to {} but was expanded to absolute path {} and does not match for segment {} and {} - fix your IDEasy installation!",
×
334
                    rootPath, absoluteRootPath, rootName, absoluteRootName);
335
                break;
×
336
              }
337
            }
338
          } else {
339
            warning("IDE_ROOT is set to {} but was expanded to a shorter absolute path {}", rootPath,
×
340
                absoluteRootPath);
341
          }
342
        }
343
        return absoluteRootPath;
×
344
      } else if (withSanityCheck) {
×
345
        warning("IDE_ROOT is set to {} that is not an existing directory - fix your IDEasy installation!", rootPath);
×
346
      }
347
    }
348
    return null;
×
349
  }
350

351
  @Override
352
  public void setCwd(Path userDir, String workspace, Path ideHome) {
353

354
    this.cwd = userDir;
3✔
355
    this.workspaceName = workspace;
3✔
356
    this.ideHome = ideHome;
3✔
357
    if (ideHome == null) {
2✔
358
      this.workspacePath = null;
3✔
359
      this.confPath = null;
3✔
360
      this.settingsPath = null;
3✔
361
      this.pluginsPath = null;
4✔
362
    } else {
363
      this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName);
9✔
364
      this.confPath = this.ideHome.resolve(FOLDER_CONF);
6✔
365
      this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS);
6✔
366
      this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID);
6✔
367
      this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS);
6✔
368
    }
369
    if (isTest()) {
3!
370
      // only for testing...
371
      if (this.ideHome == null) {
3✔
372
        this.userHome = Path.of("/non-existing-user-home-for-testing");
7✔
373
      } else {
374
        this.userHome = this.ideHome.resolve("home");
6✔
375
      }
376
    }
377
    this.userHomeIde = this.userHome.resolve(FOLDER_DOT_IDE);
6✔
378
    this.downloadPath = this.userHome.resolve("Downloads/ide");
6✔
379
    resetPrivacyMap();
2✔
380
    this.path = computeSystemPath();
4✔
381
  }
1✔
382

383
  private String getMessageIdeHomeFound() {
384

385
    String wks = this.workspaceName;
3✔
386
    if (isPrivacyMode() && !WORKSPACE_MAIN.equals(wks)) {
3!
387
      wks = "*".repeat(wks.length());
×
388
    }
389
    return "IDE environment variables have been set for " + formatArgument(this.ideHome) + " in workspace " + wks;
7✔
390
  }
391

392
  private String getMessageNotInsideIdeProject() {
393

394
    return "You are not inside an IDE project: " + formatArgument(this.cwd);
6✔
395
  }
396

397
  private String getMessageIdeRootNotFound() {
398

399
    String root = getSystem().getEnv("IDE_ROOT");
5✔
400
    if (root == null) {
2!
401
      return "The environment variable IDE_ROOT is undefined. Please reinstall IDEasy or manually repair IDE_ROOT variable.";
2✔
402
    } else {
403
      return "The environment variable IDE_ROOT is pointing to an invalid path " + formatArgument(root)
×
404
          + ". Please reinstall IDEasy or manually repair IDE_ROOT variable.";
405
    }
406
  }
407

408
  /**
409
   * @return {@code true} if this is a test context for JUnits, {@code false} otherwise.
410
   */
411
  public boolean isTest() {
412

413
    return false;
×
414
  }
415

416
  protected SystemPath computeSystemPath() {
417

418
    return new SystemPath(this);
×
419
  }
420

421
  /**
422
   * Checks if the given directory is a valid IDE home by verifying it contains both 'workspaces' and 'settings' directories.
423
   *
424
   * @param dir the directory to check.
425
   * @return {@code true} if the directory is a valid IDE home, {@code false} otherwise.
426
   */
427
  protected boolean isIdeHome(Path dir) {
428

429
    if (!Files.isDirectory(dir.resolve("workspaces"))) {
7✔
430
      return false;
2✔
431
    } else if (!Files.isDirectory(dir.resolve("settings"))) {
7!
432
      return false;
×
433
    }
434
    return true;
2✔
435
  }
436

437
  private EnvironmentVariables createVariables() {
438

439
    AbstractEnvironmentVariables system = createSystemVariables();
3✔
440
    AbstractEnvironmentVariables user = system.extend(this.userHomeIde, EnvironmentVariablesType.USER);
6✔
441
    AbstractEnvironmentVariables settings = user.extend(this.settingsPath, EnvironmentVariablesType.SETTINGS);
6✔
442
    AbstractEnvironmentVariables workspace = settings.extend(this.workspacePath, EnvironmentVariablesType.WORKSPACE);
6✔
443
    AbstractEnvironmentVariables conf = workspace.extend(this.confPath, EnvironmentVariablesType.CONF);
6✔
444
    return conf.resolved();
3✔
445
  }
446

447
  protected AbstractEnvironmentVariables createSystemVariables() {
448

449
    return EnvironmentVariables.ofSystem(this);
3✔
450
  }
451

452
  @Override
453
  public SystemInfo getSystemInfo() {
454

455
    return this.systemInfo;
3✔
456
  }
457

458
  @Override
459
  public FileAccess getFileAccess() {
460

461
    return this.fileAccess;
3✔
462
  }
463

464
  @Override
465
  public CommandletManager getCommandletManager() {
466

467
    return this.commandletManager;
3✔
468
  }
469

470
  @Override
471
  public ToolRepository getDefaultToolRepository() {
472

473
    return this.defaultToolRepository;
3✔
474
  }
475

476
  @Override
477
  public MvnRepository getMvnRepository() {
478
    if (this.mvnRepository == null) {
3✔
479
      this.mvnRepository = createMvnRepository();
4✔
480
    }
481
    return this.mvnRepository;
3✔
482
  }
483

484
  @Override
485
  public NpmRepository getNpmRepository() {
486
    if (this.npmRepository == null) {
3✔
487
      this.npmRepository = createNpmRepository();
4✔
488
    }
489
    return this.npmRepository;
3✔
490
  }
491

492
  @Override
493
  public PipRepository getPipRepository() {
494
    if (this.pipRepository == null) {
3✔
495
      this.pipRepository = createPipRepository();
4✔
496
    }
497
    return this.pipRepository;
3✔
498
  }
499

500
  @Override
501
  public CustomToolRepository getCustomToolRepository() {
502

503
    if (this.customToolRepository == null) {
3!
504
      this.customToolRepository = CustomToolRepositoryImpl.of(this);
4✔
505
    }
506
    return this.customToolRepository;
3✔
507
  }
508

509
  @Override
510
  public Path getIdeHome() {
511

512
    return this.ideHome;
3✔
513
  }
514

515
  @Override
516
  public String getProjectName() {
517

518
    if (this.ideHome != null) {
3!
519
      return this.ideHome.getFileName().toString();
5✔
520
    }
521
    return "";
×
522
  }
523

524
  @Override
525
  public VersionIdentifier getProjectVersion() {
526

527
    if (this.ideHome != null) {
3!
528
      Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
529
      if (Files.exists(versionFile)) {
5✔
530
        String version = this.fileAccess.readFileContent(versionFile).trim();
6✔
531
        return VersionIdentifier.of(version);
3✔
532
      }
533
    }
534
    return IdeMigrator.START_VERSION;
2✔
535
  }
536

537
  @Override
538
  public void setProjectVersion(VersionIdentifier version) {
539

540
    if (this.ideHome == null) {
3!
541
      throw new IllegalStateException("IDE_HOME not available!");
×
542
    }
543
    Objects.requireNonNull(version);
3✔
544
    Path versionFile = this.ideHome.resolve(IdeContext.FILE_SOFTWARE_VERSION);
5✔
545
    this.fileAccess.writeFileContent(version.toString(), versionFile);
6✔
546
  }
1✔
547

548
  @Override
549
  public Path getIdeRoot() {
550

551
    return this.ideRoot;
3✔
552
  }
553

554
  @Override
555
  public Path getIdePath() {
556

557
    Path myIdeRoot = getIdeRoot();
3✔
558
    if (myIdeRoot == null) {
2✔
559
      return null;
2✔
560
    }
561
    return myIdeRoot.resolve(FOLDER_UNDERSCORE_IDE);
4✔
562
  }
563

564
  @Override
565
  public Path getCwd() {
566

567
    return this.cwd;
3✔
568
  }
569

570
  @Override
571
  public Path getTempPath() {
572

573
    Path idePath = getIdePath();
3✔
574
    if (idePath == null) {
2!
575
      return null;
×
576
    }
577
    return idePath.resolve("tmp");
4✔
578
  }
579

580
  @Override
581
  public Path getTempDownloadPath() {
582

583
    Path tmp = getTempPath();
3✔
584
    if (tmp == null) {
2!
585
      return null;
×
586
    }
587
    return tmp.resolve(FOLDER_DOWNLOADS);
4✔
588
  }
589

590
  @Override
591
  public Path getUserHome() {
592

593
    return this.userHome;
3✔
594
  }
595

596
  /**
597
   * This method should only be used for tests to mock user home.
598
   *
599
   * @param userHome the new value of {@link #getUserHome()}.
600
   */
601
  protected void setUserHome(Path userHome) {
602

603
    this.userHome = userHome;
3✔
604
    resetPrivacyMap();
2✔
605
  }
1✔
606

607
  @Override
608
  public Path getUserHomeIde() {
609

610
    return this.userHomeIde;
3✔
611
  }
612

613
  @Override
614
  public Path getSettingsPath() {
615

616
    return this.settingsPath;
3✔
617
  }
618

619
  @Override
620
  public Path getSettingsGitRepository() {
621

622
    Path settingsPath = getSettingsPath();
3✔
623
    // check whether the settings path has a .git folder only if its not a symbolic link or junction
624
    if ((settingsPath != null) && !Files.exists(settingsPath.resolve(".git")) && !isSettingsRepositorySymlinkOrJunction()) {
12!
625
      error("Settings repository exists but is not a git repository.");
3✔
626
      return null;
2✔
627
    }
628
    return settingsPath;
2✔
629
  }
630

631
  @Override
632
  public boolean isSettingsRepositorySymlinkOrJunction() {
633

634
    Path settingsPath = getSettingsPath();
3✔
635
    if (settingsPath == null) {
2!
636
      return false;
×
637
    }
638
    return Files.isSymbolicLink(settingsPath) || getFileAccess().isJunction(settingsPath);
10!
639
  }
640

641
  @Override
642
  public Path getSettingsCommitIdPath() {
643

644
    return this.settingsCommitIdPath;
3✔
645
  }
646

647
  @Override
648
  public Path getConfPath() {
649

650
    return this.confPath;
3✔
651
  }
652

653
  @Override
654
  public Path getSoftwarePath() {
655

656
    if (this.ideHome == null) {
3✔
657
      return null;
2✔
658
    }
659
    return this.ideHome.resolve(FOLDER_SOFTWARE);
5✔
660
  }
661

662
  @Override
663
  public Path getSoftwareExtraPath() {
664

665
    Path softwarePath = getSoftwarePath();
3✔
666
    if (softwarePath == null) {
2!
667
      return null;
×
668
    }
669
    return softwarePath.resolve(FOLDER_EXTRA);
4✔
670
  }
671

672
  @Override
673
  public Path getSoftwareRepositoryPath() {
674

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

682
  @Override
683
  public Path getPluginsPath() {
684

685
    return this.pluginsPath;
3✔
686
  }
687

688
  @Override
689
  public String getWorkspaceName() {
690

691
    return this.workspaceName;
3✔
692
  }
693

694
  @Override
695
  public Path getWorkspacePath() {
696

697
    return this.workspacePath;
3✔
698
  }
699

700
  @Override
701
  public Path getDownloadPath() {
702

703
    return this.downloadPath;
3✔
704
  }
705

706
  @Override
707
  public Path getUrlsPath() {
708

709
    Path idePath = getIdePath();
3✔
710
    if (idePath == null) {
2!
711
      return null;
×
712
    }
713
    return idePath.resolve(FOLDER_URLS);
4✔
714
  }
715

716
  @Override
717
  public Path getToolRepositoryPath() {
718

719
    Path idePath = getIdePath();
3✔
720
    if (idePath == null) {
2!
721
      return null;
×
722
    }
723
    return idePath.resolve(FOLDER_SOFTWARE);
4✔
724
  }
725

726
  @Override
727
  public SystemPath getPath() {
728

729
    return this.path;
3✔
730
  }
731

732
  @Override
733
  public EnvironmentVariables getVariables() {
734

735
    if (this.variables == null) {
3✔
736
      this.variables = createVariables();
4✔
737
    }
738
    return this.variables;
3✔
739
  }
740

741
  @Override
742
  public UrlMetadata getUrls() {
743

744
    if (this.urlMetadata == null) {
3✔
745
      if (!isTest()) {
3!
746
        getGitContext().pullOrCloneAndResetIfNeeded(IDE_URLS_GIT, getUrlsPath(), null);
×
747
      }
748
      this.urlMetadata = new UrlMetadata(this);
6✔
749
    }
750
    return this.urlMetadata;
3✔
751
  }
752

753
  @Override
754
  public boolean isQuietMode() {
755

756
    return this.startContext.isQuietMode();
4✔
757
  }
758

759
  @Override
760
  public boolean isBatchMode() {
761

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

765
  @Override
766
  public boolean isForceMode() {
767

768
    return this.startContext.isForceMode();
4✔
769
  }
770

771
  @Override
772
  public boolean isForcePull() {
773

774
    return this.startContext.isForcePull();
4✔
775
  }
776

777
  @Override
778
  public boolean isForcePlugins() {
779

780
    return this.startContext.isForcePlugins();
4✔
781
  }
782

783
  @Override
784
  public boolean isForceRepositories() {
785

786
    return this.startContext.isForceRepositories();
4✔
787
  }
788

789
  @Override
790
  public boolean isOfflineMode() {
791

792
    return this.startContext.isOfflineMode();
4✔
793
  }
794

795
  @Override
796
  public boolean isPrivacyMode() {
797
    return this.startContext.isPrivacyMode();
4✔
798
  }
799

800
  @Override
801
  public boolean isSkipUpdatesMode() {
802

803
    return this.startContext.isSkipUpdatesMode();
4✔
804
  }
805

806
  @Override
807
  public boolean isNoColorsMode() {
808

809
    return this.startContext.isNoColorsMode();
×
810
  }
811

812
  @Override
813
  public NetworkStatus getNetworkStatus() {
814

815
    if (this.networkStatus == null) {
×
816
      this.networkStatus = new NetworkStatusImpl(this);
×
817
    }
818
    return this.networkStatus;
×
819
  }
820

821
  @Override
822
  public Locale getLocale() {
823

824
    Locale locale = this.startContext.getLocale();
4✔
825
    if (locale == null) {
2✔
826
      locale = Locale.getDefault();
2✔
827
    }
828
    return locale;
2✔
829
  }
830

831
  @Override
832
  public DirectoryMerger getWorkspaceMerger() {
833

834
    if (this.workspaceMerger == null) {
3✔
835
      this.workspaceMerger = new DirectoryMerger(this);
6✔
836
    }
837
    return this.workspaceMerger;
3✔
838
  }
839

840
  /**
841
   * @return the {@link #getDefaultExecutionDirectory() default execution directory} in which a command process is executed.
842
   */
843
  @Override
844
  public Path getDefaultExecutionDirectory() {
845

846
    return this.defaultExecutionDirectory;
×
847
  }
848

849
  /**
850
   * @param defaultExecutionDirectory new value of {@link #getDefaultExecutionDirectory()}.
851
   */
852
  public void setDefaultExecutionDirectory(Path defaultExecutionDirectory) {
853

854
    if (defaultExecutionDirectory != null) {
×
855
      this.defaultExecutionDirectory = defaultExecutionDirectory;
×
856
    }
857
  }
×
858

859
  @Override
860
  public GitContext getGitContext() {
861

862
    return new GitContextImpl(this);
×
863
  }
864

865
  @Override
866
  public ProcessContext newProcess() {
867

868
    ProcessContext processContext = createProcessContext();
3✔
869
    if (this.defaultExecutionDirectory != null) {
3!
870
      processContext.directory(this.defaultExecutionDirectory);
×
871
    }
872
    return processContext;
2✔
873
  }
874

875
  @Override
876
  public IdeSystem getSystem() {
877

878
    if (this.system == null) {
×
879
      this.system = new IdeSystemImpl();
×
880
    }
881
    return this.system;
×
882
  }
883

884
  /**
885
   * @return a new instance of {@link ProcessContext}.
886
   * @see #newProcess()
887
   */
888
  protected ProcessContext createProcessContext() {
889

890
    return new ProcessContextImpl(this);
×
891
  }
892

893
  @Override
894
  public IdeSubLogger level(IdeLogLevel level) {
895

896
    return this.loggers[level.ordinal()];
6✔
897
  }
898

899
  @Override
900
  public void logIdeHomeAndRootStatus() {
901
    if (this.ideRoot != null) {
3!
902
      success("IDE_ROOT is set to {}", this.ideRoot);
×
903
    }
904
    if (this.ideHome == null) {
3✔
905
      warning(getMessageNotInsideIdeProject());
5✔
906
    } else {
907
      success("IDE_HOME is set to {}", this.ideHome);
10✔
908
    }
909
  }
1✔
910

911
  @Override
912
  public String formatArgument(Object argument) {
913

914
    if (argument == null) {
2✔
915
      return null;
2✔
916
    }
917
    String result = argument.toString();
3✔
918
    if (isPrivacyMode()) {
3✔
919
      if ((this.ideRoot != null) && this.privacyMap.isEmpty()) {
3!
920
        initializePrivacyMap(this.userHome, "~");
×
921
        String projectName = getProjectName();
×
922
        if (!projectName.isEmpty()) {
×
923
          this.privacyMap.put(projectName, "project");
×
924
        }
925
      }
926
      for (Entry<String, String> entry : this.privacyMap.entrySet()) {
8!
927
        result = result.replace(entry.getKey(), entry.getValue());
×
928
      }
×
929
      result = PrivacyUtil.removeSensitivePathInformation(result);
3✔
930
    }
931
    return result;
2✔
932
  }
933

934
  /**
935
   * @param path the sensitive {@link Path} to
936
   * @param replacement the replacement to mask the {@link Path} in log output.
937
   */
938
  protected void initializePrivacyMap(Path path, String replacement) {
939

940
    if (path == null) {
×
941
      return;
×
942
    }
943
    if (this.systemInfo.isWindows()) {
×
944
      this.privacyMap.put(WindowsPathSyntax.WINDOWS.format(path), replacement);
×
945
      this.privacyMap.put(WindowsPathSyntax.MSYS.format(path), replacement);
×
946
    } else {
947
      this.privacyMap.put(path.toString(), replacement);
×
948
    }
949
  }
×
950

951
  /**
952
   * Resets the privacy map in case fundamental values have changed.
953
   */
954
  private void resetPrivacyMap() {
955

956
    this.privacyMap.clear();
3✔
957
  }
1✔
958

959

960
  @Override
961
  public String askForInput(String message, String defaultValue) {
962

963
    while (true) {
964
      if (!message.isBlank()) {
3!
965
        interaction(message);
3✔
966
      }
967
      if (isBatchMode()) {
3!
968
        if (isForceMode()) {
×
969
          return defaultValue;
×
970
        } else {
971
          throw new CliAbortException();
×
972
        }
973
      }
974
      String input = readLine().trim();
4✔
975
      if (!input.isEmpty()) {
3!
976
        return input;
2✔
977
      } else {
978
        if (defaultValue != null) {
×
979
          return defaultValue;
×
980
        }
981
      }
982
    }
×
983
  }
984

985
  @SuppressWarnings("unchecked")
986
  @Override
987
  public <O> O question(O[] options, String question, Object... args) {
988

989
    assert (options.length > 0);
4!
990
    interaction(question, args);
4✔
991
    return displayOptionsAndGetAnswer(options);
4✔
992
  }
993

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

1028
  private static String computeOptionKey(String option) {
1029
    String key = option;
2✔
1030
    int index = -1;
2✔
1031
    for (char c : OPTION_DETAILS_START.toCharArray()) {
17✔
1032
      int currentIndex = key.indexOf(c);
4✔
1033
      if (currentIndex != -1) {
3✔
1034
        if ((index == -1) || (currentIndex < index)) {
3!
1035
          index = currentIndex;
2✔
1036
        }
1037
      }
1038
    }
1039
    if (index > 0) {
2✔
1040
      key = key.substring(0, index).trim();
6✔
1041
    }
1042
    return key;
2✔
1043
  }
1044

1045
  /**
1046
   * @return the input from the end-user (e.g. read from the console).
1047
   */
1048
  protected abstract String readLine();
1049

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

1052
    O duplicate = mapping.put(key, option);
5✔
1053
    if (duplicate != null) {
2!
1054
      throw new IllegalArgumentException("Duplicated option " + key);
×
1055
    }
1056
  }
1✔
1057

1058
  @Override
1059
  public Step getCurrentStep() {
1060

1061
    return this.currentStep;
×
1062
  }
1063

1064
  @Override
1065
  public StepImpl newStep(boolean silent, String name, Object... parameters) {
1066

1067
    this.currentStep = new StepImpl(this, this.currentStep, name, silent, parameters);
11✔
1068
    return this.currentStep;
3✔
1069
  }
1070

1071
  /**
1072
   * Internal method to end the running {@link Step}.
1073
   *
1074
   * @param step the current {@link Step} to end.
1075
   */
1076
  public void endStep(StepImpl step) {
1077

1078
    if (step == this.currentStep) {
4!
1079
      this.currentStep = this.currentStep.getParent();
6✔
1080
    } else {
1081
      String currentStepName = "null";
×
1082
      if (this.currentStep != null) {
×
1083
        currentStepName = this.currentStep.getName();
×
1084
      }
1085
      warning("endStep called with wrong step '{}' but expected '{}'", step.getName(), currentStepName);
×
1086
    }
1087
  }
1✔
1088

1089
  /**
1090
   * Finds the matching {@link Commandlet} to run, applies {@link CliArguments} to its {@link Commandlet#getProperties() properties} and will execute it.
1091
   *
1092
   * @param arguments the {@link CliArgument}.
1093
   * @return the return code of the execution.
1094
   */
1095
  public int run(CliArguments arguments) {
1096

1097
    CliArgument current = arguments.current();
3✔
1098
    assert (this.currentStep == null);
4!
1099
    boolean supressStepSuccess = false;
2✔
1100
    StepImpl step = newStep(true, "ide", (Object[]) current.asArray());
8✔
1101
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, null);
6✔
1102
    Commandlet cmd = null;
2✔
1103
    ValidationResult result = null;
2✔
1104
    try {
1105
      while (commandletIterator.hasNext()) {
3✔
1106
        cmd = commandletIterator.next();
4✔
1107
        result = applyAndRun(arguments.copy(), cmd);
6✔
1108
        if (result.isValid()) {
3!
1109
          supressStepSuccess = cmd.isSuppressStepSuccess();
3✔
1110
          step.success();
2✔
1111
          return ProcessResult.SUCCESS;
4✔
1112
        }
1113
      }
1114
      this.startContext.activateLogging();
3✔
1115
      verifyIdeMinVersion(false);
3✔
1116
      if (result != null) {
2!
1117
        error(result.getErrorMessage());
×
1118
      }
1119
      step.error("Invalid arguments: {}", current.getArgs());
10✔
1120
      HelpCommandlet help = this.commandletManager.getCommandlet(HelpCommandlet.class);
6✔
1121
      if (cmd != null) {
2!
1122
        help.commandlet.setValue(cmd);
×
1123
      }
1124
      help.run();
2✔
1125
      return 1;
4✔
1126
    } catch (Throwable t) {
1✔
1127
      this.startContext.activateLogging();
3✔
1128
      step.error(t, true);
4✔
1129
      throw t;
2✔
1130
    } finally {
1131
      step.close();
2✔
1132
      assert (this.currentStep == null);
4!
1133
      step.logSummary(supressStepSuccess);
3✔
1134
    }
1135
  }
1136

1137
  public void configureJavaUtilLogging() {
1138

1139
    if (this.julConfigured) {
3✔
1140
      return;
1✔
1141
    }
1142
    Properties properties = createJavaUtilLoggingProperties();
3✔
1143
    if (properties == null) {
2!
1144
      return;
×
1145
    }
1146
    try {
1147
      ByteArrayOutputStream out = new ByteArrayOutputStream(512);
5✔
1148
      properties.store(out, null);
4✔
1149
      out.flush();
2✔
1150
      ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
6✔
1151
      LogManager.getLogManager().readConfiguration(in);
3✔
1152
      this.julConfigured = true;
3✔
1153
    } catch (IOException e) {
×
1154
      error(e);
×
1155
    }
1✔
1156
  }
1✔
1157

1158
  protected Properties createJavaUtilLoggingProperties() {
1159
    boolean logfile = false;
×
1160
    Boolean writeLogfile = IdeVariables.IDE_WRITE_LOGFILE.get(this);
×
1161
    if (Boolean.TRUE.equals(writeLogfile)) {
×
1162
      logfile = true;
×
1163
      this.startContext.setWriteLogfile(true);
×
1164
    }
1165
    return createJavaUtilLoggingProperties(false, logfile);
×
1166
  }
1167

1168
  protected final Properties createJavaUtilLoggingProperties(boolean console, boolean file) {
1169

1170
    Path idePath = getIdePath();
3✔
1171
    if (file && (idePath == null)) {
2!
1172
      file = false;
×
1173
      error("Cannot enable --logfile option since IDE_ROOT is undefined.");
×
1174
    }
1175
    if (!console && !file) {
2!
1176
      return null;
×
1177
    }
1178
    Properties properties = new Properties();
4✔
1179
    String intLevel = "FINE";
2✔
1180
    if (trace().isEnabled()) {
4!
1181
      intLevel = "FINER";
2✔
1182
    }
1183
    String extLevel = "WARNING";
2✔
1184
    properties.setProperty(".level", extLevel);
5✔
1185
    properties.setProperty("com.devonfw.tools.ide.level", intLevel);
5✔
1186
    if (file && console) {
2!
1187
      properties.setProperty("handlers", "java.util.logging.ConsoleHandler,java.util.logging.FileHandler");
×
1188
    } else if (file) {
2!
1189
      properties.setProperty("handlers", "java.util.logging.FileHandler");
×
1190
    } else {
1191
      properties.setProperty("handlers", "java.util.logging.ConsoleHandler");
5✔
1192
    }
1193
    if (file) {
2!
1194
      properties.setProperty("java.util.logging.FileHandler.level", intLevel);
×
1195
      properties.setProperty("java.util.logging.FileHandler.formatter", "java.util.logging.SimpleFormatter");
×
1196
      properties.setProperty("java.util.logging.FileHandler.encoding", "UTF-8");
×
1197
      LocalDateTime now = LocalDateTime.now();
×
1198
      Path logsPath = idePath.resolve(FOLDER_LOGS).resolve(DateTimeUtil.formatDate(now, true));
×
1199
      getFileAccess().mkdirs(logsPath);
×
1200
      properties.setProperty("java.util.logging.FileHandler.pattern", logsPath.resolve("ideasy-" + DateTimeUtil.formatTime(now) + ".log").toString());
×
1201
    }
1202
    if (console) {
2!
1203
      properties.setProperty("java.util.logging.ConsoleHandler.level", intLevel);
5✔
1204
      properties.setProperty("java.util.logging.ConsoleHandler.formatter", "java.util.logging.SimpleFormatter");
5✔
1205
      properties.setProperty("java.util.logging.ConsoleHandler.encoding", "UTF-8");
5✔
1206
    }
1207
    properties.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL [%4$s] [%3$s] %5$s%6$s%n");
5✔
1208
    return properties;
2✔
1209
  }
1210

1211
  @Override
1212
  public void runWithoutLogging(Runnable lambda, IdeLogLevel threshold) {
1213

1214
    this.startContext.deactivateLogging(threshold);
4✔
1215
    lambda.run();
2✔
1216
    this.startContext.activateLogging();
3✔
1217
  }
1✔
1218

1219
  /**
1220
   * @param cmd the potential {@link Commandlet} to {@link #apply(CliArguments, Commandlet) apply} and {@link Commandlet#run() run}.
1221
   * @return {@code true} if the given {@link Commandlet} matched and did {@link Commandlet#run() run} successfully, {@code false} otherwise (the
1222
   *     {@link Commandlet} did not match and we have to try a different candidate).
1223
   */
1224
  private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) {
1225

1226
    IdeLogLevel previousLogLevel = null;
2✔
1227
    cmd.reset();
2✔
1228
    ValidationResult result = apply(arguments, cmd);
5✔
1229
    if (result.isValid()) {
3!
1230
      result = cmd.validate();
3✔
1231
    }
1232
    if (result.isValid()) {
3!
1233
      debug("Running commandlet {}", cmd);
9✔
1234
      if (cmd.isIdeHomeRequired() && (this.ideHome == null)) {
6!
1235
        throw new CliException(getMessageNotInsideIdeProject(), ProcessResult.NO_IDE_HOME);
×
1236
      } else if (cmd.isIdeRootRequired() && (this.ideRoot == null)) {
6!
1237
        throw new CliException(getMessageIdeRootNotFound(), ProcessResult.NO_IDE_ROOT);
7✔
1238
      }
1239
      try {
1240
        if (cmd.isProcessableOutput()) {
3!
1241
          if (!debug().isEnabled()) {
×
1242
            // unless --debug or --trace was supplied, processable output commandlets will disable all log-levels except INFO to prevent other logs interfere
1243
            previousLogLevel = this.startContext.setLogLevel(IdeLogLevel.PROCESSABLE);
×
1244
          }
1245
          this.startContext.activateLogging();
×
1246
        } else {
1247
          this.startContext.activateLogging();
3✔
1248
          if (cmd.isIdeHomeRequired()) {
3!
1249
            debug(getMessageIdeHomeFound());
4✔
1250
          }
1251
          Path settingsRepository = getSettingsGitRepository();
3✔
1252
          if (settingsRepository != null) {
2!
1253
            if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || (
×
1254
                getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(
×
1255
                    settingsRepository, getSettingsCommitIdPath()))) {
×
1256
              if (isSettingsRepositorySymlinkOrJunction()) {
×
1257
                interaction(
×
1258
                    "Updates are available for the settings repository. Please pull the latest changes by yourself or by calling \"ide -f update\" to apply them.");
1259
              } else {
1260
                interaction(
×
1261
                    "Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\"");
1262
              }
1263
            }
1264
          }
1265
        }
1266
        boolean success = ensureLicenseAgreement(cmd);
4✔
1267
        if (!success) {
2!
1268
          return ValidationResultValid.get();
×
1269
        }
1270
        cmd.run();
2✔
1271
      } finally {
1272
        this.julConfigured = false;
3✔
1273
        if (previousLogLevel != null) {
2!
1274
          this.startContext.setLogLevel(previousLogLevel);
×
1275
        }
1276
      }
1✔
1277
    } else {
1278
      trace("Commandlet did not match");
×
1279
    }
1280
    return result;
2✔
1281
  }
1282

1283
  private boolean ensureLicenseAgreement(Commandlet cmd) {
1284

1285
    if (isTest()) {
3!
1286
      return true; // ignore for tests
2✔
1287
    }
1288
    getFileAccess().mkdirs(this.userHomeIde);
×
1289
    Path licenseAgreement = this.userHomeIde.resolve(FILE_LICENSE_AGREEMENT);
×
1290
    if (Files.isRegularFile(licenseAgreement)) {
×
1291
      return true; // success, license already accepted
×
1292
    }
1293
    if (cmd instanceof EnvironmentCommandlet) {
×
1294
      // 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
1295
      // 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
1296
      // printing anything anymore in such case.
1297
      return false;
×
1298
    }
1299
    boolean logLevelInfoDisabled = !this.startContext.info().isEnabled();
×
1300
    if (logLevelInfoDisabled) {
×
1301
      this.startContext.setLogLevel(IdeLogLevel.INFO, true);
×
1302
    }
1303
    boolean logLevelInteractionDisabled = !this.startContext.interaction().isEnabled();
×
1304
    if (logLevelInteractionDisabled) {
×
1305
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, true);
×
1306
    }
1307
    StringBuilder sb = new StringBuilder(1180);
×
1308
    sb.append(LOGO).append("""
×
1309
        Welcome to IDEasy!
1310
        This product (with its included 3rd party components) is open-source software and can be used free (also commercially).
1311
        It supports automatic download and installation of arbitrary 3rd party tools.
1312
        By default only open-source 3rd party tools are used (downloaded, installed, executed).
1313
        But if explicitly configured, also commercial software that requires an additional license may be used.
1314
        This happens e.g. if you configure "ultimate" edition of IntelliJ or "docker" edition of Docker (Docker Desktop).
1315
        You are solely responsible for all risks implied by using this software.
1316
        Before using IDEasy you need to read and accept the license agreement with all involved licenses.
1317
        You will be able to find it online under the following URL:
1318
        """).append(LICENSE_URL);
×
1319
    if (this.ideRoot != null) {
×
1320
      sb.append("\n\nAlso it is included in the documentation that you can find here:\n").
×
1321
          append(getIdePath().resolve("IDEasy.pdf").toString()).append("\n");
×
1322
    }
1323
    info(sb.toString());
×
1324
    askToContinue("Do you accept these terms of use and all license agreements?");
×
1325

1326
    sb.setLength(0);
×
1327
    LocalDateTime now = LocalDateTime.now();
×
1328
    sb.append("On ").append(DateTimeUtil.formatDate(now, false)).append(" at ").append(DateTimeUtil.formatTime(now))
×
1329
        .append(" you accepted the IDEasy license.\n").append(LICENSE_URL);
×
1330
    try {
1331
      Files.writeString(licenseAgreement, sb);
×
1332
    } catch (Exception e) {
×
1333
      throw new RuntimeException("Failed to save license agreement!", e);
×
1334
    }
×
1335
    if (logLevelInfoDisabled) {
×
1336
      this.startContext.setLogLevel(IdeLogLevel.INFO, false);
×
1337
    }
1338
    if (logLevelInteractionDisabled) {
×
1339
      this.startContext.setLogLevel(IdeLogLevel.INTERACTION, false);
×
1340
    }
1341
    return true;
×
1342
  }
1343

1344
  @Override
1345
  public void verifyIdeMinVersion(boolean throwException) {
1346
    VersionIdentifier minVersion = IDE_MIN_VERSION.get(this);
5✔
1347
    if (minVersion == null) {
2✔
1348
      return;
1✔
1349
    }
1350
    if (IdeVersion.getVersionIdentifier().compareVersion(minVersion).isLess()) {
5✔
1351
      String message = String.format("Your version of IDEasy is currently %s\n"
7✔
1352
          + "However, this is too old as your project requires at latest version %s\n"
1353
          + "Please run the following command to update to the latest version of IDEasy and fix the problem:\n"
1354
          + "ide upgrade", IdeVersion.getVersionIdentifier().toString(), minVersion.toString());
8✔
1355
      if (throwException) {
2✔
1356
        throw new CliException(message);
5✔
1357
      } else {
1358
        warning(message);
3✔
1359
      }
1360
    }
1361
  }
1✔
1362

1363
  /**
1364
   * @param arguments the {@link CliArguments#ofCompletion(String...) completion arguments}.
1365
   * @param includeContextOptions to include the options of {@link ContextCommandlet}.
1366
   * @return the {@link List} of {@link CompletionCandidate}s to suggest.
1367
   */
1368
  public List<CompletionCandidate> complete(CliArguments arguments, boolean includeContextOptions) {
1369

1370
    CompletionCandidateCollector collector = new CompletionCandidateCollectorDefault(this);
5✔
1371
    if (arguments.current().isStart()) {
4✔
1372
      arguments.next();
3✔
1373
    }
1374
    if (includeContextOptions) {
2✔
1375
      ContextCommandlet cc = new ContextCommandlet();
4✔
1376
      for (Property<?> property : cc.getProperties()) {
11✔
1377
        assert (property.isOption());
4!
1378
        property.apply(arguments, this, cc, collector);
7✔
1379
      }
1✔
1380
    }
1381
    Iterator<Commandlet> commandletIterator = this.commandletManager.findCommandlet(arguments, collector);
6✔
1382
    CliArgument current = arguments.current();
3✔
1383
    if (current.isCompletion() && current.isCombinedShortOption()) {
6✔
1384
      collector.add(current.get(), null, null, null);
7✔
1385
    }
1386
    arguments.next();
3✔
1387
    while (commandletIterator.hasNext()) {
3✔
1388
      Commandlet cmd = commandletIterator.next();
4✔
1389
      if (!arguments.current().isEnd()) {
4✔
1390
        completeCommandlet(arguments.copy(), cmd, collector);
6✔
1391
      }
1392
    }
1✔
1393
    return collector.getSortedCandidates();
3✔
1394
  }
1395

1396
  private void completeCommandlet(CliArguments arguments, Commandlet cmd, CompletionCandidateCollector collector) {
1397

1398
    trace("Trying to match arguments for auto-completion for commandlet {}", cmd.getName());
10✔
1399
    Iterator<Property<?>> valueIterator = cmd.getValues().iterator();
4✔
1400
    valueIterator.next(); // skip first property since this is the keyword property that already matched to find the commandlet
3✔
1401
    List<Property<?>> properties = cmd.getProperties();
3✔
1402
    // we are creating our own list of options and remove them when matched to avoid duplicate suggestions
1403
    List<Property<?>> optionProperties = new ArrayList<>(properties.size());
6✔
1404
    for (Property<?> property : properties) {
10✔
1405
      if (property.isOption()) {
3✔
1406
        optionProperties.add(property);
4✔
1407
      }
1408
    }
1✔
1409
    CliArgument currentArgument = arguments.current();
3✔
1410
    while (!currentArgument.isEnd()) {
3✔
1411
      trace("Trying to match argument '{}'", currentArgument);
9✔
1412
      if (currentArgument.isOption() && !arguments.isEndOptions()) {
6!
1413
        if (currentArgument.isCompletion()) {
3✔
1414
          Iterator<Property<?>> optionIterator = optionProperties.iterator();
3✔
1415
          while (optionIterator.hasNext()) {
3✔
1416
            Property<?> option = optionIterator.next();
4✔
1417
            boolean success = option.apply(arguments, this, cmd, collector);
7✔
1418
            if (success) {
2✔
1419
              optionIterator.remove();
2✔
1420
              arguments.next();
3✔
1421
            }
1422
          }
1✔
1423
        } else {
1✔
1424
          Property<?> option = cmd.getOption(currentArgument.get());
5✔
1425
          if (option != null) {
2✔
1426
            arguments.next();
3✔
1427
            boolean removed = optionProperties.remove(option);
4✔
1428
            if (!removed) {
2!
1429
              option = null;
×
1430
            }
1431
          }
1432
          if (option == null) {
2✔
1433
            trace("No such option was found.");
3✔
1434
            return;
1✔
1435
          }
1436
        }
1✔
1437
      } else {
1438
        if (valueIterator.hasNext()) {
3✔
1439
          Property<?> valueProperty = valueIterator.next();
4✔
1440
          boolean success = valueProperty.apply(arguments, this, cmd, collector);
7✔
1441
          if (!success) {
2✔
1442
            trace("Completion cannot match any further.");
3✔
1443
            return;
1✔
1444
          }
1445
        } else {
1✔
1446
          trace("No value left for completion.");
3✔
1447
          return;
1✔
1448
        }
1449
      }
1450
      currentArgument = arguments.current();
4✔
1451
    }
1452
  }
1✔
1453

1454
  /**
1455
   * @param arguments the {@link CliArguments} to apply. Will be {@link CliArguments#next() consumed} as they are matched. Consider passing a
1456
   *     {@link CliArguments#copy() copy} as needed.
1457
   * @param cmd the potential {@link Commandlet} to match.
1458
   * @return the {@link ValidationResult} telling if the {@link CliArguments} can be applied successfully or if validation errors ocurred.
1459
   */
1460
  public ValidationResult apply(CliArguments arguments, Commandlet cmd) {
1461

1462
    trace("Trying to match arguments to commandlet {}", cmd.getName());
10✔
1463
    CliArgument currentArgument = arguments.current();
3✔
1464
    Iterator<Property<?>> propertyIterator = cmd.getValues().iterator();
4✔
1465
    Property<?> property = null;
2✔
1466
    if (propertyIterator.hasNext()) {
3!
1467
      property = propertyIterator.next();
4✔
1468
    }
1469
    while (!currentArgument.isEnd()) {
3✔
1470
      trace("Trying to match argument '{}'", currentArgument);
9✔
1471
      Property<?> currentProperty = property;
2✔
1472
      if (!arguments.isEndOptions()) {
3!
1473
        Property<?> option = cmd.getOption(currentArgument.getKey());
5✔
1474
        if (option != null) {
2!
1475
          currentProperty = option;
×
1476
        }
1477
      }
1478
      if (currentProperty == null) {
2!
1479
        trace("No option or next value found");
×
1480
        ValidationState state = new ValidationState(null);
×
1481
        state.addErrorMessage("No matching property found");
×
1482
        return state;
×
1483
      }
1484
      trace("Next property candidate to match argument is {}", currentProperty);
9✔
1485
      if (currentProperty == property) {
3!
1486
        if (!property.isMultiValued()) {
3✔
1487
          if (propertyIterator.hasNext()) {
3✔
1488
            property = propertyIterator.next();
5✔
1489
          } else {
1490
            property = null;
2✔
1491
          }
1492
        }
1493
        if ((property != null) && property.isValue() && property.isMultiValued()) {
8!
1494
          arguments.stopSplitShortOptions();
2✔
1495
        }
1496
      }
1497
      boolean matches = currentProperty.apply(arguments, this, cmd, null);
7✔
1498
      if (!matches) {
2!
1499
        ValidationState state = new ValidationState(null);
×
1500
        state.addErrorMessage("No matching property found");
×
1501
        return state;
×
1502
      }
1503
      currentArgument = arguments.current();
3✔
1504
    }
1✔
1505
    return ValidationResultValid.get();
2✔
1506
  }
1507

1508
  @Override
1509
  public Path findBash() {
1510
    if (this.bash != null) {
3✔
1511
      return this.bash;
3✔
1512
    }
1513
    Path bashPath = findBashOnBashPath();
3✔
1514
    if (bashPath == null) {
2✔
1515
      bashPath = findBashInPath();
3✔
1516
      if (bashPath == null && (getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows())) {
6!
1517
        bashPath = findBashOnWindowsDefaultGitPath();
3✔
1518
        if (bashPath == null) {
2!
1519
          bashPath = findBashInWindowsRegistry();
3✔
1520
        }
1521
      }
1522
    }
1523
    if (bashPath == null) {
2✔
1524
      error("No bash executable could be found on your system.");
4✔
1525
    } else {
1526
      this.bash = bashPath;
3✔
1527
    }
1528
    return bashPath;
2✔
1529
  }
1530

1531
  private Path findBashOnBashPath() {
1532
    trace("Trying to find BASH_PATH environment variable.");
3✔
1533
    Path bash;
1534
    String bashPathVariableName = IdeVariables.BASH_PATH.getName();
3✔
1535
    String bashVariable = getVariables().get(bashPathVariableName);
5✔
1536
    if (bashVariable != null) {
2✔
1537
      bash = Path.of(bashVariable);
5✔
1538
      if (Files.exists(bash)) {
5✔
1539
        debug("{} environment variable was found and points to: {}", bashPathVariableName, bash);
13✔
1540
        return bash;
2✔
1541
      } else {
1542
        error("The environment variable {} points to a non existing file: {}", bashPathVariableName, bash);
13✔
1543
        return null;
2✔
1544
      }
1545
    } else {
1546
      debug("{} environment variable was not found", bashPathVariableName);
9✔
1547
      return null;
2✔
1548
    }
1549
  }
1550

1551
  /**
1552
   * @param path the path to check.
1553
   * @param toIgnore the String sequence which needs to be checked and ignored.
1554
   * @return {@code true} if the sequence to ignore was not found, {@code false} if the path contained the sequence to ignore.
1555
   */
1556
  private boolean checkPathToIgnoreLowercase(Path path, String toIgnore) {
1557
    String s = path.toAbsolutePath().toString().toLowerCase(Locale.ROOT);
6✔
1558
    return !s.contains(toIgnore);
7!
1559
  }
1560

1561
  /**
1562
   * Tries to find the bash.exe within the PATH environment variable.
1563
   *
1564
   * @return Path to bash.exe if found in PATH environment variable, {@code null} if bash.exe was not found.
1565
   */
1566
  private Path findBashInPath() {
1567
    trace("Trying to find bash in PATH environment variable.");
3✔
1568
    Path bash;
1569
    String pathVariableName = IdeVariables.PATH.getName();
3✔
1570
    if (pathVariableName != null) {
2!
1571
      Path plainBash = Path.of(BASH);
5✔
1572
      Predicate<Path> pathsToIgnore = p -> checkPathToIgnoreLowercase(p, "\\appdata\\local\\microsoft\\windowsapps") && checkPathToIgnoreLowercase(p,
16!
1573
          "\\windows\\system32");
1574
      Path bashPath = getPath().findBinary(plainBash, pathsToIgnore);
6✔
1575
      bash = bashPath.toAbsolutePath();
3✔
1576
      if (bashPath.equals(plainBash)) {
4✔
1577
        warning("No usable bash executable was found in your PATH environment variable!");
3✔
1578
        bash = null;
3✔
1579
      } else {
1580
        if (Files.exists(bashPath)) {
5!
1581
          debug("A proper bash executable was found in your PATH environment variable at: {}", bash);
10✔
1582
        } else {
1583
          bash = null;
×
1584
          error("A path to a bash executable was found in your PATH environment variable at: {} but the file is not existing.", bash);
×
1585
        }
1586
      }
1587
    } else {
1✔
1588
      bash = null;
×
1589
      // this should never happen...
1590
      error("PATH environment variable was not found");
×
1591
    }
1592
    return bash;
2✔
1593
  }
1594

1595
  /**
1596
   * Tries to find the bash.exe within the Windows registry.
1597
   *
1598
   * @return Path to bash.exe if found in registry, {@code null} if bash.exe was found.
1599
   */
1600
  protected Path findBashInWindowsRegistry() {
1601
    trace("Trying to find bash in Windows registry");
×
1602
    // If not found in the default location, try the registry query
1603
    String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
×
1604
    String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
×
1605
    for (String bashVariant : bashVariants) {
×
1606
      trace("Trying to find bash variant: {}", bashVariant);
×
1607
      for (String registryKey : registryKeys) {
×
1608
        trace("Trying to find bash from registry key: {}", registryKey);
×
1609
        String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
×
1610
        String registryPath = registryKey + "\\Software\\" + bashVariant;
×
1611

1612
        String path = getWindowsHelper().getRegistryValue(registryPath, toolValueName);
×
1613
        if (path != null) {
×
1614
          Path bashPath = Path.of(path + "\\bin\\bash.exe");
×
1615
          if (Files.exists(bashPath)) {
×
1616
            debug("Found bash at: {}", bashPath);
×
1617
            return bashPath;
×
1618
          } else {
1619
            error("Found bash at: {} but it is not pointing to an existing file", bashPath);
×
1620
            return null;
×
1621
          }
1622
        } else {
1623
          info("No bash executable could be found in the Windows registry.");
×
1624
        }
1625
      }
1626
    }
1627
    // no bash found
1628
    return null;
×
1629
  }
1630

1631
  private Path findBashOnWindowsDefaultGitPath() {
1632
    // Check if Git Bash exists in the default location
1633
    trace("Trying to find bash on the Windows default git path.");
3✔
1634
    Path defaultPath = Path.of(getDefaultWindowsGitPath());
6✔
1635
    if (!defaultPath.toString().isEmpty() && Files.exists(defaultPath)) {
4!
1636
      trace("Found default path to git bash on Windows at: {}", getDefaultWindowsGitPath());
×
1637
      return defaultPath;
×
1638
    }
1639
    debug("No bash was found on the Windows default git path.");
3✔
1640
    return null;
2✔
1641
  }
1642

1643
  @Override
1644
  public WindowsPathSyntax getPathSyntax() {
1645

1646
    return this.pathSyntax;
3✔
1647
  }
1648

1649
  /**
1650
   * @param pathSyntax new value of {@link #getPathSyntax()}.
1651
   */
1652
  public void setPathSyntax(WindowsPathSyntax pathSyntax) {
1653

1654
    this.pathSyntax = pathSyntax;
3✔
1655
  }
1✔
1656

1657
  /**
1658
   * @return the {@link IdeStartContextImpl}.
1659
   */
1660
  public IdeStartContextImpl getStartContext() {
1661

1662
    return startContext;
3✔
1663
  }
1664

1665
  /**
1666
   * @return the {@link WindowsHelper}.
1667
   */
1668
  public final WindowsHelper getWindowsHelper() {
1669

1670
    if (this.windowsHelper == null) {
3✔
1671
      this.windowsHelper = createWindowsHelper();
4✔
1672
    }
1673
    return this.windowsHelper;
3✔
1674
  }
1675

1676
  /**
1677
   * @return the new {@link WindowsHelper} instance.
1678
   */
1679
  protected WindowsHelper createWindowsHelper() {
1680

1681
    return new WindowsHelperImpl(this);
×
1682
  }
1683

1684
  /**
1685
   * Reloads this context and re-initializes the {@link #getVariables() variables}.
1686
   */
1687
  public void reload() {
1688

1689
    this.variables = null;
3✔
1690
    this.customToolRepository = null;
3✔
1691
  }
1✔
1692

1693
  @Override
1694
  public void writeVersionFile(VersionIdentifier version, Path installationPath) {
1695

1696
    assert (Files.isDirectory(installationPath));
6!
1697
    Path versionFile = installationPath.resolve(FILE_SOFTWARE_VERSION);
4✔
1698
    getFileAccess().writeFileContent(version.toString(), versionFile);
6✔
1699
  }
1✔
1700

1701
  /*
1702
   * @param home the IDE_HOME directory.
1703
   * @param workspace the name of the active workspace folder.
1704
   */
1705
  protected static record IdeHomeAndWorkspace(Path home, String workspace) {
9✔
1706

1707
  }
1708

1709
  /**
1710
   * Returns the default git path on Windows. Required to be overwritten in tests.
1711
   *
1712
   * @return default path to git on Windows.
1713
   */
1714
  public String getDefaultWindowsGitPath() {
1715
    return DEFAULT_WINDOWS_GIT_PATH;
×
1716
  }
1717

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

© 2026 Coveralls, Inc