diff --git a/src/Command.fs b/src/Command.fs index 32fbede..b31b6b3 100644 --- a/src/Command.fs +++ b/src/Command.fs @@ -171,6 +171,23 @@ module internal Commands = |> format |> output.GroupedOptions CommandName.NamespaceSeparator "Available commands:" + let private showUsage (output: Output) = + output.SimpleOptions "Usage:" [ + [ "command [options] [--] [arguments]"; "" ] + ] + + let private showOptions (output: Output) applicationOptions = + applicationOptions + |> OptionsDefinitions.format + |> output.SimpleOptions "Options:" + + let showApplicationHelp (output: Output) applicationOptions commands = + showUsage output + showOptions output applicationOptions + + commands + |> showAvailable output + let add name command (commands: Commands) = commands.Add (CommandName (Name name), command) @@ -348,16 +365,6 @@ module internal Commands = Initialize = None Interact = None Execute = Execute <| fun (input, output) -> - let showUsage () = - output.SimpleOptions "Usage:" [ - [ "command [options] [--] [arguments]"; "" ] - ] - - let showOptions () = - applicationOptions - |> OptionsDefinitions.format - |> output.SimpleOptions "Options:" - match input with | Input.Argument.OptionalValue "namespace" namespaceToList -> match commands |> byNamespace namespaceToList with @@ -365,8 +372,8 @@ module internal Commands = output.Error <| sprintf "There are no commands defined in the \"%s\" namespace.\n" namespaceToList ExitCode.Error | commandsByNamespace -> - showUsage() - showOptions() + showUsage output + showOptions output applicationOptions commandsByNamespace |> format @@ -374,11 +381,7 @@ module internal Commands = ExitCode.Success | _ -> - showUsage() - showOptions() - - commands - |> showAvailable output + showApplicationHelp output applicationOptions commands ExitCode.Success } diff --git a/src/ConsoleApplication.fs b/src/ConsoleApplication.fs index e4a000f..12be617 100644 --- a/src/ConsoleApplication.fs +++ b/src/ConsoleApplication.fs @@ -76,9 +76,9 @@ module MFConsoleApplication = let (|ContainsOption|_|) option (args: Args) = args |> containsOption option |> Bool.toOption - let getCommandName (args: Args) = + let tryGetCommandName (args: Args) = args - |> Array.pick (function + |> Array.tryPick (function | CommandName.IsCommandName commandName -> Some commandName | _ -> None ) @@ -148,6 +148,8 @@ module MFConsoleApplication = let args = match args with + | Args.ContainsOption OptionsDefinitions.help + | Args.ContainsOption OptionsDefinitions.version -> args | Args.Empty | Args.ContainsOnlyOptions -> [| @@ -160,19 +162,23 @@ module MFConsoleApplication = | Args.ContainsOption OptionsDefinitions.help -> parts |> showApplicationInfo - let rawCommandName = args |> Args.getCommandName + match args |> Args.tryGetCommandName with + | Some rawCommandName -> + return! + match parts.Commands |> Commands.find rawCommandName with + | ExactlyOne (commandName, command) -> + command + |> Help.showForCommand output parts.OptionDecorationLevel parts.ApplicationOptions commandName - return! - match parts.Commands |> Commands.find rawCommandName with - | ExactlyOne (commandName, command) -> - command - |> Help.showForCommand output parts.OptionDecorationLevel parts.ApplicationOptions commandName + Ok ExitCode.Success + | MoreThanOne (givenName, names) -> Error (ArgsError.AmbigousCommandFound (givenName, names)) + | NoCommand unknownName -> Error (ArgsError.CommandNotFound unknownName) + <@> ConsoleApplicationError.ArgsError + currentCommand + | None -> + Commands.showApplicationHelp output parts.ApplicationOptions parts.Commands - Ok ExitCode.Success - | MoreThanOne (givenName, names) -> Error (ArgsError.AmbigousCommandFound (givenName, names)) - | NoCommand unknownName -> Error (ArgsError.CommandNotFound unknownName) - <@> ConsoleApplicationError.ArgsError - currentCommand + return ExitCode.Success | Args.ContainsOption OptionsDefinitions.version -> { parts with ApplicationInfo = ApplicationInfo.OnlyNameAndVersion } |> showApplicationInfo diff --git a/tests/ApplicationHelpTests.fs b/tests/ApplicationHelpTests.fs new file mode 100644 index 0000000..9c8678f --- /dev/null +++ b/tests/ApplicationHelpTests.fs @@ -0,0 +1,165 @@ +module Feather.ConsoleApplication.Tests.ApplicationHelp + +open Expecto +open Feather.ConsoleApplication + +type TestCase = { + Description: string + Command: string + DefaultCommand: string option + ExpectedOutput: string list +} + +let private greetCommand: CommandDefinition = { + Description = "Greets a person" + Help = None + Arguments = [] + Options = [] + Initialize = None + Interact = None + Execute = Execute <| fun (_, output) -> + output.Message "greeting" + ExitCode.Success +} + +let private header = [ + "Application help test <1.0.0>" + "=============================" + "" +] + +let private applicationHelp = [ + yield! header + + "Usage:" + " command [options] [--] [arguments] " + "" + "Options:" + " -h, --help Display this help message " + " -q, --quiet Do not output any message " + " -V, --version Display this application version " + " -n, --no-interaction Do not ask any interactive question " + " --no-progress Whether to disable all progress bars " + " --no-ansi Whether to disable all markup with ansi formatting" + " -v|vv|vvv, --verbose Increase the verbosity of messages " + "" + "Available commands:" + " about Displays information about the current project" + " greet Greets a person " + " help Displays help for a command " + " list Lists commands " + "" + "" +] + +let provideApplicationHelp = seq { + { + Description = "should show application help when --help given without command" + Command = "--help" + DefaultCommand = None + ExpectedOutput = applicationHelp + } + + { + Description = "should show application help when -h given without command" + Command = "-h" + DefaultCommand = None + ExpectedOutput = applicationHelp + } + + { + Description = "should show application help when --help given with other options only" + Command = "--no-interaction --help" + DefaultCommand = None + ExpectedOutput = applicationHelp + } + + { + Description = "should show application help when --help given and custom default command configured" + Command = "--help" + DefaultCommand = Some "greet" + ExpectedOutput = applicationHelp + } + + { + Description = "should run default command when no args given and custom default command configured" + Command = "" + DefaultCommand = Some "greet" + ExpectedOutput = [ + yield! header + + "greeting" + "" + ] + } + + { + Description = "should run default command when only options without help given and custom default command configured" + Command = "--no-interaction" + DefaultCommand = Some "greet" + ExpectedOutput = [ + yield! header + + "greeting" + "" + ] + } + + { + Description = "should show name and version when --version given" + Command = "--version" + DefaultCommand = None + ExpectedOutput = [ + "Application help test <1.0.0>" + "" + ] + } +} + +[] +let applicationHelpTests = + testList "ConsoleApplication - application help" [ + yield! + provideApplicationHelp + |> Seq.map (fun { Description = desc; Command = command; DefaultCommand = customDefault; ExpectedOutput = expected } -> + testCase desc <| fun _ -> + use buffer = new Feather.ConsoleStyle.Output.BufferOutput(Feather.ConsoleStyle.Verbosity.Normal) + let console = Feather.ConsoleStyle.ConsoleStyle(buffer) + let args = + match command with + | "" -> [||] + | command -> command.Split " " + let application = + match customDefault with + | Some defaultCommandName -> + consoleApplication { + title "Feather.ConsoleApplication.Test" + name "Application help test" + version "1.0.0" + info ApplicationInfo.NameAndVersion + command "greet" greetCommand + defaultCommand defaultCommandName + useOutput console + } + | None -> + consoleApplication { + title "Feather.ConsoleApplication.Test" + name "Application help test" + version "1.0.0" + info ApplicationInfo.NameAndVersion + command "greet" greetCommand + useOutput console + } + + let result = application |> runResult args + + Expect.equal result (Ok ExitCode.Success) $"Command \"{command}\" should be successful" + let output = + (buffer.Fetch() |> console.RemoveMarkup).Split "\n" + |> List.ofArray + expected + |> List.iteri (fun i line -> + Expect.equal output.[i] line $"Line {i + 1} should match." + ) + ) + ] diff --git a/tests/tests.fsproj b/tests/tests.fsproj index 27021fd..bd7bea1 100644 --- a/tests/tests.fsproj +++ b/tests/tests.fsproj @@ -23,6 +23,7 @@ +