Skip to content

Commit

Permalink
usage string improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
eiriktsarpalis committed Jun 28, 2016
1 parent 300aa38 commit 588bfee
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 144 deletions.
6 changes: 3 additions & 3 deletions docs/content/tutorial.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ let parser = ArgumentParser.Create<CLIArguments>(programName = "gadget.exe")

(** We can get the automatically generated usage string by typing *)

let usage = parser.Usage()
let usage = parser.PrintUsage()

(** giving
Expand Down Expand Up @@ -204,15 +204,15 @@ Argu is convenient when it comes to automated process spawning:

open System.Diagnostics

let arguments = parser.PrintCommandLineFlat [ Port 42 ; Working_Directory "temp" ]
let arguments = parser.PrintCommandLineArgumentsFlat [ Port 42 ; Working_Directory "temp" ]

Process.Start("foo.exe", arguments)

(**
It can also be used to auto-generate a suitable `AppSettings` configuration file:
*)

let xml = parser.PrintAppSettings [ Port 42 ; Working_Directory "/tmp" ]
let xml = parser.PrintAppSettingsArguments [ Port 42 ; Working_Directory "/tmp" ]

(**
which would yield the following:
Expand Down
4 changes: 2 additions & 2 deletions src/Argu/Argu.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@
<Compile Include="AssemblyInfo.fs" />
<Compile Include="Types.fs" />
<Compile Include="Utils.fs" />
<Compile Include="ConfigReaders.fs" />
<Compile Include="UnionArgInfo.fs" />
<Compile Include="ParseResult.fs" />
<Compile Include="PreCompute.fs" />
<Compile Include="UnParsers.fs" />
<Compile Include="ParseResult.fs" />
<Compile Include="Parsers.fs" />
<Compile Include="ConfigReaders.fs" />
<Compile Include="ArgumentParser.fs" />
<None Include="paket.template" />
</ItemGroup>
Expand Down
70 changes: 44 additions & 26 deletions src/Argu/ArgumentParser.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,37 @@ open FSharp.Reflection
/// that is an F# discriminated union. It can then be used to parse command line arguments
/// or XML configuration.
[<NoEquality; NoComparison; AutoSerializable(false)>]
type ArgumentParser<'Template when 'Template :> IArgParserTemplate> private (argInfo : UnionArgInfo, ?programName : string, ?description : string, ?errorHandler : IExiter) =
type ArgumentParser<'Template when 'Template :> IArgParserTemplate>
internal (argInfo : UnionArgInfo, ?programName : string, ?description : string,
?usageStringCharacterWidth : int, ?errorHandler : IExiter) =

// memoize parser generation for given template type
static let argInfoLazy = lazy(preComputeUnionArgInfo<'Template> ())

let _usageStringCharacterWidth = defaultArg usageStringCharacterWidth 80
let _programName = match programName with Some pn -> pn | None -> currentProgramName.Value
let errorHandler = match errorHandler with Some e -> e | None -> new ExceptionExiter() :> _

let mkUsageString argInfo msgOpt = printUsage argInfo _programName description msgOpt |> String.build

let mkUsageString argInfo msgOpt = printUsage argInfo _programName _usageStringCharacterWidth msgOpt |> StringExpr.build

let (|ParserExn|_|) (e : exn) =
match e with
// do not display usage for App.Config parameter errors
| ParseError (msg, id, _) when id <> ErrorCode.CommandLine -> Some(id, msg)
| ParseError (msg, ErrorCode.AppSettings, _) -> Some(ErrorCode.AppSettings, msg)
| ParseError (msg, id, aI) -> Some (id, mkUsageString aI (Some msg))
| HelpText aI -> Some (ErrorCode.HelpText, mkUsageString aI None)
| HelpText aI -> Some (ErrorCode.HelpText, mkUsageString aI description)
| _ -> None

/// <summary>
/// Creates a new parser instance based on supplied F# union template.
/// </summary>
/// <param name="programName">Program identifier, e.g. 'cat'. Defaults to the current executable name.</param>
/// <param name="description">Program description placed at the top of the usage string.</param>
/// <param name="usageStringCharacterWidth">Text width used when formatting the usage string. Defaults to 80 chars.</param>
/// <param name="errorHandler">The implementation of IExiter used for error handling. Exception is default.</param>
new (?programName : string, ?description : string, ?errorHandler : IExiter) =
new (?programName : string, ?description : string, ?usageStringCharacterWidth : int, ?errorHandler : IExiter) =
new ArgumentParser<'Template>(argInfoLazy.Value, ?programName = programName,
?description = description, ?errorHandler = errorHandler)
?usageStringCharacterWidth = usageStringCharacterWidth, ?description = description, ?errorHandler = errorHandler)

/// Gets the help flags specified for the CLI parser
member __.HelpFlags = argInfo.HelpParam.Flags
Expand All @@ -64,11 +68,11 @@ type ArgumentParser<'Template when 'Template :> IArgParserTemplate> private (arg
let inputs = match inputs with None -> getEnvironmentCommandLineArgs () | Some args -> args

try
let cliResults = parseCommandLine argInfo _programName description errorHandler raiseOnUsage ignoreUnrecognized inputs
let cliResults = parseCommandLine argInfo _programName description _usageStringCharacterWidth errorHandler raiseOnUsage ignoreUnrecognized inputs
let ignoreMissing = (cliResults.IsUsageRequested && not raiseOnUsage) || ignoreMissing
let results = postProcessResults argInfo ignoreMissing None (Some cliResults)

new ParseResult<'Template>(argInfo, results, mkUsageString argInfo, errorHandler)
new ParseResult<'Template>(argInfo, results, _programName, description, _usageStringCharacterWidth, errorHandler)

with ParserExn (errorCode, msg) -> errorHandler.Exit (msg, errorCode)

Expand All @@ -82,7 +86,7 @@ type ArgumentParser<'Template when 'Template :> IArgParserTemplate> private (arg
let appSettingsResults = parseKeyValueConfig configurationReader argInfo
let results = postProcessResults argInfo ignoreMissing (Some appSettingsResults) None

new ParseResult<'Template>(argInfo, results, mkUsageString argInfo, errorHandler)
new ParseResult<'Template>(argInfo, results, _programName, description, _usageStringCharacterWidth, errorHandler)

with ParserExn (errorCode, msg) -> errorHandler.Exit (msg, errorCode)

Expand All @@ -102,10 +106,10 @@ type ArgumentParser<'Template when 'Template :> IArgParserTemplate> private (arg

try
let appSettingsResults = parseKeyValueConfig configurationReader argInfo
let cliResults = parseCommandLine argInfo _programName description errorHandler raiseOnUsage ignoreUnrecognized inputs
let cliResults = parseCommandLine argInfo _programName description _usageStringCharacterWidth errorHandler raiseOnUsage ignoreUnrecognized inputs
let results = postProcessResults argInfo ignoreMissing (Some appSettingsResults) (Some cliResults)

new ParseResult<'Template>(argInfo, results, mkUsageString argInfo, errorHandler)
new ParseResult<'Template>(argInfo, results, _programName, description, _usageStringCharacterWidth, errorHandler)

with ParserExn (errorCode, msg) -> errorHandler.Exit (msg, errorCode)

Expand Down Expand Up @@ -134,11 +138,7 @@ type ArgumentParser<'Template when 'Template :> IArgParserTemplate> private (arg
/// </summary>
/// <param name="inputs">Argument input sequence.</param>
member __.ToParseResult (inputs : seq<'Template>) : ParseResult<'Template> =
mkParseResultFromValues argInfo errorHandler (mkUsageString argInfo) inputs

/// <summary>Returns the usage string.</summary>
/// <param name="message">The message to be displayed on top of the usage string.</param>
member __.Usage (?message : string) : string = mkUsageString argInfo message
mkParseResultFromValues argInfo errorHandler _usageStringCharacterWidth _programName description inputs

/// <summary>
/// Gets a subparser associated with specific subcommand instance
Expand Down Expand Up @@ -174,26 +174,37 @@ type ArgumentParser<'Template when 'Template :> IArgParserTemplate> private (arg
let uci = expr2Uci ctorExpr
argInfo.Cases.[uci.Tag].ToArgumentCaseInfo()

/// <summary>Formats a usage string for the argument parser.</summary>
/// <param name="message">The message to be displayed on top of the usage string.</param>
/// <param name="programName">Override the default program name settings.</param>
/// <param name="usageStringCharacterWidth">Text width used when formatting the usage string.</param>
member __.PrintUsage (?message : string, ?programName : string, ?usageStringCharacterWidth : int) : string =
let programName = defaultArg programName _programName
let usageStringCharacterWidth = defaultArg usageStringCharacterWidth _usageStringCharacterWidth
printUsage argInfo programName usageStringCharacterWidth message |> StringExpr.build

/// <summary>
/// Prints command line syntax. Useful for generating documentation.
/// </summary>
/// <param name="programName">Program name identifier placed at start of syntax string</param>
member __.PrintCommandLineSyntax (?programName : string) : string =
/// <param name="usageStringCharacterWidth">Text width used when formatting the usage string.</param>
member __.PrintCommandLineSyntax (?programName : string, ?usageStringCharacterWidth : int) : string =
let programName = defaultArg programName _programName
printCommandLineSyntax argInfo programName |> String.build
let usageStringCharacterWidth = defaultArg usageStringCharacterWidth _usageStringCharacterWidth
printCommandLineSyntax argInfo "" usageStringCharacterWidth programName |> StringExpr.build

/// <summary>Prints parameters in command line format. Useful for argument string generation.</summary>
member __.PrintCommandLine (args : 'Template list) : string [] =
member __.PrintCommandLineArguments (args : 'Template list) : string [] =
printCommandLineArgs argInfo (Seq.cast args) |> Seq.toArray

/// <summary>Prints parameters in command line format. Useful for argument string generation.</summary>
member __.PrintCommandLineFlat (args : 'Template list) : string =
__.PrintCommandLine args |> flattenCliTokens
member __.PrintCommandLineArgumentsFlat (args : 'Template list) : string =
__.PrintCommandLineArguments args |> flattenCliTokens

/// <summary>Prints parameters in App.Config format.</summary>
/// <param name="args">The parameters that fill out the XML document.</param>
/// <param name="printComments">Print XML comments over every configuration entry.</param>
member __.PrintAppSettings (args : 'Template list, ?printComments : bool) : string =
member __.PrintAppSettingsArguments (args : 'Template list, ?printComments : bool) : string =
let printComments = defaultArg printComments true
let xmlDoc = printAppSettings argInfo printComments args
use writer = { new System.IO.StringWriter() with member __.Encoding = System.Text.Encoding.UTF8 }
Expand All @@ -212,14 +223,21 @@ type ArgumentParser =
/// </summary>
/// <param name="programName">Program identifier, e.g. 'cat'. Defaults to the current executable name.</param>
/// <param name="description">Program description placed at the top of the usage string.</param>
/// <param name="usageStringCharacterWidth">Text width used when formatting the usage string. Defaults to 80 chars.</param>
/// <param name="errorHandler">The implementation of IExiter used for error handling. Exception is default.</param>
static member Create<'Template when 'Template :> IArgParserTemplate>(?programName : string, ?description : string, ?errorHandler : IExiter) =
new ArgumentParser<'Template>(?programName = programName, ?description = description, ?errorHandler = errorHandler)
static member Create<'Template when 'Template :> IArgParserTemplate>(?programName : string, ?description : string, ?usageStringCharacterWidth : int, ?errorHandler : IExiter) =
new ArgumentParser<'Template>(?programName = programName, ?description = description, ?errorHandler = errorHandler, ?usageStringCharacterWidth = usageStringCharacterWidth)


[<AutoOpen>]
module ArgumentParserUtils =


type ParseResult<'Template when 'Template :> IArgParserTemplate> with
member r.Parser =
new ArgumentParser<'Template>(r.ArgInfo, r.ProgramName, ?description = r.Description,
usageStringCharacterWidth = r.CharacterWidth,
errorHandler = r.ErrorHandler)

/// converts a sequence of inputs to a ParseResult instance
let toParseResults (inputs : seq<'Template>) : ParseResult<'Template> =
ArgumentParser.Create<'Template>().ToParseResult(inputs)
Expand Down
49 changes: 24 additions & 25 deletions src/Argu/ParseResult.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@

open FSharp.Quotations

type private IParseResults =
abstract GetAllResults : unit -> seq<obj>

/// Argument parsing result holder.
[<Sealed; AutoSerializable(false); StructuredFormatDisplay("{StructuredFormatDisplay}")>]
type ParseResult<'Template when 'Template :> IArgParserTemplate>
internal (argInfo : UnionArgInfo, results : UnionParseResults, mkUsageString : string option -> string, exiter : IExiter) =
internal (argInfo : UnionArgInfo, results : UnionParseResults, programName : string, description : string option, usageStringCharWidth : int, exiter : IExiter) =

let mkUsageString message = printUsage argInfo programName usageStringCharWidth message |> StringExpr.build

// error handler functions
let error hideUsage code msg =
if hideUsage then exiter.Exit(msg, code)
else exiter.Exit(mkUsageString (Some msg), code)

// exiter wrapper
let exit hideUsage msg id =
if hideUsage then exiter.Exit(msg, id)
else exiter.Exit(mkUsageString (Some msg), id)
let errorf hideusage code fmt = Printf.ksprintf (error hideusage code) fmt

// restriction predicate based on optional parse source
let restrictF flags : UnionCaseParseResult -> bool =
Expand All @@ -26,32 +27,29 @@ type ParseResult<'Template when 'Template :> IArgParserTemplate>
let getResult rs (e : Expr) =
let id = expr2Uci e
let results = results.Cases.[id.Tag]
match Seq.tryLast results with
match Array.tryLast results with
| None ->
let aI = argInfo.Cases.[id.Tag]
exit aI.NoCommandLine (sprintf "missing argument '%s'." aI.Name) ErrorCode.PostProcess
| Some r ->
if restrictF rs r then r
else
let aI = r.ArgInfo
exit aI.NoCommandLine (sprintf "missing argument '%s'." aI.Name) ErrorCode.PostProcess
errorf aI.NoCommandLine ErrorCode.PostProcess "ERROR: missing argument '%s'." aI.Name
| Some r when restrictF rs r -> r
| Some r -> errorf r.ArgInfo.NoCommandLine ErrorCode.PostProcess "ERROR: missing argument '%s'." r.ArgInfo.Name

let parseResult (f : 'F -> 'S) (r : UnionCaseParseResult) =
try f (r.FieldContents :?> 'F)
with e ->
exit r.ArgInfo.NoCommandLine (sprintf "Error parsing '%s': %s" r.ParseContext e.Message) ErrorCode.PostProcess
with e -> errorf r.ArgInfo.NoCommandLine ErrorCode.PostProcess "ERROR parsing '%s': %s" r.ParseContext e.Message

interface IParseResults with
member __.GetAllResults () =
__.GetAllResults() |> Seq.map box
interface IParseResult with
member __.GetAllResults () = __.GetAllResults() |> Seq.map box

member __.ErrorHandler = exiter
member internal __.ProgramName = programName
member internal __.Description = description
member internal __.ArgInfo = argInfo
member internal __.CharacterWidth = usageStringCharWidth

/// Returns true if '--help' parameter has been specified in the command line.
member __.IsUsageRequested = results.IsUsageRequested

/// <summary>Returns the usage string.</summary>
/// <param name="message">The message to be displayed on top of the usage string.</param>
member __.Usage (?message : string) : string = mkUsageString message

/// Gets all unrecognized CLI parameters which
/// accumulates if parsed with 'ignoreUnrecognized = true'
member __.UnrecognizedCliParams = results.UnrecognizedCliParams
Expand Down Expand Up @@ -126,8 +124,9 @@ type ParseResult<'Template when 'Template :> IArgParserTemplate>
/// <param name="errorCode">The error code to be returned.</param>
/// <param name="showUsage">Print usage together with error message.</param>
member __.Raise (msg : string, ?errorCode : ErrorCode, ?showUsage : bool) : 'T =
let errorCode = defaultArg errorCode ErrorCode.PostProcess
let showUsage = defaultArg showUsage true
exit (not showUsage) msg (defaultArg errorCode ErrorCode.PostProcess)
error (not showUsage) errorCode msg

/// <summary>Raise an error through the argument parser's exiter mechanism. Display usage optionally.</summary>
/// <param name="error">The error to be displayed.</param>
Expand Down
Loading

0 comments on commit 588bfee

Please sign in to comment.