Pythonでコマンド行のような文字列を解析してリストに格納する上でのメモ(後半)
「Pythonでコマンド行のような文字列を解析してリストに格納する上でのメモ(前半)」の続き。
ダブルクォートされた部分とそうでない部分とをうまく処理する
色々試した結果、ダブルクォート文字を区切りとして元の文字列を分割してやることでダブルクォートされた部分を1つのまとまりとして得ることができる、ということが分かった。
(色々試しているところ) >>> 'aaa bbb ccc "ddd eee fff" ggg hhh iii'.split('"') ['aaa bbb ccc ', 'ddd eee fff', ' ggg hhh iii'] >>> 'aaa bbb ccc "ddd eee fff" ggg "hhh iii'.split('"') ['aaa bbb ccc ', 'ddd eee fff', ' ggg ', 'hhh iii'] >>> 'aaa bbb ccc "ddd eee fff" ggg "hhh iii" jjj kkk'.split('"') ['aaa bbb ccc ', 'ddd eee fff', ' ggg ', 'hhh iii', ' jjj kkk']
split()で得られるリストの左から偶数番目(リスト内の番号は0からなので奇数)の要素がダブルクォート内の内容となり、残りがダブルクォートを含まない部分でそのままスペース文字をsplit()の引数にして分割できる。
ただし、ダブルクォートされた部分がコマンド行を構成する1つの要素の全体とはならないこともあり、ダブルクォートの前後1文字がスペースかどうかで場合分けする必要があった。
色々試した結果、以下のような処理でうまく解析できることが分かったが、まだおかしい部分がある可能性やもっとうまい方法がある可能性もある。
[任意]ファイル名: parsecmdlinetest.py
#! /usr/bin/python # -*- encoding: utf-8 -*- # シェルから実行するコマンド行のような文字列を解析してリストに格納 # ダブルクォートされた部分はスペースを含むことができるようにしたもの # 連続したスペースは無視され、1つのスペースと同等に扱う def parse_cmdline(in_str): # 出力リスト out = [] # 奇数/偶数をみるために番号と同時に回す for i, part in enumerate(in_str.split('"')): if i % 2 == 0: # 偶数番はクォート外 # スペースで分割してそれぞれを追加 # 最初がスペースならクォート内を追加 quoted = '' if i >= 2: # 最初がスペースならクォート内の内容をリストの一番上から剥がして # 先頭にくっつける # 最初(0番)の要素は処理しないのでi >= 2 if part != '' and not part.startswith(' '): quoted = out.pop() # クォート外のまとまりがスペースで終わらない場合にフラグをTrueに # スペースで終わるならフラグをFalseにする # 最初の代入は0番で行われるのでfor文より上で初期化する必要はない cat_quoted_start = not part.endswith(' ') # クォート外の最初の要素を処理するときを識別するためのフラグ first_out = True for item in part.strip().split(' '): # スペースが連続した場合の対策 if item != '': # 剥がしたクォート内の内容は最初だけくっつける if first_out == True: first_out = False out.append(quoted + item) else: out.append(item) else: # 奇数番はクォート内 # 直前がスペースならクォート内そのまま # 直前がスペースでないならその内容に続けてクォート内を追加 if cat_quoted_start == True: out.append(out.pop() + part) else: out.append(part) return out str1 = 'aaa bbb ccc "ddd eee fff" ggg "hhh iii "jjj' str2 = 'aaa bbb ccc "ddd eee"fff ggg "h"hh iii' str3 = ' ls -l -a "a b c.txt"' str4 = ' ls -l -a c"d e f.tx"t' print parse_cmdline(str1) print parse_cmdline(str2) print parse_cmdline(str3) print parse_cmdline(str4)
下は実行結果。
['aaa', 'bbb', 'ccc', 'ddd eee fff', 'ggg', 'hhh iii jjj'] ['aaa', 'bbb', 'ccc', 'ddd eeefff', 'ggg', 'hhh', 'iii'] ['ls', '-l', '-a', 'a b c.txt'] ['ls', '-l', '-a', 'cd e f.txt']
関連:GLib(C言語/Vala言語)では同等のことを簡単に実現可能
(2010/1/9)Pythonと関係はないが、GLib 2ライブラリではこの用途での便利な関数が用意されており、以前「Vala言語で外部プロセスを実行する(スレッドを使用してGTK+のテキストビューに実行結果を表示・コード例)」などでも用いている。
C言語は
http://library.gnome.org/devel/glib/stable/glib-Shell-related-Utilities.html#g-shell-parse-argv
Vala言語は
http://references.valadoc.org/glib-2.0/GLib.Shell.parse_argv.html
を参照。
PythonではPyGObjectの言語バインディングにこの関数がないため、それと同じことを実現するために本記事の方法を考えることとなった。
(2010/1/15)GLibのソースを入手・展開してglib/gshell.cを開くとこの関数の中身が見られるが、処理は結構複雑で、「#」や「\」といった文字の処理なども含んでいるようだ。
shlexを使用した方法(a氏に感謝)
(2010/1/20)a氏による指摘により分かった方法として、以前扱ったshlexモジュール(関連記事)のshlex.split()を用いると簡単にこの類の分割が行え、クォートなどの処理もうまく行ってくれる。
>>> import shlex >>> shlex.split('aaa bbb ccc "ddd eee fff" ggg "hhh iii "jjj') ['aaa', 'bbb', 'ccc', 'ddd eee fff', 'ggg', 'hhh iii jjj'] >>> shlex.split('aaa bbb ccc "ddd eee"fff ggg "h"hh iii') ['aaa', 'bbb', 'ccc', 'ddd eeefff', 'ggg', 'hhh', 'iii'] >>> shlex.split(' ls -l -a "a b c.txt"') ['ls', '-l', '-a', 'a b c.txt'] >>> shlex.split(' ls -l -a c"d e f.tx"t') ['ls', '-l', '-a', 'cd e f.txt']
関連記事:
使用したバージョン:
- Python 2.6.4