timeコマンドの結果をファイルへ出力する

timeコマンドの結果をファイルへ出力するやり方を間違えたのでメモ。
ちなみにbash。

やること

a.outを実行したときの出力とそれにかかった時間をlogfileへ出力する

ダメな例

time ./a.out > logfile
time ./a.out | tee logfile

上がまるっとリダイレクトしちゃうケースで、下はteeで標準出力にも表示しながら実行するケース。
散々時間かけて実行したのに、後でlogfileを見てみたらガッカリ。timeの出力だけはlogfileには書き込まれてない。

timeの結果は標準エラー出力

なのでした。
標準エラー出力も標準出力と一緒にlogfileに書き出さないとダメだね。

まだダメな例

というわけで標準エラー出力も併せてリダイレクトするよ

time ./a.out >& logfile
time ./a.out > logfile 2>&1
time ./a.out 2>&1 | tee logfile

しかし、これもダメ。
./a.out >& logfile
./a.out > logfile 2>&1
./a.out 2>&1 | tee logfile
といったtimeコマンド後ろに書いた「a.outコマンドの実行結果をエラー出力も含めてlogfileに書き出す」のtimeを計っちゃうわけ。だからtimeコマンドの結果はlogfileには書き込まれないよ。

OKな例

やりたい事は「time ./a.out」の出力をlogfileに書くんだから…

(time ./a.out) >& logfile
(time ./a.out) > logfile 2&>1
(time ./a.out) 2>&1 | tee logfile

括弧で囲んじゃえー。これでうまく行く。つまりサブシェルで実行するのです。
ちなみに最近のbashは>&じゃなくて&>でも大丈夫なのね。

コマンドをグループ化する{ ;}でもOKみたい。

{ time ./a.out ; } >& logfile
{ time ./a.out ; } > logfile 2&1
{ time ./a.out ; } 2>&1 | tee logfile

