commandlineparser / commandline

The best C# command line parser that brings standardized *nix getopt style, for .NET. Includes F# support
MIT License
4.58k stars 480 forks source link

Formatting help text (ends of line) #529

Open downwater opened 5 years ago

downwater commented 5 years ago

The command line wiki shows what help texts look like:

yourapp 2.0.201-alpha
[...]
  --filename    Input filename.

  --help        Display this help screen.

  --version     Display version information.

Each argument entry is followed by a double end of line. I wondered if there is a simple way to get a formatting with single eol, like this grep example:

Usage: grep [OPTION]... PATTERN [FILE]...
[...]
  -E, --extended-regexp     PATTERN is an extended regular expression (ERE)
  -F, --fixed-strings       PATTERN is a set of newline-separated strings
  -G, --basic-regexp        PATTERN is a basic regular expression (BRE)

If I undestand correctly, I'll have to implement my own help screen and use HelpText.RenderUsageText or HelpText.RenderUsageTextAsLines ?

moh-hassan commented 5 years ago

You need not to use Rendering methods, only configure HelpText and set:

       AdditionalNewLineAfterOption=false

Wiki is updated to include new page: HelpText Configuration for generating custom help with examples that you can run online. Custom helpExample: Try it online

downwater commented 5 years ago

Thank you very much.

leoformaggi commented 4 years ago

You need not to use Rendering methods, only configure HelpText and set:

       AdditionalNewLineAfterOption=false

Wiki is updated to include new page: HelpText Configuration for generating custom help with examples that you can run online. Custom helpExample: Try it online

Is there a way to cut the empty lines from Default parser? Using the HelpText it works, but there's a different behavior. I am using a method for parsing subverbs, which works fine. If I use a single --help with Default parser, it shows the available verbs. But if I use HelpText.AutoBuild() setting AdditionalNewLineAfterOption = false, the single --help command won't show the available verbs.

downwater commented 4 years ago

@moh-hassan, I confirm this works (tested with VB.NET).

moh-hassan commented 4 years ago

@downwater Thanks for feedback

moh-hassan commented 4 years ago

@leoformaggi Can you post sample code to repro the issue.

downwater commented 4 years ago

@moh-hassan, I just realized I'm experiencing the same issue as @leoformaggi , but I didn't yet dig into it. I started from your sample to write the one below to show up the case:

// @nuget: CommandLineParser -Version 2.6.0
using System;
using System.Collections.Generic;
using CommandLine;
using CommandLine.Text;

public class Program
{
    public static void Main()
    {
        var args = "--help".Split();
        StartUp(args);
    }

    static void StartUp(string[] args)
    {
        var parser = new CommandLine.Parser(with => with.HelpWriter = null);
        var parserResult = parser.ParseArguments<ReadOptions, WriteOptions>(args);
        parserResult
            .WithParsed<ReadOptions>(options => Run(options))
            .WithParsed<WriteOptions>(options => Run(options))
            .WithNotParsed(errs => DisplayHelp(parserResult, errs));
    }

    static void DisplayHelp<T>(ParserResult<T> result, IEnumerable<Error> errs)
    {
        var helpText = HelpText.AutoBuild(result, h =>
        {
            h.AdditionalNewLineAfterOption = false; //remove the extra newline between options
            h.Heading = "MyVerbApp 2.0.0-beta"; //change header
            h.Copyright = "Copyright (c) 2019 Global.com"; //change copyrigt text
            return HelpText.DefaultParsingErrorsHandler(result, h);
        }, e => e);
        Console.WriteLine(helpText);
    }

    static void Run(WriteOptions options){}
    static void Run(ReadOptions options){}
}

[Verb("read", HelpText = "Reads from source.")]
class ReadOptions
{
    [Option("path", Default = false, HelpText = "Source path")]
    public bool path{get;set;}
}

[Verb("write", HelpText = "Writes to destination.")]
class WriteOptions
{
    [Option("path", Default = false, HelpText = "Destination path")]
    public bool path{get;set;}
}

Calling the program with --help will ouput this:

