Skip to content

Commit

Permalink
Magic ✨
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyrrrz committed Nov 21, 2023
1 parent 0e7d4ee commit 7801d48
Show file tree
Hide file tree
Showing 13 changed files with 296 additions and 0 deletions.
28 changes: 28 additions & 0 deletions CliWrap.Magic.Tests/CliWrap.Magic.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOsPlatform('Windows'))">$(TargetFrameworks);net48</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="CSharpier.MsBuild" Version="0.26.2" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="PolyShim" Version="1.8.0" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CliWrap.Magic\CliWrap.Magic.csproj" />
<ProjectReference Include="..\CliWrap.Tests.Dummy\CliWrap.Tests.Dummy.csproj" />
</ItemGroup>

</Project>
28 changes: 28 additions & 0 deletions CliWrap.Magic.Tests/MagicSpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
using static CliWrap.Magic.Tools;
using Dummy = CliWrap.Tests.Dummy;

namespace CliWrap.Magic.Tests;

public class MagicSpecs
{
[Fact(Timeout = 15000)]
public async Task I_can_execute_a_command_with_magic_and_get_the_stdout_and_stderr()
{
// Arrange
var cmd = Run(

Check failure on line 15 in CliWrap.Magic.Tests/MagicSpecs.cs

View workflow job for this annotation

GitHub Actions / main / test (ubuntu-latest)

I can execute a command with magic and get the stdout and stderr

Expected stdOut to be "Hello stdout and stderr", but it has unexpected whitespace at the end.

Check failure on line 15 in CliWrap.Magic.Tests/MagicSpecs.cs

View workflow job for this annotation

GitHub Actions / main / test (windows-latest)

I can execute a command with magic and get the stdout and stderr

Expected stdOut to be "Hello stdout and stderr", but it has unexpected whitespace at the end.

Check failure on line 15 in CliWrap.Magic.Tests/MagicSpecs.cs

View workflow job for this annotation

GitHub Actions / main / test (macos-latest)

I can execute a command with magic and get the stdout and stderr

Expected stdOut to be "Hello stdout and stderr", but it has unexpected whitespace at the end.
Dummy.Program.FilePath,
new[] { "echo", "Hello stdout and stderr", "--target", "all" }
);

// Act
var (exitCode, stdOut, stdErr) = await cmd;

// Assert
exitCode.Should().Be(0);
stdOut.Should().Be("Hello stdout and stderr");
stdErr.Should().Be("Hello stdout and stderr");
}
}
6 changes: 6 additions & 0 deletions CliWrap.Magic.Tests/xunit.runner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"appDomain": "denied",
"methodDisplayOptions": "all",
"methodDisplay": "method"
}
30 changes: 30 additions & 0 deletions CliWrap.Magic/CliWrap.Magic.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.0;net462</TargetFrameworks>
<IsPackable>true</IsPackable>
</PropertyGroup>

<PropertyGroup>
<Description>Extension for CliWrap that provides a shell-like experience for working with external command-line interfaces</Description>
<PackageProjectUrl>https://github.com/Tyrrrz/CliWrap/tree/master/CliWrap.Magic</PackageProjectUrl>
<PackageIcon>favicon.png</PackageIcon>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<None Include="../favicon.png" Pack="true" PackagePath="" Visible="false" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CliWrap\CliWrap.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="CSharpier.MsBuild" Version="0.26.2" PrivateAssets="all" />
<PackageReference Include="Contextual" Version="1.0.1" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />
<PackageReference Include="PolyShim" Version="1.8.0" PrivateAssets="all" />
</ItemGroup>

</Project>
31 changes: 31 additions & 0 deletions CliWrap.Magic/CliWrapExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Runtime.CompilerServices;
using CliWrap.Buffered;

namespace CliWrap.Magic;

