bitfield/script

Add a function to reverse the order of content lines

Closed this issue · 8 comments

Add a ReverseLines function to the script library that reverses the order of lines in the input pipe. It’ll be useful in cases where users need to work with data starting from the end — for example, displaying the newest log entries first or processing recent records before older ones.

Great suggestion, @Aminkbi! Shall we start by trying to write a program that shows how this feature would work for your use case? For example, if you're checking log entries for something, let's write that program, using Reverse as though it already existed, so that we can see what API would make sense.

Hi @Aminkbi, are you looking for something like tac (reverse of cat)

So you can get the file line by line in reverse order (last line to first), if that's the case then something like this can be added in scripts.

I can also think of a few use cases:

  • Viewing logs from most recent entries (instead of scrolling to the end, see the latest logs first)
  • Tail-like behavior with filters (find the most recent occurrence of a pattern (like "error")
  • Reversing output for human readability (if todo list has the oldest tasks at the top, reversing makes newer tasks more visible)
  • Use in scripting/automation (undo order for processing pipelines)
  • Reverse execution order in command histories

I'm thinking of similar implementations as cat, but it will return a pipe that reads from the file path in reverse line order

@bitfield, wondering if you'd be open to adding a utility function similar to tac. Happy to hear your thoughts on whether this aligns with the goals of the scripts package, and if there are any design guidelines you'd like me to consider.

I’d be happy to contribute this feature via a PR if you think it’s a good fit for the project. Thanks

@kumarmunish — sure! Let's start with the demonstration program I mentioned in the previous comment.

Sure, @bitfield

Reverse entire log file

package main

import (
	"fmt"
	"log"
	"slices"

	"github.com/bitfield/script"
)

func main() {
	lines, err := script.File("app.log").Slice()
	if err != nil {
		log.Fatalf("failed to read log file: %v", err)
	}

	slices.Reverse(lines)

	fmt.Println("Logs (newest first):")
	for _, line := range lines {
		fmt.Println(line)
	}
}

Wouldn't you use slices.Reverse instead? But these are the programs that you'd have to write without Reverse being a pipe method, aren't they? What we want for design purposes is the program you'd write with such a feature (I often call this the magic function approach to designing APIs.)

@bitfield you're right — thanks for that design prompt. Here's a version of the program using Reverse() as if it were already a method on *script.Pipe, just to explore how the API would feel in a real-world use case:

package main

import (
	"log"

	"github.com/bitfield/script"
)

func main() {
	err := script.File("app.log").
		Match("ERROR").
		Reverse(). // pretend this exists
		Stdout()
	if err != nil {
		log.Fatal(err)
	}
}

This kind of fluent chaining makes the intent really clear — filter the logs, reverse them, and output — all without breaking the pipeline. It also helped me realize that writing a manual reverse loop earlier was a gap in my understanding of what you were aiming for — not just implementation, but how a good API should feel to use. The “magic function” approach is a great way to think about that, and I appreciate you highlighting it.

Also, I’ve updated the previous version of the program to use slices.Reverse() now that I understand the right place for it. Thanks again for the guidance!

Great! But the improvement over the non-Reverse version is pretty minimal, isn't it? It's literally calling Reverse instead of slices.Reverse.

In general I've not added features to script that simply duplicate what's already in the standard library. If it makes sense as part of a pipeline, I've sometimes done that, but of course Reverse works against the pipeline idea. You have to read the entire pipe to completion before you can start reversing it, so it really doesn't make any difference whether Reverse is built in or not.

It was a useful exercise, and thanks for taking part in it, but I don't see a compelling case for the feature here. There's only been one request for it in all these years, and even that was a hit-and-run (we'll never see @Aminkbi around here again, I dare say).

Thanks @bitfield, for the thoughtful reply — I completely understand your point.

I agree that the improvement is minimal, since it's essentially just calling Reverse instead of slices.Reverse. My thinking was that calling Reverse directly on a Pipe felt more in line with the everything is a pipe model you mentioned elsewhere, whereas operating on slices breaks out of that flow.

That said, your point about needing to materialize the entire pipe and how that breaks the streaming model makes total sense — it clarified why this doesn’t quite fit as a built-in utility.

Also, thank you for sharing your mentoring blog — it’s full of practical insight and will help me become better at thinking through design trade-offs like this. I appreciate the time and thought you’ve put into both the response and the resources.