shlex – シェルスタイルの字句解析

目的:シェルスタイルの字句解析
利用できるバージョン:1.5.2, その後のバージョンで機能追加

shlex モジュールはシェルによく似たシンプルな構文で字句解析するクラスを実装します。それは独自のドメイン特化言語(DSL)を書いたり、引用符で囲まれた文字列(一目見て複雑に見えるタスク)を解析するために使用されます。

引用符で囲まれた文字列

入力のテキストを解析するときの共通の課題は、1つの実体として引用符で囲まれたワードのシーケンスを認識することです。引用符でテキストを分割する方法は、いつもうまく行くとは限りません。特に引用符がネストされている場合です。次のテキストを見てください。

This string has embedded "double quotes" and 'single quotes' in it,
and even "a 'nested example'".

すぐに思い付く方法は、引用符の内側のテキストから引用符の外側のテキスト部分を分離するために正規表現を組み立てても良いかもしれません。もしくはその逆の方法もあります。そういった方法は不必要に複雑になり、アポストロフィや誤字のようなエッジケースの場合にエラーを発生しがちです。もっと良い解決方法は、 shlex モジュールが提供するような本物のパーサを使用することです。次に入力ファイルで認識されたトークンを表示する簡単なサンプルを紹介します。

import shlex
import sys

if len(sys.argv) != 2:
    print 'Please specify one filename on the command line.'
    sys.exit(1)

filename = sys.argv[1]
body = file(filename, 'rt').read()
print 'ORIGINAL:', repr(body)
print

print 'TOKENS:'
lexer = shlex.shlex(body)
for token in lexer:
    print repr(token)

組み込みの引用符でそのデータを処理すると、このパーサは期待したトークンのリストを生成します。

$ python shlex_example.py quotes.txt
ORIGINAL: 'This string has embedded "double quotes" and \'single quotes\' in it,\nand even "a \'nested example\'".\n'

TOKENS:
'This'
'string'
'has'
'embedded'
'"double quotes"'
'and'
"'single quotes'"
'in'
'it'
','
'and'
'even'
'"a \'nested example\'"'
'.'

アポストロフィのような独立した引用符も扱えます。次の入力を試してみてください。

This string has an embedded apostrophe, doesn't it?

アポストロフィを持つトークンでも問題ありません。

$ python shlex_example.py apostrophe.txt
ORIGINAL: "This string has an embedded apostrophe, doesn't it?"

TOKENS:
'This'
'string'
'has'
'an'
'embedded'
'apostrophe'
','
"doesn't"
'it'
'?'

組み込みコメント

パーサはコマンド言語で使用されることを意図しているのでコメントを扱う必要があります。デフォルトでは # に続く任意のテキストをコメントと見なして無視します。パーサの特性上、コメントの接頭辞は一文字のみがサポートされます。コメントの文字セットは commenters プロパティで設定されています。

$ python shlex_example.py comments.txt
ORIGINAL: 'This line is recognized.\n# But this line is ignored.\nAnd this line is processed.'

TOKENS:
'This'
'line'
'is'
'recognized'
'.'
'And'
'this'
'line'
'is'
'processed'
'.'

Split

文字列をコメントトークンに分割する必要がある場合、便利な関数 split() がパーサ周りでシンプルなラッパーになります。

import shlex

text = """This text has "quoted parts" inside it."""
print 'ORIGINAL:', repr(text)
print

print 'TOKENS:'
print shlex.split(text)

この実行結果はリストになります。

$ python shlex_split.py
ORIGINAL: 'This text has "quoted parts" inside it.'

TOKENS:
['This', 'text', 'has', 'quoted parts', 'inside', 'it.']

トークンの他のソースを含める

shlex クラスには、その動作を制御する複数の設定プロパティがあります。 source プロパティはコード(または設定)を再利用するために、1つのトークンストリームへ他のものを含められるようにします。これは Bourne シェルの source 演算子によく似ているので、その名前が付いています。

