Linuxフィルター - ボスのようにテキストを効率化する方法

Unix哲学に精通していて、より良いスクリプトを作成する方法を知っていますか?この包括的なガイドでは、Unix哲学の一般的な定義を探り、よく書かれたスクリプトの主要な要素を調査し、パイプラインオペレーターやstdin、stdout操作などのスクリプティングの構成要素を学びます。最終的には、これらをルビー/バッシュスクリプトでの良い習慣としてどう適用するか掘り下げていきます!

目次

Unix哲学とは?

Unix哲学は元々は大家Ken Thompsonによって定義されたもので、_ミニマリスト_で_モジュラー_なソフトウェアを定義する一連の良い実践として、Unixのすべてのコアユーティリティ(findgrepなど)はこの良い実践に従っていますので、良いものであることは間違いありませんね?

元の引用はDoug Mcllroyによって記録されており、以下の項目が含まれています:

  1. 各プログラムはひとつのことをうまく行うように作りましょう。新しい仕事をするためには、「機能」を追加して古いプログラムを複雑にするのではなく、新たに始めましょう。
  2. すべてのプログラムの出力は、未知の別のプログラムへの入力になることを期待します。余分な情報で出力を汚さないでください。厳格な列形式やバイナリ入力形式を避けてください。対話式の入力を強制しないでください。
  3. ソフトウェアやオペレーティングシステムを、できれば数週間以内に早期に試せるように設計・構築しましょう。不格好な部分を捨てて再構築することをためらわないでください。
  4. プログラミングタスクを軽減するためには、道具を活用しましょう。たとえそのために道具を作る迂回をして、それらを使い終わった後に捨てることになってもです。

最後の2つの項目はもっと「一般的なプログラミング」のヒントを定義していますが、この記事の目的は完全に最初の2つのヒントに集中しています。ここで良いスクリプトは、_ひとつのことを非常にうまく行い、パイプラインに組み込むことができるもの_として知られています。次の章では、このフレーズをもう少し理解しやすくしましょう。

パイプラインとは?

パイプラインは一連のプログラムが連続して実行され、ひとつのプログラムのstdoutが次のプログラムのstdinになるものです。bash/zsh/fishシェルでは、パイプとして|オペレータを使い、さらに情報をクエリするためにこれをよく使います。例えば、ディレクトリ内にマークダウンファイルがいくつあるかを数えたいとします。次のようにできます:

find . -iname '*.md' | wc -l

順を追ってきちんと理解しましょうか?

まず、findコマンドが現在のディレクトリ内のすべてのマークダウンファイルをリストアップするために使われています。個人的には、これによって次のようになります:

$ find . -iname '*.md'
./posts/20230814T124722/README.md
./posts/20230703T214043/README.md
./posts/20230616T234323/README.md
./posts/20230625T223158/README.md
./posts/20230731T212528/README.md
./posts/20230807T203924/README.md
./posts/20230804T140043/README.md
./REPL Driven Development - For not so smart developers.md
./Como escrever uma CLI CRUD utilizando ScyllaDB + Ruby.md
./Linux filters - How to streamline text like a boss.md

数を数えずに適切なファイル名だけをリストしたい場合は、sortコマンドにパイプしてアルファベット順にリストすることもできます。次のようになります:

$ find . -iname '*.md' | sort
./Como escrever uma CLI CRUD utilizando ScyllaDB + Ruby.md
./Linux filters - How to streamline text like a boss.md
./REPL Driven Development - For not so smart developers.md
./posts/20230616T234323/README.md
./posts/20230625T223158/README.md
./posts/20230703T214043/README.md
./posts/20230731T212528/README.md
./posts/20230804T140043/README.md
./posts/20230807T203924/README.md
./posts/20230814T124722/README.md

最初のfindコマンドの出力がどのようにsortコマンドに継ぎ目なく渡されて適切にソートされて返されたかわかりますか?これが小さなツールを書く全ての意図であり、こうすることで色々なパイプラインを簡単に合成してカッコいい結果を得ることができます。

最初の例の場合、wcコマンドを使って、stdinから受け取ったものを何でも数えますが、今回は行数を数えるために-lフラグを使います。これらを全てまとめると、ヴォイラ!私たちの結果が得られます:

$ find . -iname '*.md' | wc -l
      10

stdinとstdoutとは?

