pythonglobpython-click

How to use windows_expand_args with single command click program


I have a Python program and am using Click to process the command line. I have a single command program, as in the example below. On Windows I would like to pass in a valid glob expression like *.py to Click 8.x and have the literal string "*.py" show up in the filepattern variable.

Click 8.x expands the glob patterns by default (this is different than 7.x). On Bash, if you quote the glob pattern, the shell doesn't do the expansion, and Click passes in the string. On Windows, the shell doesn't do the expansion and passes the string into Python. In this case Click expands them.

So the question here is how do I get the string "*.py" passed in as a string on Windows (if there are OR are not any files that match)? I only want to allow 1 argument (so nargs=1). This all works fine in Click 7.x.

The following example saved as mycommand.py:

import click

@click.command("mycommand")
@click.argument("filepattern", nargs=1, type=str)
def mycommand(filepattern):
    print(filepattern)


if __name__ == "__main__":
    mycommand()

If I have a directory full of, say Python files, if I invoke this as python mycommand.py somefile.py, it will succeed as one value gets passed into filepattern and it will echo somefile.py.

If I invoke as python mycommand.py *.py it fails with an error like:

Usage: mycommand.py [OPTIONS] FILEPATTERN
Try 'mycommand.py --help' for help.

Error: Got unexpected extra arguments (scratch.py mycommand.py ...)

I know there is an argument windows_expand_args for a click.group, but I can't puzzle out how to get it to work for a single command program.


Solution

  • Research in Click's GitHub repository

    A search for windows_expand_args in Click's GitHub repository gave the clue.

    From click changelog, Version 8.0.1, released 2021-05-19:

    • Pass windows_expand_args=False when calling the main command to disable pattern expansion on Windows. There is no way to escape patterns in CMD, so if the program needs to pass them on as-is then expansion must be disabled. :issue:1901

    Someone asked same question in the respective PR 1918:

    How is this used? I tried:

    import click
    
    @click.group(windows_expand_args=False)
    @click.pass_context
    def cli(ctx):
       ...
    

    and got this answer from the maintainer:

    It's an argument to the main program.

    cli(windows_expand_args=False)
    

    Example to use windows_expand_args=False

    As CrazyChucky already answered, invoke the @click.command decorated function with windows_expand_args as extra keyword argument (e.g. in your main like shown below):

    import click
    
    @click.command("mycommand")
    @click.argument("filepattern", nargs=1, type=str)
    def mycommand(filepattern):
        print(filepattern)
    
    
    if __name__ == "__main__":
        import os
        print(f"Click {click.__version__} on {os.name}")
        mycommand(windows_expand_args = False)
    

    Tests

    On Linux

    Could only test on Linux: failed, as expected by David Campbell's comment to the question 😉️.

    (a) with 1 quoted argument containing a glob like *

    python3 click_glob.py "*.py"
    
    Click 8.0.4 on posix
    *.py
    

    (b) with 1 unquoted argument containing a glob like *

    python3 click_glob.py *.py
    
    Click 8.0.4 on posix
    Usage: click_glob.py [OPTIONS] FILEPATTERN
    Try 'click_glob.py --help' for help.
    
    Error: Got unexpected extra argument (pdfminer_figures.py)
    

    (c) with 2 quoted arguments

    python3 click_glob.py "*.py" "invalid"
    
    Click 8.0.4 on posix
    Usage: click_glob.py [OPTIONS] FILEPATTERN
    Try 'click_glob.py --help' for help.
    
    Error: Got unexpected extra argument (invalid)
    

    On Windows

    Could not verify, since I have no Windows available. Maybe someone else can test on their Windows and edit this answer with results.

    (a) with 1 quoted argument containing a glob like *

    (b) with 1 unquoted argument containing a glob like *

    (c) with 2 quoted arguments