Skip to content

Commit

Permalink
PipeBuilder + refactoring and tests (#5)
Browse files Browse the repository at this point in the history
* Cli module impl and docs

* Command builder tests

* Pipes

* WIP on pipebuilder

* Downgrade .NET SDK

* Update gitignore

* Remove user dotsettings

* PipeBuilder
  • Loading branch information
UnstoppableMango authored Jan 13, 2024
1 parent ad434df commit cf96317
Show file tree
Hide file tree
Showing 17 changed files with 453 additions and 49 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
.idea/**/usage.statistics.xml
.idea/**/shelf
.idea/**/contentModel.xml
.idea/**/codeStyleConfig.xml
.idea/**/discord.xml

# Artifacts
bin/
obj/
*.nupkg

# User Specific
*.user
1 change: 1 addition & 0 deletions CliWrap.FSharp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{BC5059
.editorconfig = .editorconfig
.gitignore = .gitignore
.githooks\pre-commit = .githooks\pre-commit
CliWrap.FSharp.sln.DotSettings = CliWrap.FSharp.sln.DotSettings
EndProjectSection
EndProject
Global
Expand Down
14 changes: 13 additions & 1 deletion CliWrap.FSharp.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=execf/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=argf/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=argse/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=argsf/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=commandv/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=creds/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=credsf/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=devnull/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=envf/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=execf/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ftce/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=streamf/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=streamft/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=stringe/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
4 changes: 0 additions & 4 deletions CliWrap.FSharp.sln.DotSettings.user

This file was deleted.

16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ It attempts to mimic the builder pattern and `.With*` style methods.

```fsharp
let main args =
let built = command "dotnet" {
let cmd = command "dotnet" {
args = [ "build" ]
workingDirectory = "~/src/CliWrap.FSharp"
}
built.ExecuteAsync()
cmd.ExecuteAsync()
```

```fsharp
Expand All @@ -23,11 +23,21 @@ let main args = async {
args = [ "build" ]
workingDirectory = "~/src/CliWrap.FSharp"
}
result.ExitCode
}
```

```fsharp
let main args =
let cmd = pipeline {
"an inline string source"
Cli.wrap "echo"
}
cmd.ExecuteAsync()
```

## Idiomatic? This looks nothing like normal F# code!

I've only recently been diving further into the F# ecosystem, if something looks off please open an issue!
Expand Down
9 changes: 5 additions & 4 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"sdk": {
"version": "8.0.101"
}
}
"sdk": {
"version": "8.0.100",
"rollForward": "latestMinor"
}
}
13 changes: 13 additions & 0 deletions src/CliWrap.FSharp.Tests/CliTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module CliTests

open CliWrap
open FsCheck.Xunit
open UnMango.CliWrap.FSharp

[<Property>]
let ``Should create command`` target =
let expected = Command(target)

let actual = Cli.wrap target

expected.TargetFilePath = actual.TargetFilePath
1 change: 1 addition & 0 deletions src/CliWrap.FSharp.Tests/CliWrap.FSharp.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="CliTests.fs" />
<Compile Include="CommandBuilderTests.fs"/>
<Compile Include="Program.fs"/>
<None Include="packages.lock.json" />
Expand Down
41 changes: 41 additions & 0 deletions src/CliWrap.FSharp.Tests/CommandBuilderTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module CommandBuilderTests
open System.Collections.Generic
open System.IO
open System.Linq
open System.Text
open CliWrap
open FsCheck
open FsCheck.Xunit
Expand Down Expand Up @@ -49,3 +50,43 @@ let ``Should configure stdin`` (input: NonNull<string>) =
result.StandardInputPipe.CopyToAsync(b).Wait()

a.ToArray() = b.ToArray()


[<Property>]
let ``Should configure stdout`` () =
let a, b = StringBuilder(), StringBuilder()

let expected =
Command("echo")
.WithArguments([ "testing" ])
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(a))

let result = command "echo" {
args [ "testing" ]
stdout (PipeTarget.ToStringBuilder(b))
}

expected.ExecuteAsync().Task.Wait()
result.ExecuteAsync().Task.Wait()
a.ToString() = b.ToString()


[<Property>]
let ``Should configure stderr`` () =
let a, b = StringBuilder(), StringBuilder()

let expected =
Command("echo")
.WithArguments([ "testing" ])
.WithValidation(CommandResultValidation.None)
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(a))

let result = command "echo" {
args [ "testing" ]
stderr (PipeTarget.ToStringBuilder(b))
}

expected.ExecuteAsync().Task.Wait()
result.WithValidation(CommandResultValidation.None).ExecuteAsync().Task.Wait()

