lines

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 2, 2025 License: MIT Imports: 4 Imported by: 0

README

GoDoc reference Lint Test

Lines

Package lines provides iterator-based tools to read, search, and modify lines in text files and streams.

go get mz.attahri.com/code/lines

Example

// Count TODO comments in a file
count, err := lines.CountFunc(lines.All(r), func(line string) bool {
    return strings.Contains(line, "TODO")
})

See godoc for complete documentation and examples.

Contributions

Contributions are welcome via Pull Requests.

Documentation

Overview

Package lines provides iterator-based tools to read, search, and modify lines in text files and streams.

Generic Text Support

Many functions in this package are generic over the Text constraint, accepting both string and []byte. This allows working with line content in whichever form is most convenient for your use case.

Line Numbers

All line numbers in this package are 1-based to match editor conventions. The first line of a file is line 1, not line 0.

Iterators

The core abstraction is Scanner, a type alias for iter.Seq2[*Line, error]. Create scanners with All (forward) or Backward (reverse), then compose them with Filter, Take, Skip, and Range. Use Head and Tail for convenient access to the first or last n lines from a reader.

Searching

Use Contains, ContainsFunc, Index, and Count to search through lines. These functions accept a Scanner, allowing them to work with any iterator.

Modifying

Use Rewrite for in-place file transformations, or convenience functions like Replace, Remove, InsertAt, and Truncate. Note that Rewrite buffers the entire file in memory; for large files, use Transform with a temporary file instead.

Index

Examples

Constants

View Source
const EOL string = "\n"

EOL is the line separator used when splitting and joining lines.

Variables

View Source
var ErrDrop = errors.New("drop line")

ErrDrop is returned by a WriterFunc to indicate that the current line should be removed.

Functions

func Append

func Append[T Text](w io.Writer, lines ...T) (n int, err error)

Append writes the given lines to w. EOL is prepended to each line before it's written.

func Contains

func Contains[T Text](s Scanner, l T) (bool, error)

Contains reports whether s contains a line equal to l.

Example

ExampleContains demonstrates checking if a line exists in a scanner.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("apple\nbanana\ncherry")
	found, err := lines.Contains(lines.All(r), "banana")
	if err != nil {
		return
	}
	fmt.Println(found)
}
Output:

true

func ContainsFunc

func ContainsFunc[T Text](s Scanner, fn func(l T) bool) (content T, lineno int, err error)

ContainsFunc returns the content and 1-based line number of the first line for which fn returns true. Returns zero values if not found.

Example

The following example finds lines that match a specific regexp rule.

package main

import (
	"log"
	"regexp"
	"strings"

	_ "embed"
	"mz.attahri.com/code/lines"
)

func main() {
	rule := regexp.MustCompile(`^[a-z]+\[\d+\]$`)
	src := strings.NewReader("")

	line, _, err := lines.ContainsFunc(lines.All(src), rule.MatchString)
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("Found it: %#v", line)
}

func Count

func Count[T Text](s Scanner, l T) (int, error)

Count returns the number of lines equal to l.

Example

ExampleCount demonstrates counting lines equal to a given value.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("apple\nbanana\napple\ncherry")
	n, err := lines.Count(lines.All(r), "apple")
	if err != nil {
		return
	}
	fmt.Println(n)
}
Output:

2

func CountFunc

func CountFunc[T Text](s Scanner, fn func(T) bool) (int, error)

CountFunc returns the number of lines for which fn returns true.

Example

ExampleCountFunc demonstrates counting lines matching a predicate.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("short\na]very long line here\nmedium")
	n, err := lines.CountFunc(lines.All(r), func(s string) bool {
		return len(s) > 10
	})
	if err != nil {
		return
	}
	fmt.Println(n)
}
Output:

1

func FindAll added in v0.2.0

func FindAll[T Text](s Scanner, fn func(T) bool) ([]int, error)

FindAll returns the 1-based line numbers of all lines for which fn returns true.

Example

ExampleFindAll demonstrates finding all line numbers matching a predicate.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("TODO: first\ndone\nTODO: second\ndone")
	matches, err := lines.FindAll[string](lines.All(r), func(s string) bool {
		return strings.HasPrefix(s, "TODO")
	})
	if err != nil {
		return
	}
	fmt.Println(matches)
}
Output:

