如何在不重定向的情况下读取 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"原则。
在这种情况下,它意味着找到HiddenExec
和Exec
之间的差异,然后用函数参数化这些差异。
这是我这样做的结果:
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()
我有这个小功能可以让我免于处理可怕的事情 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"原则。
在这种情况下,它意味着找到HiddenExec
和Exec
之间的差异,然后用函数参数化这些差异。
这是我这样做的结果:
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:
中得到 ExitCodeval 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()