如何在不重定向的情况下读取 Process.StandardOutput? (F#)

How to read from Process.StandardOutput without redirecting it? (F#)

我有这个小功能可以让我免于处理可怕的事情 System.Diagnostics.Process API:

let HiddenExec (command: string, arguments: string) =
    let startInfo = new System.Diagnostics.ProcessStartInfo(command)
    startInfo.Arguments <- arguments
    startInfo.UseShellExecute <- false

    startInfo.RedirectStandardError <- true
    startInfo.RedirectStandardOutput <- true

    use proc = System.Diagnostics.Process.Start(startInfo)
    proc.WaitForExit()
    (proc.ExitCode,proc.StandardOutput.ReadToEnd(),proc.StandardError.ReadToEnd())

效果很好,因为我得到了一个包含退出代码、stdout 和 stderr 结果的三个元素的元组。

现在,假设我不想 "hide" 执行。也就是说,我想编写一个假设的、更简单的 Exec 函数。那么解决方案就是不重定向 stdout/stderr 就大功告成了:

let Exec (command: string, arguments: string) =
    let startInfo = new System.Diagnostics.ProcessStartInfo(command)
    startInfo.Arguments <- arguments
    startInfo.UseShellExecute <- false

    let proc = System.Diagnostics.Process.Start(startInfo)
    proc.WaitForExit()
    proc.ExitCode

但是,如果我可以重构这两个函数以将它们合并为一个函数,并且只向它传递一个 "hidden" bool 标志,那就太好了:

let NewExec (command: string, arguments: string, hidden: bool) =

这样,NewExec(_,_,false) 也会 return stdout,stderr(不仅是 exitCode,和以前一样)。问题是,如果我不执行重定向舞蹈 (startInfo.RedirectStandardError <- true),那么稍后我将无法通过 proc.StandardOutput.ReadToEnd() 从输出中读取,因为我收到错误 StandardOut has not been redirected or the process hasn't started yet.

另一种始终重定向输出的选项,如果传递的隐藏标志不为真,将调用 Console.WriteLine(eachOutput),但这不是很优雅,因为它会一次性写入缓冲区,而不插入屏幕中 stdout 行之间的 stderr 按它们出现的正确顺序。对于长 运行 进程,它将隐藏增量输出,直到进程完成。

那么这里还有什么选择呢?我是否需要求助于 Process class 中的那些该死的事件? :(

干杯

我会遵循"parameterize all the things"原则。

在这种情况下,它意味着找到HiddenExecExec之间的差异,然后用函数参数化这些差异。

这是我这样做的结果:

let ExecWith configureStartInfo returnFromProc (command: string, arguments: string) =
    let startInfo = new System.Diagnostics.ProcessStartInfo(command)
    startInfo.Arguments <- arguments
    startInfo.UseShellExecute <- false

    // parameterize this bit
    configureStartInfo startInfo

    use proc = System.Diagnostics.Process.Start(startInfo)
    proc.WaitForExit()

    // parameterize this bit too
    returnFromProc proc

请注意,通过传入各种 returnFromProc 函数,您可以根据需要更改 return 值的类型。

现在您可以像最初那样定义 HiddenExec 来指定重定向和三元组 return 值:

/// Specialize ExecWith to redirect the output.
/// Return the exit code and the output and error.
/// Signature: string * string -> int * string * string
let HiddenExec =

    let configureStartInfo (startInfo: System.Diagnostics.ProcessStartInfo) =
        startInfo.RedirectStandardError <- true
        startInfo.RedirectStandardOutput <- true

    let returnFromProc (proc:System.Diagnostics.Process) =       
        (proc.ExitCode,proc.StandardOutput.ReadToEnd(),proc.StandardError.ReadToEnd())

    // partial application -- the command & arguments are passed later
    ExecWith configureStartInfo returnFromProc 

签名表明我们得到了我们想要的:你传递一个命令和参数元组并在 return:

中获得三元组
val HiddenExec : string * string -> int * string * string

请注意,我在这里使用的是部分应用程序。我也可以用这样的显式参数定义 HiddenExec

let HiddenExec (command, arguments) =  // (command, arguments) passed here

    let configureStartInfo ...
    let returnFromProc ...

    ExecWith configureStartInfo returnFromProc (command, arguments) // (command, arguments) passed here

类似地,您可以将 Exec 定义为 而不是 使用重定向,如下所示:

/// Specialize ExecWith to not redirect the output.
/// Return the exit code.
/// Signature: string * string -> int
let Exec =

    let configureStartInfo _  =
        ()  // ignore the input

    let returnFromProc (proc:System.Diagnostics.Process) = 
        proc.ExitCode    

    ExecWith configureStartInfo returnFromProc

    // alternative version using `ignore` and lambda
    // ExecWith ignore (fun proc -> proc.ExitCode)    

同样,签名表明我们有我们想要的更简单的版本:你传递一个命令和参数元组,并在 return:

中得到 ExitCode
val Exec : string * string -> int 

@Groundoon 解决方案不完全是我要求的:)

最后我把this solution in C#移植到F#:

let private procTimeout = TimeSpan.FromSeconds(float 10)

let Execute (commandWithArguments: string, echo: bool, hidden: bool)
    : int * string * string =

    let outBuilder = new StringBuilder()
    let errBuilder = new StringBuilder()

    use outWaitHandle = new AutoResetEvent(false)
    use errWaitHandle = new AutoResetEvent(false)

    if (echo) then
        Console.WriteLine(commandWithArguments)

    let firstSpaceAt = commandWithArguments.IndexOf(" ")
    let (command, args) =
        if (firstSpaceAt >= 0) then
            (commandWithArguments.Substring(0, firstSpaceAt), commandWithArguments.Substring(firstSpaceAt + 1))
        else
            (commandWithArguments, String.Empty)

    let startInfo = new ProcessStartInfo(command, args)
    startInfo.UseShellExecute <- false
    startInfo.RedirectStandardOutput <- true
    startInfo.RedirectStandardError <- true
    use proc = new Process()
    proc.StartInfo <- startInfo

    let outReceived (e: DataReceivedEventArgs): unit =
        if (e.Data = null) then
            outWaitHandle.Set() |> ignore
        else
            if not (hidden) then
                Console.WriteLine(e.Data)
            outBuilder.AppendLine(e.Data) |> ignore

    let errReceived (e: DataReceivedEventArgs): unit =
        if (e.Data = null) then
            errWaitHandle.Set() |> ignore
        else
            if not (hidden) then
                Console.Error.WriteLine(e.Data)
            errBuilder.AppendLine(e.Data) |> ignore

    proc.OutputDataReceived.Add outReceived
    proc.ErrorDataReceived.Add errReceived

    let exitCode =
        try
            proc.Start() |> ignore
            proc.BeginOutputReadLine()
            proc.BeginErrorReadLine()

            if (proc.WaitForExit(int procTimeout.TotalMilliseconds)) then
                proc.ExitCode
            else
                failwith String.Format("Timeout expired for process '{0}'", commandWithArguments)

        finally
            outWaitHandle.WaitOne(procTimeout) |> ignore
            errWaitHandle.WaitOne(procTimeout) |> ignore

    exitCode,outBuilder.ToString(),errBuilder.ToString()