Compare commits

..

No commits in common. 'master' and 'implementUnicodeCharClass' have entirely different histories.

3
.gitignore vendored

@ -1 +1,2 @@
kg/kg
re

@ -8,6 +8,6 @@ vet: fmt
buildLib: vet
go build -gcflags="all=-N -l" ./...
buildCmd: buildLib
go build -C kg/ -gcflags="all=-N -l" -o kg ./...
go build -C cmd/ -gcflags="all=-N -l" -o re ./...
test: buildCmd
go test -v ./...

@ -1,21 +0,0 @@
## Kleingrep
Kleingrep is a regular expression engine, providing a library and command-line tool written in Go.
It aims to provide a more featureful engine, compared to the one in Go's
[regexp](https://pkg.go.dev/regexp), while retaining some semblance of efficiency.
The engine does __not__ use backtracking, relying on the NFA-based method described in
[Russ Cox's articles](https://swtch.com/~rsc/regexp). As such, it is immune to catastrophic backtracking.
It also includes features not present in regexp, such as lookarounds and backreferences.
### Syntax
The syntax is, for the most part, a superset of Go's regexp. A full overview of the syntax can be found [here](https://pkg.go.dev/gitea.twomorecents.org/Rockingcool/kleingrep/regex#hdr-Syntax).
__For more information, see https://pkg.go.dev/gitea.twomorecents.org/Rockingcool/kleingrep/regex__.
### How it works
I've written about the inner workings of the engine [on my blog](https://twomorecents.org/writing-regex-engine/index.html).

@ -1,11 +1,6 @@
package main
import (
"bufio"
"os"
"slices"
"strings"
)
import "slices"
type character interface {
int | rune
@ -30,23 +25,3 @@ func genRange[T character](start, end T) []T {
}
return toRet
}
// Returns whether or not the given file contains a NULL character
func fileContainsNullChar(filename string) (bool, error) {
file, err := os.Open(filename)
if err != nil {
return true, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "\000") {
return true, nil
}
}
if err := scanner.Err(); err != nil {
return true, err
}
return false, nil
}

@ -0,0 +1,227 @@
package main
import (
"bufio"
"flag"
"fmt"
"io"
"os"
"github.com/fatih/color"
reg "gitea.twomorecents.org/Rockingcool/kleingrep/regex"
)
func main() {
// Flags for the regex Compile function
flagsToCompile := make([]reg.ReFlag, 0)
invertFlag := flag.Bool("v", false, "Invert match.")
// This flag has two 'modes':
// 1. Without '-v': Prints only matches. Prints a newline after every match.
// 2. With '-v': Substitutes all matches with empty string.
onlyFlag := flag.Bool("o", false, "Print only colored content. Overrides -l.")
lineFlag := flag.Bool("l", false, "Only print lines with a match (or with no matches, if -v is enabled). Similar to grep's default.")
multiLineFlag := flag.Bool("t", false, "Multi-line mode. Treats newline just like any character.")
printMatchesFlag := flag.Bool("p", false, "Prints start and end index of each match. Can only be used with '-t' for multi-line mode.")
caseInsensitiveFlag := flag.Bool("i", false, "Case-insensitive. Disregard the case of all characters.")
matchNum := flag.Int("m", 0, "Print the match with the given index. Eg. -m 3 prints the third match.")
substituteText := flag.String("s", "", "Substitute the contents of each match with the given string. Overrides -o and -v")
flag.Parse()
// These flags have to be passed to the Compile function
if *multiLineFlag {
flagsToCompile = append(flagsToCompile, reg.RE_MULTILINE, reg.RE_SINGLE_LINE)
}
if *caseInsensitiveFlag {
flagsToCompile = append(flagsToCompile, reg.RE_CASE_INSENSITIVE)
}
// -l and -o are mutually exclusive: -o overrides -l
if *onlyFlag {
*lineFlag = false
}
// Check if substitute and matchNum flags have been enabled
substituteFlagEnabled := false
matchNumFlagEnabled := false
flag.Visit(func(f *flag.Flag) {
if f.Name == "s" {
substituteFlagEnabled = true
}
if f.Name == "m" {
matchNumFlagEnabled = true
}
})
// Validate matchNumFlag - must be positive integer
if matchNumFlagEnabled && *matchNum < 1 {
panic("Invalid match number to print.")
}
// Process:
// 1. Convert regex into postfix notation (Shunting-Yard algorithm)
// a. Add explicit concatenation operators to facilitate this
// 2. Build NFA from postfix representation (Thompson's algorithm)
// 3. Run the string against the NFA
if len(flag.Args()) != 1 { // flag.Args() also strips out program name
fmt.Println("ERROR: Missing cmdline args")
os.Exit(22)
}
var re string
re = flag.Args()[0]
var test_str string
var err error
var linesRead bool // Whether or not we have read the lines in the file
lineNum := 0 // Current line number
// Create reader for stdin and writer for stdout
reader := bufio.NewReader(os.Stdin)
out := bufio.NewWriter(os.Stdout)
regComp, err := reg.Compile(re, flagsToCompile...)
if err != nil {
fmt.Println(err)
return
}
for true {
if linesRead {
break
}
if !(*multiLineFlag) {
// Read every string from stdin until we encounter an error. If the error isn't EOF, panic.
test_str, err = reader.ReadString('\n')
lineNum++
if err != nil {
if err == io.EOF {
linesRead = true
} else {
panic(err)
}
}
if len(test_str) > 0 && test_str[len(test_str)-1] == '\n' {
test_str = test_str[:len(test_str)-1]
}
} else {
// Multi-line mode - read every line of input into a temp. string.
// test_str will contain all lines of input (including newline characters)
// as one string.
var temp string
for temp, err = reader.ReadString('\n'); err == nil; temp, err = reader.ReadString('\n') {
test_str += temp
}
// Assuming err != nil
if err == io.EOF {
if len(temp) > 0 {
test_str += temp // Add the last line (if it is non-empty)
}
linesRead = true
} else {
panic(err)
}
}
matchIndices := make([]reg.Match, 0)
if matchNumFlagEnabled {
tmp, err := regComp.FindNthMatch(test_str, *matchNum)
if err == nil {
matchIndices = append(matchIndices, tmp)
}
} else {
matchIndices = regComp.FindAllSubmatch(test_str)
}
test_str_runes := []rune(test_str) // Converting to runes preserves unicode characters
if *printMatchesFlag {
// if we are in single line mode, print the line on which
// the matches occur
if len(matchIndices) > 0 {
if !(*multiLineFlag) {
fmt.Fprintf(out, "Line %d:\n", lineNum)
}
for _, m := range matchIndices {
fmt.Fprintf(out, "%s\n", m.String())
}
err := out.Flush()
if err != nil {
panic(err)
}
}
continue
}
// Decompose the array of matchIndex structs into a flat unique array of ints - if matchIndex is {4,7}, flat array will contain 4,5,6
// This should make checking O(1) instead of O(n)
indicesToPrint := new_uniq_arr[int]()
for _, idx := range matchIndices {
indicesToPrint.add(genRange(idx[0].StartIdx, idx[0].EndIdx)...)
}
// If we are inverting, then we should print the indices which _didn't_ match
// in color.
if *invertFlag {
oldIndices := indicesToPrint.values()
indicesToPrint = new_uniq_arr[int]()
// Explanation:
// Find all numbers from 0 to len(test_str_runes) that are NOT in oldIndices.
// These are the values we want to print, now that we have inverted the match.
// Re-initialize indicesToPrint and add all of these values to it.
indicesToPrint.add(setDifference(genRange(0, len(test_str_runes)), oldIndices)...)
}
// If lineFlag is enabled, we should only print something if:
// a. We are not inverting, and have at least one match on the current line
// OR
// b. We are inverting, and have no matches at all on the current line.
// This checks for the inverse, and continues if it is true.
if *lineFlag {
if !(*invertFlag) && len(matchIndices) == 0 || *invertFlag && len(matchIndices) > 0 {
continue
}
}
// If we are substituting, we need a different behavior, as follows:
// For every character in the test string:
// 1. Check if the index is the start of any matchIndex
// 2. If so, print the substitute text, and set our index to
// the corresponding end index.
// 3. If not, just print the character.
if substituteFlagEnabled {
for i := range test_str_runes {
inMatchIndex := false
for _, m := range matchIndices {
if i == m[0].StartIdx {
fmt.Fprintf(out, "%s", *substituteText)
i = m[0].EndIdx
inMatchIndex = true
break
}
}
if !inMatchIndex {
fmt.Fprintf(out, "%c", test_str_runes[i])
}
}
} else {
for i, c := range test_str_runes {
if indicesToPrint.contains(i) {
color.New(color.FgRed).Fprintf(out, "%c", c)
// Newline after every match - only if -o is enabled and -v is disabled.
if *onlyFlag && !(*invertFlag) {
for _, idx := range matchIndices {
if i+1 == idx[0].EndIdx { // End index is one more than last index of match
fmt.Fprintf(out, "\n")
break
}
}
}
} else {
if !(*onlyFlag) {
fmt.Fprintf(out, "%c", c)
}
}
}
}
err = out.Flush()
if err != nil {
panic(err)
}
fmt.Println()
}
}

@ -36,7 +36,3 @@ func (s uniq_arr[T]) values() []T {
}
return toRet
}
func (s uniq_arr[T]) len() int {
return len(s.backingMap)
}

