Opened 2 hours ago

Closed 64 minutes ago

#35926 closed New feature (wontfix)

Support capturing the remainder of a command-line arguments in BaseCommand

Reported by: Daniel Quinn Owned by:
Component: Core (Management commands) Version: dev
Severity: Normal Keywords: unknown args
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

I wanted to write a management command that would accept a few known arguments and then capture the remaining arbitrary arguments passed as a separate value. For example:

./manage.py mycommand --expected=argument --something-arbitrary -x -y -z

So --expected-argument would be defined in .add_arguments(), but the other args would just be available as something like options["remainder"] or whatever.

This is usually done by calling parse_known_args() rather than the more common .parse_args(), but unfortunately there's currently no way to cleanly indicate this with BaseCommand. The only way seems to be to copy/paste the entirety of BaseCommand.run_from_argv() and then change where.parse_args() is invoked:

    def run_from_argv(self, argv):
        """
        Copypasta from the parent class, so I can change `.parse_args()` to
        `.parse_known_args()`.
        """

        self._called_from_command_line = True
        parser = self.create_parser(argv[0], argv[1])

        # Change --------------------------------------------------------------
        options, remainder = parser.parse_known_args(argv[2:])
        cmd_options = vars(options)
        cmd_options["remainder"] = remainder
        # /Change -------------------------------------------------------------

        # Move positional args out of options to mimic legacy optparse
        args = cmd_options.pop("args", ())
        handle_default_options(options)
        try:
            self.execute(*args, **cmd_options)
        except CommandError as e:
            if options.traceback:
                raise

            # SystemCheckError takes care of its own formatting.
            if isinstance(e, SystemCheckError):
                self.stderr.write(str(e), lambda x: x)
            else:
                self.stderr.write("%s: %s" % (e.__class__.__name__, e))
            sys.exit(e.returncode)
        finally:
            try:
                connections.close_all()
            except ImproperlyConfigured:
                # Ignore if connections aren't setup at this point (e.g. no
                # configured settings).
                pass

Obviously that's not ideal.

One option might be to just use .parse_known_args() and then allow the user to indicate whether they want to capture the remainder or not, and if so, as what attribute, but I have no strong feelings about implementation.

Change History (1)

comment:1 by Natalia Bidart, 64 minutes ago

Keywords: unknown args added; management command remainder removed
Resolution: wontfix
Status: newclosed
Version: 5.1dev

Hello Daniel, thank you for taking the time to create this ticket. I see two sides in your request:

Side A

As a Django Fellow, I think that the provided example seems a very specific need arising from a niche use case. I don't think this applies to the broader ecosystem, and Django is a framework designed to offer robust and accurate solutions for common scenarios. Furthermore, as a seasoned developer, I think that allowing any amount of unknown named params in a command is a bad programming pattern, so I would advice against that pattern.

What you could do instead, is to allow any number of arguments associated with a single argument name that is defined in your parser, something similar to what the docs shows for custom management commands when defining nargs for polls_ids:

    def add_arguments(self, parser):
        parser.add_argument("poll_ids", nargs="+", type=int)

(Yes, this limits the type of the nargs but I think that is a good thing!)

Side B

Assuming that we consider the provided use case and want to build a solution, I think the best approach would be, in your code, to monkeypatch CommandParser to be replaced with a custom class that reimplements parse_args and stores the unknown args to do something with them when needed:

  • django/core/management/base.py

    diff --git a/django/core/management/base.py b/django/core/management/base.py
    index 6232b42bd4..d8ea38ac3c 100644
    a b class CommandParser(ArgumentParser):  
    6565            args or any(not arg.startswith("-") for arg in args)
    6666        ):
    6767            self.error(self.missing_args_message)
    68         return super().parse_args(args, namespace)
     68        known, unknown = super().parse_known_args(args, namespace)
     69        # Do something with unknown args.
     70        return known
    6971
    7072    def error(self, message):

Given the above, I'll close the ticket accordingly, but if you disagree, you can consider starting a new conversation on the Django Forum, where you'll reach a wider audience and likely get extra feedback. More information in the documented guidelines for requesting features.

Note: See TracTickets for help on using tickets.
Back to Top