01. ディレクトリとファイルサーチ

ここでは、いくつかの課題について、各言語でどう記述するか、書いてみようと思います。ポイントは、プログラムで処理することで、得られた情報に対して追加処理を行う余地ができることです。実現することをピックアップします。

  1. 指定したディレクトリ以下の、すべてのファイルを表示する
    • まずは「探す手法」を理解します
  2. フィルタリングする
    • 実際には、「*.*」に対して処理をすることはほぼないと思われるので、ファイル名によるフィルタリングと特定の文字列を含むフィルタリングを行う基本形を構築します
  3. バイナリファイルを除外する
    • 拡張子によるフィルタリングでは失敗しづらいですが、ファイルの中身によるフィルタリングでは、全ファイルを調査対象とするため、その範囲にバイナリファイルがあると、shellスクリプトでは文字化けして画面がぐちゃぐちゃになったりします。

※【更新2014/11/27】 ここで表記している「バイナリファイル」とは、lessなどでファイルを開いた時に「maybe binary file」と表示されるファイルを指しています。チェッカはrubyで記述したものを他のスクリプトでも利用するようにしています。チェック基準は、ファイルの中にasciiコード以外を含むもの、としました。現在の処理方法では、2byteコード(日本語など)を含むと、binaryファイルとしてチェックされます。コメント文中の2byteコードは無視する方がよいと思いますが、現在はその仕組みを持たせていません。

--- binChecker.rb

このRubyコードは、どこかのサイトを参考に調整を加えています。必要に応じて修正してください。

使い方:binChecker.rb filename

→ 返り値0のとき、binaryファイルと判断

【修正しました:2014/12/01】

#!/usr/bin/ruby -w
def binary?(name)
  if(File.size(name) > 0)then
    File.open(name, "rb") {|io| io.read(1024)}.each_byte do |bt|
      if(bt>127)then
        return true
      end
    end
    return false
  else
    return false
  end
end
##############
if(binary?(ARGV[0]))then
  exit(0)
else
  exit(1)
end

--- tcsh

処理が重いです...

コマンドプロンプトで

> ls -1R *

と書けば、ディレクトリを再帰処理で掘ってくれます。これを利用すれば簡単に実装できそうですが、ls -1R * は

sv/work/uvm-1.1d/src/tlm2:uvm_tlm2.svhuvm_tlm2_defines.svhuvm_tlm2_exports.svh

このような出力形式になります。ディレクトリ名には「:」が付加される。ファイル名にはディレクトリ名が付加させられないようなので、プログラマブルに書く必要があります。

お試しで、こんなコードを書いてみました。

#!/usr/bin/tcsh -f
set files
set head = "."
foreach tmp ( `ls -1R *` )
  echo $tmp |grep ':$' >! /dev/null
  if ( $status == 0 ) then
    set head = `echo $tmp |sed 's/:$//'`
  else
    set file = $head/$tmp
    set files = ( $files $file )
  endif
end
foreach tmp ( $files )
  echo $tmp
end

前段のforeachで

    • lsの結果を受け取り
    • ディレクトリ情報をチェックし、
    • ディレクトリ情報をファイル名に付加して、フルパス情報を変数 files に格納

しています。後段のforeachは結果確認用。デバッグ環境では、この時点で合計31ファイルが処理対象となっているのですが、実行の重いことといったらありません。2.5秒くらいかかりました@Core2Duo 2.2GHz Win8.1 Pro cygwin

上記コードには問題があります。

    1. 処理時間がかかること
      • ls自体はすぐ結果を出しますが、1つ1つについてgrepをかけているので、重い
    2. 対象ファイル数、ディレクトリ階層が多い・深いと変数 files がオーバーフローしてしまう
      • オーバーフロー対策として、一時ファイルに files の内容を書き出せば回避可能

ファイル名によるフィルタリング

1.ファイル名に"abc"を含むファイル

foreach tmp ( $files )
    set tail = $tmp:t  #この文法についてはこちら
    if ( $tail =~ *abc* ) then
        処理
    endif
end

2.拡張子を除いたファイル名に"abc"を含むファイル

foreach tmp ( $files )
    set tail = $tmp:t
    set name = $tail:r  #この文法についてはこちら
    if ( $name =~ *abc* ) then
        処理
    endif
end

3.拡張子が"exe"のファイル

foreach tmp ( $files )
    set ext = $tmp:e  #この文法についてはこちら
    if ( $ext =~ exe ) then
        処理
    endif
end

含まれる文字列によるフィルタリング

foreach tmp ( $files )
    grep keyword $tmp >! /dev/null
    if ( $status == 0 ) then
        処理
    endif
end

バイナリファイルチェック

