<commit_before>﻿[<AutoOpen>]
module Fake.ProcessHelper

open System
open System.ComponentModel
open System.Diagnostics
open System.IO
open System.Threading

let mutable redirectOutputToTrace = false 

/// Runs the given process
/// returns the exit code
let execProcess2 infoAction silent =
    use p = new Process()
    p.StartInfo.UseShellExecute <- false
    infoAction p.StartInfo
    if silent then
        p.StartInfo.RedirectStandardOutput <- true
        p.StartInfo.RedirectStandardError <- true
        p.ErrorDataReceived.Add(fun d -> if d.Data <> null then traceError d.Data)
        p.OutputDataReceived.Add(fun d -> if d.Data <> null then trace d.Data)
    try
        p.Start() |> ignore
    with
    | exn -> failwithf "Could not execute %s. %s" p.StartInfo.FileName exn.Message
    
    if silent then 
        p.BeginErrorReadLine()
        p.BeginOutputReadLine()     
  
    p.WaitForExit()
    
    p.ExitCode  

/// Runs the given process
/// returns the exit code
let execProcessAndReturnExitCode infoAction = execProcess2 infoAction true

/// Runs the given process
/// returns if the exit code was 0
let execProcess3 infoAction = execProcessAndReturnExitCode infoAction = 0   

/// Runs the given process
/// returns the exit code
let ExecProcess infoAction = execProcess2 infoAction redirectOutputToTrace
  
/// sets the environment Settings for the given startInfo
/// existing values will be overrriden
let setEnvironmentVariables (startInfo:ProcessStartInfo) environmentSettings = 
  for key,value in environmentSettings do
    if startInfo.EnvironmentVariables.ContainsKey key then
      startInfo.EnvironmentVariables.[key] <- value
    else
      startInfo.EnvironmentVariables.Add(key, value)
          
/// Runs the given process
/// returns true if the exit code was 0
let execProcess infoAction = ExecProcess infoAction = 0    

/// Adds quotes around the string   
let toParam x = " \"" + x + "\" " 
 
/// Use default Parameters
let UseDefaults = id

/// Searches the given directories for all occurrences of the given file name
let tryFindFile dirs file =
    let files = 
        dirs
          |> Seq.map 
               (fun (path:string) ->
                   let dir =
                     path
                       |> replace "[ProgramFiles]" ProgramFiles
                       |> replace "[ProgramFilesX86]" ProgramFilesX86
                       |> directoryInfo
                   if not dir.Exists then "" else
                   let fi = dir.FullName @@ file |> fileInfo
                   if fi.Exists then fi.FullName else "")
          |> Seq.filter ((<>) "")
          |> Seq.cache

    if not (Seq.isEmpty files) then
        Some (Seq.head files)
    else
        None

/// Searches the given directories for the given file, failing if not found
let findFile dirs file =
    match tryFindFile dirs file with
    | Some found -> found
    | None -> failwithf "%s not found in %A." file dirs

/// Returns the AppSettings for the key - Splitted on ;
let appSettings (key:string) = System.Configuration.ConfigurationManager.AppSettings.[key].Split(';')

/// Tries to find the tool via AppSettings. If no path has the right tool we are trying the PATH system variable. 
let findPath settingsName tool = 
    let paths = appSettings settingsName
    match tryFindFile paths tool with
    | Some file -> file
    | None -> tool<commit_msg>Allow to run external programs as asynchronous workflows.<commit_after>﻿[<AutoOpen>]
module Fake.ProcessHelper

open System
open System.ComponentModel
open System.Diagnostics
open System.IO
open System.Threading

let mutable redirectOutputToTrace = false 

/// Runs the given process
/// returns the exit code
let execProcess2 infoAction silent =
    use p = new Process()
    p.StartInfo.UseShellExecute <- false
    infoAction p.StartInfo
    if silent then
        p.StartInfo.RedirectStandardOutput <- true
        p.StartInfo.RedirectStandardError <- true
        p.ErrorDataReceived.Add(fun d -> if d.Data <> null then traceError d.Data)
        p.OutputDataReceived.Add(fun d -> if d.Data <> null then trace d.Data)
    try
        p.Start() |> ignore
    with
    | exn -> failwithf "Could not execute %s. %s" p.StartInfo.FileName exn.Message
    
    if silent then 
        p.BeginErrorReadLine()
        p.BeginOutputReadLine()     
  
    p.WaitForExit()
    
    p.ExitCode  

/// Runs the given process
/// returns the exit code
let execProcessAndReturnExitCode infoAction = execProcess2 infoAction true

/// Runs the given process
/// returns if the exit code was 0
let execProcess3 infoAction = execProcessAndReturnExitCode infoAction = 0   

/// Runs the given process
/// returns the exit code
let ExecProcess infoAction = execProcess2 infoAction redirectOutputToTrace
  