[1 3]

func Index

func Index[T Text](s Scanner, l T) (lineno int, err error)

Index returns the 1-based line number of the first line equal to l. Returns 0 if not found.

Example

ExampleIndex demonstrates finding the line number of the first matching line.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("apple\nbanana\ncherry")
	lineno, err := lines.Index(lines.All(r), "banana")
	if err != nil {
		return
	}
	fmt.Println(lineno)
}
Output:

2

func InsertAt

func InsertAt[T Text](src Truncator, l T, n int) error

InsertAt inserts l before the line at position n (1-based).

func Join

func Join[T Text](lines ...T) T

Join concatenates lines with EOL between them.

func LastIndex added in v0.2.0

func LastIndex[T Text](s Scanner, l T) (int, error)

LastIndex returns the 1-based line number of the last line equal to l. Returns 0 if not found.

Example

ExampleLastIndex demonstrates finding the line number of the last matching line.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("apple\nbanana\napple\ncherry")
	lineno, err := lines.LastIndex(lines.All(r), "apple")
	if err != nil {
		return
	}
	fmt.Println(lineno)
}
Output:

3

func LastIndexFunc added in v0.2.0

func LastIndexFunc[T Text](s Scanner, fn func(T) bool) (int, error)

LastIndexFunc returns the 1-based line number of the last line for which fn returns true. Returns 0 if not found.

func Map added in v0.2.0

func Map[T Text](s Scanner, fn func(*Line) T) iter.Seq2[T, error]

Map returns an iterator that applies fn to each line.

Example

ExampleMap demonstrates transforming each line's content.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("hello\nworld")
	for upper, err := range lines.Map[string](lines.All(r), func(l *lines.Line) string {
		return strings.ToUpper(l.String())
	}) {
		if err != nil {
			break
		}
		fmt.Println(upper)
	}
}
Output:

HELLO
WORLD

func Remove

func Remove[T Text](f Truncator, str T, n int) error

Remove removes the first n lines equal to str. If n < 0, all matching lines are removed.

func RemoveFunc

func RemoveFunc[T Text](f Truncator, fn func(line T) bool, n int) error

RemoveFunc removes the first n lines matching fn. If n < 0, all matching lines are removed.

func Replace

func Replace[T Text](f Truncator, old, replacement T, n int) error

Replace replaces the first n lines equal to old with replacement. If n < 0, all matching lines are replaced.

func ReplaceFunc

func ReplaceFunc[T Text](f Truncator, fn func(line T) bool, replacement T, n int) error

ReplaceFunc replaces the first n lines matching fn with replacement. If n < 0, all matching lines are replaced.

func Rewrite

func Rewrite(src Truncator, fn WriterFunc) error

Rewrite applies fn to each line in src, modifying the file in place. Return ErrDrop from fn to remove a line.

The entire transformed content is buffered in memory before writing back to ensure atomicity. For large files, consider using Transform with a temporary file instead.

Example

Remove all comment lines starting with "#" from a file.

package main

import (
	"io"
	"log"
	"os"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	fn := func(w io.Writer, line *lines.Line) error {
		if strings.HasPrefix(line.String(), "#") {
			return lines.ErrDrop
		}
		_, err := w.Write(line.Content)
		return err
	}

	f, err := os.OpenFile("file.txt", os.O_RDWR, 0o644)
	if err != nil {
		log.Fatal(err)
	}

	if err := lines.Rewrite(f, fn); err != nil {
		if err := f.Close(); err != nil {
			log.Fatal(err)
		}
		log.Fatal(err)
	}
	if err := f.Close(); err != nil {
		log.Fatal(err)
	}
}

func Transform

func Transform(src io.Reader, dst io.Writer, fn WriterFunc) error

Transform applies fn to each line from src and writes results to dst. Return ErrDrop from fn to skip a line. Lines are processed in a streaming fashion without buffering the entire file, making it suitable for large files. The caller is responsible for positioning src (e.g., seek to start if needed) before calling.

func Truncate

func Truncate(src Truncator, n int) error

Truncate keeps only the first n lines in src.

Types

type Line

