challenge tailの実装

'tail'を実装してください。

巨大なファイルでも効率的に動作するようにしてください。

最低限必要な機能は、

  • 行数指定
  • 「-f」パラメータの対応

です。

Posted feedbacks - Flatten

Nested Hidden

愚直にキューをつかって。

 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
(use gauche.parseopt)
(use gauche.selector)
(use util.queue)

(define (main args)
  (let-args (cdr args)
      ((nlines "n=n" 10)
       (f? "f" #f)
       . argv)
    (call-with-input-file (car argv)
      (lambda (iport)
        (let ((q (make-queue)))
          (let loop ((n 0)
                     (line (read-line iport)))
            (unless (or (= n nlines) (eof-object? line))
              (enqueue! q line)
              (loop (+ n 1) (read-line iport))))
          (do ((line (read-line iport) (read-line iport)))
              ((eof-object? line))
            (enqueue! q line)
            (dequeue! q))
          (for-each print (queue->list q)))
        (when f?
          (with-signal-handlers ((SIGINT => (lambda _ (exit 0))))
            (lambda ()
              (let ((sel (make <selector>)))
                (selector-add! sel
                               iport
                               (lambda (input _)
                                 (and-let* ((line (read-line input))
                                            ((not (eof-object? line))))
                                   (print line))
                                 (sys-sleep 1))
                               '(r))
                (while #t (selector-select sel))))))
        0))))

tailの仕様がわかりません。
検索すると概要はわかるのですが、実際にどういった動作をさせれば良いのでしょう?
UNIX環境が無いのでどうしたらよいのか…。

Squeak Smalltalk で、それっぽい動きをするものを。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| filename file numOfLines isFollowMode lineCount savedPosition |
filename := 'in.txt'.
numOfLines := 10.
isFollowMode := false.

file := FileStream fileNamed: filename.
file setToEnd.
file binary.
lineCount := 0.
[file position > 0 and: [lineCount < numOfLines]]
    whileTrue: [file back = 13 ifTrue: [lineCount := lineCount + 1]].

file ascii.
World findATranscript: nil.
Transcript cr; show: file upToEnd.
savedPosition := file position.
file close.
isFollowMode ifTrue: [
    [Sensor keyboardPressed] whileFalse: [
        (Delay forSeconds: 5) wait.
        file reopen; position: savedPosition.
        Transcript show: file upToEnd.
        savedPosition := file position.
        file close]]

まじめに書いたらCのようになってしまいました。

 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
read.file <- function (infile, offset=0){
   con <- file(infile)
   open(con)
   seek(con, offset)
   lines <- readLines(con)
   cur.offset <- seek(con)
   close(con)
   list(l=lines, o=cur.offset)
}

my.tail <- function(infile, n=10, follow=FALSE){
   lines <- read.file(infile)
   writeLines(tail(lines$l, n))
   if(follow){
       size  <- file.info(infile)$size
       repeat{
           if(size < (s <- file.info(infile)$size)){
               lines <- read.file(infile, lines$o)
               writeLines(lines$l)
               size <- s
           }
           Sys.sleep(1)
       }
   }
}

たしかにお題を出す人にも、もうすこし丁寧に出題してほしい感じがしますね。

たぶんですが、
意訳すると以下のような感じになるのではないでしょうか?
1. 指定されたファイルの最後尾から N行表示すること
2. ファイルの更新を監視して、指定のファイルが更新された場合に、
   更新分のうち、最後尾からN行を表示すること

「2.」更新分なので前回表示続きからという点が注意事項だと思います。

# 「-f」 を実装しなさいというあたり、
  以前あったファイルの更新を監視するお題の
  派生と考えられるのかな?

対象がファイルの場合には、予めファイル末尾を基準にシークします。これによってファイルサイズに依存しない処理時間を実現しています。

起動方法は

java Tail [-f] [-<num>] [<ファイル名>]
-f: ファイル末尾まで読み込んでも終了せず、ファイルが成長した部分をポーリングで表示する(ファイル名を与えない場合は無視する)。
num: ファイル末尾からnum行を表示する(デフォルトは10行)。
ファイル名: 表示するファイル名。省略した場合は標準入力。

です。
 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
53
54
55
56
57
58
import java.io.*;

public class Tail {
    private static final int MAX_LINE_LENGTH = 1000;
    private static final long SLEEP_TIME = 500; // 500 m sec
    private static int numOfLine = 10;
    private static boolean fOption = false;
    private static String fileName = null;
    private static InputStream targetStream = System.in;

    public static void main(String[] args) throws Exception {
        for (String s: args) {
            if ("-help".equals(s)) {
                System.err.println("Usage> java Tail [-f] [-<num>] [filename]");
                return;
            }
            else if ("-f".equals(s))
                fOption = true;
            else if (s.startsWith("-"))
                numOfLine = -Integer.parseInt(s);
            else
                fileName = s;
        }
        
        if (fileName != null) {
            File target = new File(fileName);
            long len = target.length();
            targetStream = new FileInputStream(target);
            len -= MAX_LINE_LENGTH * numOfLine;
            if (len > 0)
                targetStream.skip(len);
        }
        BufferedReader br = new BufferedReader(new InputStreamReader(targetStream));
        String line;
        String[] lines = new String[numOfLine];
        int ip = 0;
        while ((line = br.readLine()) != null) {
            lines[ip] = line;
            if (++ip >= numOfLine)
                ip = 0;
        }
        int i = ip;
        do {
            if (lines[i] != null)
                System.out.println(lines[i]);
            if (++i >= numOfLine)
                i = 0;
        } while (i != ip);
        if (fOption && fileName != null) {
            while (true) {
                Thread.sleep(SLEEP_TIME);
                line = br.readLine();
                if (line != null)
                    System.out.println(line);
            }
        }
    }
}

 末尾から読み込む様にしてみました。

  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
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#! /usr/bin/perl

use strict;

package Tail;

$Tail::BUF_SIZE = 4096;

sub new {
    my    ($pkg,$lines,$follow,$file) = @_;
    my    $self = bless({},$pkg);
    $self->file($file);
    $self->lines(defined($lines) ? $lines : 10);
    $self->follow(defined($follow) ? $follow : 0);
    $self;
}

sub lines {
    my    ($self,$lines) = @_;
    $self->{'lines'} = $lines if (defined($lines));
    $self->{'lines'};
}

sub follow {
    my    ($self,$follow) = @_;
    $self->{'follow'} = $follow if (defined($follow));
    $self->{'follow'};
}

sub file {
    my    ($self,$file) = @_;
    $self->{'file'} = $file if (defined($file));
    $self->{'file'};
}

sub quit {
    my    ($self,$quit) = @_;
    $self->{'quit'} = $quit if (defined($quit));
    $self->{'quit'};
}

sub _fh {
    my    ($self,$fh) = @_;
    $self->{'_fh'} = $fh if (defined($fh));
    $self->{'_fh'};
}

sub initialize {
    my    ($self) = @_;
    my    $fh;
    open ($fh,"<".$self->file()) || die "cannot open file.";
    binmode($fh);
    $self->_fh($fh);
    $self->quit(0);
    $|=1;
    $self;
}

sub _tailn {
    my    ($self) = @_;
    my    $lines = $self->lines();
    my    $size = -s $self->file();
    my    $fh = $self->_fh();
    my    $pos = $size;
    my    $head;
    my    $tail = '';
    while ($pos > 0) {
        my    $buf = '';
        my    $ret;
        if ($pos <= $Tail::BUF_SIZE) {
            seek($fh,0,0);
            $ret = read($fh,$buf,$pos);
        } else {
            seek($fh,$pos-$Tail::BUF_SIZE,0);
            $ret = read($fh,$buf,$Tail::BUF_SIZE);
        }
        die "cannot read file." unless (defined($ret));
        $pos -= $ret;
        $tail = $buf.$tail;
        last if (scalar(split(/(?:\0x0d\x0a|\x0d|\x0d)/,$tail)) > $lines);
    }
    $pos = length($tail) - 1;
    while ($pos >= 0) {
        my    $c = substr($tail,$pos,1);
        if ($c eq "\x0d" || $c eq "\x0a") {
            last if (--$lines <= 0);
            $pos-- if ($c eq "\x0a" && $pos > 0 && substr($tail,$pos-1,1) eq "\x0d");
        }
        $pos--;
    };
    $tail =  substr($tail,$pos+1,length($tail)-$pos-1);
    $tail =~ s/(\x0d\x0a|\x0d|\x0a)/\n/g;
    print $tail;
    seek($fh,$size,0);
    $self;
}

sub _tailf {
    my    ($self) = @_;
    my    $fh = $self->_fh();
    until ($self->quit()) {
        my    $buf = '';
        seek($fh,tell($fh),0);
        until (eof($fh)) {
            die "cannot read file." unless (defined(read($fh,$buf,$Tail::BUF_SIZE)));
            $buf =~ s/(\x0d\x0a|\x0d|\x0a)/\n/g;
            print $buf;
        }
        sleep(1);
    }
    $self;
}

sub tail {
    my    ($self) = @_;
    $self->_tailn();
    $self->_tailf() if ($self->follow());
}

sub finalize {
    my    ($self) = @_;
    close($self->_fh());
}

package main;

use Getopt::Std;

my    $opt = {};
my    $tail;
eval {
    getopts('n:f',$opt);
    $tail = new Tail($opt->{'n'},$opt->{'f'},shift(@ARGV))->initialize();
    $SIG{INT} = sub { $tail->quit(1); };
    $tail->tail();
    $tail->finalize();
};
print "error: $@" if ($@);

1;

 Rubyもまだの様なので書いてみました。

 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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#! /usr/bin/ruby

class Tail
    BUF_SIZE = 4096
    
    attr_accessor :lines, :follow, :file, :quit, :_f
    private :_f
    
    def initialize(lines,follow,file)
        self.lines = lines ? lines.to_i : 10
        self.follow = follow ? follow : false
        self.file = file
        quit = false
        self.send(:_f=,File.open(file,'r').binmode)
        self
    end
    
    def _tailn
        lines = self.lines
        size = _f.lstat.size
        pos = size
        tail = ''
        tails = []
        until(pos <= 0) do
            buf = ''
            if pos < BUF_SIZE
                _f.pos = 0
                buf = _f.read(pos)
            else
                _f.pos = pos - BUF_SIZE
                buf = _f.read(BUF_SIZE)
            end
            raise IOError.new('cannot read file.') unless(buf)
            pos -= buf.size
            tail = buf + tail
            break if tail.split(/(?:\x0d\x0a|\x0d|\x0a)/).size > lines
        end
        tail = tail.gsub(/(\x0d\x0a|\x0d|\x0a)/,"\n")
        tails = tail.split(/\n/)
        tails.push("") if tail =~ /\n\z/
        print tails.reverse.first(lines).reverse.join("\n")
        STDOUT.flush
        _f.pos = size
        self
    end
    
    def _tailf
        until(quit) do
            buf = ''
            buf = _f.read
            raise IOException.new('cannot read file.') unless(buf)
            if (buf.size > 0)
                buf = buf.gsub(/(\x0d\x0a|\x0d|\x0a)/,"\n")
                print buf
                STDOUT.flush
            end
            sleep(1)
        end
        self
    end
    
    def tail
        _tailn
        _tailf if follow
    end
    
    def finalize
        _f.close
    end
end

require "optparse"

tail = nil

conf = Hash.new
opts = OptionParser.new
opts.on("-n MANDATORY") { |v| conf[:n] = v }
opts.on("-f") { |v| conf[:f] = v }
opts.parse!

if ARGV.size == 0
    puts "usage: ruby #{$0} [-n lines] [-f] filename"
else
    begin
        tail = Tail.new(conf[:n],conf[:f],ARGV.shift)
        Signal.trap(:INT) { tail.quit = true }
        tail.tail
        tail.finalize
    rescue => e
        puts e.message
    end
end

C#でやってみました。
 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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
using System;
using System.Text;
using System.Threading;
using System.IO;

class Tail
{
    private const int SLEEP_TIME = 1000;
    private static bool fOption = false;
    private static int outputLines = 10;
    private static string filename = string.Empty;
    private static string usage = "Usage: Tail.exe [-n number] [-f] filename";

    static void Main(string[] args)
    {
        try {
            for (int i = 0; i < args.Length; i++) {
                if (args[i][0] == '-') {
                    switch (args[i][1]) {
                        case 'n':
                            outputLines = int.Parse(args[++i]);
                            break;
                        case 'f':
                            fOption = true;
                            break;
                        default:
                            Console.Error.WriteLine("Invalid Option: {0}", args[i]);
                            Console.Error.WriteLine(usage);
                            return;
                    }
                }
                else
                    filename = args[i];
            }

            string buffer = string.Empty;
            int allLines = 0;
            string line = string.Empty;
            using (FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
                using (StreamReader sr = new StreamReader(fs, Encoding.Default)) {
                    while (sr.ReadLine() != null)
                        allLines++;
                    fs.Seek(0, SeekOrigin.Begin);
                    if (allLines <= outputLines)
                        buffer = sr.ReadToEnd();
                    else {
                        for (int i = 0; i < allLines; i++) {
                            line = sr.ReadLine();
                            if (i > allLines - outputLines - 1)
                                buffer += line + "\n";
                        }
                    }

                    Console.WriteLine(buffer);

                    while (fOption) {
                        Thread.Sleep(SLEEP_TIME);
                        while ((line = sr.ReadLine()) != null)
                            Console.WriteLine(line);
                    }
                }
            }
        }
        catch (Exception ex) {
            Console.Error.WriteLine(ex.Message.ToString());
            Console.Error.WriteLine(usage);
        }
    }
}

手抜きで行末記号を"\n"で決め打ちです。
 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
import sys
import time
import StringIO

def gettail(fp, n):
  start = fp.tell()
  fp.seek(0, 2)
  end = fp.tell()
  s = ''
  while len(s) < end - start and s.count('\n') <= n:
    a = min(160 * n, fp.tell() - start)
    fp.seek(-a, 1)
    s = fp.read(a) + s
  return StringIO.StringIO(s).readlines()[-n:]

def getnlines(fp, start, n):
  fp.seek(0, 2)
  pos = fp.tell()
  fp.seek(start)
  for s in gettail(fp, n):
    sys.stdout.write(s)
    sys.stdout.flush()
  return pos

n = 10
f = False
fp = None

sys.argv.pop(0)
while sys.argv:
  s = sys.argv.pop(0)
  if s == '-n':
    n = int(sys.argv.pop(0))
  elif s == '-f':
    f = True
  elif not s.startswith('-'):
    fp = file(s, 'rb')

pos = getnlines(fp, 0, n)

while f:
  time.sleep(1)
  pos = getnlines(fp, pos, n)

バッチです。カバレッジ稼ぎのようでちょっと恥ずかしいのですが、あまり処理を複雑に
したくなかったので、標準入力からの入力や-fオプションは実装しませんでした。

  e.g.
    C:\>tail 3 tail.bat
        set /a i-=%1
      endlocal & more +%i% %p%
    goto :EOF

以下の参考サイトのものはもっと複雑で頭が下がります。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
:: tail.bat
@echo off
  setlocal
    set i=0
    set p=%~fs2

    echo %1|findstr /r "[^0-9]" >NUL 2>&1
    if %ERRORLEVEL% equ 0 (call :usage & exit /b 1)

    if "%p%" equ "" (call :usage & exit /b 1)

    if not exist %p% (call :usage & exit /b 1)

    for /f %%i in ('find /c /v "" ^< %p%') do set i=%%i
    set /a i-=%1
  endlocal & more +%i% %p%
goto :EOF

:usage
  setlocal
    echo %~n0 [LINE] [FILE]
  endlocal
goto :EOF

 オプションの処理にかなり場所をとられてしまっていますが...。

  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
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import    java.io.FileNotFoundException
import    java.io.File
import    java.io.FileInputStream
import    scala.collection.mutable.HashMap

class InvalidArgumentException extends Exception {}

class GetOpt(f:String) {
    var    _params:List[Tuple2[String,Boolean]] = null
    var    _opt:HashMap[Any,Any] = null
    var    _rest:Array[String] = null
    
    _params = f.toList.foldLeft(List[Tuple2[String,Boolean]]()) { (l,c) =>
        c match {
            case ':' => l match {
                case List() => throw new InvalidArgumentException
                case h::r => (h._1,true)::r
            }
            case k => (k.toString,false)::l
        }
    }.reverse
    
    def parse(a:Array[String]):GetOpt = {
        def _parse(o:HashMap[Any,Any],l:List[String]):Tuple2[HashMap[Any,Any],List[String]] = l match {
            case List() => (o,List())
            case h::r => h.toList match {
                case '-'::k => {
                    _params.find { e => e._1 == new String(k.toArray) } match {
                        case Some(Tuple2(_,true)) => {
                            if (r.size == 0) throw new InvalidArgumentException
                            o.update(new String(k.toArray),r.head)
                            _parse(o,r.tail)
                        }
                        case Some(Tuple2(_,false)) => {
                            o.update(new String(k.toArray),true)
                            _parse(o,r)
                        }
                        case _ => throw new InvalidArgumentException
                    }
                }
                case _ => (o,l)
            }
        }
        _parse(new HashMap[Any,Any],a.toList) match {
            case Tuple2(o,r) => { _opt = o; _rest = r.toArray }
        }
        this
    }
    
    def getopt(k:String):Any = _opt.isDefinedAt(k) match {
        case true => _opt.apply(k)
        case _ => false
    }
    
    def rest:Array[String] = _rest
}

class CTail(n:Int,f:Boolean,i:File) {
    
    val    s:FileInputStream = new FileInputStream(i)
    
    def this(o:GetOpt) =this(o.getopt("n") match { case Some(false) => 10; case n => n.asInstanceOf[String].toInt },o.getopt("f").asInstanceOf[Boolean],new File(o.rest.apply(0)))
    
    def tailn:Unit = {
        
        val    BUF_SIZE:Int = 4096
        val    e:Long = i.length
        
        def _tailn(p:Long,n:Int,b:Array[Byte]):Array[Byte] = {
            var    t:Array[Byte] = null
            var    l:Int = n
            (p > BUF_SIZE) match {
                case true => {
                    t = new Array[Byte](BUF_SIZE)
                    s.getChannel.position(p - BUF_SIZE)
                    s.read(t,0,BUF_SIZE)
                }
                case _ => {
                    t = new Array[Byte](p.toInt)
                    s.read(t,0,p.toInt)
                }
            }
            t = t.reverse.takeWhile { c => if (c == '\n') l = l - 1; (l > 0) }.reverse
            ((p <= BUF_SIZE) || (l == 0)) match {
                case true => t ++ b
                case _ => _tailn(p - BUF_SIZE, l, t ++ b)
            }
        }
        
        print(new String(_tailn(e, n, new Array[Byte](0))))
        s.getChannel.position(e)
    }
    
    def tailf:Unit = {
        def _tailf(b:List[Byte]):List[Byte] = {
            val    c:Int = s.read
            (c >= 0) match {
                case true => _tailf(b + c.toByte)
                case _ => b
            }
        }
        print(new String(_tailf(List[Byte]()).toArray))
        Thread.sleep(100)
        tailf
    }
    
    def tail:CTail = {
        tailn
        if (f) tailf
        this
    }
    
    def close:Unit = s.close
}

object Tail {
    def main(args:Array[String]):Unit = {
        try {
            val    o:GetOpt = new GetOpt("n:f").parse(args)
            (new CTail(o)).tail.close
        } catch {
            case e:InvalidArgumentException => println("usage: scala Tail [-n <number of lines>] [-f] file")
            case e:FileNotFoundException => print("file not found.")
            case e:Exception => e.printStackTrace
        }
    }
}

Index

Feed

Other

Link

Pathtraq

loading...