ワンライナー
一般的な文法など
ファイルとディレクトリの操作関連


Windows用Perl(ActivePerl)のインストール
Windows PC向けのPerl処理系としては、 ActiveStateで配布されているActivePerlが最も有名だと思います。 ここでコンパイル済み処理系の.zipまたは.msiが配布されているので、インストーラを起動してインストールできます。 インストール先のデフォルトは "C:\Perl" ですが、直下に置くのが嫌なら変更できます。 PATHにperl.exeのパスは自動的に追加されます。 また、.plファイルをエクスプローラでダブルクリックするとperl.exeに解釈されるように関連付けが行われますが、 私はテキストエディタが起動するように書き換えました。


ワンライナー
grepの代わりをするワンライナー
grepコマンドでは、 OSにもよりますがこの正規表現をエスケープさせるのにどう書けばいいのか、 若干混乱します。 Perlならとりあえずシングルクォートで囲めば、 中は普通のPerlスクリプトと同じように書けば、 どんなOSでも同じように動く(はず)です。

%shell perl -ne 'print if(/error\.txt/ && ! /admin/)' process.log

上の例ではprocess.logファイルから、"error.txt" という文字を含み、"admin"という文字は含まない行を出力しています。 -eはワンライナーを、 -nはコマンドラインで渡した入力を1行ずつループしながら処理することを表すオプションです。
代わりに-pを使う(-pe)と、ワンライナーの演算結果($_)を標準出力に出力する処理も行ってくれます。

2つのファイルの内容を連結して標準出力に流すワンライナー(Windows用)
それを使って、こんなこともできます。Windowsのバッチファイルでは意外にも、 複数のファイルの内容を連結して標準出力に流すという、それだけのことができないんですってね (今はできるのかもしれない)。ちなみにUNIXだとcatコマンドがあり、 またPowerShellにはGet-Contentというコマンドがあるようです。

%shell perl -pe '' aaa.txt bbb.txt | sort > ccc.txt

余談ですがPowerShellだと

Get-Content *.txt | Set-Content ccc.txt

こんな感じで行けます。両コマンドには短縮表記があるので

cat *.txt | sc ccc.txt

でもOKです。この目的に限ればPerlの出番はないのであった(けど一応残しておきます)

先頭行を削除するワンライナー

%shell perl -nle 'print if($. > 1)' indata.txt

変数$.には、現在処理している行番号が入ります。(先頭が1)
-lは、入力を自動的にchompし、print後にまた自動的に改行してくれるオプションです。

ls -lコマンドの出力からファイルサイズだけを抽出するワンライナー
1つのディレクトリに大量にファイルがあり、それらのファイルサイズだけを抽出したいときに使います。 全ファイルの合計サイズは、これをExcelにコピーペーストして和を計算する等で何とかします (それもPerlスクリプト化可能ですが…)

%shell ls -l > filelist.txt
%shell perl -ne '@a=split;if($a[0]=~/^\-rw/){print $a[4]."\n";}' filelist.txt > filesize.txt


一般的な文法など
ファイルの文字列置換(-iオプション)
例えば、たくさんのテキストファイルに含まれる 「AM 6:00」を全て「PM 9:00」に、「Morning」を「Night」 に変換して、同じファイル名で書き出したい、というときはPerlの-iオプションがすごく便利です。 この場合は、次のようなスクリプトを作ります。(a.plとしましょう)

while(<>){
    s/AM 6:00/PM 9:00/g;
    s/Morning/Night/g;
    print;
}

そして、

shell% perl -i.orig a.pl *.txt

とすると、それぞれのファイルの処理前の内容が「ファイル名.orig」 という名前で退避され、処理後の内容が元の「ファイル名」 として置き換わります。 上のように、Perlの引数でグロブ(ワイルドカード) を指定すれば、たくさんのテキストファイルを一気に処理できます。

qwを使った配列定義
qwを使うと、引用符で囲む手間が省けて効率的です。

my @seasons = qw(
    SPRING
    SUMMER
    AUTUMN
    WINTER
);

配列のランダムソート
配列をランダムでソートします。

my @arr2 = sort { rand() <=> 0.5 } @arr1;

配列の簡単なダンプ

print "$_\n" for @arr;

配列を条件でフィルターする。
grep関数で可能です。例えば配列変数@linesの各要素の内、先頭が//か#である要素(空白系文字が前についても可) を除外した配列を得るには次のようにします。

