😾

1ファイルのみで"cat ファイル名 | …"と書くのはShellCheck違反になるので代替案を考える

2024/03/23に公開
2

TL;DR

  • 警告に従いパイプで受けるコマンド <ファイル名とするのが最も無難。

内容

技術記事の多くで当たり前のようにcat file.txt | grep …といった形で1ファイルだけの中身を読み込んでパイプで渡すような書き方をよく見かけるが、ShellCheckを導入していると以下(SC2002)に違反しており警告となる。

Useless cat. Consider cmd < file | .. or cmd file | .. instead.

なぜ警告となるかは速度の問題など上記ページのリンク先やこの番号で調べればいくらでも出てくるので割愛するが(Useless use of cat (UUOC))、そもそもcatコマンドは複数のファイルを結合することが役割なので、1ファイルだけの中身を加工すること自体が目的に沿っていない。

つまり機能仕様上は可能ではあるというだけなのだが、何故だか日本語の記事がほとんど引っかからないので見て見ぬふりをされているのか軽視されているかよくわからない。

結論から言うと、確かに代替案はありそうなものの結局最もシンプルでわかりやすい書き方となるのがcat ファイル名なので、もしかしたら皆色々検証したり検討した結果、単に最終的に落ち着いた形がこのコマンドとなっただけなのかもしれない。

だがShellCheckを導入していれば例外設定でもしない限り警告としては出続けて余計な負担になるだけなので、根本的に解決できそうな代替案を考えてみる。

案1. <を用いる

警告に従いリダイレクト<を用いて、例えば以下のように記述する。

command
grep aaaa <file.txt

catを使わない書き方の中ではおそらくこれが最もシンプルになる。

しかしながら入力のリダイレクトが右側に来てしまうと、後に処理されるはずのコマンドを先に読むことになり、通常の他コマンドで左から右へ読むことと逆行するため可読性が悪くなる。
例えば以下のように出力のリダイレクトがあると、入力ファイルがそのままリダイレクトされるように読まれてしまうかもしれないし、直感的にもかなりわかりづらくなる。

command
grep aaaa <file.txt >result.txt

もちろん、仕様上は以下のように左側に持ってきて記述することも可能ではあるが、フォーマッタによっては強制的に右側に移動させられることもあるので必ずしも適切とは言えない。

command
<file.txt grep aaaa
var=$(<file.txt grep aaaa)

案2. sedを用いる

以下のように記述する。

command
sed "" file.txt | grep aaa

sedはストリーム文字列を処理するコマンドだが、第一引数に""を渡せば何も加工せずにファイルの中身をそのまま出力することができる。
catと似たような記法ができるものの""を毎回書かなければいけないという制約があるので、あまりシンプルであるとは言えない。
また何も加工しないのにsedを使うのも、結局目的に沿っていないように思われる。

案3. grepを用いる

以下のように記述する。

command
grep "" file.txt | grep aaa

sedと全く同じ記法で書ける。検索のパターンを空文字""とすることで全部の文字列が該当することになるので結果的にファイルの中身がそのまま出力される。
これもgrepの役割としては検索して絞り込むものであるはずなので、sed同様目的に沿っていないように思われる。

案4. awkを用いる

以下のように記述する。

command
awk "{print}" file.txt | grep aaa

フォーマットを"{print}"とすることで、すべての文字列が出力される。
sedに近い役割かもしれないがこのフォーマットで出されることを想定はしていることになるので、まだ目的に沿っているほうかもしれない。
ただ、"{print}"を毎回書くのはかなりシンプルさが落ちるので使い勝手が良いとは言えない。

案5. headを用いる

こちらは裏技的な使い方なので非推奨。

command
head -n -0 file.txt | grep aaa

引数-nにマイナスの値を設定すると、絶対値分の行数を末尾から引いた文字列のみが出力される。
この仕様を利用して値を-0とすれば、末尾から0行引いた文字列が出力されることになるので、結果的に1行も引かれずそのまま出力されることになる。
だが負のゼロは一般的に扱いづらい特殊な値であるし、理解も難しいものなので基本的には避けるべきものであるうえ、headの役割と正反対な処理をさせているので適切ではない。
書き方としても2つ引数が増えるので、その点でも採用しづらい。

案6. catで引数を配列化する

こちらはただShellCheck警告を回避させているだけで実質ほぼ同じことをしているので非推奨。

command
files=(file.txt)
cat "${files[@]}" | grep aaa

catが複数ファイル前提なら、複数になり得る配列を引数にしてしまえば良いという考え。
以下を参考。

警告が消えるだけで本質的に処理自体は変わっていないうえ、無駄に変数宣言が増えるので対策としては適切ではない。

結局どう書けば良いのか

書き方は何を重視するかで決めれば良いと思われるが、一番無難というか根本的に解決できているのはリダイレクトを使うことくらいしかなさそうである。

  • とにかく可読性最優先でシンプルかつ誰でも読めるようにしたければcat ファイル名でも良いかもしれないが、その代わり様々な弊害があることを容認する必要がある。

  • 可読性は劣るが品質重視でcatの弊害をなくしたければ、素直にリダイレクトを用いる記法にすべきである。

  • リダイレクトは絶対に使いたくないのであれば、シンプルさは劣るがgrepsedで代用すれば良いかもしれない。

Discussion

ko1nksmko1nksm

海外では昔から知られているUUoCそのものが、日本で知られていないという、まあ困った状況ですね。

これをおすすめします。

 grep <file.txt aaaa

catを使った場合でも、結局のところ一番左にあるのはcatという単語なので、

 cat file.txt | cat -n

↑のような変な書き方よりも

 cat <file.txt -n

この方が良いですね。

kkddkkdd

こんにちは。下記はいかがでしょうか?

$ grep aaaa file.txt
$ awk 1 file.txt | grep aaaa