1

I have a log file I am trying to get some info out of. The information I need is on the line prior to last line and also the very last line is/could be blank. So it is actually line before last line or two before last line if last line is blank.

I know how to get to last line of the file using:

var lastLine = File.ReadLines("SomeFile.log").Last();

I can also use Linq to skip lines using .skipWhile() or .skip(1) but not going backward.

I am not sure how to get to the line I need. This is a sample of last few lines of the log file (with last line being blank):

2021/05/02 23:47:57:008989 send_status_message(2) Info: "Stream status heartbeat sent: [SY  1.3.2       ]"
2021/05/02 23:47:57:225172 send_status_message(2) Info: "Received heartbeat response: [S               ]"
2021/05/03 00:00:00:045055 set_log_dir(2) Info: "Changing log directory to /abc/def/logs/2021-05-03."
<blank-line>    

I am trying to get the timestamp on that line (i.e. 2021/05/02 23:47:57:225172).

NoBullMan
  • 2,032
  • 5
  • 40
  • 93
  • You can reverse your list using `Reverse()` method – Jawad Jun 02 '21 at 15:49
  • @Jawad this would read *and* cache everything in memory. It would be cheaper to use `ReadAllLines` and just take the last 2 lines – Panagiotis Kanavos Jun 02 '21 at 16:12
  • You want the last two lines or the second to last line? – Tim Schmelter Jun 02 '21 at 16:13
  • I don't know before hand if last line is blank or not. If it is then I need the third line from bottom; if it is not blank I need second from bottom. – NoBullMan Jun 02 '21 at 16:25
  • It seems @Jawad comment pointed me to right direction. This seems to work even if my log file has a blank last line: var lastLines = File.ReadAllLines(Path_to_Log_File).Reverse().Take(2).Reverse(); – NoBullMan Jun 02 '21 at 16:36

4 Answers4

3

File.ReadLines("SomeFile.log").Last(); will iterate over all lines and keep the last. That can be expensive for large files. At least it doesn't keep all of them in memory.

A faster alternative would be to read the last X bytes, convert them to string and split it into lines. Which isn't as easy as it sounds if you have UTF8 files, as the chunk may miss the first byte(s) of the first character. This question asks how to do this and UTF8 is left as an excercise to the reader.

To retrieve the last N items in an IEnumerable<T> you can use the TakeLast method introduced by .NET Core :

var lastLines = File.ReadLines("SomeFile.log").TakeLast(2);

There's also SkipLast, so if you wanted the second from last line, you could use:

var secondLast = File.ReadLines("SomeFile.log").TakeLast(2).SkipLast(1);

For just 1 line though, TakeLast(2).FirstOrDefault() would be enough.

For .NET Framework, you can use something like this answer's code or this one to iterate and keep the last N lines:

public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int count)
{
    if (source == null) { throw new ArgumentNullException("source"); }

    Queue<T> lastElements = new Queue<T>();
    foreach (T element in source)
    {
        lastElements.Enqueue(element);
        if (lastElements.Count > count)
        {
            lastElements.Dequeue();
        }
    }

    return lastElements;
}

This code needs a slight change to become a SkipLast(), by returning the dequeued items instead of discarding them:

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int count)
{
    if (source == null) { throw new ArgumentNullException("source"); }

    Queue<T> lastElements = new Queue<T>();
    foreach (T element in source)
    {
        lastElements.Enqueue(element);
        if (lastElements.Count > count)
        {
            var head=lastElements.Dequeue();
            yield return head;
        }
    }
}
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
2

Using C#8's range operator

If you already have the array in memory and can use C# 8, you could do this:

var Lines = File.ReadAllLines("SomeFile.log");
var SecondToLast = Lines[^2];

Without C#8.

Alternatively, as mentioned by Tim, you could do the arithmetic on the indexer:

var Lines = File.ReadAllLines("SomeFile.log");
var SecondToLast = Lines[Lines.Length - 2];

