2

For a Python script that uses argparse and has a very long argument list, is it possible to make argparse page what it prints to the terminal when calling the script with the -h option?

HelloGoodbye
  • 3,624
  • 8
  • 42
  • 57
  • You can use the formatter-class https://docs.python.org/3/library/argparse.html#formatter-class – paltaa Mar 11 '20 at 14:25
  • @paltaa Okay; how does that help me to page the output (I don't see anything about that in the page you linked to)? – HelloGoodbye Mar 11 '20 at 14:42
  • 1
    A good question, but a bad idea IMHO. At least on Linux/Unix where ``-h|--help`` is expected to print the help text and exit. The user should decide if he needs a pager. He/she might want to scroll back in the terminal window for instance. Also please note that you would have to deal with redirected input/output and enable the pager only on a tty. I would recommend to stick with the usual practice to keep the help text terse and put all details into the man page. Or each subcommand or topic can have its own help. – VPfB Mar 11 '20 at 14:58
  • @VPfB I know about the `man` shell command; do you mean I can make it display a help text for my script? How? – HelloGoodbye Mar 11 '20 at 15:34
  • 2
    The help is displayed with the `parser.print_help` method, which sends the `parser.format_help()` string to `stdout`. Conceivably you could splice the pager in here. But it would be simpler to do the paging at shell level: `python your_script.py -h | less` – hpaulj Mar 11 '20 at 16:01
  • `ipython` uses a customized version of `argparse`, producing a very long help. I just use terminal window scrolling to see the whole thing. But I also can pipe it to `less` (6 pages). – hpaulj Mar 11 '20 at 16:04
  • @HelloGoodbye You could write a man page if you want. Check the links from this answer: https://serverfault.com/a/109559 – VPfB Mar 11 '20 at 16:26
  • `awscli` achieves this through some complex yet exhaustive logic for their `aws help`: https://github.com/aws/aws-cli/blob/develop/awscli/help.py#L276 – Flair Apr 01 '21 at 22:10

1 Answers1

0

I could not find a quick answer, so I wrote a little something:

# hello.py
import argparse
import os
import shlex
import stat
import subprocess as sb
import tempfile


def get_pager():
    """
    Get path to your pager of choice, or less, or more
    """
    pagers = (os.getenv('PAGER'), 'less', 'more',)
    for path in (os.getenv('PATH') or '').split(os.path.pathsep):
        for pager in pagers:
            if pager is None:
                continue
            pager = iter(pager.split(' ', 1))
            prog = os.path.join(path, next(pager))
            args = next(pager, None) or ''
            try:
                md = os.stat(prog).st_mode
                if md & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
                    return '{p} {a}'.format(p=prog, a=args)
            except OSError:
                continue


class CustomArgParser(argparse.ArgumentParser):
    """
    A custom ArgumentParser class that prints help messages
    using either your pager, or less or more, if available.
    Otherwise, it does what ArgumentParser would do.
    Use the PAGER environment variable to force it to use your pager
    of choice.
    """
    def print_help(self, file=None):
        text = self.format_help()
        pager = get_pager()
        if pager is None:
            return super().print_help(file)
        fd, fname = tempfile.mkstemp(prefix='simeon_help_', suffix='.txt')
        with open(fd, 'w') as fh:
            super().print_help(fh)
        cmd = shlex.split('{p} {f}'.format(p=pager, f=fname))
        with sb.Popen(cmd) as proc:
            rc = proc.wait()
            if rc != 0:
                super().print_help(file)
            try:
                os.unlink(fname)
            except:
                pass


if __name__ == '__main__':
    parser = CustomArgParser(description='Some little program')
    parser.add_argument('--message', '-m', help='Your message', default='hello world')
    args = parser.parse_args()
    print(args.message)

This snippet does main things. First, it defines a function to get the absolute path to a pager. If you set the environment variable PAGER, it will try and use it to display the help messages. Second, it defines a custom class that inherits pretty much everything from argparse.ArgumentParser. The only method that gets overridden here is print_help. It implements print_help by defaulting to super().print_help() whenever a valid pager is not found. If a valid is found, then it writes the help message to a temporary file and then opens a child process that invokes the pager with the path to the temporary file. When the pager returns, the temporary file is deleted. That's pretty much it.

You are more than welcome to update get_pager to add as many pager programs as you see fit.

Call the script:

python3 hello.py --help ## Uses less
PAGER='nano --view' python3 hello.py --help ## Uses nano
PAGER=more python3 hello.py --help ## Uses more
Abdou
  • 12,931
  • 4
  • 39
  • 42