usp Tukubai

無い物は作れ Tukubai流コマンド自作文化 : 第3章 道具を夢見たコマンドたち

ユニバーサル・シェル・プログラミング研究所は、これまで2,000個にもおよぶコマンドを作っている。多い日は1日で10個作ったこともあった。しかしそれら大半のコマンドは淘汰されていった。

こうした歴史があるからこそ、道具というものの本質を考えることになり、そしてTukubaiの作法を確立するに至っている。

そこでこの章では、そんな道具を夢見て生み出されながら淘汰されていったコマンドや、これから道具として活躍する可能性を秘めたコマンドなど、いくつか紹介する。

yuniqコマンド

uniq(1)コマンドの横方向版「横uniq」ということで登場したコマンドだ。その名のとおり、横方向にuniqが実施される。

スペース区切りで、同じ綴りの文字列が連続してるとそれを1つに集約する。ただし、連続せずに出現したものに関しては集約しない。このあたりの仕様はuniq(1)コマンドと同様だ。

$ echo "aa bb bb bb bb bb1 cc" | yuniq
aa bb bb1 cc
$ echo "aa bb bb bb cc bb" | yuniq
aa bb cc bb
$ 

リスト2がyuniqの初期ソースコードだ。

リスト2. yuniq.c(横ユニーク) : 横方法にuniq処理を行うコマンドのソースファイル

/*                                           */
/* yuniq.c >>> C言語版YUNIQ         */
/*                                           */
/* Usage : yuniq < infile                 */
/*                                           */
/* Written by N.Tounaka (usb-lab)            */

#include <stdio.h>
#include <string.h>

#define BUFMAX 1024
char RECORD[BUFMAX],OUTBUF[BUFMAX];

void trim();
char *edit();

main(int argc,char **argv)
{
FILE *fp;

    /* 入力ファイルの取得 */
    if(1 != argc)   {
      if(NULL == (fp = fopen(argv[1],"r"))) {
        fprintf(stderr,"Error : %s ファイルがオープンできません.\n",argv[1]);
        exit(1);
      }
    }
    else fp = stdin;

    /* 各レコード処理 */
    while(NULL != fgets(RECORD,BUFMAX,fp))  {
      trim(RECORD);      /* 要BOF対策! */
          strcpy(OUTBUF,edit(RECORD));
      printf("%s\n",OUTBUF);
    }

    /* 読み取りエラーチェック */
    if(!feof(fp) || ferror(fp)) {
      fprintf(stderr,"Error : 入力ファイル読み取りエラー.\n");
      exit(1);
    }

    /* 終了 */
    exit(0);
}


/* レコードのゴミを取る */
void trim(char *buf)
{

    /* 行末の改行文字を削除 */
    if('\n' == *(buf+strlen(buf)-1)) *(buf+strlen(buf)-1) = '\0';

    /* 行末の空白を削除 */
    /* 空白だけからなるデータへの対応が必要! */
    while(' ' == *(buf+strlen(buf)-1)) *(buf+strlen(buf)-1) = '\0';

    return;
}


/* yuniq 処理を行う */ 
char *edit(char *buf)
{
char wbuf[BUFMAX],rbuf[BUFMAX]; /* 要BOF対策! */
char old[100],new[100];
char *p,*q,*r;

    /* 作業領域の初期化 */
    p = wbuf; strcpy(p,buf);
    r = rbuf; *r = '\0';
    *old = *new = '\0';

    /* 各フィールドを切り出す */
    while(NULL != (q = strtok(p、" "))) {

      /* 前後のフィールドの受渡し */
          strcpy(old,new);
      strcpy(new,q);

      /* 前のフィールドと異なっていたら連結 */
      if(NULL != strcmp(old,new))   {
        strcat(r,new); strncat(r、" ",1);
      }

      p = NULL;
    }

    /* 最後の空白を削除する */
    *(r+strlen(r)-1) = '\0';

    return r;
} 

yuniqを使わずに横方向uniqを実行するのであれば、Tukubaiコマンドを使えば次のようになる。

$ echo uniqしたい文字列 | tarr | uniq | yarr

単語を一旦縦に並べてソートして、横に並べ直せばいいというわけだ。

横方向uniqは使用頻度がそう高くなかったために、ユニケージエンジニア達もこのコマンドを覚える機会なかった。そのうえ、たまに横方向uniqが必要になった時も tarr→uniq(1)→yarr の組み合わせで事足りてしまったため、いつしか使われなくなってしまった。

existコマンド

名前のとおり、ファイルの有無を返すコマンドだ。標準出力から有無を調べたいファイルを一覧として受け取れるようなっている。

ファイル名を引数で直接指定すればそのファイルの有無を1(=ある)または0(=なし)で返してくれる。一方-displayというオプションを指定し、有無を調べたいファイルリストを標準入力から渡せば、存在しないファイルがフィルタリングされ、存在するファイルのみなって標準出力される。-display2オプションにすると動作が反転する。

