如何在argparse帮助文本中插入换行?

494

我正在使用 Python 2.7 中的 argparse 来解析输入选项。我的其中一个选项是多个选择之一。我想在帮助文本中制作一个列表,例如:

from argparse import ArgumentParser

parser = ArgumentParser(description='test')

parser.add_argument('-g', choices=['a', 'b', 'g', 'd', 'e'], default='a',
    help="Some option, where\n"
         " a = alpha\n"
         " b = beta\n"
         " g = gamma\n"
         " d = delta\n"
         " e = epsilon")

parser.parse_args()

然而,argparse会去除所有换行符和连续的空格。结果看起来像:

~/Downloads:52$ python2.7 x.py -h
usage: x.py [-h] [-g {a,b,g,d,e}]
test
可选参数: -h, --help 显示帮助信息并退出 -g {a,b,g,d,e} 一些选项,其中 a = alpha b = beta g = gamma d = delta e = epsilon

如何在帮助文本中插入换行符?


我没有Python 2.7,所以无法测试我的想法。使用三重引号(""" """)中的帮助文本如何?这样做新行会保留吗? - pyfunc
5
不行,剥离操作是由运行时的 argparse 完成的,而不是解释器完成的,所以改用 """...""" 也没有帮助。 - kennytm
这个对我有用。 - cardamom
15个回答

575

尝试使用RawTextHelpFormatter来保留您的所有格式:

from argparse import RawTextHelpFormatter
parser = ArgumentParser(description='test', formatter_class=RawTextHelpFormatter)

这类似于RawDescriptionHelpFormatter,但是与仅适用于描述和尾注不同,RawTextHelpFormatter也适用于所有帮助文本(包括参数)。


