文字列に含まれる複数の数値を考慮したソート

ディレクトリとファイルの整理をすることになって、ファイル名に基づいて連番を振るリネーム作業が必要になった。 2_0001.dat10_0001.dat10_0002.dat だとか、10_aa.dat10_ab.dat、といった感じのファイルをいい感じに並び替える必要があった。

見ての通り、ファイル名は複数の数値を含んでいたり、ゼロ埋めだったり、文字の連番('aa', 'ab)もあった。なので、これを考慮して並び替えたい。 (要はWindowsのファイル名の昇順と概ね同じ並びにしたい。)

GNU CoreUtilssortではできないっぽいので、perlでこう書いた。

sub is_number {
    return $_[0] =~ /^\d+(?:\.\d+)?$/;
}

sub by_number{
    my $pattern = qr/(\d+(?:\.\d+)?|\D+)/;
    my @a = ($a =~ /$pattern/g);
    my @b = ($b =~ /$pattern/g);
    for(my $i=0; $i<@a&&$i<@b; ++$i){
        my $cmp = (is_number($a[$i]) && is_number($b[$i])) ? $a[$i] <=> $b[$i] : $a[$i] cmp $b[$i];
        if($cmp){
            return $cmp;
        }
    }
    return scalar(@a) <=> scalar(@b);
}

# 使用例
my @sorted_datalist = sort by_number @datalist;

で、クソコード書き終えてから気付いた。 数値は先頭ゼロを含めて同じ桁なら、文字比較でもうまくいくはずなので、「数値を0埋めして、後は文字比較する」でよい気がする。

sub by_number{
    my $ta = $a;
    my $tb = $b;
    $ta =~ s/(\d+)/sprintf("%010d",$1)/eg; # 数値10桁を越えそうなら書き換える
    $tb =~ s/(\d+)/sprintf("%010d",$1)/eg;
    return $ta cmp $tb;
}

で、これぐらいならワンライナーに落とせそうだったので落とした。入力は1行1ファイル名。

# `#`区切りで`ゼロ埋め文字列#元文字列`に変換して出力して、並び替えたら元の文字列の部分だけをして出す。
cat "target.txt" | perl -ne 'chomp; $t=$_; s/(\d+)/sprintf("%010d",$1)/eg; print "$_#$t\n"' | sort | cut -d '#' -f 2

もっと簡単な方法はありそう。