このページの上の方にある、binChecker.rbを利用してある程度判定できます。ただし、ファイルを開いてチェックするため、処理が重くなります。

--- Perl

まず、カレントディレクトリ以下すべてのファイル(ディレクトリあれば掘る)を出力してみます。

#!/usr/bin/perl
use File::Find;
find(\&test, "./");
exit;
sub test {
  print $File::Find::name ."\n";
}

*1 use文で定義済みライブラリを読み込みます

*2 関数 find を呼び出します。第一引数に、処理用自作関数(ここではsub test)をリファレンス(\)指定します。第二引数に、サーチ元ディレクトリ(複数指定可)を指定します

find 関数は、ファイルを見つける毎にユーザ指定関数(ここではsub test)を呼び出すようです。

こちらのサイトを参考にしました。

  • ファイル名(フルパス)は、$File::Find::name、で取得します
  • ファイル名を除いたディレクトリ名は、$File::Find::dir、で取得します
  • ファイル名のみは、$_、で取得します

globを使う場合には、ディレクトリ以下を自動サーチしてくれないので、自作で再帰処理を書く必要があります。

ファイル名によるフィルタリング

上記のsub testに対して、引数渡しでフィルタリングファイル名を指定しようとしたところ、find関数でエラーが起きました。今のところ、解決策を把握していないため、「やむを得ず」関数外の変数を利用して処理を書いてみます。

#!/usr/bin/perl
use File::Find;
$key = $ARGV[0];  #フィルタリングキーワード
find(\&test, "./");
exit;
sub test {
  my $fname = "";
  my $ext = "";
  # ファイル名(拡張子ぬき)と拡張子名を分ける
  if(/(\w+)\.(\w+)/){
    $fname = $1;
    $ext = $2;
  }elsif(/(\w+)/){
    $fname = $1;
  }
  print $File::Find::name."\n" if(/$key/);                   # ファイル名サーチ
  print $File::Find::name."\n" if($fname =~ /$key/);  # ファイル名(拡張子ぬき) サーチ
  print $File::Find::name."\n" if($ext =~ /$key/);       # 拡張子サーチ
}

含まれる文字列によるフィルタリング+バイナリファイルチェック

変更箇所が飛び飛びなところがあるため、フルソースで展開します。太字のところがメインソースです。バイナリチェックを有効にすると、検索ファイルが多いと処理にとても時間がかかるため、print文でドットを打って進捗が見えるようにしています。

binChecker.rbのパスを修正しました。

#!/usr/bin/perl
use File::Find;
$mode = 1;         # ファイル名によるフィルタリングをmode=0として残しています
$key = $ARGV[0];   # 含まれる文字列の指定
$TOOLPATH = "";    # 上記のbinChecker.rbを置く場所
find(\&test, "./");
exit;
sub test {
  #print $File::Find::name ."\n";
  my $fname = "";
  my $ext = "";
  if($mode==0){
    if(/(\w+)\.(\w+)/){
      $fname = $1;
      $ext = $2;
    }elsif(/(\w+)/){
      $fname = $1;
    }
    if($fname =~ /$key/){
      print $File::Find::name."\n";
    }
  }elsif($mode==1){
    $fullfile = $File::Find::name;
    $file = $_;
    if(-f $file){
      $binchk = system($TOOLPATH . "/binChecker.rb $file");
      if($binchk==0){
        print "\n*E, maybe binary file --> $fullfile\n";
      }elsif( open(IN, $file) ){
        @RFILE = <IN>;
        close(IN);
        @match = grep(/$key/, @RFILE);
        if(@match!=0){
          print "\nmatch $fullfile\n";
        }else{
          print ".";
        }
      }else{
        print "\n*E, file open error, so skip this file -> $fullfile\n";
      }
    }else{
      if(-d $file){
      }elsif(-l $file){
      }else{
        print "\nE: Not plain file --> $fullfile\n";
      }
    }
  }
}
print "\n";

--- Ruby

まず、カレントディレクトリ以下すべてのファイル(ディレクトリあれば掘る)を出力してみます。globの中で * 指定しているので、ドットから始まるファイルは対象になりません。

#!/usr/bin/ruby -w
Dir::glob("**/*"){ |file|
  if(File::ftype(file)=="directory")then
    #puts "dir: " + file
  else
    puts "./#{file}"
  end
}

ファイル名によるフィルタリング:修正2014/12/01

変数 filter にフィルタリングしたい文字列を指定します。

mode==0のとき、ディレクトリ名を含めたフルパスファイル名に対してfilterをかけるときに有効にします。

mode==1のとき、ファイル名(パス除く、拡張子除く)に対してfilterをかけるときに有効にします。

mode==2のとき、拡張子に対してfilterをかけるときに有効にします。