MyVerbApp 2.0.0-beta
Copyright (c) 2019 Global.com

  --help       Display this help screen.
  --version    Display version information.
moh-hassan commented 4 years ago

@downwater, @leoformaggi Custom Help for Verbs need accessing internal members. I investigate the possibility of modifying these members to be public.

downwater commented 4 years ago

@moh-hassan, thank you for this first feedback.

moh-hassan commented 4 years ago

@downwater, @leoformaggi Custom help for verb is fixed in v2.7.0-preview1 by adding a new overload method to AutoBuild and DisplayHelp is simplified

var helpText = HelpText.AutoBuild(result, h => {..});

Try it online

Click to expand source code! ```cs // @nuget: CommandLineParser -Version 2.7.0-preview1 using System; using System.Collections.Generic; using CommandLine; using CommandLine.Text; public class Program { public static void Main() { StartUp("--help"); StartUp("--help write"); StartUp("write --dummy"); // Option 'dummy' is unknown } static void StartUp(string command) { Console.WriteLine($"------Args: '{command}' -------"); var args = command.Split(); var parser = new CommandLine.Parser(with => with.HelpWriter = null); var parserResult = parser.ParseArguments(args); parserResult.WithParsed(options => Run(options)).WithParsed(options => Run(options)).WithNotParsed(errs => DisplayHelp(parserResult)); } static void DisplayHelp(ParserResult result) { var helpText = HelpText.AutoBuild(result, h => { h.AdditionalNewLineAfterOption = false; //remove the extra newline between options h.Heading = "MyVerbApp 2.0.0-beta"; //change header h.Copyright = "Copyright (c) 2019 Global.com"; //change copyrigt text return h; } ); Console.WriteLine(helpText); } static void Run(WriteOptions options) { } static void Run(ReadOptions options) { } } [Verb("read", HelpText = "Reads from source.")] class ReadOptions { [Option("path", Default = false, HelpText = "Source path")] public bool path { get; set; } } [Verb("write", HelpText = "Writes to destination.")] class WriteOptions { [Option("path", Default = false, HelpText = "Destination path")] public bool path { get; set; } } ```
Click to expand Help Screen! ``` ------Args: '--help' ------- MyVerbApp 2.0.0-beta Copyright (c) 2019 Global.com read Reads from source. write Writes to destination. help Display more information on a specific command. version Display version information. ------Args: '--help write' ------- MyVerbApp 2.0.0-beta Copyright (c) 2019 Global.com --path (Default: false) Destination path --help Display this help screen. --version Display version information. ------Args: 'write --dummy' ------- MyVerbApp 2.0.0-beta Copyright (c) 2019 Global.com ERROR(S): Option 'dummy' is unknown. --path (Default: false) Destination path --help Display this help screen. --version Display version information. ```
downwater commented 4 years ago

@moh-hassan Thank you for the information. We'll be waiting for the stable release.

downwater commented 4 years ago

@moh-hassan, using the current release (2.7.82) and following the new code sample, it works.

A last detail though. If the description of a parameter is longer than the console width, it returns to the line of course, but after two line feeds:

D:\StuffXY>StuffXY.exe --help
StuffXY 1.0.0.0
Copyright (c) 2019 Some company

  rewrite-stuff_2    Lorem ipsum dolor sit amet, consectetur adipiscing
  rewrite-stuff_1    Lorem ipsum dolor sit amet 
  rewrite            Lorem ipsum dolor sit amet, consectetur double newline here

                     it just happened.
  probe-stuff        Probes stuff
moh-hassan commented 4 years ago

@downwater Thanks for feedback, The new overload method has a third parameter maxDisplayWidth which is 80 by default. Try to expand it to fit your width


public static HelpText AutoBuild<T>(ParserResult<T> parserResult, Func<HelpText, HelpText> onError, int maxDisplayWidth = DefaultMaximumLength)

