Skip to content

Commit 8009749

Browse files
authored
Merge pull request #717 from mjcheetham/which-location
Manually scan `$PATH` on POSIX systems
2 parents 1dfde3c + e62b8df commit 8009749

File tree

4 files changed

+114
-80
lines changed

4 files changed

+114
-80
lines changed

src/shared/Core.Tests/EnvironmentTests.cs

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System;
12
using System.Collections.Generic;
3+
using GitCredentialManager.Interop.Posix;
24
using GitCredentialManager.Interop.Windows;
35
using GitCredentialManager.Tests.Objects;
46
using Xunit;
@@ -7,62 +9,116 @@ namespace GitCredentialManager.Tests
79
{
810
public class EnvironmentTests
911
{
12+
private const string WindowsPathVar = @"C:\Users\john.doe\bin;C:\Windows\system32;C:\Windows";
13+
private const string WindowsExecName = "foo.exe";
14+
private const string PosixPathVar = "/home/john.doe/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
15+
private const string PosixExecName = "foo";
16+
1017
[PlatformFact(Platforms.Windows)]
1118
public void WindowsEnvironment_TryLocateExecutable_NotExists_ReturnFalse()
1219
{
13-
string pathVar = @"C:\Users\john.doe\bin;C:\Windows\system32;C:\Windows";
14-
string execName = "foo.exe";
1520
var fs = new TestFileSystem();
16-
var envars = new Dictionary<string, string> {["PATH"] = pathVar};
21+
var envars = new Dictionary<string, string> {["PATH"] = WindowsPathVar};
1722
var env = new WindowsEnvironment(fs, envars);
1823

19-
bool actualResult = env.TryLocateExecutable(execName, out string actualPath);
24+
bool actualResult = env.TryLocateExecutable(WindowsExecName, out string actualPath);
2025

2126
Assert.False(actualResult);
2227
Assert.Null(actualPath);
2328
}
2429

2530
[PlatformFact(Platforms.Windows)]
26-
public void WindowsEnvironment_TryLocateExecutable_Windows_Exists_ReturnTrueAndPath()
31+
public void WindowsEnvironment_TryLocateExecutable_Exists_ReturnTrueAndPath()
2732
{
28-
string pathVar = @"C:\Users\john.doe\bin;C:\Windows\system32;C:\Windows";
29-
string execName = "foo.exe";
3033
string expectedPath = @"C:\Windows\system32\foo.exe";
3134
var fs = new TestFileSystem
3235
{
3336
Files = new Dictionary<string, byte[]>
3437
{
35-
[@"C:\Windows\system32\foo.exe"] = new byte[0],
38+
[expectedPath] = Array.Empty<byte>()
3639
}
3740
};
38-
var envars = new Dictionary<string, string> {["PATH"] = pathVar};
41+
var envars = new Dictionary<string, string> {["PATH"] = WindowsPathVar};
3942
var env = new WindowsEnvironment(fs, envars);
4043

41-
bool actualResult = env.TryLocateExecutable(execName, out string actualPath);
44+
bool actualResult = env.TryLocateExecutable(WindowsExecName, out string actualPath);
4245

4346
Assert.True(actualResult);
4447
Assert.Equal(expectedPath, actualPath);
4548
}
4649

4750
[PlatformFact(Platforms.Windows)]
48-
public void WindowsEnvironment_TryLocateExecutable_Windows_ExistsMultiple_ReturnTrueAndFirstPath()
51+
public void WindowsEnvironment_TryLocateExecutable_ExistsMultiple_ReturnTrueAndFirstPath()
4952
{
50-
string pathVar = @"C:\Users\john.doe\bin;C:\Windows\system32;C:\Windows";
51-
string execName = "foo.exe";
5253
string expectedPath = @"C:\Users\john.doe\bin\foo.exe";
5354
var fs = new TestFileSystem
5455
{
5556
Files = new Dictionary<string, byte[]>
5657
{
57-
[@"C:\Users\john.doe\bin\foo.exe"] = new byte[0],
58-
[@"C:\Windows\system32\foo.exe"] = new byte[0],
59-
[@"C:\Windows\foo.exe"] = new byte[0],
58+
[expectedPath] = Array.Empty<byte>(),
59+
[@"C:\Windows\system32\foo.exe"] = Array.Empty<byte>(),
60+
[@"C:\Windows\foo.exe"] = Array.Empty<byte>(),
6061
}
6162
};
62-
var envars = new Dictionary<string, string> {["PATH"] = pathVar};
63+
var envars = new Dictionary<string, string> {["PATH"] = WindowsPathVar};
6364
var env = new WindowsEnvironment(fs, envars);
6465

65-
bool actualResult = env.TryLocateExecutable(execName, out string actualPath);
66+
bool actualResult = env.TryLocateExecutable(WindowsExecName, out string actualPath);
67+
68+
Assert.True(actualResult);
69+
Assert.Equal(expectedPath, actualPath);
70+
}
71+
72+
[PlatformFact(Platforms.Posix)]
73+
public void PosixEnvironment_TryLocateExecutable_NotExists_ReturnFalse()
74+
{
75+
var fs = new TestFileSystem();
76+
var envars = new Dictionary<string, string> {["PATH"] = PosixPathVar};
77+
var env = new PosixEnvironment(fs, envars);
78+
79+
bool actualResult = env.TryLocateExecutable(PosixExecName, out string actualPath);
80+
81+
Assert.False(actualResult);
82+
Assert.Null(actualPath);
83+
}
84+
85+
[PlatformFact(Platforms.Posix)]
86+
public void PosixEnvironment_TryLocateExecutable_Exists_ReturnTrueAndPath()
87+
{
88+
string expectedPath = "/usr/local/bin/foo";
89+
var fs = new TestFileSystem
90+
{
91+
Files = new Dictionary<string, byte[]>
92+
{
93+
[expectedPath] = Array.Empty<byte>(),
94+
}
95+
};
96+
var envars = new Dictionary<string, string> {["PATH"] = PosixPathVar};
97+
var env = new PosixEnvironment(fs, envars);
98+
99+
bool actualResult = env.TryLocateExecutable(PosixExecName, out string actualPath);
100+
101+
Assert.True(actualResult);
102+
Assert.Equal(expectedPath, actualPath);
103+
}
104+
105+
[PlatformFact(Platforms.Posix)]
106+
public void PosixEnvironment_TryLocateExecutable_ExistsMultiple_ReturnTrueAndFirstPath()
107+
{
108+
string expectedPath = "/home/john.doe/bin/foo";
109+
var fs = new TestFileSystem
110+
{
111+
Files = new Dictionary<string, byte[]>
112+
{
113+
[expectedPath] = Array.Empty<byte>(),
114+
["/usr/local/bin/foo"] = Array.Empty<byte>(),
115+
["/bin/foo"] = Array.Empty<byte>(),
116+
}
117+
};
118+
var envars = new Dictionary<string, string> {["PATH"] = PosixPathVar};
119+
var env = new PosixEnvironment(fs, envars);
120+
121+
bool actualResult = env.TryLocateExecutable(PosixExecName, out string actualPath);
66122

67123
Assert.True(actualResult);
68124
Assert.Equal(expectedPath, actualPath);

src/shared/Core/EnvironmentBase.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4+
using System.IO;
45
using System.Linq;
56

67
namespace GitCredentialManager
@@ -89,8 +90,6 @@ public bool IsDirectoryOnPath(string directoryPath)
8990

9091
protected abstract string[] SplitPathVariable(string value);
9192

92-
public abstract bool TryLocateExecutable(string program, out string path);
93-
9493
public virtual Process CreateProcess(string path, string args, bool useShellExecute, string workingDirectory)
9594
{
9695
var psi = new ProcessStartInfo(path, args)
@@ -104,6 +103,38 @@ public virtual Process CreateProcess(string path, string args, bool useShellExec
104103

105104
return new Process { StartInfo = psi };
106105
}
106+
107+
public bool TryLocateExecutable(string program, out string path)
108+
{
109+
// On UNIX-like systems we would normally use the "which" utility to locate a program,
110+
// but since distributions don't always place "which" in a consistent location we cannot
111+
// find it! Oh the irony..
112+
// We could also try using "env" to then locate "which", but the same problem exists in
113+
// that "env" isn't always in a standard location.
114+
//
115+
// On Windows we should avoid using the equivalent utility "where.exe" because this will
116+
// include the current working directory in the search, and we don't want this.
117+
//
118+
// The upshot of the above means we cannot use either of "which" or "where.exe" and must
119+
// instead manually scan the PATH variable looking for the program.
120+
// At least both Windows and UNIX use the same name for the $PATH or %PATH% variable!
121+
if (Variables.TryGetValue("PATH", out string pathValue))
122+
{
123+
string[] paths = SplitPathVariable(pathValue);
124+
foreach (var basePath in paths)
125+
{
126+
string candidatePath = Path.Combine(basePath, program);
127+
if (FileSystem.FileExists(candidatePath))
128+
{
129+
path = candidatePath;
130+
return true;
131+
}
132+
}
133+
}
134+
135+
path = null;
136+
return false;
137+
}
107138
}
108139

109140
public static class EnvironmentExtensions

src/shared/Core/Interop/Posix/PosixEnvironment.cs

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Diagnostics;
4-
using System.Linq;
53

64
namespace GitCredentialManager.Interop.Posix
75
{
86
public class PosixEnvironment : EnvironmentBase
97
{
10-
public PosixEnvironment(IFileSystem fileSystem) : base(fileSystem)
8+
public PosixEnvironment(IFileSystem fileSystem)
9+
: this(fileSystem, GetCurrentVariables()) { }
10+
11+
internal PosixEnvironment(IFileSystem fileSystem, IReadOnlyDictionary<string, string> variables)
12+
: base(fileSystem)
1113
{
12-
Variables = GetCurrentVariables();
14+
EnsureArgument.NotNull(variables, nameof(variables));
15+
Variables = variables;
1316
}
1417

1518
#region EnvironmentBase
@@ -29,40 +32,6 @@ protected override string[] SplitPathVariable(string value)
2932
return value.Split(':');
3033
}
3134

32-
public override bool TryLocateExecutable(string program, out string path)
33-
{
34-
// The "which" utility scans over the PATH and does not include the current working directory
35-
// (unlike the equivalent "where.exe" on Windows), which is exactly what we want. Let's use it.
36-
const string whichPath = "/usr/bin/which";
37-
var psi = new ProcessStartInfo(whichPath, program)
38-
{
39-
UseShellExecute = false,
40-
RedirectStandardOutput = true
41-
};
42-
43-
using (var where = new Process {StartInfo = psi})
44-
{
45-
where.Start();
46-
where.WaitForExit();
47-
48-
switch (where.ExitCode)
49-
{
50-
case 0: // found
51-
string stdout = where.StandardOutput.ReadToEnd();
52-
string[] results = stdout.Split(new[] {'\n'}, StringSplitOptions.RemoveEmptyEntries);
53-
path = results.First();
54-
return true;
55-
56-
case 1: // not found
57-
path = null;
58-
return false;
59-
60-
default:
61-
throw new Exception($"Unknown error locating '{program}' using {whichPath}. Exit code: {where.ExitCode}.");
62-
}
63-
}
64-
}
65-
6635
#endregion
6736

6837
private static IReadOnlyDictionary<string, string> GetCurrentVariables()

src/shared/Core/Interop/Windows/WindowsEnvironment.cs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -67,28 +67,6 @@ public override void RemoveDirectoryFromPath(string directoryPath, EnvironmentVa
6767
}
6868
}
6969

70-
public override bool TryLocateExecutable(string program, out string path)
71-
{
72-
// Don't use "where.exe" on Windows as this includes the current working directory
73-
// and we don't want to enumerate this location; only the PATH.
74-
if (Variables.TryGetValue("PATH", out string pathValue))
75-
{
76-
string[] paths = SplitPathVariable(pathValue);
77-
foreach (var basePath in paths)
78-
{
79-
string candidatePath = Path.Combine(basePath, program);
80-
if (FileSystem.FileExists(candidatePath))
81-
{
82-
path = candidatePath;
83-
return true;
84-
}
85-
}
86-
}
87-
88-
path = null;
89-
return false;
90-
}
91-
9270
public override Process CreateProcess(string path, string args, bool useShellExecute, string workingDirectory)
9371
{
9472
// If we're asked to start a WSL executable we must launch via the wsl.exe command tool

0 commit comments

Comments
 (0)