a.ToString() = b.ToString()
8 changes: 4 additions & 4 deletions src/CliWrap.FSharp.Tests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
},
"FSharp.Core": {
"type": "Direct",
"requested": "[8.0.101, )",
"resolved": "8.0.101",
"contentHash": "sOLz3O4BOxnTKfd5OChdRmDUy4Id0GfoEClRG4nzIod8LY1LJZcNyygKAV0A78XOLh8yvhA5hsDYKZXGCR9blw=="
"requested": "[8.0.100, )",
"resolved": "8.0.100",
"contentHash": "ZOVZ/o+jI3ormTZOa28Wh0tSRoyle1f7lKFcUN61sPiXI7eDZu8eSveFybgTeyIEyW0ujjp31cp7GOglDgsNEg=="
},
"Microsoft.NET.Test.Sdk": {
"type": "Direct",
Expand Down Expand Up @@ -1047,7 +1047,7 @@
"type": "Project",
"dependencies": {
"CliWrap": "[3.6.4, )",
"FSharp.Core": "[8.0.101, )"
"FSharp.Core": "[8.0.100, )"
}
}
}
Expand Down
177 changes: 174 additions & 3 deletions src/CliWrap.FSharp/Cli.fs
Original file line number Diff line number Diff line change
@@ -1,12 +1,183 @@
module UnMango.CliWrap.FSharp.Cli

open System
open System.Collections.Generic
open CliWrap
open CliWrap.Builders

/// <summary>
/// Creates a copy of this command, setting the arguments to the specified value.
/// </summary>
/// <param name="args">The string representation of the arguments.</param>
/// <param name="command">The Command object to add the arguments to.</param>
/// <returns>A new Command object with the specified arguments.</returns>
/// <remarks>
/// Avoid using this overload, as it requires the arguments to be escaped manually.
/// Formatting errors may lead to unexpected bugs and security vulnerabilities.
/// </remarks>
let arg (args: string) (command: Command) = command.WithArguments(args)

/// <summary>
/// Creates a copy of this command, setting the arguments to the value obtained by formatting the specified enumeration.
/// </summary>
/// <param name="args">The arguments to be added to the command.</param>
/// <param name="command">The command to add the arguments to.</param>
/// <returns>A new command object with the provided arguments.</returns>
let args (args: string seq) (command: Command) = command.WithArguments(args)

/// <summary>
/// Creates a copy of this command, setting the arguments to the value obtained by formatting the specified enumeration.
/// </summary>
/// <param name="args">The arguments to be added to the command.</param>
/// <param name="escape">The escape options to apply to the arguments.</param>
/// <param name="command">The original command.</param>
/// <returns>A new command object with the arguments and escape options added.</returns>
let argse args escape (command: Command) = command.WithArguments(args, escape)

/// <summary>
/// Creates a copy of this command, setting the arguments to the value configured by the specified delegate.
/// </summary>
/// <param name="f">The function that builds the arguments using an ArgumentsBuilder.</param>
/// <param name="command">The command for which the arguments are being built.</param>
/// <returns>A new command object with the arguments added.</returns>
let argsf (f: ArgumentsBuilder -> unit) (command: Command) = command.WithArguments(f)

/// <summary>
/// Creates a new command with the specified parameters.
/// </summary>
/// <param name="target">The target executable or script file.</param>
/// <param name="args">The arguments to be passed to the target.</param>
/// <param name="workDir">The working directory for the command.</param>
/// <param name="creds">The credentials to use for the command.</param>
/// <param name="env">The environment variables for the command.</param>
/// <param name="v">The validation for the command.</param>
/// <param name="stdin">The input pipe source for the command.</param>
/// <param name="stdout">The output pipe target for the command.</param>
/// <param name="stderr">The error pipe target for the command.</param>
/// <returns>A new Command instance.</returns>
/// <remarks>
/// v = verbose. Idk why you would use this, but its there if you want to
/// </remarks>
let commandv target args workDir creds env v stdin stdout stderr =
Command(target, args, workDir, creds, env, v, stdin, stdout, stderr)

/// <summary>
/// Creates a copy of this command, setting the user credentials to the specified value.
/// </summary>
/// <param name="credentials">The credentials to be applied.</param>
/// <param name="command">The command to apply the credentials to.</param>
/// <returns>The modified command with the credentials applied.</returns>
let creds (credentials: Credentials) (command: Command) = command.WithCredentials(credentials)

/// <summary>
/// Creates a copy of this command, setting the user credentials to the specified value.
/// </summary>
/// <param name="f">The function that builds the credentials with a CredentialsBuilder.</param>
/// <param name="command">The command for which the credentials are being built.</param>
/// <returns>The modified command with the credentials applied.</returns>
let credsf (f: CredentialsBuilder -> unit) (command: Command) = command.WithCredentials(f)

/// <summary>
/// Creates a copy of this command, setting the environment variables to the specified value.
/// </summary>
/// <param name="env">A sequence of tuples representing key-value pairs of environment variables.</param>
/// <param name="command">The command to modify.</param>
/// <returns>A new command with the updated environment variables.</returns>
let env (env: (string * string) seq) (command: Command) =
command.WithEnvironmentVariables((dict env).AsReadOnly())