*1 mode値取り込み時、ARGVアクセスだけだと文字列になるため、.to_i で数値化しています

#!/usr/bin/ruby -w
if(ARGV.size < 2)then
  puts "*E, argument error"
  exit
end
mode   = ARGV[0].to_i
filter = ARGV[1]
puts "*** File Search, include #{filter} ***"
Dir::glob("**/*"){ |file|
  if(File::ftype(file)=="directory")then
    #puts "dir: " + file
  elsif(File::ftype(file)=="link")then
  else
    filename = File.basename(file)
    if(/^([\w-]+)\.(\w+)/ =~ filename)then
      root = $1
      ext  = $2
    elsif(/^(\w+)$/ =~ filename)then
      root = $1
      ext  = ""
    else
      puts "*E, Failed to get filename information... -> #{file}"
      next
    end
    if(mode==0 && /#{filter}/ =~ file)then
      puts "./#{file}"
    elsif(mode==1 && /#{filter}/ =~ root)then
      puts "./#{file}"
    elsif(mode==2 && /#{filter}/ =~ ext)then
      puts "./#{file}"
    end
  end
}

含まれる文字列によるフィルタリング+バイナリファイルチェック

変数 filter に、検索したい文字列を格納します(引数渡し)。

メソッド binary? を追加しています(更新2014/12/01)

#!/usr/bin/ruby -w
### method binary?
#########################################
def binary?(name)
  if(File.size(name) > 0)then
    File.open(name, "rb") {|io| io.read(1024)}.each_byte do |bt|
      if(bt>127)then
        return true
      end
    end
    return false
  else
    return false
  end
end
### main process
#########################################
if(ARGV.size < 1)then
  puts "*E, argument error"
  exit
end
filter = ARGV[0]
puts "*** File Search, include #{filter} ***"
Dir::glob("**/*"){ |file|
  if(File::ftype(file)=="directory")then
    #puts "dir: " + file
  elsif(File::ftype(file)=="link")then
  else
    flag = 0
    if(binary?(file)==false)then
      line_cnt=0
      File.open(file).each { |line|
        line_cnt += 1
        begin
          if(/#{filter}/ =~ line)then
            flag += 1;
            break;
          end
        rescue
          puts "*E, Error detected in #{file} line=#{line_cnt}"
        end
      }
      puts "./#{file}" if(flag==1)
    else
      puts "*E, maybe binary file --> #{file}"
    end
  end
}

--- Python

まず、カレントディレクトリ以下すべてのファイル(ディレクトリあれば掘る)を出力してみます。使用しているメソッドでは、ドットから始まるファイルも含まれます。

#!/usr/bin/python
import os
for dir,dirList,fileList in os.walk("."):
    for file in fileList:
        print "%s%s" % (dir,file)

ファイル名によるフィルタリング

こんな感じになります。フィルタリングしたいファイル名は、変数 filter に指定します。

#!/usr/bin/python
import sys,os,re
# 引数指定抜けのときは落とす
if len(sys.argv)!=3:
    print "*E, argument error"
    sys.exit()
# mode(0:ファイル名全体マッチ、1:拡張子を抜いたファイル名にマッチ、2:拡張子マッチ)
mode   = int(sys.argv[1])
filter = sys.argv[2]
re0 = re.compile("([\w-]+)\.(\w+)")
for dir,dirList,fileList in os.walk("."):
    for file in fileList:
        if mode==0:
            if re.search(filter,file):
                print file
        else:
            m0 = re0.search(file)
            if m0:
                root = m0.group(1)
                ext  = m0.group(2)
                if mode==1:
                    if re.search(filter,root):
                        print file
                elif mode==2:
                    if re.search(filter,ext):
                        print file

含まれる文字列によるフィルタリング+バイナリファイルチェック

Pythonでは、ファイルのバイナリチェックを独自に実装してみました。

*1 def binaryChkの中で使用している ord というメソッドは、コード番号を返すものです

#!/usr/bin/python
import sys,os,re
### method : binaryChk
###################################
def binaryChk(filename):
    infile = open(filename, 'rb')
    data = infile.read(1024)
    for byte in data:
        if ord(byte)>127 : return True
    infile.close()
    return False
### main process
###################################
if len(sys.argv) != 2 :
    print "*E, argument error"
    sys.exit()
filter = sys.argv[1]
for dir,dirList,fileList in os.walk("."):
    for file in fileList:
        filename = "%s/%s" % (dir,file)
        if not os.path.islink(filename):
            if binaryChk(filename):
                print "maybe binary -> %s" % (filename)
            else:
                #print filename
                hit = 0
                for line in open(filename, 'r'):
                    if re.search(filter, line):
                        hit += 1
                if hit>0 : print filename