Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Close #32 Searching for a file on the system path #111

Open
wants to merge 6 commits into
base: release-1.7
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions MedallionShell.Tests/GeneralTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ namespace Medallion.Shell.Tests

public class GeneralTest
{
[Test]
public void TestCommandWithoutFullyQualifiedPath()
{
Assert.That(TestShell.Run("git", "--version").StandardOutput.ReadToEnd(), Does.StartWith("git version"));
Bartleby2718 marked this conversation as resolved.
Show resolved Hide resolved
}

[Test]
public void TestGrep()
{
Expand Down
49 changes: 49 additions & 0 deletions MedallionShell.Tests/SystemPathSearcherTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using NUnit.Framework;

namespace Medallion.Shell.Tests;

public class SystemPathSearcherTest
{
[Platform("Win", Reason = "Tests Windows-specific executables")]
[TestCase("dotnet", @"C:\Program Files\dotnet\dotnet.exe")]
[TestCase("dotnet.exe", @"C:\Program Files\dotnet\dotnet.exe")]
[TestCase("where.exe", @"C:\Windows\System32\where.exe")]
[TestCase("cmd", @"C:\Windows\System32\cmd.exe")]
[TestCase("cmd.exe", @"C:\Windows\System32\cmd.exe")]
[TestCase("powershell.exe", @"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe")]
[TestCase("explorer.exe", @"C:\Windows\explorer.exe")]
[TestCase("git.exe", @"C:\Program Files\Git\cmd\git.exe")]
[TestCase("does.not.exist", null)]
// echo is not a program on Windows but an internal command in cmd.exe or powershell.exe.
// However, things like git may still install echo (e.g. C:\Program Files\Git\usr\bin\echo.EXE)
// so there's no guarantee for echo on Windows.
public void TestGetFullPathOnWindows(string executable, string? expected)
{
StringAssert.AreEqualIgnoringCase(expected, SystemPathSearcher.GetFullPathUsingSystemPathOrDefault(executable));

var command = Command.Run("where", executable);
command.StandardOutput.ReadToEnd().Trim().ShouldEqual(
expected ?? string.Empty,
$"Exit code: {command.Result.ExitCode}, StdErr: '{command.Result.StandardError}'");
}

[Platform("Unix", Reason = "Tests Unix-specific executables")]
[TestCase("dotnet", "/usr/bin/dotnet")]
[TestCase("which", "/usr/bin/which")]
[TestCase("head", "/usr/bin/head")]
[TestCase("sh", "/bin/sh")]
[TestCase("ls", "/bin/ls")]
[TestCase("grep", "/bin/grep")]
[TestCase("sleep", "/bin/sleep")]
[TestCase("echo", "/bin/echo")]
[TestCase("does.not.exist", null)]
public void TestGetFullPathOnLinux(string executable, string? expected)
{
SystemPathSearcher.GetFullPathUsingSystemPathOrDefault(executable).ShouldEqual(expected);

var command = Command.Run("which", executable);
command.StandardOutput.ReadToEnd().Trim().ShouldEqual(
expected ?? string.Empty,
$"Exit code: {command.Result.ExitCode}, StdErr: '{command.Result.StandardError}'");
}
}
20 changes: 19 additions & 1 deletion MedallionShell/Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -38,10 +39,15 @@ public Command Run(string executable, IEnumerable<object>? arguments = null, Act

var finalOptions = this.GetOptions(options);

var executablePath = finalOptions.SearchOnSystemPath
&& SystemPathSearcher.GetFullPathUsingSystemPathOrDefault(executable) is { } fullPath
? fullPath
: executable;

var processStartInfo = new ProcessStartInfo
{
CreateNoWindow = true,
FileName = executable,
FileName = executablePath,
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
Expand Down Expand Up @@ -201,6 +207,7 @@ internal Options()
internal CommandLineSyntax CommandLineSyntax { get; private set; } = default!; // assigned in RestoreDefaults
internal bool ThrowExceptionOnError { get; private set; }
internal bool DisposeProcessOnExit { get; private set; }
internal bool SearchOnSystemPath { get; private set; }
internal TimeSpan ProcessTimeout { get; private set; }
internal Encoding? ProcessStreamEncoding { get; private set; }
internal CancellationToken ProcessCancellationToken { get; private set; }
Expand Down Expand Up @@ -318,6 +325,17 @@ public Options DisposeOnExit(bool value = true)
return this;
}

/// <summary>
/// If specified, the underlying <see cref="Process"/> will search for the system path, like a shell would.
///
/// Defaults to false
/// </summary>
public Options SearchSystemPath(bool value = false)
{
this.SearchOnSystemPath = value;
return this;
}

/// <summary>
/// Specifies the <see cref="CommandLineSyntax"/> to use for escaping arguments. Defaults to
/// an appropriate value for the current platform
Expand Down
24 changes: 24 additions & 0 deletions MedallionShell/SystemPathSearcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;

namespace Medallion.Shell;

internal static class SystemPathSearcher
{
public static string? GetFullPathUsingSystemPathOrDefault(string executable)
{
if (executable.Contains(Path.DirectorySeparatorChar)) { return null; }
if (Environment.GetEnvironmentVariable("PATH") is not { } pathEnvironmentVariable) { return null; }

var paths = pathEnvironmentVariable.Split(Path.PathSeparator);
var pathExtensions = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
&& Environment.GetEnvironmentVariable("PATHEXT") is { } pathTextEnvironmentVariable
? [.. pathTextEnvironmentVariable.Split(Path.PathSeparator), string.Empty]
: new[] { string.Empty };

return paths.SelectMany(path => pathExtensions.Select(pathExtension => Path.Combine(path, executable + pathExtension)))
.FirstOrDefault(File.Exists);
}
}