strscan

package module
v0.0.0-...-1cefdb7 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 4 Imported by: 0

README

go-ruby-strscan/strscan

strscan

A pure-Go, CGO-free reimplementation of Ruby's StringScanner (the stdlib strscan library), matching MRI 4.0.5's observable behavior. A Scanner walks a string left to right, matching regular expressions anchored at — or searched forward from — the current scan position, advancing past each match and recording it so the caller can read back the whole match, its capture groups, and the surrounding text.

Pattern matching is delegated to the sibling pure-Go Onigmo engine github.com/go-ruby-regexp/regexp, so the scanner's regex semantics — UTF-8 awareness, named groups, character classes — match Ruby exactly.

  • Pure Go, CGO-free — builds and runs on all six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x).
  • MRI-faithful — verified byte-for-byte against MRI 4.0.5 across a broad differential corpus (anchored vs. forward search, captures, named groups, multibyte UTF-8, pos/charpos, unscan, EOS edges).
  • 100% test coverage, gofmt/go vet clean.

Install

go get github.com/go-ruby-strscan/strscan

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-strscan/strscan"
)

func main() {
	s := strscan.New("2026-06-29 the rest")

	date, _ := s.Scan(`(?<y>\d+)-(?<m>\d+)-(?<d>\d+)`)
	fmt.Println(date) // 2026-06-29

	y, _ := s.GroupName("y")
	m, _ := s.Group(2)
	fmt.Println(y, m) // 2026 06

	s.Scan(` `)
	word, _ := s.ScanUntil(` `) // "the "
	fmt.Printf("%q\n", word)

	fmt.Println(s.Rest()) // rest
	fmt.Println(s.EOS())  // false
}

API

Go method Ruby StringScanner
New(s) StringScanner.new(s)
Scan(pat) scan
ScanUntil(pat) scan_until
Skip(pat) skip
SkipUntil(pat) skip_until
Match(pat) match? (returns the byte length, or -1)
Check(pat) check
CheckUntil(pat) check_until
Peek(n) peek
Getch() getch
Pos() / SetPos(n) pos / pos=
CharPos() charpos
Rest() / RestSize() rest / rest_size
EOS() eos?
Beginning() bol? / beginning_of_line?
Terminate() / Reset() terminate / reset
Matched() / MatchedSize() matched / matched? / matched_size
PreMatch() / PostMatch() pre_match / post_match
Group(i) / GroupName(name) [] (Integer / Symbol)
Unscan() unscan
Concat(s) << / concat
String() / SetString(s) string / string=
Positions are byte offsets

Pos, SetPos, Skip, SkipUntil, Match, and MatchedSize all work in byte offsets, exactly as MRI's StringScanner#pos and friends do. CharPos returns the character index — the number of UTF-8 code points before the position — the way MRI's #charpos does. For all-ASCII input the two coincide.

Anchored matching

Scan, Skip, Match, and Check match the pattern anchored at the current position — the pattern must match starting exactly at Pos. The *Until variants instead search forward and match at the first position ahead where the pattern matches. A pattern that matches only further ahead does not satisfy the anchored methods.

License

BSD-3-Clause. See LICENSE.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package strscan is a pure-Go, CGO-free reimplementation of Ruby's StringScanner (the stdlib "strscan" library) matching MRI 4.0.5's observable behavior. A Scanner walks a string left to right, matching regular expressions anchored at — or searched forward from — the current scan position and advancing past each match, recording the last match so the caller can read it back (the whole match, capture groups, pre/post text).

Patterns are matched by the sibling pure-Go Onigmo engine (github.com/go-ruby-regexp/regexp), so the scanner's regex semantics — UTF-8 awareness, named groups, character classes — match Ruby exactly.

Positions are byte offsets into the underlying string, exactly as MRI's StringScanner#pos reports them; Scanner.CharPos returns the character index (the number of UTF-8 code points before the position) the way MRI's #charpos does. For all-ASCII input the two coincide.

A Scanner is a mutable cursor and is NOT safe for concurrent use.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Error

type Error struct {
	// contains filtered or unexported fields
}

Error is returned by operations that Ruby's StringScanner reports through a StringScanner::Error, currently only Unscan with nothing to undo.

func (*Error) Error

func (e *Error) Error() string

type Scanner

type Scanner struct {
	// contains filtered or unexported fields
}

Scanner is a position cursor over a string driven by regular-expression matches, mirroring Ruby's StringScanner.

func New

func New(s string) *Scanner

New returns a Scanner positioned at the start of s with no recorded match.

func (*Scanner) Beginning

func (s *Scanner) Beginning() bool

