【问题标题】:How to read from Process.StandardOutput without redirecting it? (F#)如何从 Process.StandardOutput 读取而不重定向它? (F#)
【发布时间】:2016-11-22 09:07:14
【问题描述】:

我有这个小功能,可以让我免于处理可怕的 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())

这很好用,因为我得到了一个包含三个元素的元组,其中包含退出代码、标准输出和标准错误结果。

现在,假设我不想“隐藏”执行。也就是说,我想编写一个假设的、更简单的 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

但是,如果我可以重构这两个函数以将它们合并为一个函数,并且只需将“隐藏”布尔标志传递给它,那就太好了:

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

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

另一种总是重定向输出的选项,如果传递的隐藏标志不正确,则调用Console.WriteLine(eachOutput),但这不是很优雅,因为它会一次性写入缓冲区,而不会在标准输出行之间插入标准错误在屏幕上以他们来的正确顺序。对于长时间运行的进程,它会隐藏增量输出,直到进程完成。

那么这里的替代方案是什么?我是否需要使用 Process 类中的该死事件? :(

干杯

【问题讨论】:

标签: .net f# system.diagnostics processstartinfo redirectstandardoutput


【解决方案1】:

我会遵循“参数化所有事物”的原则。

在这种情况下,它意味着找到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函数,你可以随意改变返回值的类型。

现在您可以定义 HiddenExec 来指定重定向和三元组返回值,就像您最初所做的那样:

/// 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 

签名表明我们得到了我们想要的:你传递一个命令和参数元组并获得 3 元组作为回报:

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)    

再次,签名表明我们有我们想要的更简单的版本:您传递一个命令和参数元组,然后只得到 ExitCode 作为回报:

val Exec : string * string -> int 

【讨论】:

  • 但是你没有统一返回类型......我的问题更关注.NET API而不是F#实现
  • 您可以通过使用 DU 来表示两种选择(单 int 与元组)轻松统一返回类型。正如您在问题中所说,如果您不进行重定向,您将获得一个模型,如果您这样做,您将获得另一个模型。这正是 DU 可以捕捉到的。对我来说,这似乎很好地模拟了域。你觉得这比使用事件和其他方法更难看吗?
  • 对不起,我期待有人教我如何在缓冲区中捕获标准输出和标准错误,即使我想在屏幕上打印它们
  • 我的错 :) 我误解了你的问题!
  • 我解释得不好是我的错!
【解决方案2】:

@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()

【讨论】:

    猜你喜欢
    • 2011-02-26
    • 2021-02-15
    • 2011-08-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-28
    • 1970-01-01
    相关资源
    最近更新 更多