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

devonfw / IDEasy / 28396032515

29 Jun 2026 07:05PM UTC coverage: 71.357% (+0.01%) from 71.347%
28396032515

Pull #2089

github

web-flow
Merge d95a35902 into d0ad3d852
Pull Request #2089: #2088: skip malformed PATH entries instead of failing with InvalidPathException

4700 of 7286 branches covered (64.51%)

Branch coverage included in aggregate %.

12116 of 16280 relevant lines covered (74.42%)

3.15 hits per line

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

89.36
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.InvalidPathException;
7
import java.nio.file.LinkOption;
8
import java.nio.file.Path;
9
import java.util.ArrayList;
10
import java.util.Collections;
11
import java.util.HashMap;
12
import java.util.Iterator;
13
import java.util.List;
14
import java.util.Map;
15
import java.util.Objects;
16
import java.util.function.Predicate;
17
import java.util.regex.Pattern;
18
import java.util.stream.Stream;
19

20
import org.slf4j.Logger;
21
import org.slf4j.LoggerFactory;
22

23
import com.devonfw.tools.ide.context.IdeContext;
24
import com.devonfw.tools.ide.os.SystemInfoImpl;
25
import com.devonfw.tools.ide.os.WindowsPathSyntax;
26
import com.devonfw.tools.ide.variable.IdeVariables;
27

28
/**
29
 * 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
30
 * 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
31
 * 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
32
 * 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
33
 * construction time and are recomputed from the "software" folder. This is important as the initial {@link System#getenv(String) environment} PATH entries can
34
 * 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
35
 * projects. However, it also will ensure all the entries to IDEasy locations are automatically managed and therefore cannot be managed manually be the
36
 * end-user.
37
 */
38
public class SystemPath {
39

40
  private static final Logger LOG = LoggerFactory.getLogger(SystemPath.class);
3✔
41

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

44
  private final char pathSeparator;
45

46
  private final Map<String, Path> tool2pathMap;
47

48
  private final List<Path> paths;
49

50
  private final List<Path> extraPathEntries;
51

52
  private final IdeContext context;
53

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

56
  /**
57
   * The constructor.
58
   *
59
   * @param context {@link IdeContext}.
60
   */
61
  public SystemPath(IdeContext context) {
62

63
    this(context, System.getenv(IdeVariables.PATH.getName()));
×
64
  }
×
65

66
  /**
67
   * The constructor.
68
   *
69
   * @param context {@link IdeContext}.
70
   * @param envPath the value of the PATH variable.
71
   */
72
  public SystemPath(IdeContext context, String envPath) {
73

74
    this(context, envPath, context.getIdeRoot(), context.getSoftwarePath());
8✔
75
  }
1✔
76

77
  /**
78
   * The constructor.
79
   *
80
   * @param context {@link IdeContext} for the output of information.
81
   * @param envPath the value of the PATH variable.
82
   * @param ideRoot the {@link IdeContext#getIdeRoot() IDE_ROOT}.
83
   * @param softwarePath the {@link IdeContext#getSoftwarePath() software path}.
84
   */
85
  public SystemPath(IdeContext context, String envPath, Path ideRoot, Path softwarePath) {
86

87
    this(context, envPath, ideRoot, softwarePath, File.pathSeparatorChar, Collections.emptyList());
8✔
88
  }
1✔
89

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

102
    this(context, pathSeparator, extraPathEntries, new HashMap<>(), new ArrayList<>());
11✔
103
    String[] envPaths = envPath.split(Character.toString(pathSeparator));
5✔
104
    for (String segment : envPaths) {
16✔
105
      Path path;
106
      try {
107
        path = Path.of(segment);
5✔
108
      } catch (InvalidPathException e) {
1✔
109
        LOG.warn("Ignoring invalid PATH entry '{}' - {}", segment, e.getMessage());
6✔
110
        continue;
1✔
111
      }
1✔
112
      String tool = getTool(path, ideRoot);
4✔
113
      if (tool == null) {
2!
114
        this.paths.add(path);
5✔
115
      }
116
    }
117
    collectToolPath(softwarePath);
3✔
118
  }
1✔
119

120
  /**
121
   * @param context {@link IdeContext} for the output of information.
122
   * @param softwarePath the {@link IdeContext#getSoftwarePath() software path}.
123
   * @param pathSeparator the path separator char (';' for Windows and ':' otherwise).
124
   * @param paths the {@link List} of {@link Path}s to use in the PATH environment variable.
125
   */