@ -1,298 +0,0 @@
package main
import (
"bufio"
"flag"
"fmt"
"io"
"os"
"slices"
"github.com/fatih/color"
reg "gitea.twomorecents.org/Rockingcool/kleingrep/regex"
)
func main() {
// Flags for the regex Compile function
flagsToCompile := make([]reg.ReFlag, 0)
invertFlag := flag.Bool("v", false, "Invert match.")
// This flag has two 'modes':
// 1. Without '-v': Prints only matches. Prints a newline after every match.
// 2. With '-v': Substitutes all matches with empty string.
onlyFlag := flag.Bool("o", false, "Print only colored content. Overrides -l.")
lineFlag := flag.Bool("l", false, "Only print lines with a match (or with no matches, if -v is enabled). Similar to grep's default.")
multiLineFlag := flag.Bool("t", false, "Multi-line mode. Treats newline just like any character.")
printMatchesFlag := flag.Bool("p", false, "Prints start and end index of each match. Can only be used with '-t' for multi-line mode.")
caseInsensitiveFlag := flag.Bool("i", false, "Case-insensitive. Disregard the case of all characters.")
recursiveFlag := flag.Bool("r", false, "Recursively search all files in the given directory.")
lineNumFlag := flag.Bool("n", false, "For each line with a match, print the line number. Implies -l.")
matchNum := flag.Int("m", 0, "Print the match with the given index. Eg. -m 3 prints the third match.")
substituteText := flag.String("s", "", "Substitute the contents of each match with the given string. Overrides -o and -v")
flag.Parse()
// These flags have to be passed to the Compile function
if *multiLineFlag {
flagsToCompile = append(flagsToCompile, reg.RE_MULTILINE, reg.RE_SINGLE_LINE)
}
if *caseInsensitiveFlag {
flagsToCompile = append(flagsToCompile, reg.RE_CASE_INSENSITIVE)
}
// -l and -o are mutually exclusive: -o overrides -l
if *onlyFlag {
*lineFlag = false
}
// Check if substitute and matchNum flags have been enabled
substituteFlagEnabled := false
matchNumFlagEnabled := false
flag.Visit(func(f *flag.Flag) {
if f.Name == "s" {
substituteFlagEnabled = true
}
if f.Name == "m" {
matchNumFlagEnabled = true
}
})
// Validate matchNumFlag - must be positive integer
if matchNumFlagEnabled && *matchNum < 1 {
panic("Invalid match number to print.")
}
// Enable lineFlag if lineNumFlag is enabled
if *lineNumFlag {
*lineFlag = true
}
// Process:
// 1. Convert regex into postfix notation (Shunting-Yard algorithm)
// a. Add explicit concatenation operators to facilitate this
// 2. Build NFA from postfix representation (Thompson's algorithm)
// 3. Run the string against the NFA
if len(flag.Args()) < 1 { // flag.Args() also strips out program name
fmt.Printf("%s: ERROR: Missing cmdline args\n", os.Args[0])
os.Exit(22)
}
if *recursiveFlag && len(flag.Args()) < 2 { // File/Directory must be provided with '-r'
fmt.Printf("%s: ERROR: Missing cmdline args\n", os.Args[0])
os.Exit(22)
}
var re string
re = flag.Args()[0]
var inputFiles []*os.File
if len(flag.Args()) == 1 || flag.Args()[1] == "-" { // Either no file argument, or file argument is "-"
if !slices.Contains(inputFiles, os.Stdin) {
inputFiles = append(inputFiles, os.Stdin) // os.Stdin cannot be entered more than once into the file list
}
} else {
inputFilenames := flag.Args()[1:]
for _, inputFilename := range inputFilenames {
inputFile, err := os.Open(inputFilename)
defer inputFile.Close()
if err != nil {
fmt.Printf("%s: %s: No such file or directory\n", os.Args[0], inputFilename)
} else {
fileStat, err := inputFile.Stat()
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(2)
} else {
if fileStat.Mode().IsDir() {
fmt.Printf("%s: %s: Is a directory\n", os.Args[0], inputFilename)
} else {
var nullCharPresent bool
if nullCharPresent, err = fileContainsNullChar(inputFilename); nullCharPresent {
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(1)
} else {
fmt.Printf("%s: %s: Is a binary file\n", os.Args[0], inputFilename)
}
} else {
inputFiles = append(inputFiles, inputFile)
}
}
}
}
}
}
if len(inputFiles) == 0 { // No valid files given
os.Exit(2)
}
var test_str string
var err error
var linesRead bool // Whether or not we have read the lines in the file
lineNum := 0 // Current line number
// Create writer for stdout
out := bufio.NewWriter(os.Stdout)
// Compile regex
regComp, err := reg.Compile(re, flagsToCompile...)
if err != nil {
fmt.Println(err)
return
}
for _, inputFile := range inputFiles {
lineNum = 0
reader := bufio.NewReader(inputFile)
linesRead = false
for true {
if linesRead {
break
}
if !(*multiLineFlag) {
// Read every string from stdin until we encounter an error. If the error isn't EOF, panic.
test_str, err = reader.ReadString('\n')
lineNum++
if err != nil {
if err == io.EOF {
linesRead = true
} else {
panic(err)
}
}
if len(test_str) > 0 && test_str[len(test_str)-1] == '\n' {
test_str = test_str[:len(test_str)-1]
}
} else {
// Multi-line mode - read every line of input into a temp. string.
// test_str will contain all lines of input (including newline characters)
// as one string.
var temp string
for temp, err = reader.ReadString('\n'); err == nil; temp, err = reader.ReadString('\n') {
test_str += temp
}
// Assuming err != nil
if err == io.EOF {
if len(temp) > 0 {
test_str += temp // Add the last line (if it is non-empty)
}
linesRead = true
} else {
panic(err)
}
}
matchIndices := make([]reg.Match, 0)
if matchNumFlagEnabled {
tmp, err := regComp.FindNthMatch(test_str, *matchNum)
if err == nil {
matchIndices = append(matchIndices, tmp)
}
} else {
matchIndices = regComp.FindAllSubmatch(test_str)
}
test_str_runes := []rune(test_str) // Converting to runes preserves unicode characters
if *printMatchesFlag {
// if we are in single line mode, print the line on which
// the matches occur
if len(matchIndices) > 0 {
if !(*multiLineFlag) {
fmt.Fprintf(out, "Line %d:\n", lineNum)
}
for _, m := range matchIndices {
fmt.Fprintf(out, "%s\n", m.String())
}
err := out.Flush()
if err != nil {
panic(err)
}
}
continue
}
// Decompose the array of matchIndex structs into a flat unique array of ints - if matchIndex is {4,7}, flat array will contain 4,5,6
// This should make checking O(1) instead of O(n)
indicesToPrint := new_uniq_arr[int]()
for _, idx := range matchIndices {
indicesToPrint.add(genRange(idx[0].StartIdx, idx[0].EndIdx)...)
}
// If we are inverting, then we should print the indices which _didn't_ match
// in color.
if *invertFlag {
oldIndices := indicesToPrint.values()
indicesToPrint = new_uniq_arr[int]()
// Explanation:
// Find all numbers from 0 to len(test_str_runes) that are NOT in oldIndices.
// These are the values we want to print, now that we have inverted the match.
// Re-initialize indicesToPrint and add all of these values to it.
indicesToPrint.add(setDifference(genRange(0, len(test_str_runes)), oldIndices)...)
}
// If lineFlag is enabled, we should only print something if:
// a. We are not inverting, and have at least one match on the current line
// OR
// b. We are inverting, and have no matches at all on the current line.
// This checks for the inverse, and continues if it is true.
if *lineFlag {
if !(*invertFlag) && len(matchIndices) == 0 || *invertFlag && len(matchIndices) > 0 {
continue
} else {
if *recursiveFlag || len(flag.Args()) > 2 { // If we have 2 args, then we're only searching 1 file. We should only print the filename if there's more than 1 file.
color.New(color.FgMagenta).Fprintf(out, "%s:", inputFile.Name()) // Print filename
}
if *lineNumFlag {
color.New(color.FgGreen).Fprintf(out, "%d:", lineNum) // Print filename
}
}
}
// If we are substituting, we need a different behavior, as follows:
// For every character in the test string:
// 1. Check if the index is the start of any matchIndex
// 2. If so, print the substitute text, and set our index to
// the corresponding end index.
// 3. If not, just print the character.
if substituteFlagEnabled {
for i := range test_str_runes {
inMatchIndex := false
for _, m := range matchIndices {
if i == m[0].StartIdx {
fmt.Fprintf(out, "%s", *substituteText)
i = m[0].EndIdx
inMatchIndex = true
break
}
}
if !inMatchIndex {
fmt.Fprintf(out, "%c", test_str_runes[i])
}
}
} else {
for i, c := range test_str_runes {
if indicesToPrint.contains(i) {
color.New(color.FgRed, color.Bold).Fprintf(out, "%c", c)
// Newline after every match - only if -o is enabled and -v is disabled.
if *onlyFlag && !(*invertFlag) {
for matchIdxNum, idx := range matchIndices {
if matchIdxNum < len(matchIndices)-1 { // Only print a newline afte printing a match, if there are multiple matches on the line, and we aren't on the last one. This is because the newline that gets added at the end will take care of that.
if i+1 == idx[0].EndIdx { // End index is one more than last index of match
fmt.Fprintf(out, "\n")
break
}
}
}
}
} else {
if !(*onlyFlag) {
fmt.Fprintf(out, "%c", c)
}
}
}
}
err = out.Flush()
if err != nil {
panic(err)
}
// If the last character in the string wasn't a newline, AND we either have don't -o set or we do (and we've matched something), then print a newline
if (len(test_str_runes) > 0 && test_str_runes[len(test_str_runes)-1] != '\n') &&
(!*onlyFlag || indicesToPrint.len() > 0) {
fmt.Println()
}
}
}
}

