4 ポイント 投稿者 GN⁺ 2023-08-29 | 1件のコメント | WhatsAppで共有
  • GNU awk は stdin とファイルを自動的に走査し、フィルタリング・置換・フィールド処理を1行コマンドで組み合わせて CLI のテキスト作業を処理する
  • /regexp/!/regexp/ は現在の入力行を検査する短縮構文であり、条件が真ならデフォルトで $0 が出力される
  • sub は最初の一致だけを、gsub はすべての一致を置換し、対象を省略すると 現在の入力行 $0 に置換を適用する
  • 空白区切りのフィールド分割と $N, NF, $NF により、awk は特定の列を取り出したり条件を付けて処理する フィールドベースの作業 に向いている
  • cond{action} の断片を続けて書き、BEGIN{}END{} を加えることで、単純なフィルタを超えて文字列・数値・連想配列まで含む小さなプログラムを作れる

awk の実行方式

  • awkgrepsed のようにテキストを絞り込めるうえ、より複雑な処理のためのプログラミング機能も提供する
  • 入力は stdin またはファイルから受け取れ、基本的に行単位で走査しながら条件と動作を適用する
  • 入力行全体には特殊変数 $0 でアクセスする
    • 正確な用語は入力 record だが、この章では行単位の説明として扱う

フィルタリングとデフォルト出力

  • 正規表現は /.../ 形式で書き、文字列が正規表現に一致するかを確認するときは string ~ /regexp/ 構文を使う
  • 逆条件は string !~ /regexp/ で表す
  • 検査する文字列を省略すると、対象は $0 になる
  • 条件だけがあり動作がなければ、条件が真のとき $0 が自動的に出力される
    • awk '/regexp/'awk '$0 ~ /regexp/{print $0}' の短縮形
    • awk '!/regexp/'awk '$0 !~ /regexp/{print $0}' の短縮形
  • 例では at を含む行だけを出力したり、e を含まない行だけを出力したりして、基本的なフィルタリングの流れを示す

1 を使う慣用的な出力

  • 条件式では 0 ではない数値空でない文字列 は真と評価される
  • awk '1' はすべての入力行に対して常に真となる条件を与える慣用表現である
  • 動作がないとき、真の条件は $0 の出力につながるため、awk '1' は入力全体をそのまま出力する
  • 結果は awk '{print $0}' または単純な cat と同じである

置換: subgsub

  • awk は検索と置換のために subgsub 関数を提供する
  • sub(/:/, "-") は各入力行で最初の : だけを - に置き換える
  • gsub(/:/, "-") は各入力行のすべての :- に置き換える
  • 2つの関数は第1引数に一致させる正規表現、第2引数に置換文字列を取る
  • 入力文字列を別途指定しなければ、デフォルトの対象は $0 である
  • 置換が成功すると、対象入力が変更される
  • 置換ブロックの後ろにある 1 はブロック外の条件式として解釈され、変更された $0 を再び出力する
    • awk '{sub(/:/, "-")} 1'awk '{sub(/:/, "-"); print $0}' と同じ結果
    • print だけを書いてもデフォルトの出力対象は $0 である

grep, sed, awk を選ぶ基準

  • 単純な行フィルタリングでは、その用途に最適化された grepsedawk より速いことがある
  • 置換作業では sedawk より速いことがある
  • ツール間の機能は常に 1:1 で対応するわけではない
    • grep -osedawk で実装するには、より多くの段階が必要になる
    • 再帰検索は grep だけが提供する
  • 関連する議論は unix.stackexchange: When to use grep, sed, awk, perl, etc で読める

フィールドベースの処理

  • awk は特に フィールドベースの処理 によく使われる
  • デフォルトでは入力行を空白で分割し、各フィールドには $N でアクセスする
    • $2 は2番目のフィールド
    • $NF は最後のフィールド
    • NF は現在の入力行にあるフィールド総数
  • table.txt の例は、列単位の処理を簡潔に示している
    • 各行の2番目のフィールドを出力
    • 最後のフィールドが負数の行だけを出力
    • 1番目のフィールドでだけ bB に変更
  • 例題ファイルは example_files ディレクトリにある

1行コマンドの構造

  • 一般的な awk の1行コマンドは次の形式を取る
