著者:今泉光之 (Twitter: @bsdhack)
usp Tukubai は ユニバーサル・シェル・プログラミング研究所が開発・提供している一連のコマンド群で、商用製品として様々な分野の数多くのプロダクトとして利用されているソフトウェア製品です。
usp Tukubai はコマンド群として提供されており、通常の UNIX コマンドの様に複数のコマンドを繋ぎ合わせて使うことでシステムを構築することができます。
usp Tukubai で提供されるコマンドを使いこなすと、端末だけで集計処理ができたり、ファイルだけでDBを組んだり、ウェブシステムを組んだりと、シェルあるいはシェルスクリプトでできることが飛躍的に増えます。
特に製品版として開発されている usp Tukubaiはソースコードのレベルからチューニングされているので、非常に高速に動作するので大規模なエンタープライズシステムでの使用にも十分に対応できる性能となっています。
ユニケージ開発手法はユニバーサル・シェル・プログラミング研究所によって開発された、短期間低コストで企業システムを構築するための開発手法です。
小さなコマンドを組み合わせて問題を解決することを目的とした開発手法やコマンドに関する研究開発に取り組んでいるユニバーサル・シェル・プログラミング研究所が、そうした研究開発の成果をまとめた開発手法がユニケージ開発手法です。
本稿で扱う usp Tukubai はユニケージ開発手法を実践するための手段として開発されたコマンド群です。
ユニケージ開発手法に基づいて usp Tukubai コマンドを利用することで、数十行のプログラムでアプリケーションを記述できたり、データベースを使わずにテキストファイルのみでデータ処理が可能となります。実際にユニケージ開発手法に基づくシステム開発は小売業を中心に基幹業務システム、情報分析システム、データバッチ処理、高速検索システム、勘定系システムなど様々な分野で活用されていて、特にシステムの内製化を進める企業において採用されています。
usp Tukubaiを試用目的で利用できるようにしたバージョンで、商用利用はできません。
Windows、Linux、OS Xでそれぞれ動作するので usp Tukubai による業務システムの開発を実際に体験できます。
期間限定のライセンスで、ダウンロードした月から 6ヶ月後の月末まで使用可能となっているので、十分に usp Tukubai の機能を調べることができると思います。
usp Tukubai コマンドのうち、利用頻度の高いコマンドを厳選して Python により実装しなおし、MIT ライセンスの下でオープンソースソフトウェアとして提供されているのが Open usp Tukubaiです。
製品版の usp Tukubai と比較すると一部実装されていないコマンドがあったり、性能にある程度の差は出てしまいますが、それでも usp Tukubai の便利な機能を知るためには十分に有用だと思います。
本稿では、Open usp Tukubai についての解説となります。
Open usp Tukubai は Python により実装されているので Python が動作する環境であればインストール可能です。
公式の配布サイトからソースを取得して展開し make install するだけで Open usp Tukubai は利用可能となります。
OS X 11.6 (OS X El Capitan) では以下のコマンドでインストールできます。
$ curl ‘https://uec.usp-lab.com/TUKUBAI/DOWNLOAD/open-usp-tukubai-2014061402.tar.bz2’ -o open-usp-tukubai-2014061402.tar.bz2 $ tar zf open-usp-tukubai-2014061402.tar.bz2 $ cd open-usp-tukubai-2014061402 $ sudo make install
ここでは Open usp Tukubai のコマンドを解説します。
ただし以下のように Open usp Tukubai だけでも 55 コマンドも提供されており、その全てを解説するのは無理なので、
特に有用で興味深いコマンドを厳選して紹介します。
1 2 |
$ ls -1 /opt/local/tukubai/bin | wc -l 55 |
また、シェルスクリプトマガジンに掲載の記事なので、シェルスクリプトで Open usp Tukubai コマンドを実装してみたいと思います。今回は、calclock、comma、getlast、ketaの4つのコマンドを紹介し、シェルスクリプトでの再現を試みます。
但し今回は紙面と時間の都合で一部の機能やオプションなど実装できていない部分も多いです。
また、動作の把握とメイン処理の実装のみに絞ったので、エラー処理などはまったく実施していません。
そのために実際の業務などには殆ど利用できない実験的なスクリプトとなっています。
入力データ(ファイルや標準入力)の指定されたフィールドのデータを Epoch *1からの秒数に変換してフィールドの隣に出力します。
ファイル名に – が指定された場合は一般的な UNIX の流儀にのっとり標準入力を入力として利用します。
入力データは年月日(yyyymmdd形式)、時間(HHMMSS形式)、年月日時間(yyyymmddHHMMSS)を自動で検出します。
例えば以下の様なフォーマットのシステムへの接続ログから接続時間を取得する処理などを簡単に作ることができます。
*1 Epoch とは UNIX で一般的に利用されている時刻表現で、協定世界時(UTC)での1970年1月1日午前0時0分0秒の時刻からの形式的な経過秒数です。
1 2 3 4 5 6 7 8 |
$ cat << EOF | calclock 3 5 - | awk '{print ($6-$3)/60}' 001 foo 20170123093255 192.168.0.1 20170123104932 ……… 002 bar 20170123094528 10.8.2.132 20170123162827 ……… 002 baz 20170123100224 172.16.8.47 20170123144911 ……… EOF 194.617 1138.32 744.783 |
calclock 唯一のオプションは -r で、これを指定すると入力データは Epoch からの秒数として解釈され出力は年月日時間(yyyymmddHHMMSS)となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#!/bin/sh # 以降の処理で set を利用して位置パラメタを変更するので引数を fields に退避 fields="$*" while read line do # 引数で指定されたフィールド毎に処理 for i in ${fields} do # 入力された行を位置パラメータにセット set -- ${line} # 入力されたフィールドのサイズから date(1) の書式を取得 case $(eval echo '${#'${i}'}') in 6 ) format="%H%M%S";; 8 ) format="%Y%m%d";; 14) format="%Y%m%d%H%M%S";; esac # date(1) で Epoch からの秒を取得して変数 vX にセット eval "v${i}=$(date -j -f "${format}" $(echo $(eval echo '${'${i}'}')) '+%s')" done # 行出力処理 i=1 while [ ${i} -le $# ] do # 入力データを出力 printf "%s " $(eval echo '${'${i}'}') # フィールドに対して秒数がある場合は出力 v=$(eval echo '{v'${i}'}') test -n "${v}" && printf "%s " ${v} i=$((i + 1)) done echo done |
シェル組み込みのコマンドと date(1) を駆使して、何とか似た機能は実現できました。
引数で指定されたフィールドを変数 fields に格納しておき、read(1) で入力した行データ毎に fields に格納されたフィールドから date(1) で秒数を取得し、取得できた秒を変数 v数字 (数字はフィールド番号) に格納しています。指定された全フィールドの処理が終わると、入力データと取得した秒数を出力します。入力データの指定されたフィールド形式などのチェックしていないので、入力データに不正な値があると正しく動作しません。また、今回は -r オプションに関して実装しておらず、入力データも標準入力のみとなっています。
入力データ(ファイルや標準入力)の指定されたフィールドのデータを「カンマ区切り」にします。帳票データなどの最終的な出力に適用すると、金額などの可読性が向上します。
指定したフィールドに数字以外のデータが入力された場合はエラーとなります。
1 2 |
$ echo "12345 67890" | comma 1 2 12,345 67,890 |
+hオプションはヘッダ行をスキップするための指定で、指定された行(デフォルトは1行)をヘッダ行としてスキップします。
1 2 3 4 5 6 7 8 |
$ cat << EOF | comma +h 1 2 入金 出金 123456 654321 987654 567890 EOF 入金 出金 123,456 654,321 987,654 567,890 |
d オプションは引数で指定した文字列をカンマ区切りにします。
1 2 |
$ comma -d 1234567 1,234,567 |
-4 オプションは 4 桁区切りにします。
1 2 |
$ comma -d4 123456789 1,2345,6789 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
#!/bin/sh # 区切り数を取得 if [ "$1" = "-4" ] then digit=4 shift else digit=3 fi # 以降の処理で set を利用して位置パラメタを変更するので引数を fields に退避 fields="$*" comma(){ # 区切り桁数を取得 awk -v "digit=${digit}" ' { l = length; i = 1 s = "" # 文字列を最後から処理 while(l > digit){ s = "," substr($0, l-digit*i+1, digit) s l -= digit } if(l) s = substr($0, 0, l) s print s }' } while read line do for i in ${fields} do set -- ${line} eval "v${i}=$(echo $(eval echo '${'${i}'}') | comma)" done i=1 while [ ${i} -le $# ] do v=$(eval echo '{v'${i}'}') if [ -n "${v}" ] then printf "%s " {v} else printf "%s " $(eval echo '${'${i}'}') fi i=$((i + 1)) done echo done |
メインのロジックは awk(1) を利用することで何とか実装できました。
awk(1) の substr 関数を利用して文字列の後ろから指定された桁数で文字列を切り出し、切り出した文字列の先頭に , を付けることでカンマ区切りを実現しています。
今回は -d オプションに関して実装しておらず、入力データも標準入力のみとなっています。また Open usp Tukubai の comma では桁区切りするフィールドのデータをチェックしていて、データが数字以外の場合はエラーメッセージを出力していますが、こちらのスクリプトではその様なエラー処理をしていません。
ちなみに 3 桁区切りだけでよければ printf(1) の %’ を利用することでも殆どの場合実現可能ですが、printf(1) の %’ は現時点では非標準なので全ての環境で利用できる保証はなく、また、区切りを 4桁にする機能は対応していないので、独自に実装する必要があります。
入力データ(ファイルや標準出力)の指定されたフィールドの値が最後に出現した行を出力します。
1 2 3 4 5 6 7 8 9 10 11 |
$ cat << EOF | getlast 1 2 - 0000007 セロリ 20060201 117 0000007 セロリ 20060203 221 0000017 練馬大根 20060201 31 0000017 練馬大根 20060203 514 0000021 温州みかん 20060201 90 0000021 温州みかん 20060203 573 EOF 0000007 セロリ 20060203 221 0000017 練馬大根 20060203 514 0000021 温州みかん 20060203 573 |
同一のフィールドを指定することで単一のキーフィールドの指定も可能です。
以下の例だと第1フィールドの値のみで抽出しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ cat << EOF | getlast 1 1 - 0000007 セロリ1 20060201 117 0000007 セロリ2 20060203 221 0000017 練馬大根1 20060201 31 0000017 練馬大根2 20060203 514 0000021 温州みかん2 20060201 90 0000021 温州みかん1 20060203 573 0000025 プリンスメロン1 20060201 129 0000025 プリンスメロン1 20060203 391 EOF 0000007 セロリ2 20060203 221 0000017 練馬大根2 20060203 514 0000021 温州みかん1 20060203 573 0000025 プリンスメロン1 20060203 391 |
+ng オプションは同じ値を持つ最後の行以外を出力します。
1 2 3 4 5 6 7 8 9 10 11 12 |
#!/bin/sh awk -v "f1=$1" -v "f2=$2" '{ # 指定されたフィールドの値をキーとしたハッシュに格納 # 出力順を保持するために行データに行番号を追加 data[$f1,$f2] = NR "\t" $0 } END{ for(i in data) print data[i] }' | # 行番号でソートし行番号を削除 sort -k 1 | cut -f 2- |
メインのロジックは awk(1) のハッシュ (連想配列) を利用することで実装しました。
指定されたフィールドの値をそのまま連結してハッシュのキーとして利用することで、最新の行内容がハッシュに格納されます。
awk(1) の仕様としてハッシュの順番はランダムになりますので、
順番を維持するためにハッシュに格納する行データに行番号を付与して、出力したデータを sort(1) を利用して行番号でソートして正しい順番に並べ替えた後で、cut(1) で追加した行番号を削除して出力しています。
メインのロジックは awk(1) の機能をそのまま利用することで実現できたので、今回実装したコマンドの中では一番短く単純なスクリプトになっています。
+ng オプションは実装していません。
入力データ(ファイルや標準出力)の指定されたフィールドの桁数を揃えて、右詰で出力します。
表示するための最大幅は自動で計算されます。
1 2 3 4 5 6 7 8 9 10 11 12 |
$ cat << EOF | keta - 01 埼玉県 01 さいたま市 91 59 20 76 54 01 埼玉県 02 川越市 46 39 8 5 21 02 東京都 04 新宿区 30 50 71 36 30 02 東京都 06 港区 58 71 20 10 6 04 神奈川県 13 横浜市 92 56 83 96 75 EOF 01 埼玉県 01 さいたま市 91 59 20 76 54 01 埼玉県 02 川越市 46 39 8 5 21 02 東京都 04 新宿区 30 50 71 36 30 02 東京都 06 港区 58 71 20 10 6 04 神奈川県 13 横浜市 92 56 83 96 75 |
— オプションは左詰で出力されます。
1 2 3 4 5 6 7 8 9 10 11 12 |
$ cat << EOF | keta -- - 01 埼玉県 01 さいたま市 91 59 20 76 54 01 埼玉県 02 川越市 46 39 8 5 21 02 東京都 04 新宿区 30 50 71 36 30 02 東京都 06 港区 58 71 20 10 6 04 神奈川県 13 横浜市 92 56 83 96 75 EOF 01 埼玉県 01 さいたま市 91 59 20 76 54 01 埼玉県 02 川越市 46 39 8 5 21 02 東京都 04 新宿区 30 50 71 36 30 02 東京都 06 港区 58 71 20 10 6 04 神奈川県 13 横浜市 92 56 83 96 75 |
フィールド毎に出力する桁数を数字で指定することができ、指定する数字に – を付けることで左詰を指定することもできます。
出力する桁数は半角文字の表示幅を 1 としているので全角文字の場合は 1 文字を 2 とする必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#!/bin/sh awk '{ # 入力データの最大値を取得しつつフィールド毎の値を data 配列に格納 for(i=1; i<=NF; i++){ if(max[i] < length($i)) max[i] = length($i) data[NR,i] = $i } } END{ for(i=1; i<=NR; i++){ for(j=1; j<=NF; j++){ # printf の * を利用して出力幅を指定 printf("%*s ", max[j], data[i,j]) } printf("\n") } }' |
基本的な機能は全て awk(1) の組み込み関数で実装しました。
入力データは行番号とフィールド番号をキーとしたハッシュ dataに格納し、同時にフィールドの文字列長の最大値を別のハッシュ max に格納しています。
出力は、 awk(1) に組み込みの printf の %*s 指示子を利用することで出力幅を指定しています。
マルチバイトの文字列には対応していないので、入力データにマルチバイト文字があると正しく出力できません。
非英語圏でマルチバイト文字が正しく扱えないことは致命的ですので、業務には利用できません。
UTF-8 では、マルチバイト文字は多くの場合 3 バイトかそれ以上のバイト数を必要としていますが、OS X 標準の awk(1) の lengthコマンドは文字列のバイト数を取得しますので、例えば length(“あ”) は 3 となります。
Linux で標準的に利用されている GNU awk の場合はマルチバイト文字に対応していますので、マルチバイト文字を含む文字列の長さは正しい文字数を取得することができ、length(“あ”) は 1 となります。
しかしマルチバイト文字は 2 バイト相当の表示幅となりますので、どちらの場合でもマルチバイト文字を含む文字列の長さと表示幅は合致しません。
そのためにマルチバイト文字を含む文字列を表示する幅を正しく制御するのは非常に困難となります。
-v オプションは実装していません。
5/25発売のシェルマガvol.48では、さらなるTukubaiコマンドに挑みます
Web版シェルマガにも公開しますので、どうぞお楽しみに!
本記事掲載のシェルスクリプトマガジンvol.47は以下リンク先でご購入できます。
USP研究所 通販サイトでは、個人用uspTukubaiのご購入も可能です。