2011年3月26日

Python でコマンドラインオプションを扱う方法

コマンドライン引数の扱い方にはいくつかの方法がありますのでメモしておきます。 もちろん sys.argv を自前で解析すれば何とでもなりますが、その解析を頑張りたくはありませんね、という動機です。

  • 標準ライブラリ
  • フレームワークに特有の方法 - Django, Twisted, Tornado
  • 独立したライブラリ - gflags

標準ライブラリ

Python の標準ライブラリには次の3種類があります。

Python 以外のプログラミング言語でも同じような感じで使える、という意味で getopt は分かりやすいと言えます。 しかし、Python だけに絞れば optparse が便利です。 Python 2.7 からは optparse ではなく argparse を推奨されます。 対象とするスクリプトをどこで動かすか?という点がひとつの判断基準になると思います。

いずれも公式ドキュメントのコード例が充実していますので、見れば分かる、という感じが嬉しいところです。 チュートリアルとしては Dive Into Python を読む、ということで。

たとえば、 -v-q でログのオプションを変更する関数は次にように記述できます。

__doc__ = '''simple command line arguments and options parser.'''

import logging
import optparse

def parse_args():
    parser = optparse.OptionParser(__doc__)
    parser.add_option("-v", "--verbose", dest="verbose",
            default=False, action="store_true", help="verbose mode")
    parser.add_option("-q", "--quiet", dest="verbose",
            default=True, action="store_false", help="quiet mode")

    opts, args = parser.parse_args()

    if not args:
        parser.error("no parsing file is specified.")

    if opts.verbose:
        logging.basicConfig(level=logging.DEBUG)

    return opts, args

フレームワークに特有の方法

有名っぽいものを3つ。 いずれも easy_installpip でインストールできます。

  • Django
  • Twisted
  • Tornado

Django

Django は、Python ではたぶんもっとも広く使われている Web フレームワークです。

アプリケーションの management/commands ディレクトリに django.core.management.base.BaseCommand を継承したクラスを実装します。 option_list を定義しておくことで、 handle メソッドにキーワード引数が渡ってきます。 公式ドキュメントが充実していますので、リンク先のページが最も詳しいでしょう。

コマンドを定義しておくと、 manage.py に引数として与えることで自動的に探し出してくれます。 注意としては、各ディレクトリに __init.py__ を配置しておかないと検出できない、という点でしょうか。 とはいえ、本当に簡単なスクリプトのために Django を使うことはないと思いますので、 Python がモジュールを import するお作法どおり、ということで。

Twisted

Twisted はネットワーク処理用のライブラリです。

Twisted の場合はいくつかの方法があります。 まずは普通の処理。 Django の option_list と似た感じで optFlagsoptParameters を定義します。

from twisted.python import usage

class Options(usage.Options):
    optFlags = [["verbose", "v", "verbose mode"],]
    optParameters = [["port", "p", 8080, "run on the given port", int],]

def main():
    options = Options()
    try:
        options.parseOptions()
    except usage.UsageError, errortext:
        raise SystemExit('%s, use --help' % (errortext,))

    if options['verbose']:
        print "Verbose mode."
    print "Port #", options['port']

if __name__ == '__main__':
    main()

option-twisted.py として保存して実行すると、次のようになります。

$ python option-twisted.py --help
Usage: option-twisted.py [options]
Options:
  -v, --verbose  verbose mode
  -p, --port=    run on the given port [default: 8080]
      --version
      --help     Display this help and exit.

$ python option-twisted.py -v
Verbose mode.
Port # 8080

$ python option-twisted.py --port=8888
Port # 8888

Options ではサブコマンドも扱えます。

from twisted.python import usage

class ImportOptions(usage.Options):
    optParameters = [['module', 'm', None, None], ['release', 'r', None]]

class CheckoutOptions(usage.Options):
    optParameters = [['module', 'm', None, None], ['tag', 'r', None, None]]

class Options(usage.Options):
    subCommands = [['import', None, ImportOptions, "Do an Import"],
                   ['checkout', None, CheckoutOptions, "Do a Checkout"]]

PROC = {
    'import': lambda(opt): ("import", opt),
    'checkout': lambda(opt): ("checkout", opt)
}

def main():
    options = Options()
    try:
        options.parseOptions()
    except usage.UsageError, errortext:
        raise SystemExit('%s, use --help' % (errortext,))

    if not options.subCommand:
        raise SystemExit('unknown command, see --help')
    print PROC[options.subCommand](options.subOptions)

if __name__ == '__main__':
    main()

option-twisted-subcmd.py として保存して実行すると、次のようになります。 スクリプト自体に --help オプションを渡した場合にはコマンドの一覧が表示され、 コマンドに --help オプションを渡した場合には、そのコマンドのヘルプが表示されます。