awk 'cond1{action1} cond2{action2} ... condN{actionN}'
  • 条件がなければ動作は常に実行される
  • 動作がなければ条件が真のとき $0 が出力される
  • ブロック内では複数の文をセミコロン ; で区切れる
  • 複数のブロックを続けて書けば、多くの1行コマンドで明示的な if なしに条件ごとの動作を表現できる
    • awk '$NF<0' table.txt は最後のフィールドが負数の行だけを出力する
  • BEGIN{} は入力を読む前に実行される
  • END{} はすべての入力処理が終わった後に実行される
  • 演算子と条件式の詳細は gawk manual: Operators, gawk manual: Truth Values and Conditions を参照

文字列と数値

  • awk は CLI で短い解法を作りやすいよう、値の型を文脈に応じて判断する
  • 文字列リテラルは ダブルクォート で囲んで書く
  • 数値には整数、浮動小数点、科学表記法を使える
  • BEGIN{} は外部入力なしで awk プログラムを実行するときにも使える
  • 変数には数値と文字列を保存できる
    • 例: a=5; b=2.5; print a+b
    • 文字列を並べて書くと連結される
  • 初期化されていない変数は、文字列の文脈では空文字列、数値の文脈では 0 のように振る舞う
  • 文字列を数値式で使うと数値へ強制変換される
    • 文字列が有効な数値で始まらない場合は 0 として扱われる
    • 先頭の空白は無視される
  • 数値に文字列を連結すると、数値は文字列に変換される
  • 詳細は gawk manual: Constant Expressionsgawk manual: How awk Converts Between Strings and Numbers を参照

配列

  • awk の配列は 連想配列 であり、キーと値のペアとして動作する
  • キーは数値にも文字列にもできるが、数値キーは内部的に文字列へ変換される
  • 多次元配列も使用できる
  • 例では student["id"], student["name"] のような文字列キーで値を保存し、アクセスしている
  • キーの存在有無は "id" in student という形で確認する
  • 詳細は gawk manual: Arrays を参照

練習と次のステップ

  • この章では awk 構文の基本的な短縮記法、フィルタリング、置換、フィールド処理、型変換、配列を簡単に紹介する
  • 次の章では正規表現を扱い、この章で紹介した機能も引き続き例で使われる
  • フィールド番号を変えてみたり、負数・浮動小数点のフィールド番号のような別の入力を試したりして、構文に慣れることができる
  • 対話型練習用の TUI アプリは AwkExercises リポジトリからインストールでき、使い方は app_guide.md にある
  • すべての練習問題は Exercises.md にまとまっており、解答は Exercise_solutions.md にある