@lines = grep { !/^\s*\/\// && !/^\s*#/ } @lines;

何度書いても覚えられないlocaltime
むー、この年、月、日…の並び順が覚えられないんですよね。

# 現在の時刻を YYYYMMDDHHMMSSで返します。
sub nowstr(){
    ($s, $mi, $h, $d, $mo, $y, $w) = localtime(time);
    $mo++; $y += 1900;
    return sprintf("%4d%02d%02d%02d%02d%02d", $y, $mo, $d, $h, $mi, $s);
}

# ファイルの編集日時をYYYYMMDDHHMMSSで返します。
sub filetimestr(){
    local($filename) = @_;
    if(! -f $filename){ return undef; }
    $filetime = time - (-M $filename) * 24 * 60 * 60;
    ($s, $mi, $h, $d, $mo, $y, $w) = localtime($filetime);
    $mo++; $y += 1900;
    return sprintf("%4d%02d%02d%02d%02d%02d", $y, $mo, $d, $h, $mi, $s);
}

西暦年を得るには1900を足すことに、月は1を足す必要があることに小注意。

timelocal関数、または2つの日付の差(日数)を求める
localtime関数と逆に、年月日、時分秒の組からtime秒数を返すには、 Time::Localモジュールのtimelocal、timegmの両関数を使います。 下の例は、コマンドラインから2つの日付をYYYYMMDDで受け取って、その間の日数を出力するツールです。

use Time::Local;
($date1, $date2) = @ARGV;
if($date1 =~ /(\d\d\d\d)(\d\d)(\d\d)/){
    $time1 = timelocal(0,0,0,$3,($2-1),($1-1900));
} else { die "$date1 was not recognized, expected a date formatted by YYYYMMDD."; }
if($date2 =~ /(\d\d\d\d)(\d\d)(\d\d)/){
    $time2 = timelocal(0,0,0,$3,($2-1),($1-1900));
} else { die "$date2 was not recognized, expected a date formatted by YYYYMMDD."; }
$d = abs($time1-$time2) / (24 * 60 * 60);
print "$d\n";

timelocal関数の引数の並び順は秒、分、時、日、月、年です。
「年」は西暦から1900を引くことに、「月」は我々現実社会の月から1を引き0〜11で指定することに大注意!

左0詰の数値表現から数値を10進数で読み取る
PerlにLTRIM、RTRIM系の関数ってありましたっけ?ていうかあるべきだ!(なんじゃそりゃ)
ないものは仕方ないので愚直に正規表現を使う例が以下です。 こんなのそのままやんといえば身もふたもないですが、 例えばPHPなどのプログラミングに慣れている時にPerl世界に戻ってくると 「trim、trim、trimはどこだ…」となりがちですね。

$a =~ s/^0+//; $a += 0;

LPAD、RPADって(文字列の左右に文字を詰めて一定の文字数にする)
文字列の左右どちらかに、空白や0などのある文字を詰めて一定の文字数にするには? 速度はさておき、最も簡単で分かりやすい書き方は次のようになるでしょう。

$len = 20; # 固定の文字数
# RPAD
$s = sprintf("%-${len}s", $s);
# または
$s = substr($s . (' ' x $len), 0, $len);
# LPAD
$s = sprintf("%${len}s", $s);
# または
$s = substr((' ' x $len) . $s, -1 * $len, $len);

簡単なのはsprintf関数を使う方法ですが、別の方法も可能。 1文字ずつループで連結するのかと思ったら、まず希望の文字数と同じだけの詰め文字を連結してしまってから、 希望の文字数だけ改めてsubstrするということですね。

連想配列の要素の数を取得する
要素の数、つまりキーの数は次のようにkeys関数の戻り値をスカラーコンテキストで代入すれば得られます。

my $count = keys %fruits;

連想配列のキーと値をソートして出力する
連想配列 %arr に入っている全要素をキーのASCII順にソートして出力するには、次のようにします。

foreach (sort {$a cmp $b;} keys(%arr)){
    print $_ . "\t" . $arr{$_} . "\n";
}

置換演算子の2つのオプション
置換対象の文字列が改行文字を途中に含む場合は、「s」オプションをつけます。
置換対象を最長一致ではなく最短一致でマッチさせたい場合は、 正規表現の繰り返し指示子(*、+、?)の後ろに?をつければOKです。

s/RRR(.*?)RRR/<font color=\"red\">\1<\/font>/sg;

これは、$_のRRRとRRRで囲まれた部分をHTMLのfont colorタグに置き換えています。

require文が検索するディレクトリ
require文が検索するディレクトリのリストは、@INCに格納されています。 プログラム中からこれを操作することもできます。

push(@INC, 'C:/usr');
require 'indate-common.pl';

ファイルグロブ演算子のよくある注意点
ファイルグロブ演算子(<>)を使う場合、 <*.html>のように、ファイルパターンを直に書く場合はよいのですが、 ファイルパターンを変数で指定する場合、必ず下のように 変数名を{ }で囲みます。

$g = "*.html";
#ダメっす @files = <$g>;
@files = <${g}>;
foreach (@files){
    print "$_\n";
    convertFile($_);
}

いくつかの中級な正規表現記法
例えば次のような文字列から、「[Q]なにがし[A]なにがし」という部分文字列を切り出してみます。

$_ = "[Q]111 [A]222 [Q]333 [A]444 [Q]555 [A]666";
while(/\[Q\](.+?)\[A\](.+?)(?=\[Q\]|\Z)/sg){ print "<$1><$2>\n"; }

「.+?」や「.*?」は「.+」や「.*」のバリエーションで、 正規表現の途中(後ろに別のパターンが続く)で使われ、最短一致での照合を行います。 つまり、後続のパターンを「含まない」ことを、「?」一文字で指示できるのです。
「(?=パターン)」は、正規表現の末尾に使われ、そのパターンが後続に現れる文字列に適合しますが、 「パターン」自身は適合部分、つまり「$&」とはみなされず、 「$'」の先頭に回って次回の照合にも使われます。 適合部分に含まれないことから「先読み」と呼ばれる処理です。 親類として、そのパターンが後続に「現れない」ことを条件とする 「(?!パターン)」もあります。先読みしつつ現れることを否定するので「否定先読み」といえます。
では、後続に特定のパターンが現れるか、または文字列の末尾である (現れずに文字列終端となる)という条件を表現するには? 上の例のように 「(?=パターン|\Z)」が正解。\Zは$と似たようなものですが、 改行文字を含んでいる場合に$は「行末」ですが\Zは「文字列終端」である点が異なります。
//gオプションは、while文の条件式で使い、繰り返し照合することを指示します。 より具体的にいえば、whileループの末尾で「$_ = $'」を行いつつループします。

似た応用例です。UTF-8のファイルの各行に書かれている「○1○2○3」などの 「○1」の部分を切り出して、別ファイルに格納するプログラムです。 (※このプログラム自身もUTF-8で書く必要があります)

use Encode;
use utf8;

open(INFILE, "./input.txt") || die $!;
open(OUTFILE, ">./output.txt") || die $!;
while(<INFILE>){
    chomp;
    $_ = Encode::decode("utf8", $_);
    while(/([○□])(.+?)(?=[○□]|\Z)/sg){
        print OUTFILE Encode::encode("utf8", $1 . $2) . "\n";
    }
    print OUTFILE "------\n";
}
close(INFILE);
close(OUTFILE);


ファイルとディレクトリの操作関連
ディレクトリのファイル一覧を得る
opendirとreaddirを使うと、ディレクトリに含まれるファイルエントリの一覧が取得できます。 但しその中には「.」と「..」も入っているので、 通常それらは処理対象から外すことに注意が必要です。

opendir(INDIR, ".") || die $!;
@files = readdir(INDIR);
closedir(INDIR);

foreach (@files){
    if(/(.+)\.(html)$/){
        $new = "$1.htm";
        print "$new\n";
        rename($_, $new) || die $!;
    }
}

上のスクリプトは、カレントディレクトリに含まれる「*.html」というファイルを「*.htm」に変更する例です。

また、少し違う書き方の例も挙げておきます。

opendir(my($dh), "/the/path/of/target") or die $!;
my @files = readdir($dh);
closedir($dh);
print join("\n", @files) . "\n";

ファイル名からディレクトリ名だけを切り出す
File::Basenameというモジュールを使います。

use File::Basename;

$dir = dirname($filepath);

ディレクトリ名を取り出すにはdirname関数、主ファイル名を取り出すにはbasename関数を使います。

ファイルのコピー

use File::Copy;
if(-f $target){ unlink($target) || die $!; }
copy($source, $target) || die $!;

Windowsの場合には、「C:/」で始まるなど、ドライブ名を書かないと 「No such file or directory」というエラーになってしまうようです。

ファイルの一括改名のテンプレート
カレントディレクトリの*.jpgが空白文字を含む場合、半角ハイフンに置き換えるサンプルです。

@files = <*.jpg>;
foreach (@files){
    $infile = $outfile = $_;
    $outfile =~ s/ /\-/g;
    print "[$infile] to [$outfile]\n";
    rename($infile, $outfile) || die $!;
}

複数ディレクトリから同名のファイルを検出
一例です。

use File::Basename;
sub find_files {
    my($dir) = @_;
    foreach (glob "${dir}/*.*"){
        my($fname) = basename($_);
        if(defined($arr{$fname})){
            print "$fname already exists\n";
        } else {
            $arr{$fname} = 1;
        }
    }
}
find_files("/the/path/of/dir1");
find_files("/the/path/of/dir2");

再帰的にディレクトリを検索するサンプル
File::Findモジュールを使う方法です。以下のスクリプトは、 指定したディレクトリ以下のファイルを検索し、各ファイルのサイズと共に出力します。

use File::Find;
find(\&sub1, 'C:/the/path/of/targetdir');
sub sub1 {
    my $path = $File::Find::name;
    return if(! -f $path);
    my $size = (-s $path);
    print "${path}\t${size}"\n";
}

ファイルを辿るたびにfindで指定したサブルーチンが呼ばれます。 その際、ディレクトリが$File::Find::dir変数に、主ファイル名が$_に、 フルパスが $File::Find::name に格納されるので、それらを使って処理をします。

モジュールを使わずに記述することもできます。 下のサンプルは指定したディレクトリ以下を再帰的に検索し、ファイル名を表示します。

$level = 0;

sub showdir {
    my($indir) = @_;
    opendir(INDIR, $indir) || die $!;
    my(@files) = readdir(INDIR);
    closedir(INDIR);

    foreach (@files){
        if($_ ne '.' && $_ ne '..'){
            $path = $indir . "/" . $_;

            for($i=0; $i<$level; $i++){ print "  "; }
            print $_ . ((-d $path) ? "/" : "") . "\n";

            if(-d $path){
                $level++; & showdir($path);
            }
        }
    }
    $level--;
}

& showdir("C:/usr/lang");

もう少し実用的な例を。下のサンプルは指定したディレクトリ以下を再帰的に検索し、 ある文字列(例では"ZZ9")を含むファイルの名前を表示します。
コメント化していますが、対象とする拡張子を絞るなどの応用もすれば、いろいろ使えそうです。
なおこの処理ではreaddir時にファイル名でソートする処理も入れています。

use strict;

sub walksub {
    my($indir) = @_;
    opendir(INDIR, $indir) || die $!;
    my(@files) = sort {$a cmp $b} readdir(INDIR);
    closedir(INDIR);

    foreach my $fileName (@files){
        next if($fileName eq '.' || $fileName eq '..');

        my $path = $indir . "/" . $fileName;

        if(-d $path){
            & walksub($path);
        } else {
            # next if($fileName !~ /\.txt$/i);
            open(INFILE, $path) || die $!;
            my @lines = grep /ZZ9/, <INFILE>;
            if(@lines){
                my $rpath = $path;
                $rpath =~ s/^.*forms\///;
                print "${rpath}\n";
            }
            close(INFILE);
        }
    }
}
& walksub("C:/temptemp/startdir");

もう一つ似た例を。duコマンドは各ディレクトリのファイルサイズ合計を出力しますが、 同様にファイルの数を各ディレクトリ別に表示するスクリプトです。
各ディレクトリに含まれるサブディレクトリは数に含めます。.と..は含めません。

use strict;
my($target_dir) = $ARGV[0] || die "base directory path expected.";

sub walksub {
    my($indir) = @_;
    opendir(my $d, $indir) || die $!;
    my(@files) = readdir($d);
    closedir($d);

    print $indir."\t".(@files-2)."\n"; # .と..を除外するために-2

    foreach my $fileName (@files){
        next if($fileName eq '.' || $fileName eq '..');
        my $path = $indir . "/" . $fileName;
        walksub($path) if(-d $path);
    }
}
walksub($target_dir);

タブ区切りテキストファイルをカンマ区切りへ変換する
カンマ区切りにするついでに二重引用符で囲みます。 以下のようにmap関数を活用するととても短く書けます。

while(<>){
    chomp; @arr = split;
    @arr = map { '"' . $_ . '"' } @arr;
    print join(",", @arr) . "\n";
}

ファイル全体を文字列に読み込む
普通のプログラミング言語のように1行ずつファイルハンドルで読んでもいいのですが、 Perlにはもっと短く書ける慣用記法があります。
いくつかあるそうですが、中でも特にジャンキーなのは下のように、 改行文字を表す$/特殊変数をdoブロックの中でundefして読ませる方法だと思います。

open(INFILE, 'sample.txt') || die $!;
$text = do { local $/; <INFILE> };
close(INFILE);

ファイルの1行を1要素とする配列に読み込む
次のように、ファイルハンドルを配列コンテキストで読ませると、1行を1要素とする配列を取得できます。

open(INFILE, 'sample.txt') || die $!;
my @lines = <INFILE>;
close(INFILE);

ただし重大な注意が一つ。改行コードがCR+LFになっていると、取得した配列、 つまり上記の @lines の各要素の末尾には改行文字が残ってしまいます。 従って各要素を取得する際に忘れずにchompするか、次のようにmapにしてchompします。

open(INFILE, 'sample.txt') || die $!;
my @lines = map {chomp; $_} <INFILE>;
close(INFILE);

Misc. Topics Top

(first uploaded 2001/01/25 last updated 2025/10/26, URANO398)