Repository: spectreconsole/spectre.console

Issue:
Add '-?' as an additional alias for '-h' and '--help', just like 'dotnet -?'
It's second nature for me to just type `-?` to get help on stuff. Even in the GitHub CLI, typing `gh -?` (although not an official option to get help) gets you the default help :).

I'd say it's a common enough shorthand for help that it deserves being supported built-in.


Hints:
Sounds like a reasonable thing to add 👍

Patch:
diff --git a/src/Spectre.Console.Cli/Internal/Configuration/TemplateParser.cs b/src/Spectre.Console.Cli/Internal/Configuration/TemplateParser.cs
index d7eef9b05..125947280 100644
--- a/src/Spectre.Console.Cli/Internal/Configuration/TemplateParser.cs
+++ b/src/Spectre.Console.Cli/Internal/Configuration/TemplateParser.cs
@@ -86,7 +86,7 @@ public static OptionResult ParseOptionTemplate(string template)
 
                 foreach (var character in token.Value)
                 {
-                    if (!char.IsLetterOrDigit(character) && character != '-' && character != '_')
+                    if (!char.IsLetterOrDigit(character) && character != '-' && character != '_' && character != '?')
                     {
                         throw CommandTemplateException.InvalidCharacterInOptionName(template, token, character);
                     }
diff --git a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs
index e4fad73a3..d985dcbd2 100644
--- a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs
+++ b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs
@@ -21,7 +21,7 @@ public CommandTreeParser(CommandModel configuration, CaseSensitivity caseSensiti
     {
         _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
         _parsingMode = parsingMode ?? _configuration.ParsingMode;
-        _help = new CommandOptionAttribute("-h|--help");
+        _help = new CommandOptionAttribute("-?|-h|--help");
         _convertFlagsToRemainingArguments = convertFlagsToRemainingArguments ?? false;
 
         CaseSensitivity = caseSensitivity;
diff --git a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs
index 27e6edbc6..840b072c7 100644
--- a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs
+++ b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs
@@ -176,7 +176,7 @@ private static IEnumerable<CommandTreeToken> ScanShortOptions(CommandTreeTokeniz
                 break;
             }
 
-            if (char.IsLetter(current))
+            if (char.IsLetter(current) || current is '?')
             {
                 context.AddRemaining(current);
                 reader.Read(); // Consume


Test patch:
diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Root.QuestionMark.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root.QuestionMark.verified.txt
new file mode 100644
index 000000000..0432fe41a
--- /dev/null
+++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root.QuestionMark.verified.txt
@@ -0,0 +1,10 @@
+USAGE:
+    myapp [OPTIONS] <COMMAND>
+
+OPTIONS:
+    -h, --help    Prints help information
+
+COMMANDS:
+    dog <AGE>           The dog command
+    horse               The horse command
+    giraffe <LENGTH>    The giraffe command
\ No newline at end of file
diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Command.QuestionMark.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Command.QuestionMark.verified.txt
new file mode 100644
index 000000000..03c68750d
--- /dev/null
+++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Root_Command.QuestionMark.verified.txt
@@ -0,0 +1,17 @@
+DESCRIPTION:
+The horse command.
+
+USAGE:
+    myapp horse [LEGS] [OPTIONS]
+
+ARGUMENTS:
+    [LEGS]    The number of legs
+
+OPTIONS:
+                           DEFAULT
+    -h, --help                         Prints help information
+    -a, --alive                        Indicates whether or not the animal is alive
+    -n, --name <VALUE>
+    -d, --day <MON|TUE>
+        --file             food.txt
+        --directory
\ No newline at end of file
diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs
index 9ce30cc24..8d3af800f 100644
--- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs
+++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs
@@ -28,6 +28,27 @@ public Task Should_Output_Root_Correctly()
             return Verifier.Verify(result.Output);
         }
 
+        [Fact]
+        [Expectation("Root", "QuestionMark")]
+        public Task Should_Output_Root_Correctly_QuestionMark()
+        {
+            // Given
+            var fixture = new CommandAppTester();
+            fixture.Configure(configurator =>
+            {
+                configurator.SetApplicationName("myapp");
+                configurator.AddCommand<DogCommand>("dog");
+                configurator.AddCommand<HorseCommand>("horse");
+                configurator.AddCommand<GiraffeCommand>("giraffe");
+            });
+
+            // When
+            var result = fixture.Run("-?");
+
+            // Then
+            return Verifier.Verify(result.Output);
+        }
+
         [Fact]
         [Expectation("Root_Command")]
         public Task Should_Output_Root_Command_Correctly()
@@ -49,6 +70,27 @@ public Task Should_Output_Root_Command_Correctly()
             return Verifier.Verify(result.Output);
         }
 
+        [Fact]
+        [Expectation("Root_Command", "QuestionMark")]
+        public Task Should_Output_Root_Command_Correctly_QuestionMark()
+        {
+            // Given
+            var fixture = new CommandAppTester();
+            fixture.Configure(configurator =>
+            {
+                configurator.SetApplicationName("myapp");
+                configurator.AddCommand<DogCommand>("dog");
+                configurator.AddCommand<HorseCommand>("horse");
+                configurator.AddCommand<GiraffeCommand>("giraffe");
+            });
+
+            // When
+            var result = fixture.Run("horse", "-?");
+
+            // Then
+            return Verifier.Verify(result.Output);
+        }
+
         [Fact]
         [Expectation("Hidden_Commands")]
         public Task Should_Skip_Hidden_Commands()