//change maxDisplayWidth to 120.
var helpText = HelpText.AutoBuild(result, h => {..}, 120);
```cs
downwater commented 4 years ago

@moh-hassan Thank you for your answer.

I don't really understand (sorry), I was talking about word wrap. The HelpText object succeeds to perform word wrap with a correct left indentation, but adds an extra newline.

Increasing maxDisplayWidth formats the output as below:

D:\StuffXY>StuffXY.exe --help
StuffXY 1.0.0.0
Copyright (c) 2019 Some company

  rewrite-stuff_2    Lorem ipsum dolor sit amet, consectetur adipiscing
  rewrite-stuff_1    Lorem ipsum dolor sit amet 
  rewrite            Lorem ipsum dolor sit amet, consectetur double newline here it just
happened.
  probe-stuff        Probes stuff
leoformaggi commented 4 years ago

@moh-hassan I tested release 2.7.82 and it solved my problem like a charm. Thank you for the update.

moh-hassan commented 4 years ago

@leoformaggi Thanks for feedback and I appreciate your effort in testing v2.7.82.

moh-hassan commented 4 years ago

@downwater, I checked the help in this demo with addin a newline \n in middle of string and using large and small size and it's working fine. If there is an issue with double new lines, you can provide a test case with xunit that repro the issue, and what is the device you are using and its width, Full framework/netcore and OS: windows/linux/..

downwater commented 4 years ago

@moh-hassan,

After several tries, the first issue seems to reproduce if the following conditions are fulfilled:

To reproduce:

  1. Define a verb with the given help text (with the very string value):

    [Verb("extract", 
              HelpText = "Extracts XX image. Will extract all read stuff unless there are specified")]
    class ReadOptions
    {
        // [...]
    }
  2. Define a HelpText as below:

    static void DisplayHelp<T>(ParserResult<T> result)
    {
        var helpText = HelpText.AutoBuild(result, h =>
        {
            h.AdditionalNewLineAfterOption = false;
            return h;
        },80);
        Console.WriteLine(helpText);
    }
  3. Run with --help in a command prompt window, with width sized to 80; you can run it with Visual Studio and adding a break point right before the Console.WriteLine() call to set the buffer width on the fly.


Tested with Visual Studio 2010 on Windows Server 2008 Entreprise SP2 and Command Line Parser 2.7.82.

moh-hassan commented 4 years ago

@downwater

It seems that the boundary of 80 column cause this issue Try to resize the console to column width: 81 by the command

mode con:cols=81 

I couldn't catch this issue in xunit test.

@NeilMacMullen Kindly, can you help in fixing this issue.

NeilMacMullen commented 4 years ago

@moh-hassan I believe the issue occurs because TextWrapper.WrapAndIndextText treats the supplied number of columns as the number available for non-whitespace but then appends a newline when converting back to a single string so in the case that we've wrapped to N columns and the line is exactly N characters long, the additional newline will push it to N+1 and the console behaviour is to wrap the line at the Nth character and "display" the newline on the following line. There are are a few alternate fixes:

  1. change the columnWidth calculation in TextWrapper.WordWrap to allow an extra character for the newline.... columnWidth = Math.Max(1, columnWidth-1); //subtract 1 to allow for newlines that will be added when converting back to a block of text Unfortunately this changes the documented (in the comments) semantics of the call and some of the unit tests which assume that the number of columns being passed in are for non-whitespace so if you go this route you'll need to fix up a few things.

  2. change the call sites for TextWrapper.WordWrap to subtract 1 from the available column-width (both calls are in HelpText.cs) This is also likely to break a few unit tests which make the wrong assumption about MaximumDisplayWidth being the number of characters available for non-whitespace.

  3. Modify the setter/use of MaximumDisplayWidth to make it clear it is is the number of columns that can be used for text rather than the screen width.

Unfortunately I'm too busy to fix this myself but my suggested fix would be option 1 which is a trivial code fix that just requires some tedious test changes. Note though that TextWrapper is not the only thing that formats text and it's possible things like the copyright display also suffer from this problem.

moh-hassan commented 4 years ago

Thanks @NeilMacMullen for reply and suggestions. I appreciate your time and I will investigate your suggestions including the other possible things that may be the cause and feedback the result.