Search code examples
powershellimplementationstreamreadertail

how to get X lines back with a streamreader


Trying to implement a custom tail-like function, after checking several examples i've come to the bellow code, which works quite well (doesn't load whole file to read the X ending lines, works for network paths ...)

the problem I have is I don't how to move the stream pointer says 10 lines before its current position ?

as a workaround i move the pointer 1024 bytes before current position , but i don't know how much lines this is really concerning.

$sr=New-Object System.IO.StreamReader($fs)
$lastPosition=$sr.BaseStream.Length # final position of the file
$currentPosition=$lastPosition - 1024

Can anyone point me to right direction please ?

Here is the complete code :

function tail{
    [cmdletBinding()]
    Param(
        [Parameter(Position=0, Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $filename, # path
        [int]$n=10, # number of lines to output
        [switch]$continous, # continue to monitor for changes ?
        [switch]$hilight  # hilight lines containing keywords ?
    )

    # Initialising stuff
    $hilightError=@("erreur","error","fatal","critic")
    $hilightWarning=@("attention","warning","notice")
    $errorColor="red"
    $warningColor="darkyellow"

    if ( (test-Path $filename) -eq $false){
        write-Error "Cant read this file !"
        exit
    }

    function tailFile($ptr){
        # return each line from the pointer position to the end of file
        $sr.BaseStream.Seek($ptr,"Begin")
        $line = $sr.ReadLine() 
        while ( $line -ne $null){
            $e=$w=$false

            if( $hilight){
                $hilightError | %{ $e = $e -or ($line -match $_) } # find error keywords ?
                if( $e) {wh $line -ForegroundColor $errorColor }
                else{ 
                    $hilightWarning | %{ $w = $w -or ($line -match $_ ) } # find warning keywords ?
                    if($w){ wh $line -ForegroundColor $warningColor }
                    else{ wh $line}
                }
            }
            else{ #no hilight
                wh $line
            }
            $line = $sr.ReadLine() 
        }
    }

    # Main 
    $fs=New-Object System.IO.FileStream ($filename,"OpenOrCreate", "Read", "ReadWrite",8,"None") # use ReadWrite sharing permission to not lock the file
    $sr=New-Object System.IO.StreamReader($fs)

    $lastPosition=$sr.BaseStream.Length # final position of the file
    $currentPosition=$lastPosition - 1024 # take some more bytes  (to get the last lines)

    tailfile $currentPosition

    if($continous){
        while(1){
            start-Sleep -s 1
            # have the file changed ?
            if ($sr.BaseStream.Length -eq $lastPosition){
                write-verbose "no change..."
                continue
            }
            else {
                tailfile $lastPosition
                $lastPosition = $sr.BaseStream.Position
            write-Verbose "new position $lastPosition"
            }
        }
    }
    $sr.close()
}

Solution

  • thanks to Christian and Keith for your tips. Finally I've decided just to get backward in the streamreader until it returns enough readline(). I let the possibility to specify the codepage and seems to be ok with unicode. Here is the code if anyone is interested in

    function tail{
        [cmdletBinding()]
        Param(
            [Parameter(Position=0, Mandatory=$true)]
            [ValidateNotNullOrEmpty()]
            [System.String]
            $filename, # path
            [int]$n=10, # number of lines to output,
            [Alias("cp")]
            $codepage=65001,#utf8
            [Alias("f")]
            [switch]$continous, # continue to monitor for changes ?
            [switch]$hilight  # hilight lines containing keywords ?
        )
       # Initialising stuff
        $hilightError=@("erreur","error","fatal","critic")
        $hilightWarning=@("attention","warning","notice")
        $errorColor="red"
        $warningColor="yellow"
        [System.Text.Encoding]$enc = [System.Text.Encoding]::GetEncoding($codepage)
    
        function render($render){
            $e=$w=$false
            if( $hilight){
                foreach ($line in $render){
                    $hilightError | %{ $e = $e -or ($line -match $_) } # find error keywords ?
                    if( $e) {wh $line -ForegroundColor $errorColor }
                    else{ 
                        $hilightWarning | %{ $w = $w -or ($line -match $_ ) } # find warning keywords ?
                        if($w){ wh $line -ForegroundColor $warningColor }
                        else{ wh $line}
                    }
                    $e=$w=$false
                }
            }
            else{ #no hilight
                wh $render
            }
        }
    
    
        function TailFileBeforeEnd{
        #try to find $n lines before eof
    
            $buffer=1024
            $ptr=$lastPosition #start at the end of the file  
            $found=0
    
            while($ptr -gt 0 -and $found -lt $n){
                $ptr-=$buffer 
                if ($ptr -le 0){$ptr=0}
                $sr.BaseStream.Seek($ptr,"Begin")|out-null #step backward
                $line = $sr.ReadLine()
                $found=0
                $output=@()
                $render=@()
    
                while ( $line -ne $null){ #read to the end
                    $output+=$line
                    $found++
                    $line=$sr.ReadLine() 
                }
                if($found -ge $n){ #found enough lines
                    Write-Verbose "OK found $found / $n"
                    foreach($i in ($output.length - $n)..($output.length)){ #take only lines needed
                        $render+=$output[$i]
                    }
                    continue
    
                }
                else{ #move backward and retry to find lines
                    Write-Verbose "not enough line ($found displayed)"
                    $ptr-=$buffer
                    if ($ptr -le 0) { #eof without finding suffisant lines
                        $ptr=0
                        Write-host "not enough line ($found displayed)"
                        $render=$output
                    }
                }
             }
        render $render
        }
    
    
        function tailFile($ptr){
            # return each line from the pointer position to the end of file
            $render=@()
            $sr.BaseStream.Seek($ptr,"Begin")|out-null
            $line = $sr.ReadLine() 
            while ( $line -ne $null){
               $render+=$line
               $line = $sr.ReadLine() 
            }
            render $render
        }
    
        # Main loop
    
    
        # use ReadWrite sharing permission to not lock the file
        $fs=New-Object System.IO.FileStream ($filename,"OpenOrCreate", "Read", "ReadWrite",8,"None") 
        $sr=New-Object System.IO.StreamReader($fs, $enc)
        $lastPosition=$sr.BaseStream.Length 
    
        tailFileBeforeEnd 
    
        if($continous){
            while(1){
                start-Sleep -s 2
                # has the file changed ?
                if ($sr.BaseStream.Length -eq $lastPosition){
                    write-verbose "no change..."
                    continue
                }
                else {
                    tailfile $lastPosition
                    $lastPosition = $sr.BaseStream.Position
                    write-Verbose "new position $lastPosition"
                }
            }
        }
        $sr.close()
    }