{ の後には必ずスペースを入れようね。スペースいれないと、-bash: syntax error near unexpected token `}’ って言われちゃうよ。あとセミコロン;も忘れずに。

/usr/bin/time

ところで、bash組み込みのtimeとは別にGNUのtimeコマンドというのもあるよ。/usr/bin/timeとフルパスで書いてやると実行できる。
これもbash組み込みのtime同様、標準エラー出力に結果を出力するんだけど、サブシェルで実行しなくても、

/usr/bin/time ./a.out >& logfile
/usr/bin/time ./a.out > logfile 2>&1
/usr/bin/time ./a.out 2>&1 | tee logfile

と、括弧つけなくても大丈夫。もちろん括弧つけてもいい。
それに、ちゃんとファイル出力オプション-oがあるよ。

/usr/bin/time -o logfile ./a.out

でもこれだとtimeの結果しか書き込まれないので、追記オプション-aをつけて

/usr/bin/time -a -o logfile ./a.out > logfile

とやればいいっぽい(おそらく「./a.out > logfile」のtimeなんだろうけど)。

bashのtimeより詳細なデータが出るし、出力フォーマットの指定とかいろいろ柔軟に操作できるコマンドなので覚えておくとよいかもしれないね。
詳しくはman timeで。bashのtimeについてはhelp timeで。

改行を置換したい

改行を置換したいときのメモだよ。
改行コードCR+LFからCRを取り除くとかいう話ではなくて、改行そのものを置換したいときのメモ。CRは入ってない前提。
コマンドのバージョンとか動作環境によってはうまく動かないこともあるかもしれない。

【例】改行をカンマ’,'に置換する

A
B
C

というファイルを

A,B,C

のように書き換えたいという例でお話しする。


エディタで

Emacs

M-x replace-string Replace string:

のところでC-q C-jと入力すると改行コードを引用できて、コマンド行が改行される。Enterで決定すると

Replace string with ^J with:,

となる。改行は^Jで表されている。置換先のカンマ’,'を入力してEnterで決定すれば、現在行以下の改行がすべてカンマに置換される。

Vim

:%s/\n//g

コロンを押してコマンドモードにして%でファイル全体を指定。
%の代わりに1,$と書いてもよい。1行目から最後の行までという意味。
もし置換ではなく改行削除して行を連結するだけなら、J(Shift+j)を押しっぱなしってのもひとつの手。

コマンドラインで

エディタを開かずにコマンドラインだけで済ませたいときもある。
filename は改行を含むテキストファイル。
標準出力に表示するだけなので、必要があれば > newfilename とかやって新しいファイル名を指定して出力してね。
元と同じファイル名に吐き出して消しちゃうっていうのはやりがちなミスなので気をつけて。

tr

以上は代表的なエディタ上での置換方法だったけど、コマンドラインで済ませたいときはtrがいちばんラクかも。

tr '\n' ',' < filename
あるいは
tr '\012' ',' < filename

改行を削除したいときはーdオプション。

tr -d '\n' < filename
あるいは
tr -d '\012' < filename

awk

awk -F\n -v ORS=','  '{print}'  filename

-Fで区切り文字指定、 -v ORSで出力の区切り文字指定。

sed

sedは1行ごとに処理するのは得意なんだけど、改行をまたぐ処理はわりと面倒くさい。
たとえば上の例とは逆にカンマを改行に変えるのであれば

sed 's/,/\n/g' filename
あるいは
sed 's/,/\        ←バックスラッシュの後で普通にEnterを押す
> /g' filename

でうまくいく。1行目のバックスラッシュのあとに普通に改行ボタン押せば次の行にプロンプトが出るので、続きを入力すればよい。
ところが例題の処理をやろうとして

sed 's/\n/,/g' filename
あるいは
sed 's/\
> /,/g' filename

と書いてもうまくいかないワケ。
じゃあどうするかというと、Nコマンドというのを使えば次の行も読み込めるので、これを最後までループでまわして全体を読み込んでから置換する。

sed ':loop; N; $!b loop; ;s/\n/,/g' filename
あるいは
sed ':loop; N; $!b loop; ;s/\
> /,/g' filename

ただGNUのsedだとうまくいくけど、BSDのsedだとこのやり方でもうまくいかないかも。
[参考]sed の使い方 4.3 改行をすべて削除

Perl

perl -pe 's/\n/,/g' filename

ruby

ruby -pe 'gsub(/\n/,",")'  filename

必要なときだけ計算する

Xerces-C++でXMLからDOMツリー作ったりしてるんだけれど
最近パフォーマンス気にするようなコード書いてなかったので、非常に基礎的なところでハマってた。
テスト用の短いXMLだと問題なかったんだけど、本番用のXML食わせたらforの中では大したことやってないのに5分以上かかっちゃうわけ。

問題のコード

//...
DOMNodeList* nodelist = document->getElementsByTagName(tagname);
//...
for(int i=0;i< nodelist->getLength();i++){
    //....
}

ここだけ抜き出せばもうわかると思うけど、forの終了条件の中で nodelist->getLength() してる。こいつの計算コストがけっこう高い。
nodelist->getLength()をnodelist->getLength()回呼び出したらそりゃ遅い。
変数の名前考えるのが苦手なので、できるだけ変数宣言しないように書いてたらこうなったってのもある。

次のように書き直すべき

//...
DOMNodeList* nodelist = document->getElementsByTagName(tagname);
//...
for(int i=0,num=nodelist->getLength();i<num;i++){
    //....
}

今回作ってたケースでは、途中でリストのサイズが変わったりしないので最初に1回計算すればそれでよかったわけ。
だからforの初期条件のところで、あらかじめ計算するようにしたよ。
これだけで5分以上かかってたコードが1秒ちょっとで終わったよ。

ちなみに、Xercec-c++(2.8.0)のsrc/xercesc/dom/impl/DOMNodeListImpl.cppには、ご丁寧に

// this implementation is too stupid – needs a cache of some kind.

って書いてあるよ。

CppUnitを使ってみる

CppUnit

JavaのJUnitをC++に移植したのがCppUnitだよ。何そのJUnitって?
SmalltalkのSUnitをもとに作られたのがJUnitだよ。何そのSUnitって?
要は単体テスト(Unit Test)をうまいこと自動化してくれるC++用のフレームワークがCppUnit
XP(Extreme Programming)のテスト駆動開発(TDD)を考えているひとは是非取り入れたいものなんだよ。
よくわからないからまずは使ってみるよ。

インストール

debian環境でCppUnitを使うためにlibcppunitをインストール。

$ sudo aptitude install libcppunit-1.12.1 libcppunit-dev

libcppunit-1.12.1だけじゃなんかダメだったんでlibcppunit-devも。
必要ならlibcppunit-docも入れればいいと思うよ。

ドキュメント

CppUnit Documentation
一応ここにいろいろ書いてある。

Hello world

何はともあれCppUnitでテストしてみる。
An Hello, world program for CppUnit
にあるコードをHelloWorld.cppという名前で作成

//--- Hello, World! for CppUnit

#include <iostream>

#include <cppunit/TestRunner.h>
#include <cppunit/TestResult.h>
#include <cppunit/TestResultCollector.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/BriefTestProgressListener.h>
#include <cppunit/extensions/TestFactoryRegistry.h>

class Test : public CPPUNIT_NS::TestCase
{
  CPPUNIT_TEST_SUITE(Test);
  CPPUNIT_TEST(testHelloWorld);
  CPPUNIT_TEST_SUITE_END();

public:
  void setUp(void) {}
  void tearDown(void) {} 

protected:
  void testHelloWorld(void) { std::cout << "Hello, world!" << std::endl; }
};

CPPUNIT_TEST_SUITE_REGISTRATION(Test);

int main( int ac, char **av )
{
  //--- Create the event manager and test controller
  CPPUNIT_NS::TestResult controller;

  //--- Add a listener that colllects test result
  CPPUNIT_NS::TestResultCollector result;
  controller.addListener( &result );        

  //--- Add a listener that print dots as test run.
  CPPUNIT_NS::BriefTestProgressListener progress;
  controller.addListener( &progress );      

  //--- Add the top suite to the test runner
  CPPUNIT_NS::TestRunner runner;
  runner.addTest( CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest() );
  runner.run( controller );

  return result.wasSuccessful() ? 0 : 1;
}

コンパイル

$ g++ -o HelloWorld HelloWorld.cpp -lcppunit 

-lcppunitがポイントね。

実行

$ ./HelloWorld
Test::testHelloWorldHello, world!
 : OK

上のやり方でコンパイルや実行がうまくいかない場合はインクルードパスとかライブラリパスを確認してね。

解説

あ、うん。OKだけど?でっていう?
一応解説をダラダラと書くよ。すげー読む気失せるよ。

CPPUNIT_TESTナントカっていうのがテストを簡単に書くためのマクロなんだけど、これを使うために
cppunit/extensions/HelperMacros.h
をincludeする。その他のcppunitのincludeはmainテスト実行部分のためのものだよ。

■クラス宣言

テストのためのクラスTestはCppUnitのTestCaseというクラスを継承して作るよ。
CPPUNIT_NSはCppUnitの名前空間、とはいっても実は
#define CPPUNIT_NS CppUnit
としてるだけだったりする。

CPPUNIT_TEST_SUITE(クラス名);
ここから一連のテストケース(テストスイート)を書くよーって言ってる。

CPPUNIT_TEST(メソッド名);
このメソッドのテストするよー宣言。上の例ではtestHelloWorldを宣言

CPPUNIT_TEST_END();
テストケースはここまでだよーって言ってる。上の例ではCPPUNIT_TEST(testHelloWorld);ひとつだけなんだけど、
他にもテストしたいメソッドがあれば、CPPUNIT_TEST_SUITE~CPPUNIT_TEST_ENDの間に並べて宣言しておけばいいよ。

setUp()tearDown()はテストケースの事前処理と事後処理をそれぞれ書けるよ。
ここでは”Hello, World!”表示するだけなので、特に何もしてないよ

protected:に続くtestHelloWorldがテストケースになるメソッド。CPPUNIT_TEST(メソッド名);で書いたメソッドを宣言する。
ここでは実装も合わせて書いてるけど、クラス宣言の外で実装してもOKだよ。

クラス宣言ここまで。

CPPUNIT_TEST_SUITE_REGISTRATION(テストスイート名);
で、以上のテストスイートを登録しておく。

■main
ここからテスト実施部分のmain。だけど例によって書くの面倒くさくなってきたからテキトー。
controllerがテストのコントローラー
テスト結果resultとテスト進捗progressにはそれぞれイベントを受け取るためのリスナをつけとく。
テストランナーrunnerにテストスイートに登録されてるテストを持ってきて、controllerをrunすれば、テストケースが実行されるわけ。
で、テストがうまくいってれば最後に0を返して、コケてたら1を返す。

単に”Hello, World!”出力してるだけなのでピンと来ないよね
そういう人は、C++界では言わずと知れたεπιστημη(えぴすてーめー)氏の
第2回 C++アプリケーションの効率的なテスト手法(CppUnit編)
を見ればいいと思うんだ。値が期待通りか確認するケースとか、NGになる場合のケースなんかも紹介してるよ。
というか最初からこれ見ればいいような気もするんだ。今さらだけど。

テスト結果をXML形式で出力

で、επιστημη氏のサンプルでは、最後にreturnする前に結果を標準出力に吐くコードが入ってる。

#include <cppunit/CompilerOutputter.h>
//...
// output test result
  CPPUNIT_NS::CompilerOutputter outputter( &result, CPPUNIT_NS::stdCOut() );
  outputter.write();
//...

これを少し改造してXML形式で出力するように変更。

#include <cppunit/XmlOutputter.h>
  //....
  // output test result
  std::ofstream ofs("result.xml");
  CPPUNIT_NS::XmlOutputter outputter(&result, ofs,"UTF-8");
  outputter.write();
  //....

テスト結果をカレントディレクトリにresult.xmlという名前のXMLを出力するよ。

<?xml version="1.0" encoding='UTF-8' standalone='yes' ?>
<TestRun>
  <FailedTests></FailedTests>
  <SuccessfulTests>
    <Test id="1">
      <Name>Test::testHelloWorld</Name>
    </Test>
  </SuccessfulTests>
  <Statistics>
    <Tests>1</Tests>
    <FailuresTotal>0</FailuresTotal>
    <Errors>0</Errors>
    <Failures>0</Failures>
  </Statistics>
</TestRun>

なんでこれを出したかというと、この結果ファイルをHudsonに渡したかったの。Hudsonとの連携はまたあらためて。

テスト駆動開発

CppUnitでテストをするHello Worldを紹介したわけだけど、
大事なのは、しれっと書いてるtestHelloWorldの実装部分なんだ。これがすなわちテストケース。

このケース部分を早い段階で書いておいて、このテストをパスするように実装をしていくというのが、テスト駆動開発(TDD:Test Driven Development)という開発手法。
ケースがきちんと書けないうちは、仕様がはっきり定まっていないということになるわけで、早い段階であいまいな点を洗い出すきっかけになるんだ。
そしてあらかじめケースを書いておけば、いろいろな変更を加えた場合のリグレッションテストが楽になり、プロジェクト後期でその威力を発揮してくる。
はっきりいってプロジェクト中盤はかったるい。仕様が変更になる度、コードもケースも書き直さなくてはならなかったりするからね。
でも長期での開発保守を考えるならば、あらかじめ仕込んでおくと幸せになれるんだと思うよ。多分。ぜひプロジェクト開始時には一度検討して欲しいと思う。

Google ChromeのHudson監視拡張機能

Hudsonの状況をモニターするGoogle Chromeの拡張機能(Extension)を試してみたよ。

Hudson ExtensionHudson Monitorというのをインストールして設定してみた。
どちらもアイコンでステータスがわかるようになっている。
設定オプションとアイコンをクリックしたときの表示が微妙に異なる。一長一短あるが、これくらいのシンプルさがよいと思う。

Hudson Extension

  • アイコンにOK,Failを表示
  • HudsonサーバーURL表示
  • ジョブへのリンクとステータス表示
  • 更新時間表示
  • ユーザー権限には対応していない
  • 更新間隔は1~100分までを数値指定

Hudson Monitor

  • アイコンにビルド状況の色を表示
  • ジョブへのリンクとステータス表示
  • ユーザー権限に対応(ユーザ名、パスワードを設定に入力)
  • 表示順をジョブの名前あるいはステータスでソート可能
  • 更新間隔は5分~60分の5分おきの値をプルダウンで指定

WindowsからVNCでMacの画面を操作する

WindowsからVNCでMacのデスクトップ画面を操作するよ。

Mac OSX側

  1. システム環境設定を開く
  2. インターネットワイヤレス「共有」
  3. 「画面共有」にチェック
  4. パネル上部「コンピュータ名:」の下の「ローカルネットワーク上のコンピュータから、次のアドレスでこのコンピュータにアクセスできます: hostname」のhostnameを確認しておく。必要に応じて「編集…」を押して名前を設定する。
  5. 「コンピュータ設定…」を押して必要に応じて以下設定
    • 「VNC使用者が画面を操作することを許可」にチェック
    • パスワードを入力
    • アクセスを許可:「次のユーザーのみ:」を選択 +を押して自分を追加


Windows側

VNCのインストール

  1. RealVNCのTOPにある「VNC Downloads」をクリック
  2. Free Editionの「Download & Use」をクリック
  3. Please enter your detailsを必要に応じて入力(しなくてもよい)し、「Proceed to download」を押す
  4. VNC Free Edition for WindowsかVNC Free Edition Viewer for Windowsをダウンロード
    前者はVNCサーバー+クライアント。後者はクライアントのみ。
  5. インストールはほとんど「Next」を押してれば完了する


WindowsからMacへ接続

  1. VNC Viewerを起動する
  2. Options…を押す。
  3. 「Colour & Encoding」タブでColour levelをFullにする
  4. 「Load / Save」タブでこの設定を保存するため「Save」を押して「OK」
  5. ServerにMac側で最初に確認したhostname(あるいはIPアドレス)を入力
  6. パスワードはMac側の「コンピュータの設定…」で設定したパスワードを入力して「OK」

うっかり間違えてWindowsリモートデスクトップでアクセスしてみたら、
Mac上でVboxHeadlessで起動していたVirtualBoxのVMにつながってビビッたよ。

Hudsonにノードを追加してビルドをさせてみる

Hudsonではノードマシンをどんどん追加して、専用環境でのビルドや、分散ビルドによる高速化などができる。
今回はある特定の環境でビルドしなければならない事態を想定して、スレーブとなるビルドマシンを追加して、マスターのノードから指示を出し、新しく追加したスレーブのマシンでビルド実行するところまでやってみるよ。

ノードの追加

Hudsonの管理→ノードの管理→新規ノードの作成

ノード名を入力→ラジオボタンでダムスレーブを選択(はじめてノードを追加するときはこれしか選べない)→「OK」

同時ビルド数: 1 ←今回は分散でなく専用ビルドなので1にする
リモートFSルート: /var/lib/hudson/jobs ←ビルドを実行するスレーブ側の作業ディレクトリ
ラベル: hoge
用途: このマシーンを特定ジョブ専用にする
起動方法: SSH経由でUnixマシンのスレーブエージェントを起動
[高度な設定]

  • ホスト: slave01
  • ユーザー名: hudson ←スレーブ側のマシンslave01であらかじめ作っておいたhudson用ユーザー
  • パスワード: ********
  • 秘密鍵:
  • ポート: 22
  • JVMオプション:

可用性: 可能な限りオンラインのままにする。

ノードプロパティ
□環境変数
□ツールパス

保存

これで設定間違えてなければ、早速ビルドマシンにつながるよ。
うまくいかないときはログを参照して原因を調べてみてね。

ジョブの作成

□実行するノードを制限 にチェックを入れて
ラベル式に「hoge」と入力

上のノードの設定ででてきたラベルはここで使うのだね。

あとはレポジトリ、ビルドコマンドや、テストなども適宜入力して保存

ジョブの実行

きちんとつながってさえいればあとは通常のビルド実行と同じ。どうみても書くの飽きてきて面倒くさくなってるのバレバレだね。

あとは応用で、マシンを追加して並行してビルド実行させるとかすれば、きっとおかし食べながらネットサーフィンする時間が短くなって悲しくなるよ。仕事早く終わって、次の仕事がどんどんやってくるよ!よかったね!