@ -47,7 +47,6 @@ func (re *Reg) UnmarshalText(text []byte) error {
return err
}
// Longest makes future searches prefer the longest branch of an alternation, as opposed to the leftmost branch.
func (re *Reg) Longest() {
re.preferLongest = true
}
@ -65,7 +64,7 @@ const (
)
func isOperator(c rune) bool {
if c == '+' || c == '?' || c == '*' || c == '|' || c == concatRune || c == lazyPlusRune || c == lazyKleeneRune || c == lazyQuestionRune {
if c == '+' || c == '?' || c == '*' || c == '|' || c == concatRune {
return true
}
return false
@ -73,7 +72,7 @@ func isOperator(c rune) bool {
/* priority returns the priority of the given operator */
func priority(op rune) int {
precedence := []rune{'|', concatRune, '+', lazyPlusRune, '*', lazyKleeneRune, '?', lazyQuestionRune}
precedence := []rune{'|', concatRune, '+', '*', '?'}
return slices.Index(precedence, op)
}
@ -209,6 +208,9 @@ func shuntingYard(re string, flags ...ReFlag) ([]postfixNode, error) {
// metacharacter. Later, in thompson(), these will be converted back. This avoids
// confusion in detecting whether a character is escaped eg. detecting
// whether '\\[a]' has an escaped opening bracket (it doesn't).
//
// 5. Check for non-greedy operators. These are not supported at the moment, so an error
// must be thrown if the user attempts to use a non-greedy operator.
for i := 0; i < len(re_runes_orig); i++ {
c := re_runes_orig[i]
if c == '<' && (i == 0 || (re_runes_orig[i-1] != '\\' && re_runes_orig[i-1] != '?')) {
@ -255,16 +257,8 @@ func shuntingYard(re string, flags ...ReFlag) ([]postfixNode, error) {
} else if c == ']' && (i == 0 || re_runes[len(re_runes)-1] != '\\') {
re_runes = append(re_runes, rbracketRune)
continue
} else if slices.Contains([]rune{'+', '*', '?'}, c) && (i > 0 && re_runes_orig[i-1] != '\\') && (i < len(re_runes_orig)-1 && re_runes_orig[i+1] == '?') {
switch c {
case '+':
re_runes = append(re_runes, lazyPlusRune)
case '*':
re_runes = append(re_runes, lazyKleeneRune)
case '?':
re_runes = append(re_runes, lazyQuestionRune)
}
i++
} else if slices.Contains([]rune{'+', '*', '?'}, c) && (i < len(re_runes_orig)-1 && re_runes_orig[i+1] == '?') {
return nil, fmt.Errorf("non-greedy operators are not supported")
} else {
re_runes = append(re_runes, c)
}
@ -411,10 +405,10 @@ func shuntingYard(re string, flags ...ReFlag) ([]postfixNode, error) {
if i >= len(re_runes) {
return nil, fmt.Errorf("unclosed lookaround")
}
if (re_runes[i] == '(' && re_runes[i-1] != '\\') || re_runes[i] == nonCapLparenRune {
if re_runes[i] == '(' || re_runes[i] == nonCapLparenRune {
numOpenParens++
}
if re_runes[i] == ')' && re_runes[i-1] != '\\' {
if re_runes[i] == ')' {
numOpenParens--
if numOpenParens == 0 {
break
@ -427,7 +421,7 @@ func shuntingYard(re string, flags ...ReFlag) ([]postfixNode, error) {
}
if i < len(re_runes) && (re_runes[i] != '(' && re_runes[i] != nonCapLparenRune && re_runes[i] != '|' && re_runes[i] != '\\') || (i > 0 && re_runes[i-1] == '\\') { // Every character should be concatenated if it is escaped
if i < len(re_runes)-1 {
if re_runes[i+1] != '|' && re_runes[i+1] != '*' && re_runes[i+1] != lazyKleeneRune && re_runes[i+1] != '+' && re_runes[i+1] != lazyPlusRune && re_runes[i+1] != '?' && re_runes[i+1] != lazyQuestionRune && re_runes[i+1] != ')' && re_runes[i+1] != '{' {
if re_runes[i+1] != '|' && re_runes[i+1] != '*' && re_runes[i+1] != '+' && re_runes[i+1] != '?' && re_runes[i+1] != ')' && re_runes[i+1] != '{' {
re_postfix = append(re_postfix, concatRune)
}
}
@ -499,7 +493,7 @@ func shuntingYard(re string, flags ...ReFlag) ([]postfixNode, error) {
}
} else if re_postfix[i] == 'p' || re_postfix[i] == 'P' {
charClassInverted := (re_postfix[i] == 'P')
var charsInClass []rune
charsInClass := []rune{}
i++
if isUnicodeCharClassLetter(re_postfix[i]) {
var err error
@ -590,10 +584,10 @@ func shuntingYard(re string, flags ...ReFlag) ([]postfixNode, error) {
if i >= len(re_postfix) {
return nil, fmt.Errorf("unclosed lookaround")
}
if (re_postfix[i] == '(' && re_postfix[i-1] != '\\') || re_postfix[i] == nonCapLparenRune {
if re_postfix[i] == '(' || re_postfix[i] == nonCapLparenRune {
numOpenParens++
}
if re_postfix[i] == ')' && re_postfix[i-1] != '\\' {
if re_postfix[i] == ')' {
numOpenParens--
if numOpenParens == 0 {
break
@ -714,7 +708,7 @@ func shuntingYard(re string, flags ...ReFlag) ([]postfixNode, error) {
}
} else if re_postfix[i] == 'p' || re_postfix[i] == 'P' {
charClassInverted := (re_postfix[i] == 'P')
var charsInList []rune
charsInList := []rune{}
i++
if isUnicodeCharClassLetter(re_postfix[i]) {
var err error
@ -946,10 +940,6 @@ func shuntingYard(re string, flags ...ReFlag) ([]postfixNode, error) {
}
outQueue[idx].startReps = startRangeNum
outQueue[idx].endReps = endRangeNum
if i < len(re_postfix)-1 && re_postfix[i+1] == '?' { // lazy repitition
outQueue[idx].isLazy = true
i++
}
}
if c == '(' || c == nonCapLparenRune {
opStack = append(opStack, c)
@ -1243,9 +1233,6 @@ func thompson(re []postfixNode) (Reg, error) {
if err != nil {
return Reg{}, err
}
if c.isLazy {
stateToAdd.isLazy = true
}
nfa = append(nfa, stateToAdd)
case plusNode: // a+ is equivalent to aa*
s1 := mustPop(&nfa)
@ -1253,9 +1240,6 @@ func thompson(re []postfixNode) (Reg, error) {
if err != nil {
return Reg{}, err
}
if c.isLazy {
s2.isLazy = true
}
s1 = concatenate(s1, s2)
nfa = append(nfa, s1)
case questionNode: // ab? is equivalent to a(b|)
@ -1267,9 +1251,6 @@ func thompson(re []postfixNode) (Reg, error) {
if err != nil {
return Reg{}, err
}
if c.isLazy {
s2.isLazy = true
}
nfa = append(nfa, s2)
case pipeNode:
// A pipe operator doesn't actually need either operand to be present. If an operand isn't present,
@ -1325,9 +1306,6 @@ func thompson(re []postfixNode) (Reg, error) {
if err != nil {
return Reg{}, err
}
if c.isLazy {
s2.isLazy = true
}
stateToAdd = concatenate(stateToAdd, s2)
} else { // Case 2
for i := c.startReps; i < c.endReps; i++ {
@ -1335,9 +1313,6 @@ func thompson(re []postfixNode) (Reg, error) {
if err != nil {
return Reg{}, fmt.Errorf("error processing bounded repetition")
}
if c.isLazy {
tmp.isLazy = true
}
stateToAdd = concatenate(stateToAdd, tmp)
}
}

@ -60,24 +60,14 @@ Composition:
x|y Match x or y (prefer x)
xy|z Match xy or z (prefer xy)
Repitition:
Greedy:
x* Match x zero or more times, prefer more
x+ Match x one or more times, prefer more
x? Match x zero or one time, prefer one
x{m,n} Match x between m and n times (inclusive), prefer more
x{m,} Match x atleast m times, prefer more
x{,n} Match x between 0 and n times (inclusive), prefer more
x{m} Match x exactly m times
Lazy:
x*? Match x zero or more times, prefer fewer
x+? Match x one or more times, prefer fewer
x?? Match x zero or one time, prefer zero
x{m,n}? Match x between m and n times (inclusive), prefer fewer
x{m,}? Match x atleast m times, prefer fewer
x{,n}? Match x between 0 and n times (inclusive), prefer fewer
Repitition (always greedy, preferring more):
x* Match x zero or more times
x+ Match x one or more times
x? Match x zero or one time
x{m,n} Match x between m and n times (inclusive)
x{m,} Match x atleast m times
x{,n} Match x between 0 and n times (inclusive)
x{m} Match x exactly m times
Grouping:
@ -117,13 +107,17 @@ Numeric ranges:
The engine and the API differ from [regexp] in a few ways, some of them very subtle.
The key differences are mentioned below.
1. Byte-slices and runes:
1. Greediness:
This engine currently does not support non-greedy operators.
2. Byte-slices and runes:
My engine does not support byte-slices. When a matching function receives a string, it converts it into a
rune-slice to iterate through it. While this has some space overhead, the convenience of built-in unicode
support made the tradeoff worth it.
2. Return values
3. Return values
Rather than using primitives for return values, my engine defines two types that are used as return
values: a [Group] represents a capturing group, and a [Match] represents a list of groups.
@ -158,10 +152,9 @@ returns the 0-group.
The following features from [regexp] are (currently) NOT supported:
1. Named capturing groups
2. Negated POSIX classes
2. Non-greedy operators
3. Embedded flags (flags are instead passed as arguments to [Compile])
4. Literal text with \Q ... \E
5. Finite repetition with no start (defaulting at 0)
The following features are not available in [regexp], but are supported in my engine:
1. Lookarounds

@ -234,14 +234,14 @@ func addStateToList(str []rune, idx int, list []nfaState, state nfaState, thread
}
visited = append(visited, state)
if (state.isKleene || state.isQuestion) && (state.isLazy == false) { // Greedy quantifiers
if state.isKleene || state.isQuestion {
copyThread(state.splitState, state)
list := addStateToList(str, idx, list, *state.splitState, threadGroups, visited, preferLongest)
copyThread(state.next, state)
list = addStateToList(str, idx, list, *state.next, threadGroups, visited, preferLongest)
return list
}
if state.isAlternation || ((state.isKleene || state.isQuestion) && state.isLazy) { // Alternation or lazy quantifier
if state.isAlternation {
copyThread(state.next, state)
list := addStateToList(str, idx, list, *state.next, threadGroups, visited, preferLongest)
copyThread(state.splitState, state)

@ -16,11 +16,8 @@ var rparenRune rune = 0xF0006
var nonCapLparenRune rune = 0xF0007 // Represents a non-capturing group's LPAREN
var escBackslashRune rune = 0xF0008 // Represents an escaped backslash
var charRangeRune rune = 0xF0009 // Represents a character range
var lazyKleeneRune rune = 0xF000A // Represents a lazy kleene star
var lazyPlusRune rune = 0xF000B // Represents a lazy plus operator
var lazyQuestionRune rune = 0xF000C // Represents a lazy question operator
var specialChars = []rune{'?', lazyQuestionRune, '*', lazyKleeneRune, '\\', '^', '$', '{', '}', '(', ')', '[', ']', '+', lazyPlusRune, '|', '.', concatRune, '<', '>', lbracketRune, rbracketRune, nonCapLparenRune}
var specialChars = []rune{'?', '*', '\\', '^', '$', '{', '}', '(', ')', '[', ']', '+', '|', '.', concatRune, '<', '>', lbracketRune, rbracketRune, nonCapLparenRune}
// An interface for int and rune, which are identical
type character interface {

@ -34,7 +34,6 @@ type nfaState struct {
isKleene bool // Identifies whether current node is a 0-state representing Kleene star
isQuestion bool // Identifies whether current node is a 0-state representing the question operator
isAlternation bool // Identifies whether current node is a 0-state representing an alternation
isLazy bool // Only for split states - Identifies whether or not to flip the order of branches (try one branch before the other)
splitState *nfaState // Only for alternation states - the 'other' branch of the alternation ('next' is the first)
assert assertType // Type of assertion of current node - NONE means that the node doesn't assert anything
allChars bool // Whether or not the state represents all characters (eg. a 'dot' metacharacter). A 'dot' node doesn't store any contents directly, as it would take up too much space
@ -45,11 +44,11 @@ type nfaState struct {
groupBegin bool // Whether or not the node starts a capturing group
groupEnd bool // Whether or not the node ends a capturing group
groupNum int // Which capturing group the node starts / ends
isBackreference bool // Whether or not current node is backreference
referredGroup int // If current node is a backreference, the node that it points to
// The following properties depend on the current match - I should think about resetting them for every match.
threadGroups []Group // Assuming that a state is part of a 'thread' in the matching process, this array stores the indices of capturing groups in the current thread. As matches are found for this state, its groups will be copied over.
threadBackref int // If current node is a backreference, how many characters to look forward into the referred group
threadGroups []Group // Assuming that a state is part of a 'thread' in the matching process, this array stores the indices of capturing groups in the current thread. As matches are found for this state, its groups will be copied over.
isBackreference bool // Whether or not current node is backreference
referredGroup int // If current node is a backreference, the node that it points to
threadBackref int // If current node is a backreference, how many characters to look forward into the referred group
}
// Clones the NFA starting from the given state.
@ -78,7 +77,6 @@ func cloneStateHelper(stateToClone *nfaState, cloneMap map[*nfaState]*nfaState)
isKleene: stateToClone.isKleene,
isQuestion: stateToClone.isQuestion,
isAlternation: stateToClone.isAlternation,
isLazy: stateToClone.isLazy,
assert: stateToClone.assert,
allChars: stateToClone.allChars,
except: append([]rune{}, stateToClone.except...),
@ -86,8 +84,6 @@ func cloneStateHelper(stateToClone *nfaState, cloneMap map[*nfaState]*nfaState)
groupEnd: stateToClone.groupEnd,
groupBegin: stateToClone.groupBegin,
groupNum: stateToClone.groupNum,
isBackreference: stateToClone.isBackreference,
referredGroup: stateToClone.referredGroup,
}
cloneMap[stateToClone] = clone
for i, s := range stateToClone.output {
@ -425,7 +421,6 @@ func (s nfaState) equals(other nfaState) bool {
s.next == other.next &&
s.isKleene == other.isKleene &&
s.isQuestion == other.isQuestion &&
s.isLazy == other.isLazy &&
s.isAlternation == other.isAlternation &&
s.splitState == other.splitState &&
s.assert == other.assert &&

@ -44,7 +44,6 @@ type postfixNode struct {
lookaroundDir int // Lookbehind or lookahead
nodeContents []postfixNode // ONLY USED WHEN nodetype == CHARCLASS. Holds all the nodes inside the given CHARCLASS node.
referencedGroup int // ONLY USED WHEN nodetype == backreferenceNode. Holds the group which this one refers to. After parsing is done, the expression will be rewritten eg. (a)\1 will become (a)(a). So the return value of ShuntingYard() shouldn't contain a backreferenceNode.
isLazy bool // ONLY USED WHEN nodetype == kleene or question
}
// Converts the given list of postfixNodes to one node of type CHARCLASS.
@ -163,19 +162,10 @@ func newPostfixNode(contents ...rune) postfixNode {
switch contents[0] {
case '+':
to_return.nodetype = plusNode
case lazyPlusRune:
to_return.nodetype = plusNode
to_return.isLazy = true
case '?':
to_return.nodetype = questionNode
case lazyQuestionRune:
to_return.nodetype = questionNode
to_return.isLazy = true
case '*':
to_return.nodetype = kleeneNode
case lazyKleeneRune:
to_return.nodetype = kleeneNode
to_return.isLazy = true
case '|':
to_return.nodetype = pipeNode
case concatRune:

@ -117,7 +117,6 @@ var reTests = []struct {
{`\d{3,4}`, nil, "ababab555", []Group{{6, 9}}},
{`\bpaint\b`, nil, "paints", []Group{}},
{`\b\w{5}\b`, nil, "paint", []Group{{0, 5}}},
{`\w{}`, nil, "test", nil},
{`[^\w]`, nil, "abcdef1230[]qq';;'", []Group{{10, 11}, {11, 12}, {14, 15}, {15, 16}, {16, 17}, {17, 18}}},
{`[^\W]`, nil, "abcdef1230[]qq';;'", []Group{{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 6}, {6, 7}, {7, 8}, {8, 9}, {9, 10}, {12, 13}, {13, 14}}},
{`[\[\]]`, nil, "a[b[l]]", []Group{{1, 2}, {3, 4}, {5, 6}, {6, 7}}},
@ -489,25 +488,7 @@ var reTests = []struct {
{`[b-e]`, nil, `f`, []Group{}},
{`*?`, nil, `-`, nil},
{`a.+c`, nil, `abcabc`, []Group{{0, 6}}},
// Lazy quantifier tests
{`a.+?c`, nil, `abcabc`, []Group{{0, 3}, {3, 6}}},
{`ab*?bc`, []ReFlag{RE_CASE_INSENSITIVE}, `ABBBBC`, []Group{{0, 6}}},
{`ab+?bc`, []ReFlag{RE_CASE_INSENSITIVE}, `ABBC`, []Group{{0, 4}}},
{`ab??bc`, []ReFlag{RE_CASE_INSENSITIVE}, `ABBC`, []Group{{0, 4}}},
{`ab??bc`, []ReFlag{RE_CASE_INSENSITIVE}, `ABC`, []Group{{0, 3}}},
{`ab??bc`, []ReFlag{RE_CASE_INSENSITIVE}, `ABBBBC`, []Group{}},
{`ab??c`, []ReFlag{RE_CASE_INSENSITIVE}, `ABC`, []Group{{0, 3}}},
{`a.*?c`, []ReFlag{RE_CASE_INSENSITIVE}, `AXYZC`, []Group{{0, 5}}},
{`a.+?c`, []ReFlag{RE_CASE_INSENSITIVE}, `ABCABC`, []Group{{0, 3}, {3, 6}}},
{`a.*?c`, []ReFlag{RE_CASE_INSENSITIVE}, `ABCABC`, []Group{{0, 3}, {3, 6}}},
{`.*?\S *:`, nil, `xx:`, []Group{{0, 3}}},
{`a[ ]*? (\d+).*`, nil, `a 10`, []Group{{0, 6}}},
{`a[ ]*? (\d+).*`, nil, `a 10`, []Group{{0, 7}}},
{`"(?:\\"|[^"])*?"`, nil, `"\""`, []Group{{0, 4}}},
{`^.*?$`, nil, "one\ntwo\nthree", []Group{}},
{`a[^>]*?b`, nil, `a>b`, []Group{}},
{`^a*?$`, nil, `foo`, []Group{}},
{`a*?`, nil, `-`, nil}, // non-greedy operators are not supported
// Numeric range tests - this is a feature that I added, and doesn't exist
// in any other mainstream regex engine
@ -546,22 +527,6 @@ var reTests = []struct {
{`\pN+`, nil, `123abc456def`, []Group{{0, 3}, {6, 9}}},
{`\PN+`, nil, `123abc456def`, []Group{{3, 6}, {9, 12}}},
{`[\p{Greek}\p{Cyrillic}]`, nil, `ΣωШД`, []Group{{0, 1}, {1, 2}, {2, 3}, {3, 4}}},
{`(?<=\().*?(?=\))`, nil, `(abc)`, []Group{{1, 4}}},
{`((a|b)\2)`, nil, `aa`, []Group{{0, 2}}},
{`((a|b)\2)`, nil, `bb`, []Group{{0, 2}}},
{`((a|b)\2)`, nil, `ab`, []Group{}},
{`((a|b)\2)`, nil, `ba`, []Group{}},
{`((a|b)\2){3}`, nil, `aaaaaa`, []Group{{0, 6}}},
{`((a|b)\2){3}`, nil, `bbbbbb`, []Group{{0, 6}}},
{`((a|b)\2){3}`, nil, `bbaaaa`, []Group{{0, 6}}},
{`((a|b)\2){3}`, nil, `aabbaa`, []Group{{0, 6}}},
{`((a|b)\2){3}`, nil, `aaaabb`, []Group{{0, 6}}},
{`((a|b)\2){3}`, nil, `bbaabb`, []Group{{0, 6}}},
{`((a|b)\2){3}`, nil, `baabab`, []Group{}},
{`((a|b)\2){3}`, nil, `bbabab`, []Group{}},
}
var groupTests = []struct {
@ -754,18 +719,6 @@ var groupTests = []struct {
// {`(a|ab|c|bcd)*(d*)`, nil, `ababcd`, []Match{[]Group{{0, 6}, {3, 6}, {6, 6}}, []Group{{6, 6}, {6, 6}, {6, 6}}}},
// // Bug - this should give {0,3},{0,3},{0,0},{0,3},{3,3} but it gives {0,3},{0,2},{0,1},{1,2},{2,3}
// // {`((a*)(b|abc))(c*)`, nil, `abc`, []Match{[]Group{{0, 3}, {0, 3}, {0, 0}, {0, 3}, {3, 3}}}},
// Lazy quantifier tests
{`a(?:b|c|d)+?(.)`, nil, `ace`, []Match{[]Group{{0, 3}, {2, 3}}}},
{`a(?:b|(c|e){1,2}?|d)+?(.)`, nil, `ace`, []Match{[]Group{{0, 3}, {1, 2}, {2, 3}}}},
{`(?<!-):(.*?)(?<!-):`, nil, `a:bc-:de:f`, []Match{[]Group{{1, 9}, {2, 8}}}},
{`(?<!\\):(.*?)(?<!\\):`, nil, `a:bc\:de:f`, []Match{[]Group{{1, 9}, {2, 8}}}},
{`(?<!\?)'(.*?)(?<!\?)'`, nil, `a'bc?'de'f`, []Match{[]Group{{1, 9}, {2, 8}}}},
{`.*?x\s*\z(.*)`, []ReFlag{RE_MULTILINE, RE_SINGLE_LINE}, "xx\nx\n", []Match{[]Group{{0, 5}, {5, 5}}}},
{`.*?x\s*\z(.*)`, []ReFlag{RE_MULTILINE}, "xx\nx\n", []Match{[]Group{{3, 5}, {5, 5}}}},
{`^([ab]*?)(?=(b)?)c`, nil, `abc`, []Match{[]Group{{0, 3}, {0, 2}, {-1, -1}}}},
{`^([ab]*?)(?!(b))c`, nil, `abc`, []Match{[]Group{{0, 3}, {0, 2}, {-1, -1}}}},
{`^([ab]*?)(?<!(a))c`, nil, `abc`, []Match{[]Group{{0, 3}, {0, 2}, {-1, -1}}}},
}
func TestFind(t *testing.T) {

@ -4,5 +4,4 @@
Ideas for flags:
-m <num> : Print <num>th match (-m 1 = first match, -m 2 = second match)
-g <num> : Print the <num>th group
-r : Specify a directory instead of a file, reads recursively
4. Refactor code for flags - make each flag's code a function, which modifies the result of findAllMatches

Loading…
Cancel
Save