F# Console Application Template

If you are like me and often use console applications for a variety of purposes you would have found the F# template not much use (in fact a blank code file). As such I decided to put together a more complete Project Template that I could use.

The template can be found on the Visual Studio Gallery:

https://visualstudiogallery.msdn.microsoft.com/031891a9-06c3-47db-9c7d-8c9d4a32546a

When the template is installed you get the following template added to your F# folder when creating a new F# project:

image

The Project Template creates a Windows Console Application with the following components:

  • Program.fs: The application entry point defined
  • Arguments.fs: A simple command line parser placing all arguments into a dictionary
  • MyConsole.fs: A separate module for the custom console application code

The application entry point is quite simple:

module Program =   

    [<EntryPoint>]
    let Main(args) =

        MyConsole.Run args

        // main entry point return
        0

The custom console application handles the argument parsing, in addition to providing a TODO for the custom code:

module MyConsole =
        
    let Run args =

        // Define what arguments are expected
        let defs = [
            {ArgInfo.Command="a1"; Description="Argument 1"; Required=true };
            {ArgInfo.Command="a2"; Description="Argument 2"; Required=false } ]

        // Parse Arguments into a Dictionary
        let parsedArgs = Arguments.ParseArgs args defs
        
        // TODO add your code here
        Arguments.DisplayArgs parsedArgs
        Console.ReadLine() |> ignore

If one does not wish to use the argument parsing module the ArgInfo list can be removed. This list provides a simple means for ensuring that the start-up arguments are as expected. The returned parsed parameters is merely a Dictionary of the found arguments.

If one so desires one could map this over to the argument parsing provided by the FSharp PowerPack.

For completeness here is the argument parsing code:

module Arguments =

    // Type for simple argument checking
    type ArgInfo = { Command:string; Description:string; Required:bool }

    // Displays the help arguments
    let DisplayHelp (defs:ArgInfo list) =
        match defs with
        | [] -> Console.WriteLine "No help text defined."
        | _ ->
            Console.WriteLine "Command Arguments:"
            defs
            |> List.iter (fun def ->
                let helpText = sprintf "-%s (Required=%b) : %s" def.Command def.Required def.Description
                Console.WriteLine helpText )

    // Displays the found arguments
    let DisplayArgs (args:Dictionary<string, string>) =
        match args.Keys.Count with
        | 0 -> Console.WriteLine "No arguments found."
        | _ ->
            Console.WriteLine "Arguments Found:"
            for arg in args.Keys do
                if String.IsNullOrEmpty(args.[arg]) then
                    Console.WriteLine (sprintf "-%s" arg)
                else
                    Console.WriteLine (sprintf "-%s '%s'" arg args.[arg])

    // Parse the input arguments
    let ParseArgs (args:string array) (defs:ArgInfo list) =

        let parsedArgs = new Dictionary<string, string>()

        // Ensure help is supported if defintions provided
        let fullDefs =
            if not (List.exists (fun def -> String.Equals(def.Command, "help")) defs) then
                {ArgInfo.Command="help"; Description="Display Help Text"; Required=false } :: defs
            else
                defs

        // Report errors
        let reportError errorText =        
            DisplayArgs parsedArgs
            DisplayHelp fullDefs
            let errMessage = sprintf "Error occured: %A" errorText
            Console.Error.WriteLine errMessage
            Console.Error.Flush()
            Environment.Exit(1)

        // Capture variables
        let captureArg command value =
            match defs with
            | [] -> parsedArgs.Add(command, value)
            | _ ->                
                if not (List.exists (fun def -> String.Equals(def.Command, command)) fullDefs) then
                    reportError (sprintf "Command '%s' Not in definition list." command)
                else
                    parsedArgs.Add(command, value)            

        let (|IsCommand|_|) (command:string) =            
            let m = Regex.Match(command, "^(?:-{1,2}|\/)(?<command>.*)$", RegexOptions.IgnoreCase)
            if m.Success then Some(m.Groups.["command"].Value.ToLower()) else None

        let rec loop (argList:string list) =
            match argList with
            | [] -> ()
            | head::tail ->
                match head with
                | IsCommand command ->
                    match tail with
                    | [] -> captureArg command String.Empty
                    | iHead::iTail ->
                        match iHead with
                        | IsCommand iCommand ->
                            captureArg command String.Empty
                            loop tail
                        | _ ->
                            captureArg command iHead
                            loop iTail
                | _ -> reportError (sprintf "Expected a command but got '%s'" head)
        loop (Array.toList args)
        
        // Look to see if help has been requested if not check for required
        if (parsedArgs.ContainsKey("help")) then
            DisplayHelp defs
        else
            defs
            |> List.filter (fun def -> def.Required)
            |> List.iter ( fun def ->
                if not (parsedArgs.ContainsKey(def.Command)) then
                    reportError (sprintf "Command '%s' found but in argument list." def.Command))
                  
        parsedArgs

As always, hopefully you will find this template useful.