$ python option-twisted-subcmd.py --help
Usage: option-twisted-subcmd.py [options]
Options:
      --version
      --help     Display this help and exit.
Commands:
    import        Do an Import
    checkout      Do a Checkout

$ python option-twisted-subcmd.py import --help
Usage: option-twisted-subcmd.py [options] import [options]
Options:
  -m, --module=
  -r, --release=
      --version
      --help      Display this help and exit.

$ python option-twisted-subcmd.py import -m abc -r 1
('import', {'release': '1', 'module': 'abc'})

$ python option-twisted-subcmd.py checkout --help
Usage: option-twisted-subcmd.py [options] checkout [options]
Options:
  -m, --module=
  -r, --tag=
      --version
      --help     Display this help and exit.

$ python option-twisted-subcmd.py checkout -m def -r 2
('checkout', {'tag': '2', 'module': 'def'})

Twisted には twistd と呼ばれるスクリプトも付属しています。 これはデーモン化するための諸々を扱ってくれるもので、 説明は Twisted Daemonologie (日本語) が詳しいと思います。 低レベルな部分からの話なので非常に長いですが...

twistd にコマンドを追加するためには、Django のようにフレームワークで決められたディレクトリに、 IPlugin クラスを実装したものを定義します。 twistd の場合は「プラグイン」という単位で、このためのディレクトリは twisted/plugins になります。 基本的にはカレントディレクトリから考えるのが間違いが少ないと思いますが、 twistd-d オプションで切り替えられます。この辺は好きずき、ということで。 プラグインの名前は tapname で指定します。 既存のプラグインと重複しない名前を選ぶ必要があります。

from zope.interface import implements
from twisted.python import usage, log
from twisted.plugin import IPlugin
from twisted.application import service

class Options(usage.Options):
    optParameters = [["port", "p", 8080, "run on the given port", int],]

class SimpleServiceMaker(object):
    implements(service.IServiceMaker, IPlugin)
    tapname = "simpleservice"
    description = "A simple service."
    options = Options

    def makeService(self, options):
        top_service = service.MultiService()
        log.msg("make service with some options: " + repr(options))
        return top_service

service_maker = SimpleServiceMaker()

これを twisted/plugins/option_twistd.py として保存します。 途中のディレクトリに __init_.py を置く必要はありません。 ファイルとプラグイン名は別々で構いませんが、途中にハイフンなどは使えないようです。 とはいえ、プラグイン名とファイル名を揃えておいた方が管理しやすいでしょう。(ここでは例示のために別々にしている)

$ twistd --help |grep simpleservice
simpleservice      A simple service.

$ twistd simpleservice --help
Usage: twistd [options] simpleservice [options]
Options:
  -p, --port=    run on the given port [default: 8080]
      --version
      --help     Display this help and exit.

module(name[, doc])

Create a module object. The name must be a string; the optional doc argument can have any type.

$ twistd --nodaemon simpleservice --port=8888
(snip)
reactor が開始されるログが表示されます。
Ctrl+C で停止できます。

改めて Options を確認すると、 twistd だから特別、ということはありません。 どの場面でも同じオプション用のデータとして扱えます。

この他にも Twisted の場合は、後処理や同一オプションのカウントなども簡単に実現可能です。 詳しくは公式ドキュメントに記載されています。 twistd についても、 .tac ファイルを使う、という選択肢もあります。 こちらも詳しくは公式ドキュメントに記載されています。

Tornado

Tornado はノンブロッキングの Web サーバフレームワークです。

たとえば、Web サーバのポート番号をオプションで取得するコードです。

import tornado.options

tornado.options.define(
    "port", default=8080, type=int, help="run on the given port")

def main():
    try:
        tornado.options.parse_command_line()
    except tornado.options.Error, e:
        raise SystemExit(e)
    port = tornado.options.options.port
    print "Port #", port

if __name__ == '__main__':
    main()

これを option-tornado.py として保存して実行すると次のようになります。

$ python option-tornado.py --help
Usage: option-tornado.py [OPTIONS]

Options:
  --help                           show this help information
  --log_file_max_size              max size of log files before rollover
  --log_file_num_backups           number of log files to keep
  --log_file_prefix=PATH           Path prefix for log files. Note that if you are running multiple tornado processes, log_file_prefix must be different for each of them (e.g. include the port number)
  --log_to_stderr                  Send log output to stderr (colorized if possible). By default use stderr if --log_file_prefix is not set and no other logging is configured.
  --logging=info|warning|error|none Set the Python log level. If 'none', tornado won't touch the logging configuration.
option-tornado.py
  --port                           run on the given port

$ python option-tornado.py
Port # 8080

$ python option-tornado.py --port=8888
Port # 8888

