BREAKING NEWS

[4]

シェルでお洒落に一括処理

Linuxを使って間もない人は、ついつい、


大量のファイル名変換とかは、
コマンドラインからやったほうが便利なんですわ~


なんてことをつい口走ったりしてそうですが、 実際に「じゃぁ、君に任せるよ」とか言われてあたふたしているかもしれません。 そんな人にこのノートはこっそり役立ちます。 実際に、ばーーーっと、一括処理書けるようになりましょう。


シェルのfor構文パターン

処理を繰り返すための手段として、 まずは、シェルのfor構文を使うパターンがあります。


はじめに実際の処理を見てみましょう。 カレントディレクトリ内の拡張子が.txtのファイル全てのファイル名の頭に「hoge_」という文字列を追加する処理の例は以下のようになります。


# shell script

for i in *.txt
do
  mv ${i} hoge_${i}
done


for構文の書式パターンは次のようになっています。


# for構文の書式

for 変数 in 対象となるもののリスト
do
  処理コマンド
done


for構文では、次の2点を意識して、理解することで、 色んなパターンに応用できるようになります。


処理対象となるものはグロッピングで表現

まずは、for構文に渡す「対象となるもののリスト」に着目します。 先の具体例においては「*.txt」となっている部分です。


実はこの普段ワイルドカードとか言って何気なく使っている「*」は、 「シェル自身」がファイル名に一致するものに置き換えてくれるメタ文字です。 そして、このシェル自身がファイル名に置き換えてくれる機能のことを 「グロッビング」や「ファイル名置換」と呼んでいます。 ですから、シェルスクリプトの中で、 実際のファイル名を扱いたい場合は、 この自動的にファイル名に置き換えてくれるグロッビング機能を使うのがコツなのです。 この際、シェルが実行される時のカレントディレクトリには注意が必要だということを忘れないでおきましょう。


もちろん、スクリプト側でメタ文字を使って、対象ファイルを選別するのも良いですが、 複雑になりそうな場合は、 対象としたいファイルだけをある特定のディレクトリに手動で取り出してしまうのが 簡単で確実な場合もあります。 そうすれば、 ディレクトリ内の全てのファイルが対象となるので、 対象となるリスト表現は「*」とすれば良いだけになります。


この対象リスト部分を手動で出書きすれば以下のような形になります。


# shell script

for i in 001.txt 002.txt 003.txt 004.txt 
do
  mv ${i} hoge_${i}
done


変数展開

先の例で見た「i」は「変数」です。 そして、シェルにおいて変数の内容を取り出すことを「変数展開」等と呼びます。 これは、コマンド実行処理の前に シェルスクリプトの中で「${i}」という書式で書いておけば iの内容である値に「置き換える」処理をシェルがしてくれることを指します。


そして、実は、変数展開は単純にその内容の値に置き換えるだけではなくて、 良くある文字列処理を同時にさせるためのオプションがあります。


例えば、上記具体例のようにファイル名の先頭になにか特定の文字列を付け加えたりするだけなら、 単純な変数展開を使って簡単に表現できます。 しかし、拡張子を’.txt’から'.py'に書き換えたいとしたらどうでしょう? そんな時に使えるのが、変数展開のオプションです。


# shell script

for i in *.txt
do
  mv ${i} ${i%.txt}.py
done


mvの第2引数「${i%.txt}.py」の部分に着目しましょう。 このうち、変数展開にかかる部分は「${i%.txt}」の部分です。 これは、通常の「${i}」ならば、 単に、変数iの内容に単純に置き換わるのですが、 変数名の後ろに「%」を書くことで、%の後ろのパターンが変数iの内容の後方からマッチする場合、 その部分を削除したものに置換されるようになります。


例えば、変数iの内容が「001.txt」の場合、「.txt」という部分が後方から一致します。 そこで、「${i%.txt}」と書かれた部分は、「001.txt」から「.txt」を削除した「001」に置き換えられることになります。


上記の例では、「001」に置き換えられる部分の後ろに「.py」を付け加えることで、 全体で.txtが.pyに置き換えた文字列を作成しています。


このような通常よく見る「${hoge}」の構文は、 単に「変数の中身を見る」と把握するのではなくて、 「シェルにいろいろな値に置き換えさせる書式」として、把握しておけば 色んなことができるという機能があることに納得が行くことと思います。


上述の拡張子の置換を変数展開で行うのは、定番のパターンです。 そのほかには、変数の初期値代入というもの、例えば、ある変数に値が入っていれば、それを使うが、入っていなければ指定したものを使うという 条件で内容が変わるようなものも変数展開を使うのが、定番です。


シェルの変数展開については、次のページが見やすくわかりやすく、 また、元の情報ソースであるman bashにも言及しているので、 何処までも正確に沼に浸ることが出来ます。




「簡単に」シェルスクリプトを使うパターンのキモ

