← 一覧へ戻る

choにCSVの列名参照を追加した

CSVの列を名前で選ぶ%nameの記法と、ヘッダーを列番号に結び付ける実装の話

cho は、Lisp風の式で入力行を加工するコマンドラインツールです。通常は空白で区切られた行を列に分けて扱いますが、--csv を付けるとCSVも読み込めます。この記事では、CSVのヘッダー名で列を指定する方法と、その実装を紹介します。

choでは、入力の列を $1、$2 のように番号で参照できます。小さいファイルならこれで十分です。でも、横に長いCSVのヘッダーを左から追って「created_at は何列目だっけ」と数えるのは、かなり面倒です。しかも途中に列が一つ増えたらズレてしまいます。

そこで、ヘッダー付きのCSVでは %created_at と名前で参照できるようにしました。

例えば、次のような users.csv があるとします。

id,account_id,name,email,team,region,plan,status,owner,source,created_at,score
1,101,Alice,alice@example.com,dev,JP,free,active,bob,web,2026-09-01,90

メールアドレスと作成日を取り出す式は、列番号なら $4 と $11、列名なら %email と %created_at です。p は指定した値を空白区切りで出力します。

$ cat users.csv | cho --csv --skip-header '(p $4 $11)'
alice@example.com 2026-09-01

$ cat users.csv | cho --csv '(p %email %created_at)'
alice@example.com 2026-09-01

%name を使うと、choは先頭レコードをヘッダーとして読み、データ行から処理を始めます。--skip-header を付け忘れてヘッダー行を処理してしまう心配もありません。

GNU awkでは5.3から --csv でCSVを読めます。これがあったのでchoにもCSV入力をサポートしたいと思いました。

%name という記法にした理由

列名の前に付ける記号は、Perlのハッシュ変数から連想して % にしました。CSVのヘッダー名から対応する列を引く、という操作がキーから値を引くハッシュに少し似ているためです。

$1       1列目を参照する
%email   ヘッダーが email の列を参照する

列名に空白がある場合は、%"display name"と書けます。

choでは、式の中で列番号を指定する $1、CSVのヘッダー名を指定する %email、-C オプション の引数で列番号を指定する @1 という三つの書き方があります。@1 はシェルに展開されないようにするための記法で、指す列は $1 と同じです。

ヘッダー名は最初に列番号へ解決する

choは最初にプログラムをパースして評価用のASTに変換してから、入力を1レコードずつ処理します。ASTに変換する時点ではCSVのヘッダーをまだ読んでいないため、%email を直接「4列目」にはできません。そこで、式に現れたヘッダー名を Program.header_fields に集め、ASTの式にはその配列の添字を Expr::HeaderField(0) のように持たせます。

入力の処理を始めると、最初のCSVレコードをヘッダーとして読みます。このレコードはデータ行として評価せず、参照した名前が何列目かを調べるために使います。名前と同じ順序で列番号を header_field_indices: Vec<usize> に保存してから、次のレコード以降を処理します。データ行では、式が持つ添字から列番号を引いてフィールドを読むだけです。

1パースしてASTに変換するヘッダーを読む前compiler.rs ↗
%emailASTでは Expr::HeaderField(0)Program.header_fields: Vec<String>["email", "created_at"]
2先頭レコードで列番号を決める一度だけrunner.rs ↗
ヘッダーでは email が4列目header_field_indices: Vec<usize>[4, 11]
3データ行から値を読む各レコードで繰り返すeval.rs ↗
Expr::HeaderField(0)header_field_indices[0] = 4record.field(4)alice@example.com
(p %email %created_at) の例。配列の添字 0 は email に対応し、列番号は先頭の一度だけ調べる。

列名は完全一致で探します。式で参照した名前がヘッダーにない場合や、同じ名前の列が複数ある場合は、データ行を出力する前にエラーにします。例えば email が二列あれば、%email をどちらに結び付けるか決められません。一方、式に登場しない列名が重複していてもエラーにはしません。読まない列の名前が重なっているだけで、必要な列まで取り出せなくなるのは困るからです。

また、%name は --csv と一緒に使う記法です。通常の空白区切り入力ではヘッダー名による参照はできません。

まとめ

choの仕様はできるだけ小さく保ちたかったので、最初はCSVでも $1 のような番号指定で十分かと思っていました。しかし横に長いCSVの列を数えるのはやっぱり面倒で、列名での参照記法を追加しました。

$1の番号指定も残しているので、番号だけで済むときはこれまでどおり書けます。