Stdin(標準入力)とstdout(標準出力)は、コンピュータが外部世界と通信する主な手段です。以下で、それぞれの詳細をもう少し掘り下げます:

  • Stdin(標準入力)は通常、ユーザーとの対話(入力フィールドへのタイピングやリストアイテムの選択など)を待つものとして言及されますが、これをより一般化したものとして理解することもできます。基本的に、別のプログラムからであれユーザーとの対話からであれ、Stdinはデフォルトの_情報提供者_として考えることができます。
  • Stdout(標準出力)は通常、画面に値を出力するもの(ターミナル、ブラウザ、または何でも)として言及されます。Stdinと違ってこれは完全に正しいですが、パイプラインコンテキストに挿入される場合、すべてのstdoutは画面へ印刷される代わりに次のコマンドのstdinとして抑制されます。

悪いスクリプトとは何か、どうやって良いものに変えるか?

どのようなスクリプトが良いスクリプトかを正確に要約するのは、関わっている多くの変数のため難しいかもしれません。質問を逆にしましょう:Linux哲学に従って悪いスクリプトとはどのようなものでしょうか?そして、具体的な問題をどのように解決して、少しでも良いツールにするのでしょうか?

標準入出力のコミュニケーションを無視する

アニメーション入力やカッコいいスピナーで情報を表示するファンシーなCLIを使ったことはありませんか?それらのツールを単独で使う場合は確かにカッコいいのですが、パイプラインに組み込もうとすると、プロセス全体が壊れてしまいます。その代わりに、常に標準のコミュニケーションを使ってツールを書くようにし、ハードコーディングされた値の代わりにオプションを好むようにしてください(--output=json-o jsonといったオプションの例)。

Rubyでは、うっかり自傷行為をしない方がずっと簡単です。なぜなら、データを取得する方法を学ぶ際に標準入力をうまく活用しているからです。しかし、スクリプトをどのように設計するかに気を付けることは重要です。

例として、printname.rbというシンプルなスクリプトを考えてみましょう:

#!/usr/bin/env ruby

puts 'Type your name: '
name = gets.chomp

puts "Your name is: #{name}"

このスクリプトは一見して単独で実行してもパイプラインで実行しても機能しますが、両方で生成される出力を観察してみましょう:

$ echo "Cherry Ramatis" | ./printname.rb
Type your name:
Your name is: Cherry Ramatis

$ ./printname.rb
Type your name:
Cherry Ramatis
Your name is: Cherry Ramatis

いつもType your nameメッセージを印刷していることに気が付くでしょう。この場合、次の2つのオプションがあります:

  • --name="Cherry Ramatis"というパラメータフラグでこの名前を受け入れる
  • Type your nameメッセージを削除し、スクリプトをもっとパイプラインに乗せやすい(いい言葉でしょう?)ものにする

同じツールで多くをする、いわゆるモノリシックスクリプト

ツールを書いているときに、段々と範囲が大きくなってくることに気づくかもしれません。小さなツールがいつの間にか大きなプロジェクトに成長して、様々な対話型のステップが含まれるようになります。この段階に達すると、多くのサブコマンドを追加することによって同じツールを繰り返し反復しても魅力的になるかもしれませんが、Unix哲学は_ひとつのことを正しく行う_ツールを書くことの価値が、巨大なベヒモスを書くことよりも価値があると理解するのに役立ちます。

ですので、多くのコマンドが含まれる大きなCLIを作る代わりに、相互に接続し補完する小さな個々のツールを書くことで考えるようにしてみてください。例えば:

データベースから特定の順序ですべての曲をリストし、ユーザに選ばせたいとします。_ひとつの_コマンドでこれらすべての機能を書く代わりに、異なるものと組み合わせることができます:

$ list_songs | sort | update_song

list_songsupdate_songが別のスクリプトであることに注意してください。どちらも標準入出力で通信し、ユーザーが任意のコマンドにパイプラインを通しても同じ振る舞いが得られるようになっています(たとえば、リストをソートされた方法で見たいからsortにパイプしています)。

vimでのバングオペレーター

Vimユーザーの皆さん、輝きの時が訪れました。VimはUnixシステムで標準的なエディターの一つであり、ミニバッファにコマンドを入力する際に!オペレーターを通じて直接バイナリとやり取りできるなど、Unixシステムで標準的なエディターとしての一連のメリットがあります。

Vimでは、ノーマルモード!!を押すと、ミニバッファに:.!というコマンドが表示されます。!の後に入力したコマンドは、STDINとして現在の行に使用されます。すごいでしょう?以下の例を観察してみてください:

Rubyスクリプトを次のように考えてみます:

#!/usr/bin/env ruby

STDIN.each do |line|
 puts "- #{line}"
end

このス

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/cherryramatis/linux-filters-how-to-streamline-text-like-a-boss-2dp4