シェルのfor構文処理で「簡単」に記述するためには、 グロッピングで対象ファイルを簡単に指定できること、及び、 シェルの変数展開だけでコマンド引数を簡単に記述できることが必要になります。 上記の例で見たように、 単純なファイル名へのプレフィックス文字列追加や拡張子変更の場面では、 for構文を書くだけで簡単に大量処理を行うことが出来ます。 そして、このとき、わざわざスクリプトファイルを書くまでもなく、 慣れればターミナル上でそのまま記述するようになります。 また、僕はzshを使っていますが、for構文等の複数行処理を書いた場合でも、 コマンド履歴で複数行呼び出しが出来、 その複数行についての編集もできるので、 書けば書くほど、手数が増えていきます。


正規表現が得意なsed

次は、「ett20210401001.wav」というファイル名を「ett2021_0401_001.wav」に変換したい場合を想像してください。


例えば、英会話の音声教材を通 し番号付きで管理していて、 ファイル名は2021年4月の第1週の一回目を表す通し番号のつもりで付けけていたのですが、 実際に使ってみると数字の羅列が長くてぱっと見でよくわからなくなってきました。 特に10月とか11月辺りになると目が悪いのでもうダメです。


先にやったシェルの変数展開には、 文字数を指定した文字列取り出しや、文字列置換も行えるので、 先の方法でも出来なくはないでしょうが、 割と暗号チックな処理になってしまいそうです。


一方、このようにファイル名が日付等の決まった書式になっているものを、 別の決まった書式に構成し直す場合には、 正規表現を扱うのが得意なsedコマンドを利用すると見通しの良い処理がかけます。


$ ls | sed -rn 's/(ett[0-9]{4})([0-9]{4})(.*)/mv \0 \1_\2_\3/p'


カレントディレクトリに適切なファイルがある場合、 上記コマンドを実行すると標準出力に次のような出力がなされます。


mv ett20210401001.wav ett2021_0401_001.wav
mv ett20210401002.wav ett2021_0401_002.wav
mv ett20210401003.wav ett2021_0401_003.wav
mv ett20210401004.wav ett2021_0401_004.wav
mv ett20210401005.wav ett2021_0401_005.wav


sedを使った方法で一番はじめに把握すべきなのは、 sedは結果として文字列を出力するコマンドだということです。 上述のように、sedは、 ファイル名をリネームするための「コマンド文字列を全て作成」してくれるだけです。 実際にこの処理を行うためには、出来上がった文字列をパイプでシェルに渡したり、 ファイルに書き出した後、シェルに読み込んだり、 と改めてコマンドを実行するための処理が必要となります。


また、実はsedへの「入力」もlsの出力をパイプしてsedの標準入力に渡しています。 処理対象がファイル名である限り、lsコマンドの出力をパイプで渡すパターンにすれば 問題ありません。


入力と出力

sedは、文字列を受け取って、加工した文字列を返すコマンドです。 この機能を利用して、一括処理の場合には次の様にsedコマンドを使います。


入力は「対象ファイル名」。 具体的には次の通り。


ett20200401001.wav


一方、出力は「処理のためのコマンド文字列」 具体的には次の通り。


mv ett20210401001.wav ett2021_0401_001.wav


やりたい処理が決まっている場合、 動的に変化するのは、 取得するファイル名と そのファイル名を元にしてあるルールのもと作られる新しいファイル名であり これらは正規表現を使って表現できます。


sedの置換構文

sedの置換構文を改めてみてみます。


's/(ett[0-9]{4})([0-9]{4})(.*)/mv \0 \1_\2_\3/p'


置換構文の書式は以下のように、3つの"/"デリミタで区切られた前半部分に、 「置換すべき対象」を記述し、後半部分に「置換する文字列」


's/置換すべき対象/置換する文字列/'


さて、一括処理でこのsedを利用する時には、ある定形のパターンを使います。 まず、sedへの入力は、必ず対象となる元のファイル名にします。 これは、lsコマンドの出力をsedに渡すことで実現できます。


次に、置換構文の前半部分では、必ず、対象ファイル名そのもの全体に一致するようにします。 こうすることで、「\0」書式が「入力された元のファイル名」に展開されると同時に、 置換構文の後半部分で記述したもの以外の余計なものが出力されなくなります。


名前変換等で書き換えが必要となる部分等を表現するためのパーツになるように、 正規表現を「(」「)」記号で区切って、後方参照できるようにします。 正規表現の中では「(」「)」記号で区切った位置によって、 それぞれ左側から「\1」「\2」「\3」...と、いう書式で それぞれの具体的なマッチ文字列に展開されます。 ですから、この展開文字列を組み合わせることで、 置換構文後半部分で、処理したいコマンド文字列を作成することが出来ます。


sedのオプション

上記例でsedに-nrとオプションを渡しています。 nと置換スイッチのpは、一括処理記述の際のパターンとしてこの通りにしておけばOKです。 一方、rオプションは、部分指定の「(」「)」記号を書く際に、rオプションをつけていないと、 「\(」「\)」記号を書く際に「\」というエスケープ文字が必要になります。 巷のsedの解説を見る時に、「これって何?」とならないように、そういうものがあるということだけは把握しておきましょう。


数字を表す正規表現