ログに関する設定がデフォルトで組み込まれており、独自に定義したオプションはその下に表示されます。

独立したライブラリ - gflags

python-gflags は Google がオープンソースとして公開しているライブラリで、 C++ 用の google-gflags の Python 版です。 基本的な機能は標準ライブラリのものと同様ですが、「そのオプションが使われるソースファイルでフラグを定義できる」という点がユニークです。

gflags モジュールは google-api-python-client でも利用されています。 たとえば次のように使用できます。 gflags.py 自体に使い方が詳述されていますので、英語を読解すれば何とかなりそうです。

import logging
import os
import sys

import gflags
from apiclient.discovery import build
from oauth2client.file import Storage
from oauth2client.client import OAuth2WebServerFlow
from oauth2client.tools import run

FLAGS = gflags.FLAGS
gflags.DEFINE_enum('logging_level', 'ERROR',
    ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
    'Set the level of logging detail.')

gflags.DEFINE_string('basedir',
    os.path.join(os.environ['HOME'], '.google_api'),
    'Directory where credentials saved.')

gflags.DEFINE_integer('day_term', 7,
    'Term of days to begin with.', lower_bound=0)

def main(argv):
    try:
        argv = FLAGS(argv)
    except gflags.FlagsError, e:
        print '%s\nUsage: %s ARGS\n%s' % (e, argv[0], FLAGS)
        sys.exit(1)

    logging.getLogger().setLevel(getattr(logging, FLAGS.logging_level))

    print "Base directory:",  FLAGS.basedir
    print "Term:", FLAGS.day_term

if __name__ == '__main__':
    main(sys.argv)

これを option-gflags.py として保存して実行すると次のようになります。

$ python option-gflags.py --help

USAGE: option-gflags.py [flags]

flags:

option-gflags.py:
  --basedir: Directory where credentials saved.
    (default: '/Users/shigeru/.google_api')
  --day_term: Term of days to begin with.
    (default: '7')
    (a non-negative integer)
  -?,--[no]help: show this help
  --[no]helpshort: show usage only for this module
  --[no]helpxml: like --help, but generates XML output
  --logging_level: <DEBUG|INFO|WARNING|ERROR|CRITICAL>: Set the level of logging
    detail.
    (default: 'ERROR')

apiclient.model:
  --[no]dump_request_response: Dump all http server requests and responses. Must
    use apiclient.model.LoggingJsonModel as the model.
    (default: 'false')

oauth2client.tools:
  --auth_host_name: Host name to use when running a local web server to handle
    redirects during OAuth authorization.
    (default: 'localhost')
  --auth_host_port: Port to use when running a local web server to handle
    redirects during OAuth authorization.;
    repeat this option to specify a list of values
    (default: '[8080, 8090]')
    (an integer)
  --[no]auth_local_webserver: Run a local web server to handle redirects during
    OAuth authorization.
    (default: 'true')

gflags:
  --flagfile: Insert flag definitions from the given file into the command line.
    (default: '')
  --undefok: comma-separated list of flag names that it is okay to specify on
    the command line even if the program does not define a flag with that name.
    IMPORTANT: flags in this list that have arguments MUST use the --flag=value
    format.
    (default: '')

$ python option-gflags.py
Base directory: /Users/shigeru/.google_api
Term: 7

$ python option-gflags.py --basedir=/etc/google_api
Base directory: /etc/google_api
Term: 7

import 先のオプション (oauth2client.tools)、およびその先の import (apiclient.model) のオプションも扱うことができます。 oauth2client.tools は OAuth 2.0 の認証フローを実行するためのものです。 ライブラリで隠蔽してある設定情報も簡単に与えられるのは便利だと思います。 ここではコマンドライン引数 (sys.argv) を与えていますが、これは単なるリストですから、 任意の実行環境 (たとえば Google App Engine) でオプションを制御できますね。

終わりに

コマンドラインオプションの扱い方をざーっと見てみました。 フレームワークを使う場合には、そのお作法に沿って記述するのがマナーだと思いますが、 それぞれの方法ごとにヘルプメッセージや入力値のチェックが異なりますので、 たまには比較してみるのも良いのかな、と感じます。

  • 組込みのオプション項目
  • 出力されるヘルプテキスト
  • ショートオプションとロングオプションを対応付ける方法
  • デフォルト値の設定方法
  • データ型の限定、制限値の定義方法
  • 選択肢を限定する方法
  • オプション定義を記述できる場所

とはいえ、 gflags の使い方をメモしておきたかった、というのが一番です。 ライブラリコードの中でオプションを定義できることは一長一短あるでしょうが、 OAuth 2.0 のリダイレクトコールバックを受け取るような用途だと便利だな、と思いました。

コメントを投稿