Beginning reports whether the position is at the beginning of a line: the start of the string, or immediately after a newline. (Ruby's #bol? / #beginning_of_line?.) It is false at a position past the end of the string.

func (*Scanner) CharPos

func (s *Scanner) CharPos() int

CharPos returns the current scan position as a character index — the number of UTF-8 code points in the text before the position. (Ruby's #charpos.)

func (*Scanner) Check

func (s *Scanner) Check(pattern string) (string, bool)

Check matches pattern anchored at the current position WITHOUT advancing, returning the matched text. "" / false and a cleared match on no match. (Ruby's StringScanner#check.)

func (*Scanner) CheckUntil

func (s *Scanner) CheckUntil(pattern string) (string, bool)

CheckUntil searches forward for pattern like ScanUntil but does NOT advance, returning everything from the current position through the match. "" / false on no match. (Ruby's StringScanner#check_until.)

func (*Scanner) Concat

func (s *Scanner) Concat(more string) *Scanner

Concat appends more text to the end of the scanned string without moving the position, so the scanner can keep going past what was previously the end. It returns the receiver. (Ruby's StringScanner#<< / #concat.)

func (*Scanner) EOS

func (s *Scanner) EOS() bool

EOS reports whether the position is at (or past) the end of the string. (Ruby's #eos?.)

func (*Scanner) Getch

func (s *Scanner) Getch() (string, bool)

Getch returns the single character at the current position and advances one character; "" / false at end-of-string. It records the character as the matched text (with no capture groups). (Ruby's StringScanner#getch.)

func (*Scanner) Group

func (s *Scanner) Group(i int) (string, bool)

Group returns the i-th capture group of the most recent match: 0 is the whole match, a positive index the n-th group. It returns "" / false when there is no current match, the index is out of range, or the group did not participate. For a getch match only group 0 (the character) is present. (Ruby's StringScanner#[] with an Integer.)

func (*Scanner) GroupName

func (s *Scanner) GroupName(name string) (string, bool)

GroupName returns the capture group with the given name from the most recent match, or "" / false when there is no current match, no group has that name, or the group did not participate. (Ruby's StringScanner#[] with a Symbol/String.)

func (*Scanner) Match

func (s *Scanner) Match(pattern string) (int, bool)

Match reports the byte length of a match anchored at the current position WITHOUT advancing it, recording the match; length is -1 / false on no match. (Ruby's StringScanner#match?.)

func (*Scanner) Matched

func (s *Scanner) Matched() (string, bool)

Matched returns the text of the most recent match, or "" / false when the last match attempt failed or nothing has matched yet. (Ruby's #matched / #matched?.)

func (*Scanner) MatchedSize

func (s *Scanner) MatchedSize() int

MatchedSize returns the byte length of the most recent match, or -1 when the last match attempt failed or nothing has matched yet. (Ruby's #matched_size.)

func (*Scanner) Peek

func (s *Scanner) Peek(n int) string

Peek returns up to len bytes... up to n characters? Ruby's #peek takes a byte length and returns that many bytes from the current position (clamped to the end), without advancing. (Ruby's StringScanner#peek.)

func (*Scanner) Pos

func (s *Scanner) Pos() int

Pos returns the current scan position as a byte offset. (Ruby's #pos.)

func (*Scanner) PostMatch

func (s *Scanner) PostMatch() (string, bool)

PostMatch returns the text after the end of the most recent match, or "" / false when there is no current match. (Ruby's #post_match.)

func (*Scanner) PreMatch

func (s *Scanner) PreMatch() (string, bool)

PreMatch returns the text before the start of the most recent match, or "" / false when there is no current match. (Ruby's #pre_match.)

func (*Scanner) Reset

func (s *Scanner) Reset() *Scanner

Reset returns the position to the start and clears the recorded match, returning the receiver. (Ruby's #reset.)

func (*Scanner) Rest

func (s *Scanner) Rest() string

Rest is the unscanned remainder of the string. (Ruby's #rest.)

func (*Scanner) RestSize

func (s *Scanner) RestSize() int

RestSize is the byte length of the unscanned remainder. (Ruby's #rest_size.)

func (*Scanner) Scan

func (s *Scanner) Scan(pattern string) (string, bool)

Scan matches pattern anchored at the current position; on success it records the match, advances past it, and returns the matched text. On failure it returns "" / false, clears the recorded match, and leaves the position. (Ruby's StringScanner#scan.)

func (*Scanner) ScanUntil

func (s *Scanner) ScanUntil(pattern string) (string, bool)

ScanUntil advances to and past the next occurrence of pattern anywhere ahead, returning everything from the old position through the match. On no match it returns "" / false and does not move. #matched holds just the pattern match. (Ruby's StringScanner#scan_until.)

func (*Scanner) SetPos

func (s *Scanner) SetPos(n int) error

SetPos moves the scan position to the byte offset n. A negative n counts back from the end. An offset outside [0, len] returns an error and leaves the position unchanged, mirroring MRI's RangeError. Setting the position clears nothing about the recorded match but, like MRI, makes the next Unscan invalid.

func (*Scanner) SetString

func (s *Scanner) SetString(str string)

SetString replaces the string being scanned, resets the position to the start, and clears the recorded match. (Ruby's #string=.)

func (*Scanner) Skip

func (s *Scanner) Skip(pattern string) (int, bool)

Skip behaves like Scan but returns the byte length of the matched text rather than the text; n is -1 / false on no match. (Ruby's StringScanner#skip.)

func (*Scanner) SkipUntil

func (s *Scanner) SkipUntil(pattern string) (int, bool)

SkipUntil behaves like ScanUntil but returns the byte length from the old position through the match (-1 / false on no match). (Ruby's #skip_until.)

func (*Scanner) String

func (s *Scanner) String() string

String returns the whole string being scanned. (Ruby's #string.)

func (*Scanner) Terminate

func (s *Scanner) Terminate() *Scanner

Terminate jumps the position to the end of the string and clears the recorded match; it returns the receiver for chaining. (Ruby's #terminate.)

func (*Scanner) Unscan

func (s *Scanner) Unscan() error

Unscan undoes the most recent advancing match, restoring the position to where it was before that match and clearing the recorded match. It returns an error (Ruby's StringScanner::Error) when there is nothing to undo — no prior successful scan/skip/getch, or the last match attempt failed. (Ruby's #unscan.)

Jump to

Keyboard shortcuts

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