/// <summary>
/// Extensions for <see cref="CliWrap" /> types.
/// </summary>
public static class CliWrapExtensions
{
/// <summary>
/// Deconstructs the result into its components.
/// </summary>
public static void Deconstruct(
this BufferedCommandResult result,
out int exitCode,
out string standardOutput,
out string standardError
)
{
exitCode = result.ExitCode;
standardOutput = result.StandardOutput;
standardError = result.StandardError;
}

/// <summary>
/// Executes the command with buffering and returns the awaiter for the result.
/// </summary>
public static TaskAwaiter<BufferedCommandResult> GetAwaiter(this Command command) =>
command.ExecuteBufferedAsync().GetAwaiter();
}
15 changes: 15 additions & 0 deletions CliWrap.Magic/Contexts/EnvironmentVariablesContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using Contextual;

namespace CliWrap.Magic.Contexts;

internal class EnvironmentVariablesContext : Context
{
public IReadOnlyDictionary<string, string?> Variables { get; }

public EnvironmentVariablesContext(IReadOnlyDictionary<string, string?> variables) =>
Variables = variables;

public EnvironmentVariablesContext()
: this(new Dictionary<string, string?>()) { }
}
14 changes: 14 additions & 0 deletions CliWrap.Magic/Contexts/StandardErrorPipeContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using Contextual;

namespace CliWrap.Magic.Contexts;

internal class StandardErrorPipeContext : Context
{
public PipeTarget Pipe { get; }

public StandardErrorPipeContext(PipeTarget pipe) => Pipe = pipe;

public StandardErrorPipeContext()
: this(PipeTarget.ToStream(Console.OpenStandardError())) { }
}
14 changes: 14 additions & 0 deletions CliWrap.Magic/Contexts/StandardInputPipeContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using Contextual;

namespace CliWrap.Magic.Contexts;

internal class StandardInputPipeContext : Context
{
public PipeSource Pipe { get; }

public StandardInputPipeContext(PipeSource pipe) => Pipe = pipe;

public StandardInputPipeContext()
: this(PipeSource.FromStream(Console.OpenStandardInput())) { }
}
14 changes: 14 additions & 0 deletions CliWrap.Magic/Contexts/StandardOutputPipeContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using Contextual;

namespace CliWrap.Magic.Contexts;

internal class StandardOutputPipeContext : Context
{
public PipeTarget Pipe { get; }

public StandardOutputPipeContext(PipeTarget pipe) => Pipe = pipe;

public StandardOutputPipeContext()
: this(PipeTarget.ToStream(Console.OpenStandardOutput())) { }
}
14 changes: 14 additions & 0 deletions CliWrap.Magic/Contexts/WorkingDirectoryContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.IO;
using Contextual;

namespace CliWrap.Magic.Contexts;

internal class WorkingDirectoryContext : Context
{
public string Path { get; }

public WorkingDirectoryContext(string path) => Path = path;

public WorkingDirectoryContext()
: this(Directory.GetCurrentDirectory()) { }
}
84 changes: 84 additions & 0 deletions CliWrap.Magic/Tools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using CliWrap.Magic.Contexts;
using Contextual;

namespace CliWrap.Magic;

