[Linux]ls + sed + bashで泥臭いファイル周りの作業が捗る話

はじめに

以前にも、[Linux]大量のファイル名のリネームをコマンド一発で実行するヒントという記事を書きましたが、このls + sed + bashは使い勝手がよくて、今回いい感じの型が作れたのでメモに残します。

ケース1. 大量の行があるファイルから一部抜き出し、別のファイルに書き出す処理を、大量のファイルに対して行う

大量の行・ファイルを処理するシステムを実装中に、挙動確認用にテストデータを作りたいときなど。

前提

  • フォルダaの中には数百のファイル
  • それぞれのファイルの中身から一部抜き出し、フォルダbに同名ファイルで書き出す

コマンド

# a, bは同一ディレクトリ内に配置
$ ls -1 a | sed -r 's/.+/head a\/& > b\/&/' | bash

解説

ls -1 a

で、aの中身のファイル名を一覧表示します。-1オプションでファイルごとに改行して表示します。

sed -r 's/.+/head a\/& > b\/&/'

で、lsで出した出力結果を正規表現を使って置換しています。

より分解すると:

  • .+で対象範囲を指定します。ここでは1行まるごと(=ファイル名)対象にしています。
  • headで対象ファイルの冒頭10行を表示します
  • >headの結果を指定したパスに書き出します
  • &は置換対象としている文字列全体を指します(ここではファイル名になります)

ということです。
例えば、

$ ls -1 a
hoge.tsv
fuga.tsv
piyo.tsv

として出力した内容を、

head a/hoge.tsv > b/hoge.tsv
head a/fuga.tsv > b/fuga.tsv
head a/piyo.tsv > b/piyo.tsv

という出力に変換する、という処理です。

最後に| bashでこの出力をコマンドとして実行します。

ケース2. 大量のgzファイルの中身を一部抜き出し、別のファイルに書き出す処理

ケース1は対象がテキストファイルですが、サイズの大きなファイルだと、gz形式に圧縮されていることもしばしば。
そんなときは少しコマンドを改良します。

コマンド

$ ls -1 a/*.gz | sed -r 's/a\/.+/gzip -dc & | head > b\/\1/' | bash

解説

ls -1 a/*.gz

でa配下にある.gzとつくファイルを一覧表示します。この場合、結果が

a/hoge.tsv,gz
a/fuga.tsv,gz
a/piyo.tsv,gz

のように、ディレクトリ名も含む点に注意が必要です。

sed -r 's/a\/(.+)\.gz/gzip -dc & | head > b\/\1/'

で、lsして出したファイルそれぞれを、解凍せずに中身を見てその一部を別ファイルに書き出しています。

余談ですがgzip -dcは対象ファイルを解凍しないので、元となるファイルを変更せずに済みますし、むやみにファイルをコピーして容量を食う必要もないので重宝してます。

sedの部分をもう少し分解すると:

  • a\/(.+)\.gzで、ディレクトリ名を含んだファイルパスから、ファイル名のみ、かつ.gzという拡張子は除いた状態でキャプチャします
  • gzip -dc &で、対象ファイルを解凍せずに中身を見ます
  • head > の部分はケース1と同じです
  • b\/\1で、ファイル名を指定します。\1はキャプチャした部分、つまりファイル名を意味します。

という感じになります。

例えば、

$ ls -1 a/*.gz
a/hoge.tsv,gz
a/fuga.tsv,gz
a/piyo.tsv,gz

として出力した結果を

gzip -dc a/hoge.tsv,gz | head > b/hoge.tsv
gzip -dc a/fuga.tsv,gz | head > b/fuga.tsv
gzip -dc a/piyo.tsv,gz | head > b/piyo.tsv

という出力に変換する、という処理です。
それを| bashで実行する、というところはケース1と同様です。

まとめ

肝になるのはsedによる正規表現での扱いです。
やはり正規表現大事ですね。

参考