126
  public SystemPath(IdeContext context, Path softwarePath, char pathSeparator, List<Path> paths) {
127
    this(context, pathSeparator, new ArrayList<>(), new HashMap<>(), paths);
11✔
128
    collectToolPath(softwarePath);
3✔
129
  }
1✔
130

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

133
    super();
2✔
134
    this.context = context;
3✔
135
    this.pathSeparator = pathSeparator;
3✔
136
    this.extraPathEntries = extraPathEntries;
3✔
137
    this.tool2pathMap = tool2PathMap;
3✔
138
    this.paths = paths;
3✔
139
  }
1✔
140

141
  private void collectToolPath(Path softwarePath) {
142

143
    if (softwarePath == null) {
2✔
144
      return;
1✔
145
    }
146
    if (Files.isDirectory(softwarePath)) {
5✔
147
      try (Stream<Path> children = Files.list(softwarePath)) {
3✔
148
        Iterator<Path> iterator = children.iterator();
3✔
149
        while (iterator.hasNext()) {
3✔
150
          Path child = iterator.next();
4✔
151
          String tool = child.getFileName().toString();
4✔
152
          if (!"extra".equals(tool) && Files.isDirectory(child)) {
9!
153
            Path toolPath = child;
2✔
154
            Path bin = child.resolve("bin");
4✔
155
            if (Files.isDirectory(bin)) {
5✔
156
              toolPath = bin;
2✔
157
            }
158
            this.tool2pathMap.put(tool, toolPath);
6✔
159
          }
160
        }
1✔
161
      } catch (IOException e) {
×
162
        throw new IllegalStateException("Failed to list children of " + softwarePath, e);
×
163
      }
1✔
164
    }
165
  }
1✔
166

167
  private static String getTool(Path path, Path ideRoot) {
168

169
    if (ideRoot == null) {
2✔
170
      return null;
2✔
171
    }
172
    if (path.startsWith(ideRoot)) {
4✔
173
      Path relativized = ideRoot.relativize(path);
4✔
174
      int count = relativized.getNameCount();
3✔
175
      if (count >= 3) {
3✔
176
        if (relativized.getName(1).toString().equals("software")) {
7!
177
          return relativized.getName(2).toString();
×
178
        }
179
      }
180
    }
181
    return null;
2✔
182
  }
183

184
  private Path findBinaryInOrder(Path path, String tool) {
185

186
    List<String> extensionPriority = List.of("");
3✔
187
    if (this.context.getSystemInfo().isWindows() || SystemInfoImpl.INSTANCE.isWindows()) {
8!
188
      extensionPriority = EXTENSION_PRIORITY;
2✔
189
    }
190
    for (String extension : extensionPriority) {
10✔
191

192
      Path fileToExecute = path.resolve(tool + extension);
6✔
193

194
      if (Files.exists(fileToExecute, LinkOption.NOFOLLOW_LINKS)) {
9✔
195
        return fileToExecute;
2✔
196
      }
197
    }
1✔
198

199
    return null;
2✔
200
  }
201

202
  /**
203
   * @param binaryName the name of the tool.
204
   * @return {@code true} if the given {@code tool} is a binary that can be found on the PATH, {@code false} otherwise.
205
   */
206
  public boolean hasBinaryOnPath(String binaryName) {
207
    Path binary = Path.of(binaryName);
×
208
    Path resolvedBinary = findBinary(binary);
×
209
    return (resolvedBinary != binary);
×
210
  }
211

212
  /**
213
   * @param binaryName the name of the tool.
214
   * @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
215
   *     was not found on PATH, the same {@link Path} instance is returned that was given as argument.
216
   */
217
  public Path findBinaryPathByName(String binaryName) {
218
    return findBinary(Path.of(binaryName));
×
219
  }
220

221
  /**
222
   * @param toolPath the {@link Path} to the tool installation.
223
   * @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
224
   *     was not found on PATH, the same {@link Path} instance is returned that was given as argument.
225
   */
226
  public Path findBinary(Path toolPath) {
227
    return findBinary(toolPath, p -> true);
7✔
228
  }
229

230
  /**
231
   * Finds a binary for {@code toolPath} but allows excluding candidates via {@code filter}. If the filter rejects a candidate, the search continues. If no
232
   * acceptable candidate is found, the original {@code toolPath} is returned unchanged.
233
   *
234
   * @param toolPath the {@link Path} to the tool installation or a simple name (e.g., "git").
235
   * @param filter a predicate that must return {@code true} for acceptable candidates; {@code false} rejects and continues searching.
236
   * @return the first acceptable {@link Path} found; if none, the original {@code toolPath}.
237
   */
