列番号を列名から簡単に求める

たとえばテキストファイルの5列目を抜き出したければawk '{print $5}' txtのようにprint $列番号と書く。しかし1行目がヘッダー行だとして、ヘッダーの列名を指定して抜き出す方法はないため、列名から列番号を求める必要がある。

前から5列目くらいであれば目で見てすぐに数えられるが、8列目くらいからは正確に数えるのもめんどくさくなってくる。しかしxargs -n1sedの行数を表示する=を組み合わせると簡単に求めることができる。

このようなファイルがあるとする。

$ cat txt
x_col y_col z_col
x1 y1 z1
x2 y2 z2
x3 y3 z3

1行目を取り出して、それにxargs -n1を実行すると、行を列に変換できる。

$ cat txt | head -1 | xargs -n1
x_col
y_col
z_col

これにsedの行数を表示する=を組み合わせる。ここではy_colという列名の列番号を求めたいとする。

$ cat txt | head -1 | xargs -n1 | sed -n "/^y_col$/="
2

2列目だということがわかったので、print $列番号print $2と書けばよい。

$ awk '{print $2}' txt
y_col
y1
y2
y3

ワンライナーで

列番号を求めてから改めてawkで列を抜き出しているので、ワンライナーで一発で処理する方法を考えたい。

colという変数に列番号を求めるコマンドの結果を代入し、awkに-vオプションで渡してあげるとワンライナーでできる。

$ awk -vcol=$(head -n1 txt | xargs -n1 | sed -n "/^y_col$/=") \
    '{print $col}' txt
y_col
y1
y2
y3

head -n1awk自体がtxtを読んでいるので、txtに二回アクセスしている部分が若干気になるが、片方はhead -n1なので性能上問題なく使用できる。

ファイルではなくコマンドの実行結果を処理したい場合

ファイルではなくコマンドの実行結果を処理したい場合、先ほどのワンライナーの「二回アクセスしている部分」が問題になってくる。

コマンドの実行結果を一旦ファイルに書いておけばいいが、中間ファイルを作らずに実行結果をそのままパイプで繋いで処理をしたければ、実行結果に二回アクセスする方法を工夫しなければならない。

実行結果を変数に格納する

簡単に思いつくのは、グループコマンド(group command) 内でコマンドの実行結果を変数に格納して、変数に二回アクセスする方法。

$ cat txt | {
        d=$(cat -);
        echo "$d" | awk -vcol=$(echo "$d" | head -n1 | xargs -n1 | sed -n "/^y_col$/=") \
            '{print $col}';
    }
y_col
y1
y2
y3

しかし、コマンドの実行結果が大きいと、変数に一旦すべて格納しなければならないのはメモリの無駄。

readコマンドで1行だけ読み出す

おすすめなのがreadコマンドで1行だけ読み出す方法。

$ cat txt | {
        read header;
        col=$(echo "$header" | xargs -n1 | sed -n "/^y_col$/=");
        awk -vcol="$col" '{print $col}';
    }
y1
y2
y3

readでコマンドの実行結果を1行だけ読みだし、headerという変数に格納している。col変数には、header変数に対してxargs -n1による行列の入れ替えとsedの行数表示=を使って、y_colの列番号を代入する。あとはawkを実行すると、readで読み込んでいない残りの部分をawkが読み込んでくれる。

ちなみに、ヘッダーが出ていないが、出したければecho y_col;awkの前で実行すればいいだけ。

その他

環境

  • CentOS 7.7
  • Gnu bash 4.2.46

関連

sed=については wc -lでは文末に改行がないファイルの行数を正しく数えられない でも使用した。

グループコマンド(group command) とreadについてはps -ef | grep でヘッダを出す方法でも同様の手法が使える。