試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

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']

関連記事:

使用したバージョン: