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

devonfw / IDEasy / 19640865660

24 Nov 2025 04:05PM UTC coverage: 69.024% (+0.1%) from 68.924%
19640865660

push

github

web-flow
#1561: Fix BASH_PATH not used properly (#1577)

Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>

3562 of 5651 branches covered (63.03%)

Branch coverage included in aggregate %.

9262 of 12928 relevant lines covered (71.64%)

3.15 hits per line

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

89.5
cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java
1
package com.devonfw.tools.ide.common;
2

3
import java.io.File;
4
import java.io.IOException;
5
import java.nio.file.Files;
6
import java.nio.file.LinkOption;
7
import java.nio.file.Path;
8
import java.util.ArrayList;
9
import java.util.Collections;
10
import java.util.HashMap;
11
import java.util.Iterator;
12
import java.util.List;
13
import java.util.Map;
14
import java.util.Objects;
15
import java.util.function.Predicate;
16
import java.util.regex.Pattern;
17
import java.util.stream.Stream;
18

19
import com.devonfw.tools.ide.context.IdeContext;
20
import com.devonfw.tools.ide.os.WindowsPathSyntax;
21
import com.devonfw.tools.ide.variable.IdeVariables;
22

23
/**
24
 * Represents the PATH variable in a structured way. The PATH contains the system path entries together with the entries for the IDEasy tools. The generic
25
 * system path entries are stored in a {@link List} ({@code paths}) and the tool entries are stored in a {@link Map} ({@code tool2pathMap}) as they can change
26
 * dynamically at runtime (e.g. if a new tool is installed). As the tools must have priority the actual PATH is build by first the entries for the tools and
27
 * then the generic entries from the system PATH. Such tool entries are ignored from the actual PATH of the {@link System#getenv(String) environment} at
28
 * construction time and are recomputed from the "software" folder. This is important as the initial {@link System#getenv(String) environment} PATH entries can
29
 * come from a different IDEasy project and the use may have changed projects before calling us again. Recomputing the PATH ensures side-effects from other
30
 * projects. However, it also will ensure all the entries to IDEasy locations are automatically managed and therefore cannot be managed manually be the
31
 * end-user.
32
 */
33
public class SystemPath {
34

35
  private static final Pattern REGEX_WINDOWS_PATH = Pattern.compile("([a-zA-Z]:)?(\\\\[a-zA-Z0-9\\s_.-]+)+\\\\?");
3✔
36

37
  private final char pathSeparator;
38

39
  private final Map<String, Path> tool2pathMap;
40

41
  private final List<Path> paths;
42

43
  private final List<Path> extraPathEntries;
44

45
  private final IdeContext context;
46

47
  private static final List<String> EXTENSION_PRIORITY = List.of(".exe", ".cmd", ".bat", ".msi", ".ps1", "");
9✔
48

49
  /**
50
   * The constructor.
51
   *
52
   * @param context {@link IdeContext}.
53
   */
54
  public SystemPath(IdeContext context) {
55

56
    this(context, System.getenv(IdeVariables.PATH.getName()));
×
57
  }
×
58

59
  /**
60
   * The constructor.
61
   *
62
   * @param context {@link IdeContext}.
63
   * @param envPath the value of the PATH variable.
64
   */
65
  public SystemPath(IdeContext context, String envPath) {
66

67
    this(context, envPath, context.getIdeRoot(), context.getSoftwarePath());
8✔
68
  }
1✔
69

70
  /**
71
   * The constructor.
72
   *
73
   * @param context {@link IdeContext} for the output of information.
74
   * @param envPath the value of the PATH variable.
75
   * @param ideRoot the {@link IdeContext#getIdeRoot() IDE_ROOT}.
76
   * @param softwarePath the {@link IdeContext#getSoftwarePath() software path}.
77
   */
78
  public SystemPath(IdeContext context, String envPath, Path ideRoot, Path softwarePath) {
79

80
    this(context, envPath, ideRoot, softwarePath, File.pathSeparatorChar, Collections.emptyList());
8✔
81
  }
1✔
82

83
  /**
84
   * The constructor.
85
   *
86
   * @param context {@link IdeContext} for the output of information.
87
   * @param envPath the value of the PATH variable.
88
   * @param ideRoot the {@link IdeContext#getIdeRoot() IDE_ROOT}.
89
   * @param softwarePath the {@link IdeContext#getSoftwarePath() software path}.
90
   * @param pathSeparator the path separator char (';' for Windows and ':' otherwise).
91
   * @param extraPathEntries the {@link List} of additional {@link Path}s to prepend.
92
   */
93
  public SystemPath(IdeContext context, String envPath, Path ideRoot, Path softwarePath, char pathSeparator, List<Path> extraPathEntries) {
94

95
    this(context, pathSeparator, extraPathEntries, new HashMap<>(), new ArrayList<>());
11✔
96
    String[] envPaths = envPath.split(Character.toString(pathSeparator));
5✔
97
    for (String segment : envPaths) {
16✔
98
      Path path = Path.of(segment);
5✔
99
      String tool = getTool(path, ideRoot);
4✔
100
      if (tool == null) {
2!
101
        this.paths.add(path);
5✔
102
      }
103
    }
104
    collectToolPath(softwarePath);
3✔
105
  }
1✔
106

107
  /**
108
   * @param context {@link IdeContext} for the output of information.
109
   * @param softwarePath the {@link IdeContext#getSoftwarePath() software path}.
110
   * @param pathSeparator the path separator char (';' for Windows and ':' otherwise).
111
   * @param paths the {@link List} of {@link Path}s to use in the PATH environment variable.
112
   */
113
  public SystemPath(IdeContext context, Path softwarePath, char pathSeparator, List<Path> paths) {
114
    this(context, pathSeparator, new ArrayList<>(), new HashMap<>(), paths);
11✔
115
    collectToolPath(softwarePath);
3✔
116
  }
1✔
117

118
  private SystemPath(IdeContext context, char pathSeparator, List<Path> extraPathEntries, Map<String, Path> tool2PathMap, List<Path> paths) {
119

120
    super();
2✔
121
    this.context = context;
3✔
122
    this.pathSeparator = pathSeparator;
3✔
123
    this.extraPathEntries = extraPathEntries;
3✔
124
    this.tool2pathMap = tool2PathMap;
3✔
125
    this.paths = paths;
3✔
126
  }
1✔
127

128
  private void collectToolPath(Path softwarePath) {
129

130
    if (softwarePath == null) {
2✔
131
      return;
1✔
132
    }
133
    if (Files.isDirectory(softwarePath)) {
5✔
134
      try (Stream<Path> children = Files.list(softwarePath)) {
3✔
135
        Iterator<Path> iterator = children.iterator();
3✔
136
        while (iterator.hasNext()) {
3✔
137
          Path child = iterator.next();
4✔
138
          String tool = child.getFileName().toString();
4✔
139
          if (!"extra".equals(tool) && Files.isDirectory(child)) {
9!
140
            Path toolPath = child;
2✔
141
            Path bin = child.resolve("bin");
4✔
142
            if (Files.isDirectory(bin)) {
5✔
143
              toolPath = bin;
2✔
144
            }
145
            this.tool2pathMap.put(tool, toolPath);
6✔
146
          }
147
        }
1✔
148
      } catch (IOException e) {
×
149
        throw new IllegalStateException("Failed to list children of " + softwarePath, e);
×
150
      }
1✔
151
    }
152
  }
1✔
153

154
  private static String getTool(Path path, Path ideRoot) {
155

156
    if (ideRoot == null) {
2✔
157
      return null;
2✔
158
    }
159
    if (path.startsWith(ideRoot)) {
4✔
160
      Path relativized = ideRoot.relativize(path);
4✔
161
      int count = relativized.getNameCount();
3✔
162
      if (count >= 3) {
3✔
163
        if (relativized.getName(1).toString().equals("software")) {
7!
164
          return relativized.getName(2).toString();
×
165
        }
166
      }
167
    }
168
    return null;
2✔
169
  }
170

171
  private Path findBinaryInOrder(Path path, String tool) {
172

173
    List<String> extensionPriority = List.of("");
3✔
174
    if (this.context.getSystemInfo().isWindows()) {
5✔
175
      extensionPriority = EXTENSION_PRIORITY;
2✔
176
    }
177
    for (String extension : extensionPriority) {
10✔
178

179
      Path fileToExecute = path.resolve(tool + extension);
6✔
180

181
      if (Files.exists(fileToExecute, LinkOption.NOFOLLOW_LINKS)) {
9✔
182
        return fileToExecute;
2✔
183
      }
184
    }
1✔
185

186
    return null;
2✔
187
  }
188

189
  /**
190
   * @param binaryName the name of the tool.
191
   * @return {@code true} if the given {@code tool} is a binary that can be found on the PATH, {@code false} otherwise.
192
   */
193
  public boolean hasBinaryOnPath(String binaryName) {
194
    Path binary = Path.of(binaryName);
×
195
    Path resolvedBinary = findBinary(binary);
×
196
    return (resolvedBinary != binary);
×
197
  }
198

199
  /**
200
   * @param binaryName the name of the tool.
201
   * @return the {@link Path} to the binary executable of the tool. E.g. if "mvn" is given then ".../software/mvn/bin/mvn" could be returned. If the executable
202
   *     was not found on PATH, the same {@link Path} instance is returned that was given as argument.
203
   */
204
  public Path findBinaryPathByName(String binaryName) {
205
    return findBinary(Path.of(binaryName));
×
206
  }
207

208
  /**
209
   * @param toolPath the {@link Path} to the tool installation.
210
   * @return the {@link Path} to the binary executable of the tool. E.g. if "mvn" is given then ".../software/mvn/bin/mvn" could be returned. If the executable
211
   *     was not found on PATH, the same {@link Path} instance is returned that was given as argument.
212
   */
213
  public Path findBinary(Path toolPath) {
214
    return findBinary(toolPath, p -> true);
7✔
215
  }
216

217
  /**
218
   * Finds a binary for {@code toolPath} but allows excluding candidates via {@code filter}. If the filter rejects a candidate, the search continues. If no
219
   * acceptable candidate is found, the original {@code toolPath} is returned unchanged.
220
   *
221
   * @param toolPath the {@link Path} to the tool installation or a simple name (e.g., "git").
222
   * @param filter a predicate that must return {@code true} for acceptable candidates; {@code false} rejects and continues searching.
223
   * @return the first acceptable {@link Path} found; if none, the original {@code toolPath}.
224
   */
225
  public Path findBinary(Path toolPath, Predicate<Path> filter) {
226
    Objects.requireNonNull(toolPath, "toolPath");
4✔
227
    Objects.requireNonNull(filter, "filter");
4✔
228

229
    Path parent = toolPath.getParent();
3✔
230
    String fileName = toolPath.getFileName().toString();
4✔
231

232
    if (parent == null) {
2✔
233
      for (Path path : this.tool2pathMap.values()) {
12✔
234
        Path binaryPath = findBinaryInOrder(path, fileName);
5✔
235
        if (binaryPath != null && filter.test(binaryPath)) {
6✔
236
          return binaryPath;
2✔
237
        }
238
      }
1✔
239
      for (Path path : this.paths) {
11✔
240
        Path binaryPath = findBinaryInOrder(path, fileName);
5✔
241
        if (binaryPath != null && filter.test(binaryPath)) {
6✔
242
          return binaryPath;
2✔
243
        }
244
      }
2✔
245
    } else {
246
      Path binaryPath = findBinaryInOrder(parent, fileName);
5✔
247
      if (binaryPath != null && filter.test(binaryPath)) {
6!
248
        return binaryPath;
2✔
249
      }
250
    }
251

252
    return toolPath;
2✔
253
  }
254

255
  /**
256
   * @param tool the name of the tool.
257
   * @return the {@link Path} to the directory of the tool where the binaries can be found or {@code null} if the tool is not installed.
258
   */
259
  public Path getPath(String tool) {
260

261
    return this.tool2pathMap.get(tool);
6✔
262
  }
263

264
  /**
265
   * @param tool the name of the tool.
266
   * @param path the new {@link #getPath(String) tool bin path}.
267
   */
268
  public void setPath(String tool, Path path) {
269

270
    this.tool2pathMap.put(tool, path);
6✔
271
  }
1✔
272

273
  @Override
274
  public String toString() {
275

276
    return toString(null);
4✔
277
  }
278

279
  /**
280
   * @param pathSyntax the {@link WindowsPathSyntax} to convert to.
281
   * @return this {@link SystemPath} as {@link String} for the PATH environment variable.
282
   */
283
  public String toString(WindowsPathSyntax pathSyntax) {
284

285
    char separator;
286
    if (pathSyntax == WindowsPathSyntax.MSYS) {
3!
287
      separator = ':';
×
288
    } else {
289
      separator = this.pathSeparator;
3✔
290
    }
291
    StringBuilder sb = new StringBuilder(128);
5✔
292
    for (Path path : this.extraPathEntries) {
11✔
293
      appendPath(path, sb, separator, pathSyntax);
5✔
294
    }
1✔
295
    for (Path path : this.tool2pathMap.values()) {
12✔
296
      appendPath(path, sb, separator, pathSyntax);
5✔
297
    }
1✔
298
    for (Path path : this.paths) {
11✔
299
      appendPath(path, sb, separator, pathSyntax);
5✔
300
    }
1✔
301
    return sb.toString();
3✔
302
  }
303

304
  /**
305
   * Derive a new {@link SystemPath} from this instance with the given parameters.
306
   *
307
   * @param overriddenPath the entire PATH to override and replace the current one from this {@link SystemPath} or {@code null} to keep the current PATH.
308
   * @param extraPathEntries the {@link List} of additional PATH entries to add to the beginning of the PATH. May be empty to add nothing.
309
   * @return the new {@link SystemPath} derived from this instance with the given parameters applied.
310
   */
311
  public SystemPath withPath(String overriddenPath, List<Path> extraPathEntries) {
312

313
    if (overriddenPath == null) {
2!
314
      return new SystemPath(this.context, this.pathSeparator, extraPathEntries, this.tool2pathMap, this.paths);
13✔
315
    } else {
316
      return new SystemPath(this.context, overriddenPath, null, null, this.pathSeparator, extraPathEntries);
×
317
    }
318
  }
319

320
  private static void appendPath(Path path, StringBuilder sb, char separator, WindowsPathSyntax pathSyntax) {
321

322
    if (sb.length() > 0) {
3✔
323
      sb.append(separator);
4✔
324
    }
325
    String pathString;
326
    if (pathSyntax == null) {
2✔
327
      pathString = path.toString();
4✔
328
    } else {
329
      pathString = pathSyntax.format(path);
4✔
330
    }
331
    sb.append(pathString);
4✔
332
  }
1✔
333

334
  /**
335
   * Method to validate if a given path string is a Windows path or not
336
   *
337
   * @param pathString The string to check if it is a Windows path string.
338
   * @return {@code true} if it is a valid windows path string, else {@code false}.
339
   */
340
  public static boolean isValidWindowsPath(String pathString) {
341

342
    return REGEX_WINDOWS_PATH.matcher(pathString).matches();
5✔
343
  }
344
}
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