diff --git a/README.md b/README.md index 7247bfd..3c8ce2f 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,36 @@ cli { |> Command.execute ``` +Write output to a stream: +```fsharp +// using a console stream +cli { + Exec "dotnet" + Arguments "--list-sdks" + Output (new StreamWriter(Console.OpenStandardOutput())) +} +|> Command.execute + +// using a file stream +cli { + Exec "dotnet" + Arguments "--list-sdks" + Output (new StreamWriter(new FileStream("test.txt", FileMode.OpenOrCreate))) +} +|> Command.execute +``` +Hint: Using `Output (new StreamWriter(...))` will redirect the output text to your desired target and not save it into `Output.Text` nor `Output.Error` but in order to fix that you can use `Output.from`: +```fsharp +let sb = StringBuilder() +cli { + Exec "dotnet" + Arguments "--list-sdks" + Output (new StringWriter(sb)) +} +|> Command.execute +|> Output.from (sb.ToString()) +``` + Add environment variables for the executing program: ```fsharp cli { @@ -284,6 +314,7 @@ Provided `Fli.Outputs`: - `File of string` a string with an absolute path of the output file. - `StringBuilder of StringBuilder` a StringBuilder which will be filled with the output text. - `Custom of Func` a custom function (`string -> unit`) that will be called with the output string (logging, printing etc.). +- `Stream of TextWriter` a stream that will redirect the output text to the designated target (file, console etc.). Provided `Fli.WindowStyle`: - `Hidden` (default) diff --git a/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs b/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs index b2876ad..f59540b 100644 --- a/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs +++ b/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs @@ -4,6 +4,7 @@ open NUnit.Framework open FsUnit open Fli open System +open System.IO open System.Text open System.Diagnostics @@ -82,6 +83,43 @@ let ``Get output in StringBuilder`` () = sb.ToString() |> should equal "Test\r\n" +[] +[] +let ``Get new stream in StringBuilder`` () = + let sb = StringBuilder() + + cli { + Shell CMD + Command "echo Test" + Output (new StringWriter(sb)) + } + |> Command.execute + |> ignore + + sb.ToString() |> should contain "Test\r\n" + +[] +[] +let ``Use from to recreate a valid Output when using stream`` () = + let sb = StringBuilder() + + let output = + cli { + Shell CMD + Command "echo Test" + Output (new StringWriter(sb)) + } + |> Command.execute + + output + |> Output.toText + |> should equal "" + + output + |> Output.from (sb.ToString()) + |> Output.toText + |> should contain "Test" + [] [] let ``CMD returning non zero process id`` () = diff --git a/src/Fli/CE.fs b/src/Fli/CE.fs index 3b98d9e..16a6dcc 100644 --- a/src/Fli/CE.fs +++ b/src/Fli/CE.fs @@ -4,6 +4,7 @@ module CE = open System.Text + open System.IO open Domain type ICommandContext<'a> with @@ -55,6 +56,11 @@ module CE = member _.Output(context: ICommandContext, func: string -> unit) = Cli.output (Custom func) context.Context + /// Extra `Output` that is being executed immediately after getting output from execution. + [] + member _.Output(context: ICommandContext, stream: TextWriter) = + Cli.output (Stream stream) context.Context + /// Current executing `working directory`. [] member _.WorkingDirectory(context: ICommandContext, workingDirectory) = @@ -127,6 +133,11 @@ module CE = member _.Output(context: ICommandContext, func: string -> unit) = Program.output (Custom func) context.Context + /// Extra `Output` that is being executed immediately after getting output from execution. + [] + member _.Output(context: ICommandContext, stream: TextWriter) = + Program.output (Stream stream) context.Context + /// Current executing `working directory`. [] member _.WorkingDirectory(context: ICommandContext, workingDirectory) = diff --git a/src/Fli/Command.fs b/src/Fli/Command.fs index dbbdd45..4ede09c 100644 --- a/src/Fli/Command.fs +++ b/src/Fli/Command.fs @@ -109,25 +109,31 @@ module Command = |> Async.AwaitTask #endif - let private startProcess (inputFunc: Process -> unit) (outputFunc: string -> unit) psi = + let private startProcess (inputFunc: Process -> unit) (outputFunc: string -> unit) (isStreaming: bool) psi = let proc = Process.Start(startInfo = psi) proc |> inputFunc - let text = - if psi.UseShellExecute |> not then + let mutable text = + if psi.UseShellExecute |> not && isStreaming |> not then proc.StandardOutput.ReadToEnd() else + proc.BeginOutputReadLine() "" - let error = - if psi.UseShellExecute |> not then + let mutable error = + if psi.UseShellExecute |> not && isStreaming |> not then proc.StandardError.ReadToEnd() else + proc.BeginErrorReadLine() "" + proc.OutputDataReceived.Add(fun args -> outputFunc args.Data) + proc.ErrorDataReceived.Add(fun args -> outputFunc args.Data) + proc.WaitForExit() - text |> outputFunc + if isStreaming |> not then + text |> outputFunc { Id = proc.Id Text = text |> trim |> toOption @@ -199,6 +205,7 @@ module Command = | Outputs.File(file) -> File.WriteAllText(file, output) | Outputs.StringBuilder(stringBuilder) -> output |> stringBuilder.Append |> ignore | Outputs.Custom(func) -> func.Invoke(output) + | Outputs.Stream(stream) -> stream.WriteLine(output) ; stream.Flush() | None -> () let private setupCancellationToken (cancelAfter: int option) = @@ -273,19 +280,31 @@ module Command = /// Executes the given context as a new process. static member execute(context: ShellContext) = + let isStreaming: bool = + match context.config.Output with + | Some s -> match s with Outputs.Stream(s) -> true | _ -> false + | _ -> false + context |> Command.buildProcess |> startProcess (writeInput context.config.Input context.config.Encoding) (writeOutput context.config.Output) + isStreaming /// Executes the given context as a new process. static member execute(context: ExecContext) = + let isStreaming: bool = + match context.config.Output with + | Some s -> match s with Outputs.Stream(s) -> true | _ -> false + | _ -> false + context |> Command.buildProcess |> startProcess (writeInput context.config.Input context.config.Encoding) (writeOutput context.config.Output) + isStreaming #if NET /// Executes the given context as a new process asynchronously. diff --git a/src/Fli/Domain.fs b/src/Fli/Domain.fs index b1a081d..7cfed4e 100644 --- a/src/Fli/Domain.fs +++ b/src/Fli/Domain.fs @@ -4,6 +4,7 @@ module Domain = open System + open System.IO open System.Text type ICommandContext<'a> = @@ -34,6 +35,7 @@ module Domain = | File of string | StringBuilder of StringBuilder | Custom of Func + | Stream of TextWriter and WindowStyle = | Hidden diff --git a/src/Fli/Output.fs b/src/Fli/Output.fs index d53e754..f890d8c 100644 --- a/src/Fli/Output.fs +++ b/src/Fli/Output.fs @@ -35,3 +35,17 @@ module Output = /// Throws exception if exit code is not 0. let throwIfErrored = throw (fun o -> o.ExitCode <> 0) + + let from (str: string) (output: Output): Output = + if output.Text = None |> not || output.Error = None |> not then + output + elif output.ExitCode = 0 then + { Id = output.Id + Text = Some(str) + ExitCode = output.ExitCode + Error = None } + else + { Id = output.Id + Text = None + ExitCode = output.ExitCode + Error = Some(str) } \ No newline at end of file