diff --git a/.gitignore b/.gitignore index 7e1ba71..9544b85 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CliWrap.FSharp.sln b/CliWrap.FSharp.sln index 7b5cce0..a396a7f 100644 --- a/CliWrap.FSharp.sln +++ b/CliWrap.FSharp.sln @@ -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 diff --git a/CliWrap.FSharp.sln.DotSettings b/CliWrap.FSharp.sln.DotSettings index 8e7600a..bc79155 100644 --- a/CliWrap.FSharp.sln.DotSettings +++ b/CliWrap.FSharp.sln.DotSettings @@ -1,2 +1,14 @@  - True \ No newline at end of file + True + True + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/CliWrap.FSharp.sln.DotSettings.user b/CliWrap.FSharp.sln.DotSettings.user deleted file mode 100644 index b3e5154..0000000 --- a/CliWrap.FSharp.sln.DotSettings.user +++ /dev/null @@ -1,4 +0,0 @@ - - <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <Solution /> -</SessionState> \ No newline at end of file diff --git a/README.md b/README.md index 57b03f1..564555e 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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! diff --git a/global.json b/global.json index e53eaeb..014e236 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,6 @@ { - "sdk": { - "version": "8.0.101" - } -} \ No newline at end of file + "sdk": { + "version": "8.0.100", + "rollForward": "latestMinor" + } +} diff --git a/src/CliWrap.FSharp.Tests/CliTests.fs b/src/CliWrap.FSharp.Tests/CliTests.fs new file mode 100644 index 0000000..38d7a0e --- /dev/null +++ b/src/CliWrap.FSharp.Tests/CliTests.fs @@ -0,0 +1,13 @@ +module CliTests + +open CliWrap +open FsCheck.Xunit +open UnMango.CliWrap.FSharp + +[] +let ``Should create command`` target = + let expected = Command(target) + + let actual = Cli.wrap target + + expected.TargetFilePath = actual.TargetFilePath diff --git a/src/CliWrap.FSharp.Tests/CliWrap.FSharp.Tests.fsproj b/src/CliWrap.FSharp.Tests/CliWrap.FSharp.Tests.fsproj index afda23e..ed625f4 100644 --- a/src/CliWrap.FSharp.Tests/CliWrap.FSharp.Tests.fsproj +++ b/src/CliWrap.FSharp.Tests/CliWrap.FSharp.Tests.fsproj @@ -9,6 +9,7 @@ + diff --git a/src/CliWrap.FSharp.Tests/CommandBuilderTests.fs b/src/CliWrap.FSharp.Tests/CommandBuilderTests.fs index 2c6ba1b..a348fdd 100644 --- a/src/CliWrap.FSharp.Tests/CommandBuilderTests.fs +++ b/src/CliWrap.FSharp.Tests/CommandBuilderTests.fs @@ -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 @@ -49,3 +50,43 @@ let ``Should configure stdin`` (input: NonNull) = result.StandardInputPipe.CopyToAsync(b).Wait() a.ToArray() = b.ToArray() + + +[] +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() + + +[] +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() diff --git a/src/CliWrap.FSharp.Tests/packages.lock.json b/src/CliWrap.FSharp.Tests/packages.lock.json index f506b86..f4f6fcf 100644 --- a/src/CliWrap.FSharp.Tests/packages.lock.json +++ b/src/CliWrap.FSharp.Tests/packages.lock.json @@ -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", @@ -1047,7 +1047,7 @@ "type": "Project", "dependencies": { "CliWrap": "[3.6.4, )", - "FSharp.Core": "[8.0.101, )" + "FSharp.Core": "[8.0.100, )" } } } diff --git a/src/CliWrap.FSharp/Cli.fs b/src/CliWrap.FSharp/Cli.fs index ace0a7b..f54912c 100644 --- a/src/CliWrap.FSharp/Cli.fs +++ b/src/CliWrap.FSharp/Cli.fs @@ -1,12 +1,183 @@ module UnMango.CliWrap.FSharp.Cli +open System +open System.Collections.Generic open CliWrap +open CliWrap.Builders +/// +/// Creates a copy of this command, setting the arguments to the specified value. +/// +/// The string representation of the arguments. +/// The Command object to add the arguments to. +/// A new Command object with the specified arguments. +/// +/// Avoid using this overload, as it requires the arguments to be escaped manually. +/// Formatting errors may lead to unexpected bugs and security vulnerabilities. +/// +let arg (args: string) (command: Command) = command.WithArguments(args) + +/// +/// Creates a copy of this command, setting the arguments to the value obtained by formatting the specified enumeration. +/// +/// The arguments to be added to the command. +/// The command to add the arguments to. +/// A new command object with the provided arguments. +let args (args: string seq) (command: Command) = command.WithArguments(args) + +/// +/// Creates a copy of this command, setting the arguments to the value obtained by formatting the specified enumeration. +/// +/// The arguments to be added to the command. +/// The escape options to apply to the arguments. +/// The original command. +/// A new command object with the arguments and escape options added. +let argse args escape (command: Command) = command.WithArguments(args, escape) + +/// +/// Creates a copy of this command, setting the arguments to the value configured by the specified delegate. +/// +/// The function that builds the arguments using an ArgumentsBuilder. +/// The command for which the arguments are being built. +/// A new command object with the arguments added. +let argsf (f: ArgumentsBuilder -> unit) (command: Command) = command.WithArguments(f) + +/// +/// Creates a new command with the specified parameters. +/// +/// The target executable or script file. +/// The arguments to be passed to the target. +/// The working directory for the command. +/// The credentials to use for the command. +/// The environment variables for the command. +/// The validation for the command. +/// The input pipe source for the command. +/// The output pipe target for the command. +/// The error pipe target for the command. +/// A new Command instance. +/// +/// v = verbose. Idk why you would use this, but its there if you want to +/// +let commandv target args workDir creds env v stdin stdout stderr = + Command(target, args, workDir, creds, env, v, stdin, stdout, stderr) + +/// +/// Creates a copy of this command, setting the user credentials to the specified value. +/// +/// The credentials to be applied. +/// The command to apply the credentials to. +/// The modified command with the credentials applied. +let creds (credentials: Credentials) (command: Command) = command.WithCredentials(credentials) + +/// +/// Creates a copy of this command, setting the user credentials to the specified value. +/// +/// The function that builds the credentials with a CredentialsBuilder. +/// The command for which the credentials are being built. +/// The modified command with the credentials applied. +let credsf (f: CredentialsBuilder -> unit) (command: Command) = command.WithCredentials(f) + +/// +/// Creates a copy of this command, setting the environment variables to the specified value. +/// +/// A sequence of tuples representing key-value pairs of environment variables. +/// The command to modify. +/// A new command with the updated environment variables. +let env (env: (string * string) seq) (command: Command) = + command.WithEnvironmentVariables((dict env).AsReadOnly()) + +/// +/// Creates a copy of this command, setting the environment variables to the value configured by the specified delegate. +/// +/// The function that builds environment variables using an EnvironmentVariablesBuilder. +/// The command to which the environment variables should be applied. +/// The modified command with the updated environment variables. +let envf (f: EnvironmentVariablesBuilder -> unit) (command: Command) = command.WithEnvironmentVariables(f) + +/// +/// Executes the command asynchronously. +/// +/// The command to execute. +/// An representing the asynchronous operation. let exec (command: Command) = command.ExecuteAsync() |> CommandTask.op_Implicit |> Async.AwaitTask -module C = - let exec (command: Command) cancellationToken = command.ExecuteAsync(cancellationToken) +/// +/// Creates a copy of this command, setting the standard input pipe to the specified source. +/// +/// The pipe to attach to the standard input. +/// The command to attach the pipe to. +/// A copy of the command with the standard input pipe attached. +let stdin pipe (command: Command) = command.WithStandardInputPipe(pipe) - let execf (command: Command) forceful graceful = +/// +/// Creates a copy of this command, setting the standard output pipe to the specified target. +/// +/// The pipe to redirect the standard output to. +/// The command to redirect the standard output from. +/// A copy of the command with the standard output pipe attached. +let stdout pipe (command: Command) = command.WithStandardOutputPipe(pipe) + +/// +/// Creates a copy of this command, setting the standard error pipe to the specified target. +/// +/// The pipe to attach. +/// The command to attach the pipe to. +/// The modified command with the standard error pipe attached. +let stderr pipe (command: Command) = command.WithStandardErrorPipe(pipe) + +/// +/// Creates a copy of this command, setting the target file path to the specified value. +/// +/// The target file to set. +/// The original command instance. +/// A new Command instance with the updated target file. +let target target (command: Command) = command.WithTargetFile(target) + +/// +/// Creates a copy of this command, setting the validation options to the specified value. +/// +/// The validation options to apply. +/// The command to validate. +/// A copy of the command with the validation options applied. +let validation validation (command: Command) = command.WithValidation(validation) + +/// +/// Creates a copy of this command, setting the working directory path to the specified value. +/// +/// The directory to set as the working directory. +/// The command to modify. +/// +/// A new command object with the working directory set to the specified directory. +/// +let workDir dir (command: Command) = command.WithWorkingDirectory(dir) + +/// +/// Creates a new command to execute the target specified by . +/// +/// The target to be wrapped. +/// A command to execute . +let wrap target = Command(target) + +module Task = + /// + /// Executes the command asynchronously. + /// + /// The cancellation token. + /// The command to execute. + /// A task representing the execution of the command. + /// can be ed like a . + let exec cancellationToken (command: Command) = command.ExecuteAsync(cancellationToken) + + /// + /// Executes the command asynchronously. + /// + /// The cancellation token to forcefully exit. + /// The cancellation token to gracefully exit. + /// The command to execute. + /// A task representing the execution of the command. + /// can be ed like a . + let execf forceful graceful (command: Command) = command.ExecuteAsync(graceful, forceful) + +let toString (command: Command) = command.ToString() diff --git a/src/CliWrap.FSharp/CliWrap.FSharp.fsproj b/src/CliWrap.FSharp/CliWrap.FSharp.fsproj index a71abf9..058ae27 100644 --- a/src/CliWrap.FSharp/CliWrap.FSharp.fsproj +++ b/src/CliWrap.FSharp/CliWrap.FSharp.fsproj @@ -8,10 +8,11 @@ - + + + - diff --git a/src/CliWrap.FSharp/CommandBuilder.fs b/src/CliWrap.FSharp/CommandBuilder.fs index 092362b..4363cee 100644 --- a/src/CliWrap.FSharp/CommandBuilder.fs +++ b/src/CliWrap.FSharp/CommandBuilder.fs @@ -1,52 +1,50 @@ [] module UnMango.CliWrap.FSharp.CommandBuilder -open System.Collections.Generic open System.ComponentModel open CliWrap type CommandBuilder(target: string) = [] - member _.Yield(_: unit) = Command(target) + member _.Yield(_: unit) = Cli.wrap target [] member _.Run(command: Command) = command [] - member _.Env(command: Command, env) = - command.WithEnvironmentVariables((dict env).AsReadOnly()) + member _.Env(command: Command, env) = Cli.env env command [] - member _.WorkingDirectory(command: Command, directory) = command.WithWorkingDirectory(directory) + member _.WorkingDirectory(command: Command, directory) = Cli.workDir directory command [] - member _.Args(command: Command, args: string) = command.WithArguments(args) + member _.Args(command: Command, args: string) = Cli.arg args command [] - member _.Args(command: Command, args: string seq) = command.WithArguments(args) + member _.Args(command: Command, args: string seq) = Cli.args args command [] - member _.StdErr(command: Command, pipe) = command.WithStandardErrorPipe(pipe) + member _.StdErr(command: Command, pipe) = Cli.stderr pipe command [] member _.StdErr(command: Command, stream) = - PipeTarget.ToStream(stream) |> command.WithStandardErrorPipe + Cli.stderr (PipeTarget.ToStream(stream)) command [] - member _.StdIn(command: Command, pipe) = command.WithStandardInputPipe(pipe) + member _.StdIn(command: Command, pipe) = Cli.stdin pipe command [] member _.StdIn(command: Command, stream) = - PipeSource.FromStream(stream) |> command.WithStandardInputPipe + Cli.stdin (PipeSource.FromStream(stream)) command [] - member _.StdOut(command: Command, pipe) = command.WithStandardOutputPipe(pipe) + member _.StdOut(command: Command, pipe) = Cli.stdout pipe command [] member _.StdOut(command: Command, stream) = - PipeTarget.ToStream(stream) |> command.WithStandardOutputPipe + Cli.stdout (PipeTarget.ToStream(stream)) command [] - member _.Validation(command: Command, validation) = command.WithValidation(validation) + member _.Validation(command: Command, validation) = Cli.validation validation command let command target = CommandBuilder(target) diff --git a/src/CliWrap.FSharp/PipeBuilder.fs b/src/CliWrap.FSharp/PipeBuilder.fs new file mode 100644 index 0000000..d68df98 --- /dev/null +++ b/src/CliWrap.FSharp/PipeBuilder.fs @@ -0,0 +1,114 @@ +[] +module UnMango.CliWrap.FSharp.PipeBuilder + +open System +open System.ComponentModel +open System.IO +open System.Text +open System.Threading +open System.Threading.Tasks +open CliWrap + +module private Tuple = + let map f (a, b) = (f a, f b) + +type PipeBuilder() = + [] + member _.Run(state: Command) = state + + [] + member _.Combine(source: Command, target: PipeTarget) = source.WithStandardErrorPipe(target) + + [] + member this.Combine(source: Command, stream: Stream) = + this.Combine(source, PipeTarget.ToStream(stream)) + + [] + member this.Combine(source: Command, builder: StringBuilder) = + this.Combine(source, PipeTarget.ToStringBuilder(builder)) + + [] + member this.Combine(source: Command, f: string -> CancellationToken -> Task) = + this.Combine(source, PipeTarget.ToDelegate(f)) + + [] + member this.Combine(source: Command, f: string -> Task) = + this.Combine(source, PipeTarget.ToDelegate(f)) + + [] + member this.Combine(source: Command, f: string -> unit) = + this.Combine(source, PipeTarget.ToDelegate(f)) + + [] + member this.Combine(source: Command, (stdout: PipeTarget, stderr: PipeTarget)) = + source.WithStandardOutputPipe(stdout).WithStandardErrorPipe(stderr) + + [] + member this.Combine(source: Command, target: Stream * Stream) = + this.Combine(source, target |> Tuple.map PipeTarget.ToStream) + + [] + member this.Combine(source: Command, target: StringBuilder * StringBuilder) = + this.Combine(source, target |> Tuple.map PipeTarget.ToStringBuilder) + + [] + member this.Combine + ( + source: Command, + (stdout: string -> CancellationToken -> Task, stderr: string -> CancellationToken -> Task) + ) = + this.Combine(source, (PipeTarget.ToDelegate(stdout), PipeTarget.ToDelegate(stderr))) + + [] + member this.Combine(source: Command, (stdout: string -> Task, stderr: string -> Task)) = + this.Combine(source, (PipeTarget.ToDelegate(stdout), PipeTarget.ToDelegate(stderr))) + + [] + member this.Combine(source: Command, (stdout: string -> unit, stderr: string -> unit)) = + this.Combine(source, (PipeTarget.ToDelegate(stdout), PipeTarget.ToDelegate(stderr))) + + [] + member _.Combine(_: unit, source: PipeSource) = source + + [] + member _.Combine(source: PipeSource, command: Command) = command.WithStandardInputPipe(source) + + [] + member this.Combine(source: Stream, command: Command) = + this.Combine(PipeSource.FromStream(source), command) + + [] + member this.Combine(source: ReadOnlyMemory, command: Command) = + this.Combine(PipeSource.FromBytes(source), command) + + [] + member this.Combine(source: byte array, command: Command) = + this.Combine(PipeSource.FromBytes(source), command) + + [] + member this.Combine(source, command: Command) = + this.Combine(PipeSource.FromString(source), command) + + [] + member this.Combine(source: Command, command: Command) = + this.Combine(PipeSource.FromCommand(source), command) + + [] + member _.Yield(x: PipeSource) = x + + [] + member _.Yield(x: Command) = x + + [] + member _.Yield(x: string) = x + + [] + member _.Yield(x: Stream) = x + + [] + member _.Yield(x: StringBuilder) = x + + [] + member _.Delay f = f () + +let pipeline = PipeBuilder() diff --git a/src/CliWrap.FSharp/Pipes.fs b/src/CliWrap.FSharp/Pipes.fs new file mode 100644 index 0000000..a0a21bb --- /dev/null +++ b/src/CliWrap.FSharp/Pipes.fs @@ -0,0 +1,51 @@ +[] +module UnMango.CliWrap.FSharp.Pipes + +open System +open System.IO +open System.Threading +open System.Threading.Tasks +open CliWrap + +module PipeTo = + let stdout = Console.OpenStandardOutput() |> PipeTarget.ToStream + let stderr = Console.OpenStandardError() |> PipeTarget.ToStream + + let all (targets: PipeTarget list) = PipeTarget.Merge(targets) + let string = PipeTarget.ToStringBuilder + + let stringe builder encoding = + PipeTarget.ToStringBuilder(builder, encoding) + + let devnull = PipeTarget.Null + let nullStream = Stream.Null |> PipeTarget.ToStream + let f (f: string -> unit) = PipeTarget.ToDelegate(f) + let fe encoding (f: string -> unit) = PipeTarget.ToDelegate(f, encoding) + let fs (f: Stream -> unit) = PipeTarget.Create(f) + let ft (f: string -> Task) = PipeTarget.ToDelegate(f) + let ftc (f: string -> CancellationToken -> Task) = PipeTarget.ToDelegate(f) + let ftce encoding (f: string -> CancellationToken -> Task) = PipeTarget.ToDelegate(f, encoding) + let fte encoding (f: string -> Task) = PipeTarget.ToDelegate(f, encoding) + let file = PipeTarget.ToFile + let stream = PipeTarget.ToStream + let streamf stream flush = PipeTarget.ToStream(stream, flush) + let streamft (f: Stream -> CancellationToken -> Task) = PipeTarget.Create(f) + + +module ReadFrom = + let stdin = Console.OpenStandardInput() |> PipeSource.FromStream + + let devnull = PipeSource.Null + let nullStream = Stream.Null |> PipeSource.FromStream + let string = PipeSource.FromString + let stringe value encoding = PipeSource.FromString(value, encoding) + let f (f: Stream -> unit) = PipeSource.Create(f) + let ft (f: Stream -> CancellationToken -> Task) = PipeSource.Create(f) + let file = PipeSource.FromFile + let stream = PipeSource.FromStream + let streamf stream flush = PipeSource.FromStream(stream, flush) + let bytes (bytes: byte array) = PipeSource.FromBytes(bytes) + let mem (memory: ReadOnlyMemory) = PipeSource.FromBytes(memory) + let command = PipeSource.FromCommand + +let inline (|>>) command source = Cli.stdin command source diff --git a/src/CliWrap.FSharp/Types.fs b/src/CliWrap.FSharp/Types.fs deleted file mode 100644 index a9b4061..0000000 --- a/src/CliWrap.FSharp/Types.fs +++ /dev/null @@ -1,11 +0,0 @@ -namespace UnMango.CliWrap.FSharp - -open System -open CliWrap - -module PipeTo = - let stdout = Console.OpenStandardOutput() |> PipeTarget.ToStream - let stderr = Console.OpenStandardError() |> PipeTarget.ToStream - -module ReadFrom = - let stdin = Console.OpenStandardInput() |> PipeSource.FromStream diff --git a/src/CliWrap.FSharp/packages.lock.json b/src/CliWrap.FSharp/packages.lock.json index 90415e5..a4a4e10 100644 --- a/src/CliWrap.FSharp/packages.lock.json +++ b/src/CliWrap.FSharp/packages.lock.json @@ -10,9 +10,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.SourceLink.GitHub": { "type": "Direct",