LoginSignup
2
8

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-08-20

リングバッファーを使った 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で公開したものを加筆・修正したものです。

2
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
8