sys.argvの不思議で便利な仕様,ワイルドカードでファイル名のリストを再帰的に取得する

以前のエントリ(条件に合致するファイルを連続して読み込み,処理後にファイル名を変えて保存 - 惰力飛行)でも実はsys.argvの仕様について触れているのだが,そのときにはなぜかあまり疑問に思わなかった.しかし改めて考えると変なことが起こっている.なんのことかというと,

import sys
a = sys.argv

などとしてコマンドライン引数を取得し,

python code.py /Volumes/h*g*

などとしてコマンドライン引数にワイルドカードを用いたパスを書いてやると,aの中身は,

['code.py', '/Volumes/hogehoge', '/Volumes/hugahuga', '/Volumes/hhgghhgg']

などといったことになる.

sys.argvの仕様をちゃんと調べてないのであれだが,別にパスを入力する場所として指定されているわけではないし,普通の文字列を取得することもできる.にもかかわらずなぜか,ワイルドカードでパスを入力すると,そのとき存在するパスを要素とするリストの形に展開して取得してくれる.
これはどちらかというとpythonないしsys.argvの仕様というより,シェルがそう解釈しているということだろうか.だとすると,シェルを経由せずに渡したワイルドカードは正しく展開されないということになりそうだ.検証は面倒なのでまた今度.

ついでに,このsys.argvの挙動を,ディレクトリを再帰的に探索してファイルとディレクトリの一覧を取得するos.walk()と組み合わせて使うと,探索する最上位のディレクトリを指定するだけで条件に合致するファイルの一覧を簡単に取得できる.
例えば,ファイル名に'hoge'を含むファイルを探したいなら,以下のようにすると標準出力にフルパスの一覧が出る.

import os
import sys

a = sys.argv
list = []

for root, dir, files in os.walk(a):
    for f in files:
        if 'hoge' in f:
            path = os.path.join(root,f)
            print path

ちなみにos.path.joinはパスらしき文字列をパスとして使える形に整形して繋げてくれるらしい.どういうことかはまだよく調べていない.

このsys.argvの挙動,ワイルドカード検索の専用モジュールのglob.glob()とほぼ同様に使えるように思う.
上記のコードを置き換えるなら,/Volumes/以下を探すとして,下記のようになろうか.

import os
import glob

a = glob.glob('/Volumes')

list = []

for root, dir, files in os.walk(a):
    for f in files:
        if 'hoge' in f:
            path = os.path.join(root,f)
            print path

globを使ったほうのコードの動作確認はしてません.そのうち書き直すかも.

目的によってはいちいちコマンドラインからパスを取得する必要はないんだけど,そうだとしてもglobのほうが簡単に書けるというわけでもないし,sys.argvのほうが慣れているのでこちらを使い続けるように思う.

2018/12/7追記

さすがにいろいろひどすぎるので,訂正を書き加えておく.
sys.argvのワイルドカード展開はもちろんシェルがやっていることなので,避けたほうが無難な書き方である.
ワイルドカードはまずワイルドカードの文字列のまま受け取って(argparseの位置引数かなにかで),それをglobで展開するのが妥当だと思う.

下から2番目のコードサンプル

import os
import sys

# a = sys.argv -> これだとスクリプト名も含んだリストになる
a = sys.argv[1:]
# list = [] -> 無駄なリスト定義

for root, dir, files in os.walk(a):
    for f in files: # -> files(os.walkの3つ目の戻り値)はリストなので,展開してひとつひとつの要素ごとに処理
        if 'hoge' in f:
            path = os.path.join(root,f)
            print path
# -> /root/hoge

一番下のコードサンプル

import os
import glob

# a = glob.glob('/Volumes') # -> ['/Volumes']
a = glob.glob('/Volumes/*')

# glob.glob()はリストを返すが,os.walk()はリストを引数に取れない
for i in a:
    for root, dir, files in os.walk(i):
        for f in files:
            if 'hoge' in f:
                path = os.path.join(root, f)
# -> /root/hoge