238
  public Path findBinary(Path toolPath, Predicate<Path> filter) {
239
    Objects.requireNonNull(toolPath, "toolPath");
4✔
240
    Objects.requireNonNull(filter, "filter");
4✔
241

242
    Path parent = toolPath.getParent();
3✔
243
    String fileName = toolPath.getFileName().toString();
4✔
244

245
    if (parent == null) {
2✔
246
      for (Path path : this.tool2pathMap.values()) {
12✔
247
        Path binaryPath = findBinaryInOrder(path, fileName);
5✔
248
        if (binaryPath != null && filter.test(binaryPath)) {
6✔
249
          return binaryPath;
2✔
250
        }
251
      }
1✔
252
      for (Path path : this.paths) {
11✔
253
        Path binaryPath = findBinaryInOrder(path, fileName);
5✔
254
        if (binaryPath != null && filter.test(binaryPath)) {
6✔
255
          return binaryPath;
2✔
256
        }
257
      }
2✔
258
    } else {
259
      Path binaryPath = findBinaryInOrder(parent, fileName);
5✔
260
      if (binaryPath != null && filter.test(binaryPath)) {
6!
261
        return binaryPath;
2✔
262
      }
263
    }
264

265
    return toolPath;
2✔
266
  }
267

268
  /**
269
   * @param tool the name of the tool.
270
   * @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.
271
   */
272
  public Path getPath(String tool) {
273

274
    return this.tool2pathMap.get(tool);
6✔
275
  }
276

277
  /**
278
   * @param tool the name of the tool.
279
   * @param path the new {@link #getPath(String) tool bin path}.
280
   */
281
  public void setPath(String tool, Path path) {
282

283
    this.tool2pathMap.put(tool, path);
6✔
284
  }
1✔
285

286
  @Override
287
  public String toString() {
288

289
    return toString(null);
4✔
290
  }
291

292
  /**
293
   * @param pathSyntax the {@link WindowsPathSyntax} to convert to.
294
   * @return this {@link SystemPath} as {@link String} for the PATH environment variable.
295
   */
296
  public String toString(WindowsPathSyntax pathSyntax) {
297

298
    char separator;
299
    if (pathSyntax == WindowsPathSyntax.MSYS) {
3!
300
      separator = ':';
×
301
    } else {
302
      separator = this.pathSeparator;
3✔
303
    }
304
    StringBuilder sb = new StringBuilder(128);
5✔
305
    for (Path path : this.extraPathEntries) {
11✔
306
      appendPath(path, sb, separator, pathSyntax);
5✔
307
    }
1✔
308
    for (Path path : this.tool2pathMap.values()) {
12✔
309
      appendPath(path, sb, separator, pathSyntax);
5✔
310
    }
1✔
311
    for (Path path : this.paths) {
11✔
312
      appendPath(path, sb, separator, pathSyntax);
5✔
313
    }
1✔
314
    return sb.toString();
3✔
315
  }
316

317
  /**
318
   * Derive a new {@link SystemPath} from this instance with the given parameters.
319
   *
320
   * @param overriddenPath the entire PATH to override and replace the current one from this {@link SystemPath} or {@code null} to keep the current PATH.
321
   * @param extraPathEntries the {@link List} of additional PATH entries to add to the beginning of the PATH. May be empty to add nothing.
322
   * @return the new {@link SystemPath} derived from this instance with the given parameters applied.
323
   */
324
  public SystemPath withPath(String overriddenPath, List<Path> extraPathEntries) {
325

326
    if (overriddenPath == null) {
2!
327
      return new SystemPath(this.context, this.pathSeparator, extraPathEntries, this.tool2pathMap, this.paths);
13✔
328
    } else {
329
      return new SystemPath(this.context, overriddenPath, null, null, this.pathSeparator, extraPathEntries);
×
330
    }
331
  }
332

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

335
    if (sb.length() > 0) {
3✔
336
      sb.append(separator);
4✔
337
    }
338
    String pathString;
339
    if (pathSyntax == null) {
2✔
340
      pathString = path.toString();
4✔
341
    } else {
342
      pathString = pathSyntax.format(path);
4✔
343
    }
344
    sb.append(pathString);
4✔
345
  }
1✔
346

347
  /**
348
   * Method to validate if a given path string is a Windows path or not
349
   *
350
   * @param pathString The string to check if it is a Windows path string.
351
   * @return {@code true} if it is a valid windows path string, else {@code false}.
352
   */
353
  public static boolean isValidWindowsPath(String pathString) {
354

355
    return REGEX_WINDOWS_PATH.matcher(pathString).matches();
5✔
356
  }
357
}
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