type Line struct {
	Content []byte // line content without the trailing newline
	Number  int    // 1-based line number
	// contains filtered or unexported fields
}

Line represents a single line from a source. It implements io.Reader, io.ReaderAt, and io.WriterTo.

func Collect added in v0.2.0

func Collect(s Scanner) ([]*Line, error)

Collect materializes the iterator into a slice.

Example

ExampleCollect demonstrates materializing a scanner into a slice.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("one\ntwo\nthree")
	all, err := lines.Collect(lines.All(r))
	if err != nil {
		return
	}
	fmt.Printf("Got %d lines\n", len(all))
	fmt.Println(all[1].String())
}
Output:

Got 3 lines
two

func Get

func Get(s Scanner, n int) (*Line, error)

Get returns the line at position n (1-based). Returns nil if n is out of range.

Example

ExampleGet demonstrates retrieving a specific line by number.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("one\ntwo\nthree")
	line, err := lines.Get(lines.All(r), 2)
	if err != nil {
		return
	}
	fmt.Println(line.String())
}
Output:

two

func (*Line) Bytes

func (l *Line) Bytes() []byte

Bytes returns a copy of the line content as a byte slice.

func (*Line) Equal

func (l *Line) Equal(o *Line) bool

Equal reports whether l and o have the same content and number.

func (*Line) Len added in v0.2.0

func (l *Line) Len() int

Len returns the length of the line content in bytes.

func (*Line) Read

func (l *Line) Read(p []byte) (n int, err error)

Read implements io.Reader.

func (*Line) ReadAt

func (l *Line) ReadAt(p []byte, off int64) (n int, err error)

ReadAt implements io.ReaderAt.

func (*Line) Reset added in v0.2.0

func (l *Line) Reset()

Reset resets the read cursor for the io.Reader interface.

func (*Line) String

func (l *Line) String() string

String returns the line content as a string.

func (*Line) WriteTo

func (l *Line) WriteTo(w io.Writer) (int64, error)

WriteTo implements io.WriterTo.

type Scanner

type Scanner = iter.Seq2[*Line, error]

Scanner iterates over lines, yielding each line and any error encountered.

func All

func All(r io.Reader) Scanner

All returns an iterator over all lines in r, starting from the beginning.

Example

ExampleAll demonstrates iterating over all lines from a reader.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	const txt = "First line\nSecond line\nThird line"

	for line, err := range lines.All(strings.NewReader(txt)) {
		if err != nil {
			break
		}
		fmt.Printf("%d: %s\n", line.Number, line.Content)
	}
}
Output:

1: First line
2: Second line
3: Third line

func Backward

func Backward(r io.ReadSeeker) Scanner

Backward returns an iterator over all lines in r, starting from the end. The returned lines are in reverse order (last line first). Line numbers are assigned sequentially starting from 1, so the last line of the file has Number 1, the second-to-last has Number 2, and so on.

Example

ExampleBackward demonstrates iterating over lines in reverse order.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	const txt = "First line\nSecond line\nThird line"

	for line, err := range lines.Backward(strings.NewReader(txt)) {
		if err != nil {
			break
		}
		fmt.Printf("%d: %s\n", line.Number, line.Content)
	}
}
Output:

1: Third line
2: Second line
3: First line

func Filter

func Filter[T Text](s Scanner, l T) Scanner

Filter returns an iterator that yields only lines equal to l.

Example

ExampleFilter demonstrates filtering lines that exactly match a given value.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("apple\nbanana\napricot\ncherry")
	for line, err := range lines.Filter(lines.All(r), "banana") {
		if err != nil {
			break
		}
		fmt.Printf("Line %d: %s\n", line.Number, line.String())
	}
}
Output:

Line 2: banana

func FilterFunc

func FilterFunc[T Text](s Scanner, fn func(T) bool) Scanner

FilterFunc returns an iterator that yields only lines for which fn returns true.

Example

ExampleFilterFunc demonstrates filtering lines using a custom predicate function.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("apple\nbanana\napricot\ncherry")
	for line, err := range lines.FilterFunc[string](lines.All(r), func(s string) bool {
		return strings.HasPrefix(s, "a")
	}) {
		if err != nil {
			break
		}
		fmt.Println(line.String())
	}
}
Output:

