google / python-fire

Python Fire is a library for automatically generating command line interfaces (CLIs) from absolutely any Python object.
Other
27.05k stars 1.44k forks source link

feature request: tell fire to stop parsing argument and pass the remaining unparsed args to function #403

Open link89 opened 2 years ago

link89 commented 2 years ago

Feature Description

I am working on a tool to wrap some existed command line tools to make them easier to use (like auto install, inject argument automatically, etc). And I hope that I can have a way to tell Fire to stop parsing after encount some specific command.

Example Here are some real world examples like poetry run python3 ./my-scripts.py or conda run -n my-env python3 ./my-scripts.py

Without this feature I can only put everything else as string, just like what -c option of bash does, e.g. bash -c "echo 'hello world'", which could still work, but the user have to type extra string quote and dealing with escaping in some cases.

I am not sure if it is possbile to provide some decorator to archive this, for example

class Conda:
  def __init__(verbose=False):
    ...

  @fire.decorators.SkipParse
  def run(self, *argv):
    print(argv)

fire.Fire(Conda)

And when running with python fake-conda.py --verbose run which -a python3, the output should be ('which', '-a', 'python3')

melsabagh commented 2 years ago

I think part of the problem here is that -a requires disambiguation so that Fire can tell whether it's supposed to be treated as a positional argument or an option. In POSIX, this is typically done by passing -- to indicate the end of options. However, Fire has its own custom flags that use the same syntax (e.g., -- --interactive) which limits what you can do with -- here.

A hacky workaround I have came up with in the past was to disable Fire's custom flags altogether and force -- to behave like POSIX. Something like:

#!/usr/bin/env python3

import fire

def _patch_fire():
    # Disable Fire's custom flags.
    def _SeparateFlagArgs(args):
        return args, []
    fire.core.parser.SeparateFlagArgs = _SeparateFlagArgs

    # Make -- behave like POSIX, i.e., separate positional args from options.
    orig__ParseKeywordArgs = fire.core._ParseKeywordArgs
    def _ParseKeywordArgs(*args, **kwargs):
        kwargs, remaining_kwargs, remaining_args = orig__ParseKeywordArgs(*args, **kwargs)
        if remaining_kwargs:
            try:
                ddash_index = remaining_kwargs.index('--')
                remaining_args.extend(remaining_kwargs[ddash_index + 1:])
                remaining_kwargs = remaining_kwargs[:ddash_index]
            except ValueError:
                pass
        return kwargs, remaining_kwargs, remaining_args
    fire.core._ParseKeywordArgs = _ParseKeywordArgs

_patch_fire()

class Conda:
    def __init__(self, verbose=False):
        ...

    def run(self, *argv):
        print(argv)

if __name__ == '__main__':
    fire.Fire(Conda)

Now you can get the output you expect:

$ python3 fake-conda.py --verbose - run -- which -a python3
('which', '-a', 'python3')

And you can pass options too as long as you remember to end the options with --. For example, if you change the Conda class above to:

class Conda:
    def __init__(self, verbose=False):
        ...

    def run(self, *argv, foo=None, bar=None):
        print(argv, foo, bar)

You can still do things like:

$ python3 fake-conda.py --verbose - run --foo --bar=10 -- which -a pytthon3
('which', '-a', 'python3') True 10

Of course this hack is too intrusive and will likely brick the moment Fire changes anything this code touches.. but it might be enough for your needs until a proper feature for this lands in Fire.

link89 commented 2 years ago

Hi @melsabagh-kw Thank you for the sample code. I think it's a good idea to use -- to indicate the end of options, but that's not what I intended. I don't need to have fire to parse partial arguments and pass the remaining to the target function like the one in your sample code, but just stop parsing anything and pass all of the remining args to the target function.

def run(self, *argv, foo=None, bar=None):  # That would be too complicated to have fire handle partial of args
  pass

@fire.decorators.SkipParse
def run(self, *argv):  # that's the simple form I want to solve, by just stop paring anything
  pass

I have already make a PR for it and it works well in my use case to delegate arguments to the downstream commands. And this is the reason why I want this feature to create a tool to turn this

java -jar jenkins-cli.jar -s http://localhost:9090/ -webSocket list-jobs

into this

jenkins-fire-cli run list-jobs  

by wrapping the original command line to reduce some noise for the end user.