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 ¶
- Constants
- Variables
- func Append[T Text](w io.Writer, lines ...T) (n int, err error)
- func Contains[T Text](s Scanner, l T) (bool, error)
- func ContainsFunc[T Text](s Scanner, fn func(l T) bool) (content T, lineno int, err error)
- func Count[T Text](s Scanner, l T) (int, error)
- func CountFunc[T Text](s Scanner, fn func(T) bool) (int, error)
- func FindAll[T Text](s Scanner, fn func(T) bool) ([]int, error)
- func Index[T Text](s Scanner, l T) (lineno int, err error)
- func InsertAt[T Text](src Truncator, l T, n int) error
- func Join[T Text](lines ...T) T
- func LastIndex[T Text](s Scanner, l T) (int, error)
- func LastIndexFunc[T Text](s Scanner, fn func(T) bool) (int, error)
- func Map[T Text](s Scanner, fn func(*Line) T) iter.Seq2[T, error]
- func Remove[T Text](f Truncator, str T, n int) error
- func RemoveFunc[T Text](f Truncator, fn func(line T) bool, n int) error
- func Replace[T Text](f Truncator, old, replacement T, n int) error
- func ReplaceFunc[T Text](f Truncator, fn func(line T) bool, replacement T, n int) error
- func Rewrite(src Truncator, fn WriterFunc) error
- func Transform(src io.Reader, dst io.Writer, fn WriterFunc) error
- func Truncate(src Truncator, n int) error
- type Line
- func (l *Line) Bytes() []byte
- func (l *Line) Equal(o *Line) bool
- func (l *Line) Len() int
- func (l *Line) Read(p []byte) (n int, err error)
- func (l *Line) ReadAt(p []byte, off int64) (n int, err error)
- func (l *Line) Reset()
- func (l *Line) String() string
- func (l *Line) WriteTo(w io.Writer) (int64, error)
- type Scanner
- func All(r io.Reader) Scanner
- func Backward(r io.ReadSeeker) Scanner
- func Filter[T Text](s Scanner, l T) Scanner
- func FilterFunc[T Text](s Scanner, fn func(T) bool) Scanner
- func Head(r io.Reader, n int) Scanner
- func Range(s Scanner, start, end int) Scanner
- func Skip(s Scanner, n int) Scanner
- func SkipWhile[T Text](s Scanner, fn func(T) bool) Scanner
- func Tail(r io.ReadSeeker, n int) Scanner
- func Take(s Scanner, n int) Scanner
- func TakeWhile[T Text](s Scanner, fn func(T) bool) Scanner
- type Text
- type Truncator
- type WriterFunc
Examples ¶
Constants ¶
const EOL string = "\n"
EOL is the line separator used when splitting and joining lines.
Variables ¶
var ErrDrop = errors.New("drop line")
ErrDrop is returned by a WriterFunc to indicate that the current line should be removed.
Functions ¶
func Append ¶
Append writes the given lines to w. EOL is prepended to each line before it's written.
func Contains ¶
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 ¶
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 ¶
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 ¶
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
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 ¶
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 LastIndex ¶ added in v0.2.0
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
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
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 ¶
Remove removes the first n lines equal to str. If n < 0, all matching lines are removed.
func RemoveFunc ¶
RemoveFunc removes the first n lines matching fn. If n < 0, all matching lines are removed.
func Replace ¶
Replace replaces the first n lines equal to old with replacement. If n < 0, all matching lines are replaced.
func ReplaceFunc ¶
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 ¶
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.
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
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 ¶
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) ReadAt ¶
ReadAt implements io.ReaderAt.
type Scanner ¶
Scanner iterates over lines, yielding each line and any error encountered.
func All ¶
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 ¶
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 ¶
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 ¶
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
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
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
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 ¶
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
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 Truncator ¶
type Truncator interface {
io.ReadWriteSeeker
Truncate(n int64) error
}
Truncator is an io.ReadWriteSeeker that can be truncated. *os.File satisfies this interface.