我有这个小功能,可以省去处理可怕的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
班级的该死的事件? :(
干杯
答案 0 :(得分:5)
我会遵循“参数化所有事物”的原则。
在这种情况下,它意味着找出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
函数,您可以根据需要更改返回值的类型。
现在您可以定义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()