/// sets the environment Settings for the given startInfo
/// existing values will be overrriden
let setEnvironmentVariables (startInfo:ProcessStartInfo) environmentSettings = 
  for key,value in environmentSettings do
    if startInfo.EnvironmentVariables.ContainsKey key then
      startInfo.EnvironmentVariables.[key] <- value
    else
      startInfo.EnvironmentVariables.Add(key, value)
          
/// Runs the given process
/// returns true if the exit code was 0
let execProcess infoAction = ExecProcess infoAction = 0    

/// Adds quotes around the string if needed
let quote str =
    if isNullOrEmpty str then
        ""
    elif str.Contains " " then
        "\"" + str + "\""
    else
        str

/// Adds quotes and a blank around the string   
let toParam x = " " + quote x
 
/// Use default Parameters
let UseDefaults = id

/// Searches the given directories for all occurrences of the given file name
let tryFindFile dirs file =
    let files = 
        dirs
          |> Seq.map 
               (fun (path:string) ->
                   let dir =
                     path
                       |> replace "[ProgramFiles]" ProgramFiles
                       |> replace "[ProgramFilesX86]" ProgramFilesX86
                       |> directoryInfo
                   if not dir.Exists then "" else
                   let fi = dir.FullName @@ file |> fileInfo
                   if fi.Exists then fi.FullName else "")
          |> Seq.filter ((<>) "")
          |> Seq.cache

    if not (Seq.isEmpty files) then
        Some (Seq.head files)
    else
        None

/// Searches the given directories for the given file, failing if not found
let findFile dirs file =
    match tryFindFile dirs file with
    | Some found -> found
    | None -> failwithf "%s not found in %A." file dirs

/// Returns the AppSettings for the key - Splitted on ;
let appSettings (key:string) = System.Configuration.ConfigurationManager.AppSettings.[key].Split(';')

/// Tries to find the tool via AppSettings. If no path has the right tool we are trying the PATH system variable. 
let findPath settingsName tool = 
    let paths = appSettings settingsName
    match tryFindFile paths tool with
    | Some file -> file
    | None -> tool


// See: http://stackoverflow.com/questions/2649161/need-help-regarding-async-and-fsi/
module Event =
    let guard f (e:IEvent<'Del, 'Args>) = 
        let e = Event.map id e
        { new IEvent<'Args> with 
          member this.AddHandler d = 
             e.AddHandler d 
             f() //must call f here!
          member this.RemoveHandler d = e.RemoveHandler d
          member this.Subscribe observer = let rm = e.Subscribe observer in f(); rm }

type ExecParams = {
    /// The path to the executable, without arguments. 
    Program          : string
    /// The working directory for the program. Defaults to "".
    WorkingDirectory : string
    /// Command-line parameters in a string.
    CommandLine      : string
    /// Command-line argument pairs. The value will be quoted if it contains
    /// a string, and the result will be appended to the CommandLine property.
    /// If the key ends in a letter or number, a space will be inserted between
    /// the key and the value.
    Args             : (string * string) list
}

let defaultParams = {
    Program          = ""
    WorkingDirectory = ""
    CommandLine      = ""
    Args             = []
}

let private formatArgs args =
    let delimit (str:string) =
        if isLetterOrDigit (str.Chars(str.Length - 1))
        then str + " " else str

    args
    |> Seq.map (fun (k, v) -> delimit k + quote v)
    |> separated " "

/// Execute an external program asynchronously and return the exit code,
/// logging output and error messages to FAKE output. You can compose the result
/// with Async.Parallel to run multiple external programs at once, but be
/// sure that none of them depend on the output of another.
let asyncShellExec (args:ExecParams) = async {
    if isNullOrEmpty args.Program then
        invalidArg "args" "You must specify a program to run!"
    let commandLine = args.CommandLine + " " + formatArgs args.Args
    let info = ProcessStartInfo( args.Program, 
                                 UseShellExecute = false,
                                 RedirectStandardError = true,
                                 RedirectStandardOutput = true,
                                 WindowStyle = ProcessWindowStyle.Hidden,
                                 WorkingDirectory = args.WorkingDirectory,
                                 Arguments = commandLine )

    use proc = new Process(StartInfo = info, EnableRaisingEvents = true)
    proc.ErrorDataReceived.Add(fun e -> if e.Data <> null then traceError e.Data)
    proc.OutputDataReceived.Add(fun e -> if e.Data <> null then trace e.Data)
    
    let! exit = proc.Exited 
                |> Event.guard (fun () -> proc.Start() |> ignore
                                          proc.BeginErrorReadLine()
                                          proc.BeginOutputReadLine())
                |> Async.AwaitEvent
    
    return proc.ExitCode
}

/// Execute an external program and return the exit code.
let shellExec = asyncShellExec >> Async.RunSynchronously
{'fraction_non_alphanumeric': 0.08032328876471762, 'fraction_numerical': 0.0031929754540011976, 'mean_word_length': 2.2094140249759846, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 58, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2469229', 'n_tokens_mistral': 2687, 'n_tokens_neox': 2557, 'n_words': 973}