7
我认为不行。你可以创建一个子类,但不幸的是,“这个类的名称是唯一被视为公共API的部分,该类提供的所有方法都被视为实现细节。” 所以可能不是一个好主意,尽管这可能并不重要,因为2.7被认为是最后的2.x Python版本,你最终需要为3.x重构很多东西。我实际上正在运行安装了argparse的2.6版本,所以文档本身可能已经过时了。 - intuited
3
一些链接:针对 Python 2.7Python 3.*。根据它的维基页面,2.6版本应该符合官方2.7版本。从文档中可以看到:“将 RawDescriptionHelpFormatter 作为 formatter_class 参数传递,表示描述和 epilog 已经正确格式化,不需要换行”。 - Stefano
122
尝试使用formatter_class=RawDescriptionHelpFormatter,它仅作用于描述和结尾部分,而不是帮助文本。 - MarkHu
5
即使使用了 RawTextHelpFormatter 格式化帮助文本,我注意到开头和结尾的换行符还是会被移除。为了解决这个问题,你可以添加两个或更多连续的换行符;除最后一个换行符外,其它的都会保留。 - MrMas
29
你可以结合多个格式化方式,例如 class Formatter( argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass,然后在参数中使用 formatter_class=Formatter - Terry Brown
显示剩余3条评论

102
如果您只想覆盖一个选项,就不应该使用RawTextHelpFormatter。相反,应该子类化HelpFormatter并为应该“原始”处理的选项提供特殊介绍(我使用"R|rest of help")。
import argparse

class SmartFormatter(argparse.HelpFormatter):

    def _split_lines(self, text, width):
        if text.startswith('R|'):
            return text[2:].splitlines()  
        # this is the RawTextHelpFormatter._split_lines
        return argparse.HelpFormatter._split_lines(self, text, width)

并使用它:

from argparse import ArgumentParser

parser = ArgumentParser(description='test', formatter_class=SmartFormatter)

parser.add_argument('-g', choices=['a', 'b', 'g', 'd', 'e'], default='a',
    help="R|Some option, where\n"
         " a = alpha\n"
         " b = beta\n"
         " g = gamma\n"
         " d = delta\n"
         " e = epsilon")

parser.parse_args()

任何调用 .add_argument() 的帮助信息不以 R| 开头的都将按照正常方式进行包装。
这是 我对argparse的改进 的一部分。完整的 SmartFormatter 还支持为所有选项添加默认值,并且原始输入工具的描述。完整版本还有自己的 _split_lines 方法,因此对于例如版本字符串的任何格式化都将被保留:
parser.add_argument('--version', '-v', action="version",
                    version="version...\n   42!")

2
@mc_electron,SmartFormatter的完整版本也有自己的_split_lines并保留换行符(如果您想要该选项,则无需在开头指定“R |”,请修补_VersionAction.__call__方法即可。 - Anthon
1
@mc_electron 我指的是我在Bitbucket上发布的改进(根据答案中关于argparse改进的链接)。但是你也可以通过在定义def smart_version(self, parser, namespace, values, option_string=None): ...之后执行argparse._VersionAction.__call__ = smart_version来修补_VersionAction中的__call__ - Anthon
1
嘿,这很棒!尽管在完整版本的源代码中没有出现,但在 _split_lines 顶部的注释中说 # this is the RawTextHelpFormatter._split_lines 有点误导。如果将该注释移至 if 语句下面,因为那是原始的 RawTextHelpFormatter 返回,该注释可能会更少引起困惑。无论如何,这个答案给了我我需要的东西,再次感谢! - svenevs
1
真的很喜欢这个解决方案,但我觉得只在找到帮助文本中的新行时应用它更加优雅,像这样:class SmartFormatter(argparse.HelpFormatter): def _split_lines(self, text, width): if '\n' in text: return text.splitlines() return argparse.HelpFormatter._split_lines(self, text, width)抱歉,不确定如何在此处保留空格 :) - Fotis Gimian
1
或者这样(按照TeX中的双行间隔进行分割): def _split_lines(self, text, width): return [sln for ln in text.split('\n\n') for sln in argparse.HelpFormatter._split_lines(self, ln, width)] - undefined
显示剩余3条评论

41
textwrap 是另一种简单的方法。
例如,
import argparse, textwrap
parser = argparse.ArgumentParser(description='some information',
        usage='use "python %(prog)s --help" for more information',
        formatter_class=argparse.RawTextHelpFormatter)

parser.add_argument('--argument', default=somedefault, type=sometype,
        help= textwrap.dedent('''\
        First line
        Second line
        More lines ... '''))

通过这种方式,我们可以避免每个输出行前面的长时间空白。

usage: use "python your_python_program.py --help" for more information

Prepare input file

optional arguments:
-h, --help            show this help message and exit
--argument ARGUMENT
                      First line
                      Second line
                      More lines ...

14

我曾经遇到过类似的问题(Python 2.7.6)。我试图使用 RawTextHelpFormatter描述部分拆分成几行:

parser = ArgumentParser(description="""First paragraph 

                                       Second paragraph

                                       Third paragraph""",  
                                       usage='%(prog)s [OPTIONS]', 
                                       formatter_class=RawTextHelpFormatter)

options = parser.parse_args()

所以RawTextHelpFormatter不是一个解决方案。因为它按照源代码中出现的样式打印描述,保留所有空格字符(我希望在我的源代码中保留额外的制表符以提高可读性,但我不想打印它们全部。另外,原始格式化程序在行过长(例如超过80个字符)时不换行)。

感谢@Anton,他启发了正确的方向上面。但该解决方案需要稍作修改才能格式化description部分。

无论如何,需要自定义格式化程序。我扩展了现有的HelpFormatter类,并重写了_fill_text方法,如下所示:

import textwrap as _textwrap
class MultilineFormatter(argparse.HelpFormatter):
    def _fill_text(self, text, width, indent):
        text = self._whitespace_matcher.sub(' ', text).strip()
        paragraphs = text.split('|n ')
        multiline_text = ''
        for paragraph in paragraphs:
            formatted_paragraph = _textwrap.fill(paragraph, width, initial_indent=indent, subsequent_indent=indent) + '\n\n'
            multiline_text = multiline_text + formatted_paragraph
        return multiline_text

与来自 argparse 模块的原始源代码进行比较:

def _fill_text(self, text, width, indent):
    text = self._whitespace_matcher.sub(' ', text).strip()
    return _textwrap.fill(text, width, initial_indent=indent,
                                       subsequent_indent=indent)
在原始代码中,整个描述被包装在一起。而在上面的自定义格式化程序中,整个文本被分成几个块,并且每个块都独立地进行格式化。
因此,在使用自定义格式化程序时:
parser = ArgumentParser(description= """First paragraph 
                                        |n                              
                                        Second paragraph
                                        |n
                                        Third paragraph""",  
                usage='%(prog)s [OPTIONS]',
                formatter_class=MultilineFormatter)

options = parser.parse_args()

输出结果为:

使用方法:play-with-argparse.py [选项]
第一段
第二段
第三段
可选参数: -h, --help 显示帮助信息并退出

1
这太棒了 --- 在几乎放弃并考虑重新实现帮助参数的整个过程中,发生了这件事... 为我节省了很多麻烦。 - Paul Gowder
2
子类化 HelpFormatter 是有问题的,因为 argparse 的开发人员只保证类名在未来版本中仍然存在。他们基本上为自己写了一张空白支票,所以如果方便的话,他们可以更改方法名称。我觉得这很令人沮丧;他们至少可以在 API 中公开一些方法。 - MrMas
不完全是OP所问的,但正是我想要的,谢谢! - Huw Walters

12

我承认这是一次非常令人沮丧的经历,因为许多其他人似乎也有同样的经历,鉴于我看到的解决方案数量以及我在网络上看到这个问题的次数。但是,我发现大多数解决方案对我来说过于复杂,我想分享我拥有的最简洁、最简单的解决方案。

以下是演示脚本:

#!/usr/bin/python3
import textwrap
from argparse import ArgumentParser, HelpFormatter

class RawFormatter(HelpFormatter):
    def _fill_text(self, text, width, indent):
        return "\n".join([textwrap.fill(line, width) for line in textwrap.indent(textwrap.dedent(text), indent).splitlines()])

program_descripton = f'''
    FunkyTool v1.0

    Created by the Funky Guy on January 1 2020
    Copyright 2020. All rights reserved.

    Licensed under The Hippocratic License 2.1
    https://firstdonoharm.dev/

    Distributed on an "AS IS" basis without warranties
    or conditions of any kind, either express or implied.

    USAGE:
    '''

parser = ArgumentParser(description=program_descripton, formatter_class=RawFormatter)
args = parser.parse_args()

这是在 test.py 中的样子:

$ ./test.py --help
usage: test.py [-h]

FunkyTool v1.0

Created by the Funky Guy on January 1 2020
Copyright 2020. All rights reserved.

Licensed under The Hippocratic License 2.1
https://firstdonoharm.dev/

Distributed on an "AS IS" basis without warranties
or conditions of any kind, either express or implied.

USAGE:

optional arguments:
  -h, --help  show this help message and exit

因此,原始描述中的所有基本格式都得到了整洁的保留,我们不得不使用自定义格式化程序,但它只有一行代码。可以更清晰地编写为:

所以,原来的描述中所有基本的格式都被保持的很好,但是我们不得不使用一个自定义格式化工具,不过这只有一行代码。可以更明确地表达为:

class RawFormatter(HelpFormatter):
    def _fill_text(self, text, width, indent):
        text = textwrap.dedent(text)          # Strip the indent from the original python definition that plagues most of us.
        text = textwrap.indent(text, indent)  # Apply any requested indent.
        text = text.splitlines()              # Make a list of lines
        text = [textwrap.fill(line, width) for line in text] # Wrap each line 
        text = "\n".join(text)                # Join the lines again
        return text

但我更喜欢将其放在一行上。


这是一个很好的答案!但它不适用于OP问题中的参数帮助文本。我在我的答案中添加了该功能。 - idbrii

8

使用RawTextHelpFormatter这种简单方法来获取新行,并处理缩进的另一种方式是:

import argparse

parser = argparse.ArgumentParser(
    description='test', formatter_class=argparse.RawTextHelpFormatter)

parser.add_argument('-g', choices=['a', 'b', 'g', 'd', 'e'], default='a',
                    help=('Some option, where\n'
                          ' a = alpha\n'
                          ' b = beta\n'
                          ' g = gamma\n'
                          ' d = delta\n'
                          ' e = epsilon'))

parser.parse_args()


输出结果为:
$ python2 x.py -h
usage: x.py [-h] [-g {a,b,g,d,e}]

test

optional arguments:
  -h, --help      show this help message and exit
  -g {a,b,g,d,e}  Some option, where
                   a = alpha
                   b = beta
                   g = gamma
                   d = delta
                   e = epsilon


3

从上述SmartFormatter开始,我得出了以下解决方案:

class SmartFormatter(argparse.HelpFormatter):
    '''
         Custom Help Formatter used to split help text when '\n' was 
         inserted in it.
    '''

    def _split_lines(self, text, width):
        r = []
        for t in text.splitlines(): r.extend(argparse.HelpFormatter._split_lines(self, t, width))
        return r

请注意,奇怪的是传递给顶级解析器的formatter_class参数不会被子解析器继承,必须为每个创建的子解析器再次传递该参数。

2

我希望在描述文本中既有手动换行,又能自动换行;但是这里的建议都没有对我起作用 - 所以我最终修改了答案中提供的SmartFormatter类; 尽管argparse方法名称不是公共API的问题仍然存在,但这就是我所拥有的(作为一个名为test.py的文件):

import argparse
from argparse import RawDescriptionHelpFormatter

# call with: python test.py -h

class SmartDescriptionFormatter(argparse.RawDescriptionHelpFormatter):
  #def _split_lines(self, text, width): # RawTextHelpFormatter, although function name might change depending on Python
  def _fill_text(self, text, width, indent): # RawDescriptionHelpFormatter, although function name might change depending on Python
    #print("splot",text)
    if text.startswith('R|'):
      paragraphs = text[2:].splitlines()
      rebroken = [argparse._textwrap.wrap(tpar, width) for tpar in paragraphs]
      #print(rebroken)
      rebrokenstr = []
      for tlinearr in rebroken:
        if (len(tlinearr) == 0):
          rebrokenstr.append("")
        else:
          for tlinepiece in tlinearr:
            rebrokenstr.append(tlinepiece)
      #print(rebrokenstr)
      return '\n'.join(rebrokenstr) #(argparse._textwrap.wrap(text[2:], width))
    # this is the RawTextHelpFormatter._split_lines
    #return argparse.HelpFormatter._split_lines(self, text, width)
    return argparse.RawDescriptionHelpFormatter._fill_text(self, text, width, indent)

parser = argparse.ArgumentParser(formatter_class=SmartDescriptionFormatter, description="""R|Blahbla bla blah blahh/blahbla (bla blah-blabla) a blahblah bl a blaha-blah .blah blah

Blah blah bla blahblah, bla blahblah blah blah bl blblah bl blahb; blah bl blah bl bl a blah, bla blahb bl:

  blah blahblah blah bl blah blahblah""")

options = parser.parse_args()

这是2.7和3.4中的工作方式:
$ python test.py -h
usage: test.py [-h]

Blahbla bla blah blahh/blahbla (bla blah-blabla) a blahblah bl a blaha-blah
.blah blah

Blah blah bla blahblah, bla blahblah blah blah bl blblah bl blahb; blah bl
blah bl bl a blah, bla blahb bl:

  blah blahblah blah bl blah blahblah

optional arguments:
  -h, --help  show this help message and exit

1

Bernd的回答非常有帮助,但不适用于参数帮助字符串。这是对其进行扩展的方法,适用于所有帮助文本(遵循RawTextHelpFormatter示例)。

DescriptionWrappedNewlineFormatter是他原始的RawFormatter,而WrappedNewlineFormatter还将额外包装参数。

import argparse
import textwrap

class DescriptionWrappedNewlineFormatter(argparse.HelpFormatter):
    """An argparse formatter that:
    * preserves newlines (like argparse.RawDescriptionHelpFormatter),
    * removes leading indent (great for multiline strings),
    * and applies reasonable text wrapping.

    Source: https://dev59.com/AG865IYBdhLWcg3wW9VE#64102901
    """
    def _fill_text(self, text, width, indent):
        # Strip the indent from the original python definition that plagues most of us.
        text = textwrap.dedent(text)
        text = textwrap.indent(text, indent)  # Apply any requested indent.
        text = text.splitlines()  # Make a list of lines
        text = [textwrap.fill(line, width) for line in text]  # Wrap each line
        text = "\n".join(text)  # Join the lines again
        return text


class WrappedNewlineFormatter(DescriptionWrappedNewlineFormatter):
    """An argparse formatter that:
    * preserves newlines (like argparse.RawTextHelpFormatter),
    * removes leading indent and applies reasonable text wrapping (like DescriptionWrappedNewlineFormatter),
    * applies to all help text (description, arguments, epilogue).
    """
    def _split_lines(self, text, width):
        # Allow multiline strings to have common leading indentation.
        text = textwrap.dedent(text)
        text = text.splitlines()
        lines = []
        for line in text:
            wrapped_lines = textwrap.fill(line, width).splitlines()
            lines.extend(subline for subline in wrapped_lines)
            if line:
                lines.append("")  # Preserve line breaks.
        return lines


if __name__ == "__main__":
    def demo_formatter(formatter):
        parser = argparse.ArgumentParser(
            description="""
                A program that does things.
                Lots of description that describes how the program works.

                very long lines are wrapped. very long lines are wrapped. very long lines are wrapped. very long lines are wrapped. very long lines are wrapped. very long lines are wrapped.

                existing wrapping will be preserved if within width. existing
                wrapping is preserved. existing wrapping will be preserved.
                existing wrapping is preserved. existing wrapping will be
                preserved. existing wrapping is preserved. existing wrapping
                will be preserved. existing wrapping is preserved unless it goes too long for the display width.
                """,
            formatter_class=formatter,
        )
        parser.add_argument(
            "--option",
            choices=[
                "red",
                "blue",
            ],
            help="""
                Lots of text describing different choices.
                    red: a warning colour
                    text on the next line

                    blue: a longer blah blah keeps going going going going going going going going going going
                """,
        )
        print("\n\nDemo for {}\n".format(formatter.__name__))
        parser.print_help()

    demo_formatter(DescriptionWrappedNewlineFormatter)
    demo_formatter(WrappedNewlineFormatter)

WrappedNewlineFormatter的演示输出

usage: arg.py [-h] [--option {red,blue}]

A program that does things.
Lots of description that describes how the program works.

very long lines are wrapped. very long lines are wrapped. very long lines are
wrapped. very long lines are wrapped. very long lines are wrapped. very long
lines are wrapped.

existing wrapping will be preserved if within width. existing
wrapping is preserved. existing wrapping will be preserved.
existing wrapping is preserved. existing wrapping will be
preserved. existing wrapping is preserved. existing wrapping
will be preserved. existing wrapping is preserved unless it goes too long for
the display width.

optional arguments:
  -h, --help           show this help message and exit
                       
  --option {red,blue}  Lots of text describing different choices.
                       
                           red: a warning colour
                       
                           text on the next line
                       
                           blue: a longer blah blah keeps going going going
                       going going going going going going going
                       

DescriptionWrappedNewlineFormatter演示输出

usage: arg.py [-h] [--option {red,blue}]

A program that does things.
Lots of description that describes how the program works.

very long lines are wrapped. very long lines are wrapped. very long lines are
wrapped. very long lines are wrapped. very long lines are wrapped. very long
lines are wrapped.

existing wrapping will be preserved if within width. existing
wrapping is preserved. existing wrapping will be preserved.
existing wrapping is preserved. existing wrapping will be
preserved. existing wrapping is preserved. existing wrapping
will be preserved. existing wrapping is preserved unless it goes too long for
the display width.

optional arguments:
  -h, --help           show this help message and exit
  --option {red,blue}  Lots of text describing different choices. red: a
                       warning colour text on the next line blue: a longer
                       blah blah keeps going going going going going going
                       going going going going
                       ```


1

这是我找到的最简单的解决方案,它保持了自动换行到终端的能力,同时保留了手动换行。它归结为用一个版本替换默认格式化程序对textwrap.wrap()的调用,该版本保留手动换行。

我覆盖的_split_lines()_fill_text()当然是argparse的实现细节,所以这可能会在将来出现问题。如果发生这种情况,请在评论中注明!

import textwrap
from argparse import ArgumentParser, HelpFormatter


def wrap_paragraphs(text: str, width: int, indent: str):
    """
    Wrapper around `textwrap.wrap()` which keeps newlines in the input string
    intact.
    """
    lines = list[str]()

    for i in text.splitlines():
        paragraph_lines = \
            textwrap.wrap(i, width, initial_indent=indent, subsequent_indent=indent)

        # `textwrap.wrap()` will return an empty list when passed an empty
        # string (which happens when there are two consecutive line breaks in
        # the input string). This would lead to those line breaks being
        # collapsed into a single line break, effectively removing empty lines
        # from the input. Thus, we add an empty line in that case.
        lines.extend(paragraph_lines or [''])

    return lines


class Formatter(HelpFormatter):
    def _split_lines(self, text, width):
        return wrap_paragraphs(text, width, '')

    def _fill_text(self, text, width, indent):
        return '\n'.join(wrap_paragraphs(text, width, indent))


parser = ArgumentParser(
    prog='guide',
    formatter_class=Formatter,
    description='The Hitch Hiker\'s Guide to the Galaxy is a wholly remarkable '
                'book. It has been compiled and recompiled many times over '
                'many years and under many different editorships. It contains'
                'contributions from countless numbers of travellers and '
                'researchers.\n'
                '\n'
                'The introduction begins like this:\n'
                '\n'
                '"Space," it says "is big. Really big"\n')

parser.add_argument(
    '--probability',
    help='"But what does it mean?" cried Arthur.\n'
         '"What, the custard?"\n'
         '"No, the measurement of probability!"\n')


parser.print_help()

usage: guide [-h] [--probability PROBABILITY]

The Hitch Hiker's Guide to the Galaxy is a wholly remarkable book. It has been
compiled and recompiled many times over many years and under many different
editorships. It containscontributions from countless numbers of travellers and
researchers.

The introduction begins like this:

"Space," it says "is big. Really big"

options:
  -h, --help            show this help message and exit
  --probability PROBABILITY
                        "But what does it mean?" cried Arthur.
                        "What, the custard?"
                        "No, the measurement of probability!"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接