import shlex

text = """This text says to source quotes.txt before continuing."""
print 'ORIGINAL:', repr(text)
print

lexer = shlex.shlex(text)
lexer.wordchars += '.'
lexer.source = 'source'

print 'TOKENS:'
for token in lexer:
    print repr(token)

オリジナルのテキスト内に source quotes.txt という文字列があることに着目してください。レクサの source プロパティが “source” にセットされているので、そのキーワードを見つけたときに次にあるファイル名が自動的に読み込まれます。ファイル名を1つのトークンにするために . 文字をワードに含まれる文字リストへ追加する必要があります(言い換えると “quotes.txt” は “quotes“, “.“, “txt” の3つのトークンになります)。この出力は次のようになります。

$ python shlex_source.py
ORIGINAL: 'This text says to source quotes.txt before continuing.'

TOKENS:
'This'
'text'
'says'
'to'
'This'
'string'
'has'
'embedded'
'"double quotes"'
'and'
"'single quotes'"
'in'
'it'
','
'and'
'even'
'"a \'nested example\'"'
'.'
'before'
'continuing.'

“source” 機能は、入力ソースを追加して読み込むために sourcehook() というメソッドを使用します。そのため、独自実装で自由にデータを読み込ませるために shlex をサブクラス化できます。

パーサを管理する

ワードに含まれる文字を管理するために wordchars を変更するサンプルを前節で紹介しました。さらに quotes 文字も追加や代替の引用符に変更することもできます。それぞれの引用符は一文字でなければならないので、始まりと終わりを表す引用符を違う文字にすることはできません(例えば、一対になっていないと解析されません)。

import shlex

text = """|Col 1||Col 2||Col 3|"""
print 'ORIGINAL:', repr(text)
print

lexer = shlex.shlex(text)
lexer.quotes = '|'

print 'TOKENS:'
for token in lexer:
    print repr(token)

このサンプルでは、それぞれの表のセルは縦線(|)で囲まれます。

$ python shlex_table.py
ORIGINAL: '|Col 1||Col 2||Col 3|'

TOKENS:
'|Col 1|'
'|Col 2|'
'|Col 3|'

さらにワードの分割に使用する空白文字を変更することもできます。shlex_example.py のサンプルにピリオドとカンマを含めるように変更するなら次のようにします。

import shlex
import sys

if len(sys.argv) != 2:
    print 'Please specify one filename on the command line.'
    sys.exit(1)

filename = sys.argv[1]
body = file(filename, 'rt').read()
print 'ORIGINAL:', repr(body)
print

print 'TOKENS:'
lexer = shlex.shlex(body)
lexer.whitespace += '.,'
for token in lexer:
    print repr(token)

実行結果は次のように変わります。

$ python shlex_whitespace.py quotes.txt
ORIGINAL: 'This string has embedded "double quotes" and \'single quotes\' in it,\nand even "a \'nested example\'".\n'

TOKENS:
'This'
'string'
'has'
'embedded'
'"double quotes"'
'and'
"'single quotes'"
'in'
'it'
'and'
'even'
'"a \'nested example\'"'

エラー制御

全ての引用符が閉じられる前にパーサがその入力の終わりに到達するとき ValueError が発生します。その入力を処理しようとするときにこの例外が発生するので、パーサのプロパティを調査するのに便利です。例えば、 infile は処理されるファイル名を参照します(ファイルソースが別のものである場合はオリジナルのファイルではない可能性があります)。 lineno はエラーが発生した行をレポートします。 lineno は通常、最初の引用符から離れてファイルの終わりになるかもしれません。 token 属性は有効なトークンではないテキストのバッファを含みます。 error_leader() メソッドは、Unix コンパイラとよく似たメッセージ形式で接頭辞を生成します。このエラーメッセージは emacs のようなエディタでエラー解析できて無効な行を直接ユーザへ伝えます。

import shlex