Comments based edit. From your comments, it seems like you're not quite sure of how many empty lines you'll get. If that's the case, you might be better off using a more general approach, such as this:

    static string FirstNotEmpty(string[] Lines, bool BottomUp = false)
    {
        if (BottomUp)
        {
            for (int i = Lines.Length - 1; i >= 0; i--)
            {
                var CurrentLine = Lines[i];
                if (!string.IsNullOrWhiteSpace(CurrentLine))
                    return CurrentLine;
            }
        }
        else
        {
            for (int i = 0; i <= Lines.Length-1; i++)
            {
                var CurrentLine = Lines[i];
                if (!string.IsNullOrWhiteSpace(CurrentLine))
                    return CurrentLine;
            }
        }
        return null; //Or something else.
    }

In your case, you'd call it like this:

var FirstNotEmptyLine = FirstNotEmpty(Lines, BottomUp: true);

You could also preemptively remove empty lines from your array:

var WithoutEmptyLines = Lines.Where(x => !string.IsNullOrWhiteSpace(x));

And then "safely" get the last line.

  • 1
    If OP uses C#8 and just wants the second to last and not the last two lines, this is the best answer +1. But you could provide the aqlternative if he can't use C#8 which is `Lines[Lines.Length - 2];`(handle the case that there's just one line) – Tim Schmelter Jun 02 '21 at 16:14
  • WithoutEmptyLines.ElementAt(Lines.Length - 2) returns what I am looking for. Can't apply indexing to IEnumerable apparently (Lines[Lines.Length - 2]). – NoBullMan Jun 02 '21 at 17:04
  • 1
    you can add `Range` and `Index` to earlier language versions with nuget packages. These features of those languages were all developed and prototyped in earlier language versions afterall. Edit: though tfm is still a consideration to packages – Brett Caswell Jun 02 '21 at 17:20
0

Maybe you could use this extension method:

public static class EnumerableExtensions
{
    public static T GetLastItem<T>(this IEnumerable<T> seq, int countFromEnd)
    {
        if(seq is IList<T> list) return list[^countFromEnd];
        using var enumerator = seq.Reverse().GetEnumerator();
        while(enumerator.MoveNext())
        {
            if(--countFromEnd == 0) return enumerator.Current;
        }
        throw new ArgumentOutOfRangeException();
    }
}

Usage:

var secondLastLine = File.ReadLines("SomeFile.log").GetLastItem(2);

If you don't use C#8, so you can't use Ranges, replace return list[^countFromEnd] with return list[list.Count - countFromEnd].

Tim Schmelter
  • 450,073
  • 74
  • 686
  • 939
  • .NET Core has a [TakeLast](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.takelast?view=net-5.0). Just found out about it too – Panagiotis Kanavos Jun 02 '21 at 16:32
  • @PanagiotisKanavos: Yes, but OP seems to want a single line, the second to last. `TakeLast(2)` would give you the last two lines. But you're right, i also always forget about that method – Tim Schmelter Jun 02 '21 at 16:37
  • There's a `SkipLast` too, so `TakeLast(2).SkipLast(1)`. Or `TakeLast(2).First()` – Panagiotis Kanavos Jun 02 '21 at 16:40
  • @PanagiotisKanavos: Yes, or `TakeLast(2).ElementAtOrDefault(0)`. – Tim Schmelter Jun 02 '21 at 16:40
  • "list.Length", in usage section of response, had to change to list.Count. Also it seems "using var ..." is for C# 8 which I am not using. – NoBullMan Jun 02 '21 at 16:47
  • Most of the answers posted here worked. However, I marked this one as answer since it uses extension that I can re-use in other instances. – NoBullMan Jun 02 '21 at 17:07
  • @NoBullMan: You're right, fixed it. `IList` has a `Count` property, but it works also with arrays. With everything else it needs to enumerate it. And yes, the `using var` is also a C#8 feature. Needs to be replaced with `using(var ...{})` – Tim Schmelter Jun 02 '21 at 17:27
-1

Something like this might do it for you

 var lines = System.IO.File.ReadLines(@"SomeFile.log");
 var secondLastIdx = lines.Count() - 2;
 var secondlast = lines.Skip(secondLastIdx ).First();

You might need to use something better to figure out secondLastIdx

ShanieMoonlight
  • 1,623
  • 3
  • 17
  • 28