自分でつかってて、「あれ、なんで??? あぁ、そうなんやぁ」と思ったところです。 1や2という「数字」を表現するための正規表現として「\d」をよく使いますが、sedでは使えません。 そして、巷では、幾つかある文字グループのひとつとして「[:digit:]」という標記が紹介されています。 しかし、実際にsedの正規表現でこれを書くためには角カッコをもうひとつ外側にも書いて「[[:digit:]]」と表記する必要があります。 というわけで、書く量が多くなるので数字表現は「[0-9]」が一番見やすいんじゃないかとおもい、例ではこっちを使ってます。


よくやる変換

iphoneの写真HEICをpngに

「なんか、最近iphoneで撮った写真をダウンロードしたら拡張子がHEICだったから、jpgとかに変換したいんですけどーー、linuxでもできますか??」 なんていう質問がそのへんに転がってそうなので、早速実践しましょう。 heif形式の画像を扱うライブラリのパッケージとしてarchlinuxでは「libheif」パッケージがあります。 このパッケージをインストールすると、HEICをjpegとかpng形式に変換してくれる「heif-convert」コマンドが使えるようになります。 早速パッケージをインストール。


$ sudo pacman -S libheif


やるべきことは、HEICの画像ファイルをjpg形式に変換する。 ファイル名は拡張子のみ変える。 という方針で行きましょう。 ファイル名はイジらないので、for構文アンド変数展開で十分です。 準備が出来たら適当なディレクトリに拡張子がHEICの画像ファイルを入れます。


# iPhoneの写真を変換する
for i in *.HEIC
do
  heif-convert ${i} ${i%.HEIC}.jpg
done


wavファイルをmp3でコンパクトに

パソコンで英語教材を聞いている時arecordで録音したりするのですが、wavファルで保存されるのでmp3ファイルにしてかさを減らします。 変換に使うのはlameコマンド。archlinuxではlameパッケージに入っています。


$ sudo pacman -S lame


変換したいファイルを特定のディレクトリにいれます。 lameコマンドは入力ファイルを引数に渡すだけで、 自動的に拡張子が.mp3のファイルが出力されます。 ですからスクリプトは超かんたん。for構文でファイルを渡すだけです。


# wavをmp3に変換する
for i in *.wav
do
  lame ${i}
done


シェルの沼

初心者の頃は「シェルって何」とおもっていても、 普通にコマンドを使うように慣れば、「シェルって何」とは思わなくなって来ると思います。 しかし、「グロッピング」や「変数展開」というものがあるということを知り、 それらのマニュアルを興味を持って自分から読みたいと思えるようになってくると、 「シェル」に対する見え方がまた変わってくるんじゃないでしょうか。


更に、上述紹介のページでは「bash」のことが説明されていますが、 実は、zshにはzsh独自のグロッピングや変数展開についての便利追加オプションがあり、 便利なんだけど一般といわれるshシェルスクリプトと互換性が無いなんて文句をいわれたりします。


例えば、グロッビングの「*」ですが、通常はカレントディレクトリの全てのファイルとディレクトリに「展開」されます。しかし、zshだとこれにファイルの種類を選択するスイッチを追加できます。 例えば、「一般のファイル」に限定したい場合「*(.)」として、「ディレクトリ」に限定したい場合は、「*(/)」として記述することで、展開されるものを限定することができるんですよっ!すごくないですか?


さて、シェル自体に興味が湧いている人にお勧めできる本があります。 zshについての結構古い本ですが、「zshの本」という本です。 買った当初は、zshものつ高度な補完に関する仕組みに興味があったのですが、 よくよく読むと、シェル自体の機能について丁寧に解説されていることに気が付きました。 シェル自体に興味をもってから、この本を読むことで、色んな発見ができ、 「シェルそのものの造り」に親しみが持てるようになりました。 また、筆者のシェルに対する深い思いが伝わります。



ここでいう「簡単なシェルでのファイル一括処理」は、 シェル芸とは対極にある本当に簡単で便利なシェルの処理の話であり、 文字通りお手軽簡単に分かりやすく日常ファイル処理をしようという話なのです。


しかし、普段遣いしているうちにふと、 記号の羅列をみているにもかかわらず、 「あれ?これって、こう書いたら、こうなるじゃね?」なんて思っている自分に気がつくかもしれません。 そう、もう沼にハマってます。 ただ、シェルの沼はそれほど危険ではありません。 デスクトップカスタマイズなどのように時間を延々と食われたりしないし、 何しろ身につけばほんとに色々と役立ちます。 存分に、どっぷりと浸かりましょう。


シェルでお洒落に一括処理 シェルでお洒落に一括処理 Reviewed by shunsk on 2/11/2022 Rating: 5

No comments:

Sora Templates

Image Link [https://scontent.xx.fbcdn.net/v/t1.0-9/13343039_103147363441877_5912666874811349341_n.jpg?oh=32de40bba33988d454d1a3104dde18ac&oe=57D1E6F4] Author Name [Syunkichi] Author Description [Let's write codes staylishly!!!!!] Twitter Username [webgeek7] Facebook Username [syunkichi.nekota] GPlus Username [114037852049523769954] Pinterest Username [pcgeek91] Instagram Username [s.nekota]