/// <summary>
/// Creates a copy of this command, setting the environment variables to the value configured by the specified delegate.
/// </summary>
/// <param name="f">The function that builds environment variables using an EnvironmentVariablesBuilder.</param>
/// <param name="command">The command to which the environment variables should be applied.</param>
/// <returns>The modified command with the updated environment variables.</returns>
let envf (f: EnvironmentVariablesBuilder -> unit) (command: Command) = command.WithEnvironmentVariables(f)

/// <summary>
/// Executes the command asynchronously.
/// </summary>
/// <param name="command">The command to execute.</param>
/// <returns>An <see cref="Async{CommandResult}"/> representing the asynchronous operation.</returns>
let exec (command: Command) =
command.ExecuteAsync() |> CommandTask.op_Implicit |> Async.AwaitTask

module C =
let exec (command: Command) cancellationToken = command.ExecuteAsync(cancellationToken)
/// <summary>
/// Creates a copy of this command, setting the standard input pipe to the specified source.
/// </summary>
/// <param name="pipe">The pipe to attach to the standard input.</param>
/// <param name="command">The command to attach the pipe to.</param>
/// <returns>A copy of the command with the standard input pipe attached.</returns>
let stdin pipe (command: Command) = command.WithStandardInputPipe(pipe)

let execf (command: Command) forceful graceful =
/// <summary>
/// Creates a copy of this command, setting the standard output pipe to the specified target.
/// </summary>
/// <param name="pipe">The pipe to redirect the standard output to.</param>
/// <param name="command">The command to redirect the standard output from.</param>
/// <returns>A copy of the command with the standard output pipe attached.</returns>
let stdout pipe (command: Command) = command.WithStandardOutputPipe(pipe)

/// <summary>
/// Creates a copy of this command, setting the standard error pipe to the specified target.
/// </summary>
/// <param name="pipe">The pipe to attach.</param>
/// <param name="command">The command to attach the pipe to.</param>
/// <returns>The modified command with the standard error pipe attached.</returns>
let stderr pipe (command: Command) = command.WithStandardErrorPipe(pipe)

/// <summary>
/// Creates a copy of this command, setting the target file path to the specified value.
/// </summary>
/// <param name="target">The target file to set.</param>
/// <param name="command">The original command instance.</param>
/// <returns>A new Command instance with the updated target file.</returns>
let target target (command: Command) = command.WithTargetFile(target)

/// <summary>
/// Creates a copy of this command, setting the validation options to the specified value.
/// </summary>
/// <param name="validation">The validation options to apply.</param>
/// <param name="command">The command to validate.</param>
/// <returns>A copy of the command with the validation options applied.</returns>
let validation validation (command: Command) = command.WithValidation(validation)

/// <summary>
/// Creates a copy of this command, setting the working directory path to the specified value.
/// </summary>
/// <param name="dir">The directory to set as the working directory.</param>
/// <param name="command">The command to modify.</param>
/// <returns>
/// A new command object with the working directory set to the specified directory.
/// </returns>
let workDir dir (command: Command) = command.WithWorkingDirectory(dir)

/// <summary>
/// Creates a new command to execute the target specified by <paramref name="target"/>.
/// </summary>
/// <param name="target">The target to be wrapped.</param>
/// <returns>A command to execute <paramref name="target"/>.</returns>
let wrap target = Command(target)

module Task =
/// <summary>
/// Executes the command asynchronously.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="command">The command to execute.</param>
/// <returns>A task representing the execution of the command.</returns>
/// <remarks><see cref="CommandTask"/> can be <see cref="await"/>ed like a <see cref="Task"/>.</remarks>
let exec cancellationToken (command: Command) = command.ExecuteAsync(cancellationToken)

/// <summary>
/// Executes the command asynchronously.
/// </summary>
/// <param name="forceful">The cancellation token to forcefully exit.</param>
/// <param name="graceful">The cancellation token to gracefully exit.</param>
/// <param name="command">The command to execute.</param>
/// <returns>A task representing the execution of the command.</returns>
/// <remarks><see cref="CommandTask"/> can be <see cref="await"/>ed like a <see cref="Task"/>.</remarks>
let execf forceful graceful (command: Command) =
command.ExecuteAsync(graceful, forceful)

let toString (command: Command) = command.ToString()
5 changes: 3 additions & 2 deletions src/CliWrap.FSharp/CliWrap.FSharp.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="Types.fs" />
<Compile Include="Cli.fs" />
<Compile Include="Pipes.fs" />
<Compile Include="PipeBuilder.fs" />
<Compile Include="CommandBuilder.fs" />
<Compile Include="ExecBuilder.fs" />
<Compile Include="Cli.fs" />
<None Include="packages.lock.json" />
</ItemGroup>

Expand Down
Loading

0 comments on commit cf96317

Please sign in to comment.