[PYTHON] The minimum Cement information you need

What's Cement

A command line application framework for python. cement. I don't have much Japanese information, so make a note for myself to google later.

It says "A Framework, for the CLI Ninja." Are all the cool things ninja?

Installation

You can install it with pip.

pip install cement

Command line application development

Simple application

Something like this. If you want to make a single-function tool, this level of template is enough.

myapp1 -h
myapp1 --option <option>
myapp1 --option -F <argument>
myapp1 --option -F <argument> <省略可能なargument>
myapp1 --option -F <argument> <省略可能なargument> <省略可能なargument> .... 

One controller. The options to be defined individually are -F and --option. There is one more required argument. An arbitrary number of arguments.

myapp.py


#-*- coding:utf-8 -*-
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
from cement.core import handler

class BaseController(CementBaseController):
    class Meta:
        label = 'base'
        description = "This is the explanation of this command"
        arguments = [
            ( ['-o', '--option'],
              dict(action='store', default="default option value",help='I will specify the option') ),
            ( ['-F'],
              dict(action='store_true', help='Uppercase F option') ),
            (['param1'], dict(action='store', nargs=1, help = "It's the first argument")),
            (['param2'], dict(action='store', nargs="*", metavar="PARAM2", help = "It's the second argument", default = ["default ext value"])),
            ]

    @expose(hide=True)
    def default(self):
        self.app.log.debug("Default processing-start")
        if self.app.pargs.option:
            print "The parameters specified by option are<%s>" % self.app.pargs.option

        if self.app.pargs.F:
            print "The F option was specified"

        if self.app.pargs.param1:
            print "argument: %s" % self.app.pargs.param1[0]

        if self.app.pargs.param2:
            print "argument: %s" % self.app.pargs.param2[0]

        self.app.log.info("Default processing")
        self.app.log.debug("Default processing-End")

class App(CementApp):
    class Meta:
        label = 'app'
        base_controller = 'base'
        handlers = [BaseController]

with App() as app:
    app.run()

Subcommand

The controller interprets the method with @exporse () as a subcommand. The default method is called when the subcommand is omitted.

    @expose(aliases=["y!", "sb"], help="Description of the default method")
    def yahoo(self):
        self.app.log.info("yahoo processing")
        
    @expose(hide=True)
    def default(self):
        self.app.log.info("It's the default process")

Subcommands and position arguments are difficult to use. This is because the argument and option settings are set for each controller, and if a subcommand collides with an argument, it will be interpreted as a subcommand. For example, in the above example, if you omit the subcommand and pass default or yahoo as the first argument, it will not work. It will be interpreted as a subcommand.

This is unavoidable because it is inevitable when designing a subcommand type CLI.

By the way, the definition of the argument is common in the same controller. Therefore, if there is a controller that has one required argument, a subcommand that does not have an argument cannot be defined in it.

If you want to do such a design, use Namespace (nested controller) described later.

Namespace

You can nest controllers as Namespaces. Example --Multiple Stacked Controllers is easy to understand.

Create the following command system.

myapp2.py <argument>
myapp2.py sub
myapp2.py sub hello
myapp2.py sub world

Note that the plain call to myapp2.py (that is, MainController) requires the first argument, but the namespace sub does not. Both hello and world are subcommands of the namespace sub, not arguments.

myapp2.py


#-*- coding:utf-8 -*-
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
from cement.core import handler

class BaseController(CementBaseController):
    class Meta:
        label = 'base'
        description = "It ’s an explanation of the base command."


class MainController(CementBaseController):
    class Meta:
        label = 'main'
        description = "It's a description of the main controller"
        stacked_on = 'base'
        stacked_type = 'embedded'
        arguments = [
            (['param1'], dict(action='store', nargs=1, help="Required first argument"))
            ]

    @expose(hide=True)
    def default(self):
        self.app.log.debug("Default processing-start")
        print "argument: %s" % self.app.pargs.param1[0]
        self.app.log.info("Default processing")
        self.app.log.debug("Default processing-End")

class SubController(CementBaseController):
    class Meta:
        label = 'sub'
        description = "It's a description of the sub controller"
        stacked_on = 'base'
        stacked_type = 'nested'

        arguments = [ ]

    @expose(hide=True)
    def default(self):
        self.app.log.info("Subcontroller processing")

    @expose()
    def hello(self):
        self.app.log.info("hello world")

    @expose()
    def world(self):
        self.app.log.info("the world")

class App(CementApp):
    class Meta:
        label = 'app'
        base_controller = 'base'
        handlers = [BaseController, MainController, SubController]

with App() as app:
    app.run()

Well, however, from the command user's point of view, sub is a subcommand.

Returns any status code

All you have to do is assign a value to app.exit_code.

    @expose(hide=True)
    def default(self):
        self.app.log.error('Not yet implemented')
        self.app.exit_code = 1

Be aware that the return value of a subcommand may inadvertently become the return value of the entire command.

Receive input on a pipe

Make it work with both pipe and file specification.

cement3.py hello.txt
cat hello.txt | cement3.py

You can write smartly by combining ʻargparse.FileTypeanddefault = sys.stdin`. Can be an optional argument with nargs = "?".

cement3.py


#-*- coding:utf-8 -*-
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
from cement.core import handler
import argparse
import sys

class BaseController(CementBaseController):
    class Meta:
        label = 'base'
        description = "This is the explanation of this command"
        arguments = [
            (["input"], dict(nargs='?', type=argparse.FileType('r'), default=sys.stdin ))
            ]

    @expose(hide=True)
    def default(self):
        self.app.log.debug("Default processing-start")
        for line in self.app.pargs.input:
          print ">>> %s" % line
        self.app.log.debug("Default processing-End")

class App(CementApp):
    class Meta:
        label = 'app'
        base_controller = 'base'
        handlers = [BaseController]

with App() as app:
    app.run()

Summary

--Cement The design is pretty cool, so let's use it up and make a cool CLI. -Demonized as an extension is supported. --For argument processing, argparse is used as it is, so refer to that. --Support for reading settings from conf file. ――The development method of memcached handler and plugin system, the method of customizing the output, etc. are also prepared, and it can be used enough for full-scale big application development. However, since it is a good library for developing simple tools in a unified way, I think that it can be used from a small scale.

Recommended Posts

The minimum Cement information you need
You may need git to install the jupyter lab extension
How do you collect information?
Can you delete the file?
[Simplified version] How to export the minimum reading meter recording information