apple
apricot
func Head(r io.Reader, n int) Scanner

Head returns an iterator over the first n lines in r. Returns an empty iterator if n <= 0.

Example

ExampleHead demonstrates iterating over the first n lines of a reader.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("one\ntwo\nthree\nfour\nfive")
	for line, err := range lines.Head(r, 3) {
		if err != nil {
			break
		}
		fmt.Println(line.String())
	}
}
Output:

one
two
three

func Range added in v0.2.0

func Range(s Scanner, start, end int) Scanner

Range returns an iterator over lines from start to end (1-based, inclusive). Returns an empty iterator if start > end or start < 1.

Example

ExampleRange demonstrates extracting a range of lines by line number.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("one\ntwo\nthree\nfour\nfive")
	for line, err := range lines.Range(lines.All(r), 2, 4) {
		if err != nil {
			break
		}
		fmt.Println(line.String())
	}
}
Output:

two
three
four

func Skip added in v0.2.0

func Skip(s Scanner, n int) Scanner

Skip returns an iterator that skips the first n lines from s. Returns the original iterator if n <= 0.

Example

ExampleSkip demonstrates skipping the first n lines of a scanner.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("one\ntwo\nthree\nfour\nfive")
	for line, err := range lines.Skip(lines.All(r), 3) {
		if err != nil {
			break
		}
		fmt.Println(line.String())
	}
}
Output:

four
five

func SkipWhile added in v0.2.0

func SkipWhile[T Text](s Scanner, fn func(T) bool) Scanner

SkipWhile returns an iterator that skips lines while fn returns true. Once fn returns false, all remaining lines are yielded.

Example

ExampleSkipWhile demonstrates skipping lines while a predicate holds true.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("# comment 1\n# comment 2\ncode\nmore code")
	for line, err := range lines.SkipWhile[string](lines.All(r), func(s string) bool {
		return strings.HasPrefix(s, "#")
	}) {
		if err != nil {
			break
		}
		fmt.Println(line.String())
	}
}
Output:

code
more code

func Tail

func Tail(r io.ReadSeeker, n int) Scanner

Tail returns an iterator over the last n lines in r. Lines are returned in reverse order (last line first). Returns an empty iterator if n <= 0.

Example

ExampleTail demonstrates iterating over the last n lines of a reader in reverse order.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("one\ntwo\nthree\nfour\nfive")
	for line, err := range lines.Tail(r, 2) {
		if err != nil {
			break
		}
		fmt.Println(line.String())
	}
}
Output:

five
four

func Take

func Take(s Scanner, n int) Scanner

Take returns an iterator limited to the first n lines from s. Returns an empty iterator if n <= 0.

Example

ExampleTake demonstrates limiting a scanner to the first n lines.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("one\ntwo\nthree\nfour\nfive")
	for line, err := range lines.Take(lines.All(r), 2) {
		if err != nil {
			break
		}
		fmt.Println(line.String())
	}
}
Output:

one
two

func TakeWhile added in v0.2.0

func TakeWhile[T Text](s Scanner, fn func(T) bool) Scanner

TakeWhile returns an iterator that yields lines while fn returns true. Iteration stops at the first line for which fn returns false.

Example

ExampleTakeWhile demonstrates yielding lines while a predicate holds true.

package main

import (
	"fmt"
	"strings"

	"mz.attahri.com/code/lines"
)

func main() {
	r := strings.NewReader("# comment 1\n# comment 2\ncode\n# not a header")
	for line, err := range lines.TakeWhile[string](lines.All(r), func(s string) bool {
		return strings.HasPrefix(s, "#")
	}) {
		if err != nil {
			break
		}
		fmt.Println(line.String())
	}
}
Output:

# comment 1
# comment 2

type Text

type Text interface {
	~string | ~[]byte
}

Text is a constraint for types that can represent line content.

type Truncator

type Truncator interface {
	io.ReadWriteSeeker
	Truncate(n int64) error
}

Truncator is an io.ReadWriteSeeker that can be truncated. *os.File satisfies this interface.

type WriterFunc

type WriterFunc func(w io.Writer, line *Line) error

WriterFunc is called for each line during Transform or Rewrite. Write to w to output the transformed line content. Return ErrDrop to remove the line entirely.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL