From fdd009d641929badeab622bf0dc2f384a95de1cb Mon Sep 17 00:00:00 2001 From: flsl0 Date: Wed, 30 Jul 2025 17:40:56 +0200 Subject: [PATCH 01/14] Add stream option --- src/Fli/CE.fs | 37 +++++++++++++++++++++++++++++++++++++ src/Fli/Command.fs | 28 +++++++++++++++++++++++----- src/Fli/Domain.fs | 4 ++++ src/Fli/Dsl.fs | 18 ++++++++++++++++++ 4 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/Fli/CE.fs b/src/Fli/CE.fs index 3b98d9e..665f815 100644 --- a/src/Fli/CE.fs +++ b/src/Fli/CE.fs @@ -55,6 +55,25 @@ 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 _.Stream(context: ICommandContext, output: Outputs) = Cli.stream output context.Context + + /// Extra `Output` that is being executed immediately after getting output from execution. + [] + member _.Stream(context: ICommandContext, filePath: string) = + Cli.stream (File filePath) context.Context + + /// Extra `Output` that is being executed immediately after getting output from execution. + [] + member _.Stream(context: ICommandContext, stringBuilder: StringBuilder) = + Cli.stream (StringBuilder stringBuilder) context.Context + + /// Extra `Output` that is being executed immediately after getting output from execution. + [] + member _.Strem(context: ICommandContext, func: string -> unit) = + Cli.stream (Custom func) context.Context + /// Current executing `working directory`. [] member _.WorkingDirectory(context: ICommandContext, workingDirectory) = @@ -127,6 +146,24 @@ module CE = member _.Output(context: ICommandContext, func: string -> unit) = Program.output (Custom func) context.Context + [] + member _.Stream(context: ICommandContext, output: Outputs) = Program.stream output context.Context + + /// Extra `Output` that is being executed immediately after getting output from execution. + [] + member _.Stream(context: ICommandContext, filePath: string) = + Program.stream (File filePath) context.Context + + /// Extra `Output` that is being executed immediately after getting output from execution. + [] + member _.Stream(context: ICommandContext, stringBuilder: StringBuilder) = + Program.stream (StringBuilder stringBuilder) context.Context + + /// Extra `Output` that is being executed immediately after getting output from execution. + [] + member _.Stream(context: ICommandContext, func: string -> unit) = + Program.stream (Custom func) 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..c925143 100644 --- a/src/Fli/Command.fs +++ b/src/Fli/Command.fs @@ -109,22 +109,27 @@ module Command = |> Async.AwaitTask #endif - let private startProcess (inputFunc: Process -> unit) (outputFunc: string -> unit) psi = + let private startProcess (inputFunc: Process -> unit) (outputFunc: string -> unit) (streamFunc: string -> unit) (isNotStreaming: bool) psi = let proc = Process.Start(startInfo = psi) proc |> inputFunc - let text = - if psi.UseShellExecute |> not then + let mutable text = + if psi.UseShellExecute |> not && isNotStreaming then proc.StandardOutput.ReadToEnd() else + proc.BeginOutputReadLine() "" - let error = - if psi.UseShellExecute |> not then + let mutable error = + if psi.UseShellExecute |> not && isNotStreaming then proc.StandardError.ReadToEnd() else + proc.BeginErrorReadLine() "" + proc.OutputDataReceived.Add(fun args -> text <- text + args.Data ; streamFunc args.Data) + proc.ErrorDataReceived.Add(fun args -> error <- error + args.Data ; streamFunc args.Data) + proc.WaitForExit() text |> outputFunc @@ -201,6 +206,15 @@ module Command = | Outputs.Custom(func) -> func.Invoke(output) | None -> () + let private streamOutput (outputType: Outputs option) (output: string): unit = + match outputType with + | Some(o) -> + match o with + | Outputs.File(file) -> File.AppendAllText(file, output) + | Outputs.StringBuilder(stringBuilder) -> output |> stringBuilder.Append |> ignore + | Outputs.Custom(func) -> func.Invoke(output) + | None -> () + let private setupCancellationToken (cancelAfter: int option) = let cts = new CancellationTokenSource() @@ -278,6 +292,8 @@ module Command = |> startProcess (writeInput context.config.Input context.config.Encoding) (writeOutput context.config.Output) + (streamOutput context.config.Stream) + (context.config.Stream.IsNone) /// Executes the given context as a new process. static member execute(context: ExecContext) = @@ -286,6 +302,8 @@ module Command = |> startProcess (writeInput context.config.Input context.config.Encoding) (writeOutput context.config.Output) + (streamOutput context.config.Stream) + (context.config.Stream.IsNone) #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..4aa1f02 100644 --- a/src/Fli/Domain.fs +++ b/src/Fli/Domain.fs @@ -14,6 +14,7 @@ module Domain = Command: string option Input: string option Output: Outputs option + Stream: Outputs option WorkingDirectory: string option EnvironmentVariables: (string * string) list option Encoding: Encoding option @@ -46,6 +47,7 @@ module Domain = Arguments: Arguments option Input: string option Output: Outputs option + Stream: Outputs option WorkingDirectory: string option Verb: string option UserName: string option @@ -96,6 +98,7 @@ module Domain = Command = None Input = None Output = None + Stream = None WorkingDirectory = None EnvironmentVariables = None Encoding = None @@ -106,6 +109,7 @@ module Domain = Arguments = None Input = None Output = None + Stream = None WorkingDirectory = None Verb = None UserName = None diff --git a/src/Fli/Dsl.fs b/src/Fli/Dsl.fs index ed5bd56..8cb3084 100644 --- a/src/Fli/Dsl.fs +++ b/src/Fli/Dsl.fs @@ -27,6 +27,15 @@ module Cli = { context with config.Output = outputsOption } + let stream (output: Outputs) (context: ShellContext) = + let outputsOption = + match output with + | File path -> path |> toOptionWithDefault output + | _ -> Some output + + { context with + config.Stream = outputsOption } + let workingDirectory (workingDirectory: string) (context: ShellContext) = { context with config.WorkingDirectory = Some workingDirectory } @@ -76,6 +85,15 @@ module Program = { context with config.Output = outputsOption } + let stream (output: Outputs) (context: ExecContext) = + let outputsOption = + match output with + | File path -> path |> toOptionWithDefault output + | _ -> Some output + + { context with + config.Stream = outputsOption } + let workingDirectory (workingDirectory: string) (context: ExecContext) = { context with config.WorkingDirectory = Some workingDirectory } From ea18ad45eec51bbc75fe9ca44d287d72bd59a390 Mon Sep 17 00:00:00 2001 From: flsl0 Date: Wed, 30 Jul 2025 17:41:11 +0200 Subject: [PATCH 02/14] Add unit test for stream option --- .../ShellCommandExecuteWindowsTests.fs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs b/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs index b2876ad..5220330 100644 --- a/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs +++ b/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs @@ -82,6 +82,21 @@ let ``Get output in StringBuilder`` () = sb.ToString() |> should equal "Test\r\n" +[] +[] +let ``Get stream in StringBuilder`` () = + let sb = StringBuilder() + + cli { + Shell CMD + Command "echo Test" + Stream sb + } + |> Command.execute + |> ignore + + sb.ToString() |> should equal "Test" + [] [] let ``CMD returning non zero process id`` () = From 5916037174279eb9108e0b3fadbce69fedc4699c Mon Sep 17 00:00:00 2001 From: flsl0 Date: Wed, 30 Jul 2025 17:41:27 +0200 Subject: [PATCH 03/14] Update README with a showcase of the stream option --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7247bfd..a4eed44 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ cli { ``` (Hint: if file extension is not assigned to any installed program, it will throw a `System.NullReferenceException`) -Write output to a specific file: +Write output or stream to a specific file: ```fsharp cli { Exec "dotnet" @@ -84,9 +84,16 @@ cli { Output @"absolute\path\to\dotnet-sdks.txt" } |> Command.execute + +cli { + Exec "dotnet" + Arguments "--list-sdks" + Stream @"absolute\path\to\dotnet-sdks.txt" +} +|> Command.execute ``` -Write output to a function (logging, printing, etc.): +Write output or stream to a function (logging, printing, etc.): ```fsharp let log (output: string) = Debug.Log($"CLI log: {output}") @@ -96,6 +103,13 @@ cli { Output log } |> Command.execute + +cli { + Exec "dotnet" + Arguments "--list-sdks" + Stream log +} +|> Command.execute ``` Add environment variables for the executing program: From 025254af90d24cdebf83aa2f96e15dd5d135c359 Mon Sep 17 00:00:00 2001 From: flsl0 Date: Wed, 6 Aug 2025 10:11:42 +0200 Subject: [PATCH 04/14] Fix typo and comments --- src/Fli/CE.fs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Fli/CE.fs b/src/Fli/CE.fs index 665f815..7d18709 100644 --- a/src/Fli/CE.fs +++ b/src/Fli/CE.fs @@ -55,23 +55,23 @@ 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. + /// Extra `Stream` that is being executed immediately after getting output from execution. [] member _.Stream(context: ICommandContext, output: Outputs) = Cli.stream output context.Context - /// Extra `Output` that is being executed immediately after getting output from execution. + /// Extra `Stream` that is being executed immediately after getting output from execution. [] member _.Stream(context: ICommandContext, filePath: string) = Cli.stream (File filePath) context.Context - /// Extra `Output` that is being executed immediately after getting output from execution. + /// Extra `Stream` that is being executed immediately after getting output from execution. [] member _.Stream(context: ICommandContext, stringBuilder: StringBuilder) = Cli.stream (StringBuilder stringBuilder) context.Context - /// Extra `Output` that is being executed immediately after getting output from execution. + /// Extra `Stream` that is being executed immediately after getting output from execution. [] - member _.Strem(context: ICommandContext, func: string -> unit) = + member _.Stream(context: ICommandContext, func: string -> unit) = Cli.stream (Custom func) context.Context /// Current executing `working directory`. From 247c60af39cdac4dfad92bcdf10a0551e2511bb8 Mon Sep 17 00:00:00 2001 From: flsl0 Date: Wed, 6 Aug 2025 10:21:38 +0200 Subject: [PATCH 05/14] Revert legacy stream option --- README.md | 18 +-------- .../ShellCommandExecuteWindowsTests.fs | 15 -------- src/Fli/CE.fs | 37 ------------------- src/Fli/Command.fs | 28 +++----------- src/Fli/Domain.fs | 4 -- src/Fli/Dsl.fs | 18 --------- 6 files changed, 7 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index a4eed44..7247bfd 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ cli { ``` (Hint: if file extension is not assigned to any installed program, it will throw a `System.NullReferenceException`) -Write output or stream to a specific file: +Write output to a specific file: ```fsharp cli { Exec "dotnet" @@ -84,16 +84,9 @@ cli { Output @"absolute\path\to\dotnet-sdks.txt" } |> Command.execute - -cli { - Exec "dotnet" - Arguments "--list-sdks" - Stream @"absolute\path\to\dotnet-sdks.txt" -} -|> Command.execute ``` -Write output or stream to a function (logging, printing, etc.): +Write output to a function (logging, printing, etc.): ```fsharp let log (output: string) = Debug.Log($"CLI log: {output}") @@ -103,13 +96,6 @@ cli { Output log } |> Command.execute - -cli { - Exec "dotnet" - Arguments "--list-sdks" - Stream log -} -|> Command.execute ``` Add environment variables for the executing program: diff --git a/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs b/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs index 5220330..b2876ad 100644 --- a/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs +++ b/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs @@ -82,21 +82,6 @@ let ``Get output in StringBuilder`` () = sb.ToString() |> should equal "Test\r\n" -[] -[] -let ``Get stream in StringBuilder`` () = - let sb = StringBuilder() - - cli { - Shell CMD - Command "echo Test" - Stream sb - } - |> Command.execute - |> ignore - - sb.ToString() |> should equal "Test" - [] [] let ``CMD returning non zero process id`` () = diff --git a/src/Fli/CE.fs b/src/Fli/CE.fs index 7d18709..3b98d9e 100644 --- a/src/Fli/CE.fs +++ b/src/Fli/CE.fs @@ -55,25 +55,6 @@ module CE = member _.Output(context: ICommandContext, func: string -> unit) = Cli.output (Custom func) context.Context - /// Extra `Stream` that is being executed immediately after getting output from execution. - [] - member _.Stream(context: ICommandContext, output: Outputs) = Cli.stream output context.Context - - /// Extra `Stream` that is being executed immediately after getting output from execution. - [] - member _.Stream(context: ICommandContext, filePath: string) = - Cli.stream (File filePath) context.Context - - /// Extra `Stream` that is being executed immediately after getting output from execution. - [] - member _.Stream(context: ICommandContext, stringBuilder: StringBuilder) = - Cli.stream (StringBuilder stringBuilder) context.Context - - /// Extra `Stream` that is being executed immediately after getting output from execution. - [] - member _.Stream(context: ICommandContext, func: string -> unit) = - Cli.stream (Custom func) context.Context - /// Current executing `working directory`. [] member _.WorkingDirectory(context: ICommandContext, workingDirectory) = @@ -146,24 +127,6 @@ module CE = member _.Output(context: ICommandContext, func: string -> unit) = Program.output (Custom func) context.Context - [] - member _.Stream(context: ICommandContext, output: Outputs) = Program.stream output context.Context - - /// Extra `Output` that is being executed immediately after getting output from execution. - [] - member _.Stream(context: ICommandContext, filePath: string) = - Program.stream (File filePath) context.Context - - /// Extra `Output` that is being executed immediately after getting output from execution. - [] - member _.Stream(context: ICommandContext, stringBuilder: StringBuilder) = - Program.stream (StringBuilder stringBuilder) context.Context - - /// Extra `Output` that is being executed immediately after getting output from execution. - [] - member _.Stream(context: ICommandContext, func: string -> unit) = - Program.stream (Custom func) context.Context - /// Current executing `working directory`. [] member _.WorkingDirectory(context: ICommandContext, workingDirectory) = diff --git a/src/Fli/Command.fs b/src/Fli/Command.fs index c925143..dbbdd45 100644 --- a/src/Fli/Command.fs +++ b/src/Fli/Command.fs @@ -109,27 +109,22 @@ module Command = |> Async.AwaitTask #endif - let private startProcess (inputFunc: Process -> unit) (outputFunc: string -> unit) (streamFunc: string -> unit) (isNotStreaming: bool) psi = + let private startProcess (inputFunc: Process -> unit) (outputFunc: string -> unit) psi = let proc = Process.Start(startInfo = psi) proc |> inputFunc - let mutable text = - if psi.UseShellExecute |> not && isNotStreaming then + let text = + if psi.UseShellExecute |> not then proc.StandardOutput.ReadToEnd() else - proc.BeginOutputReadLine() "" - let mutable error = - if psi.UseShellExecute |> not && isNotStreaming then + let error = + if psi.UseShellExecute |> not then proc.StandardError.ReadToEnd() else - proc.BeginErrorReadLine() "" - proc.OutputDataReceived.Add(fun args -> text <- text + args.Data ; streamFunc args.Data) - proc.ErrorDataReceived.Add(fun args -> error <- error + args.Data ; streamFunc args.Data) - proc.WaitForExit() text |> outputFunc @@ -206,15 +201,6 @@ module Command = | Outputs.Custom(func) -> func.Invoke(output) | None -> () - let private streamOutput (outputType: Outputs option) (output: string): unit = - match outputType with - | Some(o) -> - match o with - | Outputs.File(file) -> File.AppendAllText(file, output) - | Outputs.StringBuilder(stringBuilder) -> output |> stringBuilder.Append |> ignore - | Outputs.Custom(func) -> func.Invoke(output) - | None -> () - let private setupCancellationToken (cancelAfter: int option) = let cts = new CancellationTokenSource() @@ -292,8 +278,6 @@ module Command = |> startProcess (writeInput context.config.Input context.config.Encoding) (writeOutput context.config.Output) - (streamOutput context.config.Stream) - (context.config.Stream.IsNone) /// Executes the given context as a new process. static member execute(context: ExecContext) = @@ -302,8 +286,6 @@ module Command = |> startProcess (writeInput context.config.Input context.config.Encoding) (writeOutput context.config.Output) - (streamOutput context.config.Stream) - (context.config.Stream.IsNone) #if NET /// Executes the given context as a new process asynchronously. diff --git a/src/Fli/Domain.fs b/src/Fli/Domain.fs index 4aa1f02..b1a081d 100644 --- a/src/Fli/Domain.fs +++ b/src/Fli/Domain.fs @@ -14,7 +14,6 @@ module Domain = Command: string option Input: string option Output: Outputs option - Stream: Outputs option WorkingDirectory: string option EnvironmentVariables: (string * string) list option Encoding: Encoding option @@ -47,7 +46,6 @@ module Domain = Arguments: Arguments option Input: string option Output: Outputs option - Stream: Outputs option WorkingDirectory: string option Verb: string option UserName: string option @@ -98,7 +96,6 @@ module Domain = Command = None Input = None Output = None - Stream = None WorkingDirectory = None EnvironmentVariables = None Encoding = None @@ -109,7 +106,6 @@ module Domain = Arguments = None Input = None Output = None - Stream = None WorkingDirectory = None Verb = None UserName = None diff --git a/src/Fli/Dsl.fs b/src/Fli/Dsl.fs index 8cb3084..ed5bd56 100644 --- a/src/Fli/Dsl.fs +++ b/src/Fli/Dsl.fs @@ -27,15 +27,6 @@ module Cli = { context with config.Output = outputsOption } - let stream (output: Outputs) (context: ShellContext) = - let outputsOption = - match output with - | File path -> path |> toOptionWithDefault output - | _ -> Some output - - { context with - config.Stream = outputsOption } - let workingDirectory (workingDirectory: string) (context: ShellContext) = { context with config.WorkingDirectory = Some workingDirectory } @@ -85,15 +76,6 @@ module Program = { context with config.Output = outputsOption } - let stream (output: Outputs) (context: ExecContext) = - let outputsOption = - match output with - | File path -> path |> toOptionWithDefault output - | _ -> Some output - - { context with - config.Stream = outputsOption } - let workingDirectory (workingDirectory: string) (context: ExecContext) = { context with config.WorkingDirectory = Some workingDirectory } From c2eca142a5e9b77f3c6b3b869900339390fe97fa Mon Sep 17 00:00:00 2001 From: flsl0 Date: Wed, 6 Aug 2025 20:02:14 +0200 Subject: [PATCH 06/14] Add new stream option --- src/Fli/CE.fs | 11 +++++++++++ src/Fli/Command.fs | 23 +++++++++++++++++------ src/Fli/Domain.fs | 2 ++ 3 files changed, 30 insertions(+), 6 deletions(-) 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..3bc6af2 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 -> text <- text + args.Data ; outputFunc args.Data) + proc.ErrorDataReceived.Add(fun args -> error <- error + args.Data ; 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,23 @@ 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 -> s.IsStream | _ -> 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 -> s.IsStream | _ -> 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 From d46cc721e1aaef6fcbec745c924b148d3d573059 Mon Sep 17 00:00:00 2001 From: flsl0 Date: Wed, 6 Aug 2025 20:02:36 +0200 Subject: [PATCH 07/14] Add unit test for the new stream option --- .../ShellCommandExecuteWindowsTests.fs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs b/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs index b2876ad..4dc5751 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,21 @@ 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 ``CMD returning non zero process id`` () = From 927a7782d0d682f8cacfd687b5d3587797812684 Mon Sep 17 00:00:00 2001 From: flsl0 Date: Wed, 6 Aug 2025 20:04:39 +0200 Subject: [PATCH 08/14] Update README with a showcase of the new stream using a file stream and console stream --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 7247bfd..4ce8c74 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,25 @@ 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 +``` + Add environment variables for the executing program: ```fsharp cli { From a6d95fb3e71f075f3fd7e255aa67617056d8c72a Mon Sep 17 00:00:00 2001 From: flsl0 Date: Fri, 8 Aug 2025 15:00:04 +0200 Subject: [PATCH 09/14] Update README by adding the stream option in the list of possible outputs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4ce8c74..8fbdbc5 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,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) From 7de6d84a8436ce57f51845f7a2ee95f599930599 Mon Sep 17 00:00:00 2001 From: flsl0 Date: Fri, 8 Aug 2025 22:20:53 +0200 Subject: [PATCH 10/14] Remove the use of the .Is* property in favor of the old fashioned match with pattern --- src/Fli/Command.fs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Fli/Command.fs b/src/Fli/Command.fs index 3bc6af2..86dea75 100644 --- a/src/Fli/Command.fs +++ b/src/Fli/Command.fs @@ -280,7 +280,11 @@ 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 -> s.IsStream | _ -> false + let isStreaming: bool = + match context.config.Output with + | Some s -> match s with Outputs.Stream(s) -> true | _ -> false + | _ -> false + context |> Command.buildProcess |> startProcess @@ -290,7 +294,11 @@ module Command = /// Executes the given context as a new process. static member execute(context: ExecContext) = - let isStreaming: bool = match context.config.Output with Some s -> s.IsStream | _ -> false + let isStreaming: bool = + match context.config.Output with + | Some s -> match s with Outputs.Stream(s) -> true | _ -> false + | _ -> false + context |> Command.buildProcess |> startProcess From f12fb9943e9fedce27d66fdcf2893d42a867fdec Mon Sep 17 00:00:00 2001 From: flsl0 Date: Thu, 14 Aug 2025 13:23:30 +0200 Subject: [PATCH 11/14] Remove saving output and error when streaming + Add new Output.from to save a given string into an output object --- src/Fli/Command.fs | 4 ++-- src/Fli/Output.fs | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Fli/Command.fs b/src/Fli/Command.fs index 86dea75..4ede09c 100644 --- a/src/Fli/Command.fs +++ b/src/Fli/Command.fs @@ -127,8 +127,8 @@ module Command = proc.BeginErrorReadLine() "" - proc.OutputDataReceived.Add(fun args -> text <- text + args.Data ; outputFunc args.Data) - proc.ErrorDataReceived.Add(fun args -> error <- error + args.Data ; outputFunc args.Data) + proc.OutputDataReceived.Add(fun args -> outputFunc args.Data) + proc.ErrorDataReceived.Add(fun args -> outputFunc args.Data) proc.WaitForExit() 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 From c691a108a2fe45207efc27165f2073f82cf924c7 Mon Sep 17 00:00:00 2001 From: flsl0 Date: Thu, 14 Aug 2025 13:24:40 +0200 Subject: [PATCH 12/14] Add unit test for the new method Output.from --- .../ShellCommandExecuteWindowsTests.fs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs b/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs index 4dc5751..b19ae32 100644 --- a/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs +++ b/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs @@ -98,6 +98,28 @@ let ``Get new stream in StringBuilder`` () = 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 + |> _.Text + |> should equal None + + output + |> Output.from (sb.ToString()) + |> Output.toText + |> should contain "Test" + [] [] let ``CMD returning non zero process id`` () = From c13aee6d5e4dcb398937a8d8e1f9c35d53d78056 Mon Sep 17 00:00:00 2001 From: flsl0 Date: Thu, 14 Aug 2025 13:27:16 +0200 Subject: [PATCH 13/14] Add an hint in the README to guide the user on the effect of using the stream option and how to fix it --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 8fbdbc5..3c8ce2f 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,17 @@ cli { } |> 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 From 05cadf90628464586ba04829c63840eeae12b2c4 Mon Sep 17 00:00:00 2001 From: flsl0 Date: Thu, 14 Aug 2025 14:00:49 +0200 Subject: [PATCH 14/14] Minor improvment to the Output.from unit test --- src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs b/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs index b19ae32..f59540b 100644 --- a/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs +++ b/src/Fli.Tests/ShellContext/ShellCommandExecuteWindowsTests.fs @@ -112,8 +112,8 @@ let ``Use from to recreate a valid Output when using stream`` () = |> Command.execute output - |> _.Text - |> should equal None + |> Output.toText + |> should equal "" output |> Output.from (sb.ToString())