pallets / click

Python composable command line interface toolkit
https://click.palletsprojects.com
BSD 3-Clause "New" or "Revised" License
15.75k stars 1.4k forks source link

get help does not remove multiline marker #1597

Closed bitranox closed 3 years ago

bitranox commented 4 years ago

if You have commands with a multi-line help, the \b is not brushed off the help output

if You use -h it looks correct in the terminal :

$> cli_command -h 
Usage: test_cli_help.py [OPTIONS] COMMAND [ARGS]...

  some help

 Options:
   --version   Show the version and exit.
   -h, --help  Show this message and exit.

 Commands:
   command1  command1 without arguments and options
   command2  command2 with arguments
   command3  command3 with multi line help arguments and options
   command4  command4 with arguments, options and sub_command and a very...

but if You parse the text, You will see that the \b is not brushed off, where it should be, see the  near command3, command4. instead of brushing off the \b, You just added one whitespace.

$> cli_command -h > ./help.txt

# Content of ./help.txt : 
Usage: test_cli_help.py [OPTIONS] COMMAND [ARGS]...

  some help

 Options:
   --version   Show the version and exit.
   -h, --help  Show this message and exit.

 Commands:
   command1  command1 without arguments and options
   command2  command2 with arguments
   command3   command3 with multi line help arguments and options
   command4   command4 with arguments, options and sub_command and a very...

this is my example:

# EXT
import click

# CONSTANTS
CLICK_CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
CLICK_CONTEXT_SETTINGS_NO_HELP = dict(help_option_names=[])

@click.group(help='some help', context_settings=CLICK_CONTEXT_SETTINGS)
@click.version_option(version='1.1.1',
                      prog_name='program name',
                      message='{} version %(version)s'.format('cli command'))
def cli_main() -> None:             # pragma: no cover
    pass                            # pragma: no cover

# command1 without arguments and options
@cli_main.command('command1', context_settings=CLICK_CONTEXT_SETTINGS_NO_HELP)
def cli_command1() -> None:         # pragma: no cover
    """ command1 without arguments and options """
    pass

# command2 with arguments
@cli_main.command('command2', context_settings=CLICK_CONTEXT_SETTINGS)
@click.argument('argument1')
@click.argument('argument2')
def cli_command2(argument1: str, argument2: str) -> None:
    """ command2 with arguments """
    pass                            # pragma: no cover

# command3 with arguments and options
@cli_main.command('command3', context_settings=CLICK_CONTEXT_SETTINGS)
@click.argument('argument1')
@click.argument('argument2')
@click.option('-a', '--a_option', is_flag=True)             # no help here
@click.option('-b', '--b_option', type=int, default=-1, help='help for b_option')
@click.option('-c', '--c_option', help='help for c_option')
def cli_command3(argument1: str, argument2: str, a_option: bool, b_option: int, c_option: str) -> None:
    """\b
        command3 with multi
        line help arguments and options
        """
    pass                            # pragma: no cover

# command4 with arguments, options and sub_command
# groups must not have arguments or we can not parse them
# because to get help for the sub command we need to put :
# program command4 arg1 arg2 command5 -h
# and we dont know the correct type of arg1, arg2

@cli_main.group('command4', context_settings=CLICK_CONTEXT_SETTINGS)
@click.argument('argument1')
@click.argument('argument2')
@click.option('-a', '--a_option', is_flag=True)             # no help here
@click.option('-b', '--b_option', type=int, default=-1, help='help for b_option')
@click.option('-c', '--c_option', help='help for c_option')
def cli_command4(argument1: str, argument2: str, a_option: bool, b_option: int, c_option: str) -> None:
    """\b
     command4 with arguments,
     options and sub_command
     and a very long
     multiline help
     what for sure will not fit into one terminal line
     what for sure will not fit into one terminal line
     what for sure will not fit into one terminal line
     what for sure will not fit into one terminal line
     """
    pass                            # pragma: no cover

# command5, sub_command of command4 with arguments, options
@cli_command4.command('command5', context_settings=CLICK_CONTEXT_SETTINGS)
@click.argument('argument1')
@click.argument('argument2')
@click.option('-a', '--a_option', is_flag=True)             # no help here
@click.option('-b', '--b_option', type=int, default=-1, help='help for b_option')
@click.option('-c', '--c_option', help='help for c_option')
def cli_command5(argument1: str, argument2: str, a_option: bool, b_option: int, c_option: str) -> None:
    """command5, sub_command of command4 with arguments, options"""
    pass                            # pragma: no cover

# entry point if main
if __name__ == '__main__':
    cli_main()
parabolize commented 4 years ago

This can be avoided by adding a short_help or a summary at the start of the doc string. Below is a patch that would handle the example:

diff --git a/src/click/utils.py b/src/click/utils.py
index 0bff5c0..89d726d 100644
--- a/src/click/utils.py
+++ b/src/click/utils.py
@@ -54,6 +54,13 @@ def make_default_short_help(help, max_length=45):
     result = []
     done = False

+    # Use only the first line when string shouldn't be rewrapped.
+    if words[0] == '\b':
+        for line in help.splitlines():
+            if line and line != '\b':
+                words = line.split()
+                break
+
     for word in words:
         if word[-1:] == ".":
             done = True
Saif807380 commented 3 years ago

Hey, I'm a fellow from MLH. I'd like to work on this issue.