1
votes

Symfony Console is really great, pretty flexible, and easy to program.

I'm looking for a similar solution. Maybe not so cool, but with the basics described below.

Main requirements for this CLI programming tool:

  1. first argument is a command.
    E.g. python myapp.py command1
  2. nesting commands
    E.g.: foo - one command, foo:bar subcommand
    python myapp.py foo and python myapp.py foo:bar
  3. one command - one object
    E.g. from symfony: cli_app.add(FooBarCommand(name='foo:bar'))
  4. (most important) arguments and options are different entities
    E.g.: myapp.py foo:bar arg1 arg2 --opt1 --opt2-w-value myvalue
    {'arguments': ['arg1', 'arg2'], 'options': {'opt1': true, 'opt2-w-value': 'myvalue'}
  5. Options can be set before arguments
    E.g.: myapp.py foo:bar --opt1 arg1
  6. An option can be an array
    E.g. myapp.py foo:bar --opt5 val1 --opt5 val2 => {'opt5': ['val1', 'val2']}
2
Found an article codeburst.io/…. But haven't tried any solution yet. - Kirby

2 Answers

1
votes

I know that the library Cleo used to be a port of the symfony console component and I used it for a project a long time ago. It worked really well. I'm not sure what the status of that project is now a days.

1
votes

Nessaid CLI frameworks let's us do this in a slightly different way. The package provides a python Cmd like interface with readline integration and helps us to specify the commands using a grammar integrated as docstrings for methods. The 5 objectives can be implemented by the following example.

import sys
from nessaid_cli.cmd import NessaidCmd

from nessaid_cli.tokens import (
    StringToken,
    RangedIntToken,
    RangedStringToken
)

class TestCmd(NessaidCmd):
    """
    token STRING_TOKEN StringToken();
    token RANGED_STRING_TOKEN_1 RangedStringToken(5, 10);
    token RANGED_INT_TOKEN_1 RangedIntToken(1, 1000);
    """

    def get_token_classes(self):
        return [StringToken, RangedIntToken, RangedStringToken]

    def do_command_1(self):
        """
        "command1"
        """
        print("This is command 1")

    def do_foo(self, foo, bar):
        """
        "foo"
        RANGED_STRING_TOKEN_1 << $foo = $2; >>
        {
            # Braced parts are optional
            RANGED_STRING_TOKEN_1 << $bar = $1; >>
        }
        """
        print("Foo:", foo)
        if bar:
            print("Bar:", bar)

    def do_foo_arg_optionals(self, foo, bar, args, optionals):
        """
        <<
            $opt1 = "";
            $args = list();
            $optionals = list();
        >>
        "foo1"
        RANGED_STRING_TOKEN_1 << $foo = $2; >>
        {
            # Braced parts are optional
            RANGED_STRING_TOKEN_1 << $bar = $1; >>
        }
        (
            STRING_TOKEN << $args = append($args, $1); >>
            STRING_TOKEN << $args = append($args, $2); >>
            {
                (
                    "true" << $optionals = append($optionals, $1); >>
                    |
                    "false" << $optionals = append($optionals, $1); >>
                )
                (
                    "opt2"
                    RANGED_STRING_TOKEN_1
                    << $optionals = append($optionals, $2); >>
                )
            }
        )
        """
        print("Foo:", foo)
        if bar:
            print("Bar:", bar)
        print("args:", args)
        print("optionals:", optionals)

    def do_command4(self, opt, arg):
        """
        "command4"
        {
            "opt1"
            RANGED_STRING_TOKEN_1 << $opt = $2; >>
        }
        (
            RANGED_STRING_TOKEN_1 << $arg = $3; >>
        )
        """
        if opt:
            print("opt:", opt)
        print("arg:", arg)

    def do_array(self, array):
        """
        <<$array = list();>>
        "array"
        (
            "opt5"
            RANGED_STRING_TOKEN_1
            <<$array = append($array, $2);>>
        ) * (1: 5)
        "other-args"
        (
            "opt5"
            RANGED_STRING_TOKEN_1
            <<$array = append($array, $2);>>
        ) * (1: 2)
        """
        print("Array:", array)


if __name__ == '__main__':
    cmd = TestCmd(prompt="nessaid-cmd # ", show_grammar=True)
    #show_grammar will print the generated grammar specification
    try:
        cmd.loop.run_until_complete(cmd.cmdloop(intro="Starting Nessaid CMD Demo"))
    except KeyboardInterrupt:
        sys.exit(0)
    except Exception as e:
        print("Exception in cmdloop:", e)
        sys.exit(1)
    sys.exit(0)

https://github.com/saithalavi/nessaid_cli

This will give an interface like cmd module. The commands can be developed with a grammar to specify sequencing, alternatives, optionals and repititions, with helpstring, suggestions and tab completion at each level. It also supports cutom input tokens.

The output of the above example

nessaid-cmd # <TAB>
array
command1
command4
end
exit
foo
foo1
quit

1.
nessaid-cmd # comm
command1
command4
nessaid-cmd # command1
NEWLINE: Complete command
nessaid-cmd # command1
This is command 1

2.
nessaid-cmd # fo
foo
foo1
nessaid-cmd # foo
Any string of length (5-10)
nessaid-cmd # foo asdfgh
Any string
Any string of length (5-10)
NEWLINE: Complete command
nessaid-cmd # foo asdfgh
Foo: asdfgh
nessaid-cmd # foo asdfgh 123456
Foo: asdfgh
Bar: 123456
nessaid-cmd #

3.
nessaid-cmd # foo1
Any string of length (5-10)
nessaid-cmd # foo1 asd
Failed to parse: Could not match any rule for this sequence
nessaid-cmd # foo1 asd123
Any string
Any string of length (5-10)
nessaid-cmd # foo1 asd123 as
Any string
nessaid-cmd # foo1 asd123 as df
NEWLINE: Complete command
false
true
nessaid-cmd # foo1 asd123 as df tr
true
nessaid-cmd # foo1 asd123 as df true
opt2
nessaid-cmd # foo1 asd123 as df true op
opt2
nessaid-cmd # foo1 asd123 as df true opt2
Any string of length (5-10)
nessaid-cmd # foo1 asd123 as df true opt2 123456
Any string of length (5-10)
nessaid-cmd # foo1 asd123 as df true opt2 123456
Any string of length (5-10)
nessaid-cmd # foo1 asd123 as df true opt2 123456
NEWLINE: Complete command
nessaid-cmd # foo1 asd123 as df true opt2 123456
Foo: asd123
args: ['foo1', 'asd123']
optionals: ['true', 'opt2']
nessaid-cmd #


4.
nessaid-cmd # command4 aaaaaa
arg: aaaaaa
nessaid-cmd # command4 opt1 asdfgh 123456
opt: asdfgh
arg: 123456

5.
nessaid-cmd # array opt5 12345 opt5 qwerty other-args opt5 2222223
Array: ['12345', 'qwerty', '2222223']
nessaid-cmd # array opt5 12345 opt5 qwerty other-args opt5 2222223 opt5 69874566
Array: ['12345', 'qwerty', '2222223', '69874566']
nessaid-cmd # array opt5 qwerty other-args opt5 2222223 opt5 69874566
Array: ['qwerty', '2222223', '69874566']

[UPDATE] The library now supports passing the input from command line. Just like opening the commandline and entering the last command, ie

nessaid-cmd # array opt5 qwerty other-args opt5 2222223 opt5 69874566

The program itself can be invoked with the input. eg. For the last example,

python3 test.py array opt5 qwerty other-args opt5 2222223 opt5 69874566

Will print the same output Array: ['qwerty', '2222223', '69874566']