1件のコメント

 
GN⁺ 2023-08-29
Hacker News の意見
  • awk が好きでかなりよく使っているが、主な用途の一つは 状態を持つ sed のように使うこと
    たとえば、ある行が直前の特定の行の後に出てきたときだけマッチさせる、といった具合で、臨時のリンターを作るときに便利
    最近は、非常に大きなテーブルで問題になり得る CREATE INDEXCONCURRENTLY なしで入っているマイグレーションファイルを探すチェックを作ったが、SQL 文は複数行にまたがることがあるので単純なマッチングは難しかった
    awk なら複数行にわたって「create 文の中にいる」「インデックスを作成している」といった 状態追跡 ができるので、ざっくり組んだスクリプトが1年ほど問題なく動いている

    • いつか awk を学ばなければと思いつつ、その間に sed のより深い 状態ベースの機能 を身につけた
      前の行が特定のパターンだったときだけ現在の行を出力するには、sed -ne 'x' -e '/PREV/ {x; /CURR/ p; x}' のようにできる
      例: echo -e "PREV\nCURR\nCURR\nCURR\nPREV\nRED" | sed -ne 'x' -e '/PREV/ {x; /CURR/ p; x}'CURR だけを出力する
      これは sed の hold buffer を使う方法で、-n でデフォルト出力を抑えたうえで、p で必要な行だけ出力する
      x は現在の行と hold buffer を交換し、/PREV/ { ... } ブロック内で再び交換したあと、現在の行が CURR を含むときだけ出力する
      最後の x は、マッチが重なる場合に備えて元に戻すためのもの
      もちろん awk スクリプトのほうがずっと単純で直接的である可能性が高いが、sed でもこうできる
      これを学ぶ時間は awk に使ったほうがよかったかもしれないが、https://www.grymoire.com/Unix/Sed.html のチュートリアルが非常に良く、sed について知っていることのほとんどをここで学んだ
    • awk で SQL の状態を追跡した例を共有してもらえるとうれしい
  • このスレッドを読んでいる人なら、2週間前の “Ask HN: Share a shell script you like” にも興味がありそう
    コメントは78件で、期待したほど盛り上がらなかったが参考になる: https://news.ycombinator.com/item?id=37112991

  • Lisp などと同じく Awk にも少し関心を持ち続けていて、2022年に cppawk を作った: https://www.kylheku.com/cgit/cppawk/about/
    cppawk は Awk に プリプロセス機能 を拡張してくれる
    複数の節をサポートするループマクロがあり、節を組み合わせて並列反復やデカルト積の反復を作ることができ、ユーザー拡張も可能
    簡単なマクロを5つ書けば新しい節を定義できる
    Awk を使うなら役に立つかもしれず、複数の man page で文書化されていて、gawk と mawk で動く単体テストも備えている

  • 古いシステム管理者の感覚が出ているだけかもしれないが、同じことを単に Perl で書くのに比べて awk の利点が何なのかよく分からない
    ジュニアのシステム管理者が作ったひどいシェルスクリプトをたくさん見てきて、そのたびに「テキスト処理の部分は Perl ならずっときれいだったのに」と思っていた

    • Perl を常に使う環境なら、その意見はかなり妥当
      私にとって awk は、数か月触っていなくても10分あれば感覚を取り戻せる数少ない言語の一つ
      直感的な面があり、一般的なコマンドラインツールとも自然によく合う
    • スクリプト用として Awk と Perl を比べるなら、Perl や Python を好む
      ただしこの記事は一時的な用途の短い ワンライナー についてのもので、その場合は Perl より sed/awk のほうがよい
      すでに Perl を知っているなら、あえて道具をさらに学ばず、そのまま Perl を使い続けてもよい
    • Awk の強みは、無料で付いてくる 読み取りループ、フィールド分割、パターン/条件マッチングモデル
      言語自体は「そこそこ」だが、十分使い物になる
      Perl でも当然すべてできるが、awk が無料でやってくれるボイラープレートを自分で書く必要がある
      Perl 言語の利点は、私にとって awk を捨てるほど大きくなく、awk を「スクリプティング」よりもデータ処理や一回限りのファイル加工に主に使っているので、Perl の深さが恋しくはならない
      より深い機能が必要なら別のところへ行く
    • 技術的な利点は特になく、Perl の機能は意図的に awk の 上位集合 に近い
      今の世代が Perl を学んでいないため、ストリーム処理という考え方を awk で新たに発見しているように見える
      awk は1970年代後半の基準では優れたアイデアで、模倣する価値のある本物の革新だった
      その後 Perl がそれを模倣し、さらに上回ったが忘れられたため、再び awk が発見されている様子は少し気恥ずかしい
    • 皮肉ではなく、なぜ Perl が Python よりスクリプトに向いているのか気になる
  • gawk であまり知られていない点の一つは、たいてい有用な 拡張機能 が一緒に提供されていること
    readdir()ord()chr()gettimeofday()sleep() のような機能にアクセスできる
    https://www.gnu.org/software/gawk/manual/html_node/Extension-Samples.html

  • awk のワンライナーは本当に強力
    難しい問いは、より複雑な awk プログラミングに投資する価値があるのかという点
    処理作業が複雑なロジックを要求する場合、awk はそれを提供してくれるが、初期のコンピューティング時代のような短く難解な形で提供する
    とはいえ現代的な代替を選ぶとそれはそれで負担があり、特に pandas のように直感的ではない場合があり、性能問題まで生じることもある

    • 私にとって大きな問題は ライブラリ
      個人用の共通関数ファイルでさえ十分にサポートされている感じがせず、サードパーティライブラリを取り込む方法も適切なものがなさそう
      ヘルパー関数が必要になり、複数行に分け始めると、たいてい Python に移る
      するとプログラムが2〜3倍に膨らんで、ため息が出る
      Ruby は awk の代替として優れているが、職場の同僚が Ruby を知らなければ保守を期待しにくい
    • ChatGPT 以前は、難解な awk/sed のワンライナーは好きではなかった
      最近はそうした読みにくいコマンドを AI に貼り付けると、段階的にかなりうまく説明してくれるので、悪くなくなった
      それでも単体テスト可能性のために Python 数行のほうを好むが、素早いデータ加工作業ではそもそも単体テストが不要な場合もある
  • 新版の “CLI text processing with GNU awk” 電子書籍を公開
    GNU awk コマンドを初級から上級まで、数百の例と練習問題で段階的に学べる。フィールド処理、フィルタリング、複数ファイル処理、複数レコードに依存する解法、ファイル間のレコード・フィールド比較、入力順を保った重複検出、正規表現などを深く扱う
    PDF/EPUB: https://learnbyexample.gumroad.com/l/gnu_awk(2023年8月31日まで無料)
    Web版: https://learnbyexample.github.io/learn_gnuawk/
    Markdown ソースと例ファイル: https://github.com/learnbyexample/learn_gnuawk
    練習用の対話型 TUI アプリ: https://github.com/learnbyexample/TUI-apps/blob/main/AwkExercises
    バンドルでは grep、sed、awk、perl、ruby のワンライナー集 Magical one-liners が $5: https://learnbyexample.gumroad.com/l/oneliners/new_awk_release、全13冊の電子書籍バンドルが $12: https://learnbyexample.gumroad.com/l/all-books/new_awk_release
    フィードバックは、誤字、コードのミス、うまく合っていた部分やそうでなかった部分など、何でも大きな助けになる
    以前の議論としては https://news.ycombinator.com/item?id=15549318https://news.ycombinator.com/item?id=22758217 がある

    • Magical one-liners のような資料に実際にお金を払う人がどれくらいいるのか、また「見た後で支払いを選ぶ」モデルを試したことがあるのか気になる
      $5 や $15 を払うだけの有用性はあるかもしれないが、個人用のワンライナーファイルにすでに保存してある内容と変わらないかもしれず、確認するためにお金を払うのはためらわれる
    • 多くの作業を注ぎ込んだことが見て取れて、特に TUI アプリケーション が気に入った
      Web版は8月31日以降も引き続き無料のままなのか気になる
  • 以前 AWK でハートを描く コードゴルフをしたことがある: https://gist.github.com/auselen/906a53b47a7d616b080dbef85eb8f776

  • awk 利用の 99.9% は、連続する空白を捨てながら行を分割する用途
    例: echo "key: value" | awk '{print $1}'
    もっと単純な代替があるとよい

    • https://github.com/c-blake/bu/blob/main/doc/cols.md を検討する価値がある
      Nim 製だが大きな障壁ではないかもしれず、bu/ 内の他のツールにも関心を持てるかもしれない
    • cut でも可能: echo "key: value" | cut -wf 2
      ただし実際により単純かどうかは議論の余地がある
      確認したところ GNU cut には -w がないので BSD 専用
      結局 awk を使うほうがよさそう
    • https://github.com/sstadick/hckhttps://github.com/theryangeary/choose も確認する価値がある
      どちらも cut/awk の代替で、正規表現ベースの区切りにも対応しているが、先頭/末尾の空白除去は記憶ではできない
      自作の https://github.com/learnbyexample/regexp-cutawk を使って正規表現ベースの分割、負のインデックスなどを提供する cut 風ツールで、awk のデフォルト動作のおかげで先頭/末尾の空白も処理する
    • おそらく echo "key: value" | awk '{print $2}' と言いたかったのだと思う
  • 以前、bash から移植した diff2html スクリプトを awk で書いたことがあるが、当然の理由でずっと速かった
    bash スクリプトより awk のほうがはるかに読みやすく、一晩で言語を学び、デバッグし、バグを理解して修正できた
    慣用的な awk の書き方なのかは分からないが、本当に良い言語だと感じた
    https://github.com/berry-thawson/diff2html/blob/master/diff2html.sh