formatc1702
formatc1702

Reputation: 13

Python 3 + Click: CLI arguments get butchered when calling one command from another

I am using Python 3.9 and Click to build a small command line interface utility, but I am getting strange errors, specifically when I attempt to call one function decorated as a @click.command() from another function that is also decorated the same way.

I have distilled my program down to the bare minimum to explain what I mean.

This is my program

import click

@click.group()
def cli():
    pass

@click.command()
@click.argument('id')
def outer(id):
    print('outer id',id,type(id))
    inner(id)  # run inner() from within outer(), pass argument unchanged

@click.command()       # *
@click.argument('id')  # *
def inner(id):
    print('inner id',id,type(id))

cli.add_command(outer)
cli.add_command(inner)  # *

if __name__ == '__main__':
    cli()

This is the CLI result when I run my script using both the inner and outer commands:

% python3 test.py inner 123
inner id 123 <class 'str'>
% python3 test.py outer 123
outer id 123 <class 'str'>
Usage: test.py [OPTIONS] ID
Try 'test.py --help' for help.

Error: Got unexpected extra arguments (2 3)
%

Interestingly, it works when I use a single character argument:

% python3 test.py outer 1
outer id 1 <class 'str'>
inner id 1 <class 'str'>
%

If I comment out the three lines marked with # *, running the outer command behaves as expected, passing on the id argument to inner() without changes or issues, no matter the length of the argument:

% python3 test.py outer 123
outer id 123 <class 'str'>
inner id 123 <class 'str'>
%

Clearly, the decorators are somehow messing with the arguments. Can anybody explain why this is, and how I can achieve the desired behavior of passing on the arguments unchanged? Maybe I am missing something super obvious?

Thanks in advance!

Upvotes: 1

Views: 816

Answers (1)

Barmar
Barmar

Reputation: 781129

Use the context operations to invoke other commands

import click

@click.group()
def cli():
    pass

@click.command()
@click.argument('id')
@click.pass_context
def outer(ctx, id):
    print('outer id',id,type(id))
    ctx.forward(inner)  # run inner(), pass argument unchanged
    ctx.invoke(inner, id=id+1) # run inner(), pass modified argument

@click.command()       # *
@click.argument('id')  # *
def inner(id):
    print('inner id',id,type(id))

cli.add_command(outer)
cli.add_command(inner)  # *

if __name__ == '__main__':
    cli()

Upvotes: 2

Related Questions