/// <summary>
/// Utility methods for working with the shell environment.
/// </summary>
public static class Tools
{
/// <summary>
/// Creates a new command that targets the specified command-line executable, batch file, or script.
/// </summary>
public static Command Run(string targetFilePath) =>
Cli.Wrap(targetFilePath)
.WithWorkingDirectory(Context.Use<WorkingDirectoryContext>().Path)
.WithEnvironmentVariables(Context.Use<EnvironmentVariablesContext>().Variables)
.WithStandardInputPipe(Context.Use<StandardInputPipeContext>().Pipe)
.WithStandardOutputPipe(Context.Use<StandardOutputPipeContext>().Pipe)
.WithStandardErrorPipe(Context.Use<StandardErrorPipeContext>().Pipe);

/// <summary>
/// Creates a new command that targets the specified command-line executable, batch file, or script,
/// with the provided command-line arguments.
/// </summary>
public static Command Run(
string targetFilePath,
IEnumerable<string> arguments,
bool escape = true
) => Run(targetFilePath).WithArguments(arguments, escape);

/// <summary>
/// Changes the current working directory to the specified path.
/// </summary>
/// <remarks>
/// <para>
/// This method only affects commands created by <see cref="Run(string)" />.
/// It does not change the working directory of the current process.
/// </para>
/// <para>
/// In order to reset the working directory to its original value, dispose the returned object.
/// </para>
/// </remarks>
public static IDisposable ChangeDirectory(string workingDirPath) =>
Context.Provide(new WorkingDirectoryContext(workingDirPath));

/// <summary>
/// Gets the value of the specified environment variable.
/// </summary>
/// <remarks>
/// This method reads environment variables both from the context created by <see cref="Environment(string, string?)" />,
/// as well as from the current process.
/// </remarks>
public static string? Environment(string name) =>
Context.Use<EnvironmentVariablesContext>().Variables.TryGetValue(name, out var value)
? value
: System.Environment.GetEnvironmentVariable(name);

/// <summary>
/// Sets the value of the specified environment variable.
/// </summary>
/// <remarks>
/// <para>
/// This method only affects commands created by <see cref="Run(string)" />.
/// It does not change the environment variables of the current process.
/// </para>
/// <para>
/// In order to reset the environment variables to their original state, dispose the returned object.
/// </para>
/// </remarks>
public static IDisposable Environment(string name, string? value)
{
var variables = new Dictionary<string, string?>(StringComparer.Ordinal);

foreach (var (lastKey, lastValue) in Context.Use<EnvironmentVariablesContext>().Variables)
variables[lastKey] = lastValue;

variables[name] = value;

return Context.Provide(new EnvironmentVariablesContext(variables));
}
}
12 changes: 12 additions & 0 deletions CliWrap.sln
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliWrap.Benchmarks", "CliWr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliWrap.Signaler", "CliWrap.Signaler\CliWrap.Signaler.csproj", "{A0E41A11-D314-45C4-890B-831385450DF8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliWrap.Magic", "CliWrap.Magic\CliWrap.Magic.csproj", "{4759BBA0-54AF-438B-B858-FD550CB459D5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliWrap.Magic.Tests", "CliWrap.Magic.Tests\CliWrap.Magic.Tests.csproj", "{39060B45-181E-4160-ADCF-6CEC548C2CB8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -46,6 +50,14 @@ Global
{A0E41A11-D314-45C4-890B-831385450DF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0E41A11-D314-45C4-890B-831385450DF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0E41A11-D314-45C4-890B-831385450DF8}.Release|Any CPU.Build.0 = Release|Any CPU
{4759BBA0-54AF-438B-B858-FD550CB459D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4759BBA0-54AF-438B-B858-FD550CB459D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4759BBA0-54AF-438B-B858-FD550CB459D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4759BBA0-54AF-438B-B858-FD550CB459D5}.Release|Any CPU.Build.0 = Release|Any CPU
{39060B45-181E-4160-ADCF-6CEC548C2CB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39060B45-181E-4160-ADCF-6CEC548C2CB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39060B45-181E-4160-ADCF-6CEC548C2CB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39060B45-181E-4160-ADCF-6CEC548C2CB8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
6 changes: 6 additions & 0 deletions CliWrap/CommandResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,10 @@ public CommandResult(int exitCode, DateTimeOffset startTime, DateTimeOffset exit
StartTime = startTime;
ExitTime = exitTime;
}

/// <summary>
/// Converts the result to a boolean value indicating whether the command execution was successful.
/// </summary>
// This is only needed by CliWrap.Magic, but we can't implement this operator in another assembly
public static implicit operator bool(CommandResult result) => result.IsSuccess;
}

0 comments on commit 7801d48

Please sign in to comment.