Edited at

C#:自作RingBufferクラスを使い簡易Tailコマンドを書いてみた

リングバッファーを使った Tailコマンドを書いてみました。


作成したTailコマンドの使い方

Tail [行数] [ファイル名]

というとても単純なものです。たとえば、sample.txtの最後の30行を表示したい場合は、

Tail 30 sample.txt

のように入力します。 行数を省略した場合は、最後の10行が表示されます。

ファイル名を省略した場合は、 標準入力からの入力となるようにしました。


RingBufferクラス

Tailコマンドを作成するために、リングバッファークラス(RingBuffer)を自作しました。

RingBufferクラスは、リング状になった配列のような構造をしています。格納できるのは、配列と同様、一定の量だけで、それを超えてデータを格納しようとすると、先頭に戻ってデータを格納する構造になっています。

Wikipediaに説明がありますので、リングバッファについての詳しい説明は割愛します。

wikipwdia:リングバッファ

ここで作成した RingBufferクラスは、

public class RingBuffer<T> : IEnumerable<T> {

とジェネリッククラスにしているので、用途にあった型のデータを入れることができます。

また、IEnumerable<T> を実装していますので、foreachで簡単に要素を取り出すことができます。

なお、この実装では、取り出した要素はバッファから削除していますので、同じ要素を複数回バッファーの中から取り出すことはできません。こういった動きをIEnumerable<T>として実装しても良いの?という疑問も頭をよぎりますが、まあ良しとしましょう。

RingBuffer の簡単な使い方を示します。

// バッファサイズ4のRIngBufferを生成

RingBuffer<string> rb = new RingBuffer<string>(4);
rb.Add("111111");
rb.Add("222222");
rb.Add("333333");
rb.Add("444444");
rb.Add("555555");
rb.Add("666666");
// 最後の4つを取り出す
foreach (var s in rb) {
Console.WriteLine(s);
}

このコードを実行すると、以下のように最後に追加した4つの文字列が表示されます。

333333

444444
555555
666666

RingBufferクラスのソースコードはこの記事の最後に掲載しています。


Tailコマンドを実装する

コードはこの後に掲載しているので、それと合わせて読んで欲しいのですが、このプログラムの実質的なメイン部分は、DoTailメソッドです。

観ていただければ分かると思いますが、

「ファイルを1行ずつ読み込み、RingBufferに追加していき、

最後まで読み終わったら、RingBufferの中身をforeachで書き出す」

という、とてもシンプルなコードになっています。Mainメソッドでごにょごにょやってるのはコマンドパラメータの解析なので、本質部分ではありません。

シンプルなコードとなったのは、RingBufferクラスの存在が大きいですね。

こういったクラスがなかったら、 かなり複雑なコードを書かないといけなくなってしまいますし、デバッグも大変です。

using Gushwell.Etude;

using System;
using System.IO;

namespace _23TailCommand {
class Program {
static void Main(string[] args) {
int count = 10;
string filepath = null;
if (args.Length > 0) {
if (int.TryParse(args[0], out count) == false) {
count = 10;
filepath = args[0];
} else if (args.Length == 2)
filepath = args[1];
}
DoTail(count, filepath);
}

private static void DoTail(int linecount, string filename) {
using (var tr = string.IsNullOrEmpty(filename)
         ? Console.In : new StreamReader(filename)) {
RingBuffer<string> rb = new RingBuffer<string>(linecount);
string line;
while ((line = tr.ReadLine()) != null) {
rb.Add(line);
}
foreach (var s in rb)
Console.WriteLine(s);
}
}

}
}


RingBufferのソースコード

RingBufferクラスは、ある程度汎用性を持たせましたが、 一般的なRingBufferのインターフェースが良くわからないので、僕なりのインターフェースとしました。

他の用途で利用するには、機能不足のところもあるかもしれません。

特に、Get や foreachでデータを取り出した時に、バッファからデータを除去する仕様としましたが、 除去したくない場合もあると思います。そのような場合は、新たなメソッドを追加する必要があります。

一方、Tailコマンドでの利用だけを考えた場合は、オーバースペックの部分があります。

こういった汎用クラスの設計がなかなか難しいものです。

using System;

using System.Collections;
using System.Collections.Generic;

namespace Gushwell.Etude {
public class RingBuffer<T> : IEnumerable<T> {
private int _size;
private T[] _buffer;
private int _writeIndex = -1; // 書き終わった位置
private bool _isFull = false;

public RingBuffer(int size) {
_size = size;
_buffer = new T[size];
_writeIndex = -1;
}

private int NextIndex(int ix) {
return ++ix % _size;
}

private int GetStartIndex() {
return _isFull ? NextIndex(_writeIndex) : 0;
}

public void Add(T value) {
_writeIndex = NextIndex(_writeIndex);
if (_writeIndex == _size - 1)
_isFull = true;
_buffer[_writeIndex] = value;
}

public bool Exists {
get { return Count > 0; }
}

public int Count {
get { return _isFull ? _size : _writeIndex + 1; }
}

public void Clear() {
_writeIndex = -1;
_isFull = false;
}

public IEnumerator<T> GetEnumerator() {
var index = GetStartIndex();
for (int i = 0; i < Count; i++) {
yield return _buffer[index];
index = NextIndex(index);
}
}
IEnumerator IEnumerable.GetEnumerator() {
return this.GetEnumerator();
}
}
}

上記ソースを GitHubで公開しています。


この記事は、Gushwell's C# Programming Pageで公開したものを加筆・修正したものです。