text = """This line is ok.
This line has an "unfinished quote.
This line is ok, too.
"""

print 'ORIGINAL:', repr(text)
print

lexer = shlex.shlex(text)

print 'TOKENS:'
try:
    for token in lexer:
        print repr(token)
except ValueError, err:
    first_line_of_error = lexer.token.splitlines()[0]
    print 'ERROR:', lexer.error_leader(), str(err), 'following "' + first_line_of_error + '"'

このサンプルは次のエラーになります。

$ python shlex_errors.py
ORIGINAL: 'This line is ok.\nThis line has an "unfinished quote.\nThis line is ok, too.\n'

TOKENS:
'This'
'line'
'is'
'ok'
'.'
'This'
'line'
'has'
'an'
ERROR: "None", line 4:  No closing quotation following ""unfinished quote."

POSIX 対 非 POSIX 解析

パーサのデフォルトの動作は POSIX 準拠ではない後方互換性のあるスタイルを使用します。POSIX に準拠するには、パーサを構築するときに posix 引数に True をセットしてください。

import shlex

for s in [ 'Do"Not"Separate',
           '"Do"Separate',
           'Escaped \e Character not in quotes',
           'Escaped "\e" Character in double quotes',
           "Escaped '\e' Character in single quotes",
           r"Escaped '\'' \"\'\" single quote",
           r'Escaped "\"" \'\"\' double quote',
           "\"'Strip extra layer of quotes'\"",
           ]:
    print 'ORIGINAL :', repr(s)
    print 'non-POSIX:',

    non_posix_lexer = shlex.shlex(s, posix=False)
    try:
        print repr(list(non_posix_lexer))
    except ValueError, err:
        print 'error(%s)' % err

    
    print 'POSIX    :',
    posix_lexer = shlex.shlex(s, posix=True)
    try:
        print repr(list(posix_lexer))
    except ValueError, err:
        print 'error(%s)' % err

    print

解析処理が違うサンプルは次のようになります。

$ python shlex_posix.py
ORIGINAL : 'Do"Not"Separate'
non-POSIX: ['Do"Not"Separate']
POSIX    : ['DoNotSeparate']

ORIGINAL : '"Do"Separate'
non-POSIX: ['"Do"', 'Separate']
POSIX    : ['DoSeparate']

ORIGINAL : 'Escaped \\e Character not in quotes'
non-POSIX: ['Escaped', '\\', 'e', 'Character', 'not', 'in', 'quotes']
POSIX    : ['Escaped', 'e', 'Character', 'not', 'in', 'quotes']

ORIGINAL : 'Escaped "\\e" Character in double quotes'
non-POSIX: ['Escaped', '"\\e"', 'Character', 'in', 'double', 'quotes']
POSIX    : ['Escaped', '\\e', 'Character', 'in', 'double', 'quotes']

ORIGINAL : "Escaped '\\e' Character in single quotes"
non-POSIX: ['Escaped', "'\\e'", 'Character', 'in', 'single', 'quotes']
POSIX    : ['Escaped', '\\e', 'Character', 'in', 'single', 'quotes']

ORIGINAL : 'Escaped \'\\\'\' \\"\\\'\\" single quote'
non-POSIX: error(No closing quotation)
POSIX    : ['Escaped', '\\ \\"\\"', 'single', 'quote']

ORIGINAL : 'Escaped "\\"" \\\'\\"\\\' double quote'
non-POSIX: error(No closing quotation)
POSIX    : ['Escaped', '"', '\'"\'', 'double', 'quote']

ORIGINAL : '"\'Strip extra layer of quotes\'"'
non-POSIX: ['"\'Strip extra layer of quotes\'"']
POSIX    : ["'Strip extra layer of quotes'"]

See also

shlex
本モジュールの標準ライブラリドキュメント
cmd
インタラクティブコマンドインタープリタのツール
optparse
コマンドラインオプション解析
getopt
コマンドラインオプション解析
Bookmark and Share