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

时间:2016-07-19 12:47:34

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

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

这很好用,因为我得到了一个包含exitcode,stdout和stderr结果的三个元素的元组。

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

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

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

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

总是重定向输出的另一个选项,如果传递的隐藏标志不是真的,那就是调用Console.WriteLine(eachOutput),但这不是很优雅,因为它会一次写入缓冲区,而不会在两者之间插入stderr屏幕上的标准输出线按照它们来的正确顺序排列。对于长时间运行的进程,它会隐藏增量输出,直到进程完成。

那么这里的替代方案是什么?我是否需要诉诸Process班级的该死的事件? :(

干杯

2 个答案:

答案 0 :(得分:5)

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

在这种情况下,它意味着找出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来指定重定向和3元组返回值,就像您最初一样:

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

签名表明我们拥有我们想要的东西:你传递一个命令&amp;参数元组并获得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)    

同样,签名显示我们拥有我们想要的更简单的版本:您传递命令&amp;参数元组,只返回ExitCode:

val Exec : string * string -> int 

答案 1 :(得分:0)

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