リングバッファーを使った Tailコマンドを書いてみました。
作成したTailコマンドの使い方
Tail [行数] [ファイル名]
というとても単純なものです。たとえば、sample.txtの最後の30行を表示したい場合は、
Tail 30 sample.txt
のように入力します。 行数を省略した場合は、最後の10行が表示されます。
ファイル名を省略した場合は、 標準入力からの入力となるようにしました。
RingBufferクラス
Tailコマンドを作成するために、リングバッファークラス(RingBuffer)を自作しました。
RingBufferクラスは、リング状になった配列のような構造をしています。格納できるのは、配列と同様、一定の量だけで、それを超えてデータを格納しようとすると、先頭に戻ってデータを格納する構造になっています。
Wikipediaに説明がありますので、リングバッファについての詳しい説明は割愛します。
ここで作成した 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で公開したものを加筆・修正したものです。