$ exist /etc/resolv.conf
1
$ exist /etc/no_such_file
0
$ echo "/etc/resolv.conf"  >  filelist.txt
$ echo "/etc/no_such_file" >> filelist.txt
$ exist -display < filelist.txt
/etc/resolv.conf
$ 

ファイルの有無を確認したいなら、わざわざ新しいコマンド覚えて使わずとも、test(1)または[の-fや-eを使えばよい。結局、このコマンドは使われなくなり、廃れていった。

これまでの論理からいえば、なぜこのようなコマンドを作ったのかという疑問が生じるが、これには理由がある。複数あるファイルの有無確認を、シェルスクリプトや標準のUnixコマンドの範囲で行おうとするとループ構文が必要になる。しかし、これはパイプで処理をこなす合理性を追求するユニケージ開発手法からすると避けたい手段だった。

このためexistコマンドに関しては現在でも必要・不要の議論が起こっており、依然として決着はついていない。

seniqコマンド

由来はselect field and uniqだという。Tukubaiコマンドselfコマンド+uniqコマンドというものだ。

基本的な機能はuniqコマンド同様「行の集約」であるが、前述した動作例の場合、最初の3行は引数で指定された第1、第3フィールドがいずれも一致しているので(最初の行に)集約される。一方、4行目は第3フィールドが一致しておらず、この行だけ集約されずに出力される。

$ cat 週末の予定.txt
先週末 朝方 自宅 部屋を掃除
先週末 午後 自宅 DVD鑑賞
先週末 夜間 自宅 焼肉パーティー
来週末 終日 都内 美術館巡り
$ cat 週末の予定.txt | seniq 1 3
先週末 朝方 自宅 部屋を掃除
来週末 終日 都内 美術館巡り
$ 

リスト3がseniqのソースコードだ※1

seniq : 一部のフィールドだけを見てuniq処理を行うコマンド

#!/bin/csh -ef
#
# seniq >>> 指定フィールドのみでuniq処理を行う。
#        (seniq >> select field and uniq)
#
# Usage : seniq f1 f2 ... < infile > outfile
#
# Written by N.Tounaka (usp-lab)

#プロセスidの取得
set tmp = $$

#作業ディレクトリの設定
set d   = /tmp

#コマンド書式の表示
if($#argv == 0) then
  echo2 "Usage : seniq f1 f2 ... < infile > outfile"
  exit 1
endif

#入力ファイルの同定
if(! -e $argv[$#argv]) then
  cat - > $d/$tmp-01
  set file = $d/$tmp-01
  set dif  = 0
  goto NEXT
endif

set m = 1
while(1)
if(! -e $argv[$m]) then
    @ m ++
else
  set file = "$argv[$m-$#argv]"
  @ dif  = $#argv - $m
  @ dif ++
  break
endif
end

NEXT:
#ファイルの存在チェック
if(! -e $file) then
  echo2 "Error : $file ファイルがありません"
  exit 1
endif

@ n = $#argv - $dif

#セニーク編集をするAWKスクリプト
cat << FIN > $d/$tmp-02
NR==1 { 
  a="$argv[1-$n]"; n=split(a,x," ")
  m=1
  for(i=1;i<=n;i++) {
          if(x[i]~/\//) {
    start=transnumber(substr(x[i],1,index(x[i],"/")-1))
    last =transnumber(substr(x[i],index(x[i],"/")+1))
    for(k=start;k<=last;k++) { y[m++]=k }
    }
    else  y[m++]=transnumber(x[i])
  }
  m--
  ore=nre=\$0; old=new=makerecord()
}

{
  new=makerecord()
  if(old != new)  { print ore; old=new; ore=\$0 }
}

END { if(flg==1) exit; print ore }

function makerecord() {
  s=""; for(i=1;i<=NF;i++)  {
    for(j=1;j<=m;j++) if(i==y[j]) { s=s " " \$i; break }
    }
  return substr(s,2)
}

function transnumber(q) {
  gsub("NF",NF,q)
  pp=index(q,"-")
  if(pp != 0) q=substr(q,1,pp-1)-substr(q,pp+1)
  return q+0
}
FIN

#実際の編集
awk -f $d/$tmp-02 $file

#一時ファイルの除去
#rm $d/$tmp-*

#正常終了
exit 0

次のようなデータがあったとする。

銘柄 日付 時刻 株価
という4フィールドから構成された株式チャート表がある。銘柄・日時毎に取引開始から終了まで1分刻みで株価が記録されている。この表から各銘柄の毎日の始値を抽出してもらいたい。

この時、seniq 1 2とすれば、銘柄・日付毎の始値を抽出できるというわけだ。

このように、ある項目の先頭行だけを取り出してもらいたいという需要はよくある。しかしながら、これを既存Unixコマンドやその簡単な組み合わせで実現することは難しく、そうした経緯で生み出されたコマンドだった。

seniq自体は淘汰されたが、同様のものがgetfirstという名前のTukubaiコマンドとなってリリースされた。名が体を表していないために名前が変えられたということだ。確かにgetfirstという名の方が直感性に優れている。そして、兄弟コマンドとしてgetlastもリリースされた。

一致を確かめたいフィールドの指定方法も、個々に列挙するのではなく範囲指定に変更された。これは前章で述べたsm2の書式の決まり方と同様の理由だ。

sm3コマンド

Tukubaiコマンド一覧を見ると、sm2の次がsm4sm5になっている。sm1やsm3は道具になることができず、消えていった。

sm3は、ソートせずにsum-upを行うコマンドだった。たとえば2012年日本シリーズのスコアがイニング毎に記録されたファイルNipponSeries2012.txtがあったとする。

$ cat NipponSeries2012.txt
第1戦 1回表 日ハム 0
第1戦 1回裏 巨人 0
第1戦 2回表 日ハム 0
第1戦 2回裏 巨人 0
     :
第2戦 1回表 日ハム 0
第2戦 1回裏 巨人 1
     :
     :
第5戦 2回表 巨人 2
第5戦 2回裏 日ハム 1
第5戦 3回表 巨人 3
第5戦 3回裏 日ハム 1
     :
     :
第6戦 9回表 日ハム 0
第6戦 9回裏 巨人 0
$ 

この表から1戦毎のスコア合計を求めたい場合、sm3があれば次のようにして一発で求めることができる。

$ sm3 1 1 3 3 4 4 NipponSeries2012.txt
第1戦 巨人 8
第1戦 日ハム 1
第2戦 巨人 1
第2戦 日ハム 0
     :
第6戦 巨人 4
第6戦 日ハム 3
$
      

現在リリースされているTukubaiコマンドの範囲でやるなら、delfでイニングのフィールドを消し、更に試合番号とチーム名フィールドでソートしてからsm2にかける必要がある。

$ delf 2 NipponSeries2012.txt | sort -k1,2 | sm2 1 2 3 3
第1戦 巨人 8
第1戦 日ハム 1
第2戦 巨人 1
第2戦 日ハム 0
     :
第6戦 巨人 4
第6戦 日ハム 3
$ 

前述した代替例で示したとおり、簡単な記述で他のコマンドに置き換えられるため消えていった。長らくコマンドを作っては消していった経験からいけば、このように単機能でないコマンドは、よほど頻繁に用いられない限り淘汰される傾向にある。

sm1は横方向のsum-upを行うためのものだったという。これは先程のseniqコマンドと同様の運命を辿り、ysum(横サム)コマンドとなって現在に至る。

dayslashコマンド

これは淘汰されたコマンドではなく、将来Tukubaiコマンドへ追加すべき候補として現在有力視されているものだ。8桁の年月日文字列YYYYMMDDを与えると年と月と日の間にスラッシュ/を挿入する。

たとえば第1フィールドに年月日、第2フィールドにその日が誕生日である人の名前が書いてあるファイルbirthdays.txtがあったとする。

これを、第1フィールドをyyyy/mm/ddというフォーマットに直したい旨を示したdayslashコマンドに与えると、年月日の間にスラッシュが入る。

$ cat birthdays.txt
19410809 Alfred_Vaino_Aho
19420101 Brian_Kernighan
19420806 Peter_Jay_Weinberger
$ dayslash yyyy/mm/dd 1 birthdays.txt
1941/08/09 Alfred_Vaino_Aho
1942/01/01 Brian_Kernighan
1942/08/06 Peter_Jay_Weinberger
$ 

YYYYMMDD形式の年月日値にスラッシュを挿入するだけならawk(1)で実装できる。sed(1)でもよい。

$ awk '{print substr($1,1,4) "/" substr($1,5,2) "/" substr($1,7,2)、$2}' birthdays.txt
1941/08/09 Alfred_Vaino_Aho
1942/01/01 Brian_Kernighan
1942/08/06 Peter_Jay_Weinberger
$ sed 's/^\(....\)\(..\)\(..\)/\1\/\2\/\3/' birthdays.txt
1941/08/09 Alfred_Vaino_Aho
1942/01/01 Brian_Kernighan
1942/08/06 Peter_Jay_Weinberger
$ 

それでもこのコマンドが有力視されているのは、このようにして日付フォーマットを変更したいという需要が多く、その度にこのような長いawk(1)やsed(1)の記述を強いられるのが苦痛だからだ。

しかし、そういう思いでこれまでいくつものコマンドが作り出され、消えていった。果たしてこのコマンドは、そんな淘汰の波を乗り越え、将来Tukubaiコマンドに追加されることになるのか、現在でも議論が続けられている。

※1 パーサの実装の不味さなどの理由から、通常csh系のシェルをシェルスクリプトの採用することは推奨されない。ユニバーサル・シェル・プログラミング研究所ではまずは実践してみるという姿勢から、このようにさまざまなスクリプトで開発を進めた時期があったが、現在ではcshを使ったシェルスクリプト作りは実施していないし、推奨もしていない。

USP MAGAZINE 2013 winter「【特集】無いものは作れ Tukubai流コマンド自作文化」より加筆修正後転載

last modified: 2014-01-13 16:01:13