13 Commits

8 changed files with 62 additions and 19 deletions

View File

@@ -2,8 +2,8 @@
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 standard library](https://pkg.go.dev/regexp), while retaining some semblance of efficiency.
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.

View File

@@ -64,18 +64,30 @@ func main() {
// 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
if len(flag.Args()) < 1 || len(flag.Args()) > 2 { // flag.Args() also strips out program name
fmt.Println("ERROR: Missing cmdline args")
os.Exit(22)
}
var re string
re = flag.Args()[0]
var inputFile *os.File
if len(flag.Args()) == 1 || flag.Args()[1] == "-" { // Either no file argument, or file argument is "-"
inputFile = os.Stdin
} else {
var err error
inputFile, err = os.Open(flag.Args()[1])
if err != nil {
fmt.Printf("%s: No such file or directory\n", flag.Args()[1])
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 reader for stdin and writer for stdout
reader := bufio.NewReader(os.Stdin)
reader := bufio.NewReader(inputFile)
out := bufio.NewWriter(os.Stdout)
regComp, err := reg.Compile(re, flagsToCompile...)
@@ -204,10 +216,12 @@ func main() {
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
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
}
}
}
}
@@ -222,6 +236,10 @@ func main() {
if err != nil {
panic(err)
}
fmt.Println()
// 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()
}
}
}

View File

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

View File

@@ -410,10 +410,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] == nonCapLparenRune {
if (re_runes[i] == '(' && re_runes[i-1] != '\\') || re_runes[i] == nonCapLparenRune {
numOpenParens++
}
if re_runes[i] == ')' {
if re_runes[i] == ')' && re_runes[i-1] != '\\' {
numOpenParens--
if numOpenParens == 0 {
break
@@ -498,7 +498,7 @@ func shuntingYard(re string, flags ...ReFlag) ([]postfixNode, error) {
}
} else if re_postfix[i] == 'p' || re_postfix[i] == 'P' {
charClassInverted := (re_postfix[i] == 'P')
charsInClass := []rune{}
var charsInClass []rune
i++
if isUnicodeCharClassLetter(re_postfix[i]) {
var err error
@@ -589,10 +589,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] == nonCapLparenRune {
if (re_postfix[i] == '(' && re_postfix[i-1] != '\\') || re_postfix[i] == nonCapLparenRune {
numOpenParens++
}
if re_postfix[i] == ')' {
if re_postfix[i] == ')' && re_postfix[i-1] != '\\' {
numOpenParens--
if numOpenParens == 0 {
break
@@ -713,7 +713,7 @@ func shuntingYard(re string, flags ...ReFlag) ([]postfixNode, error) {
}
} else if re_postfix[i] == 'p' || re_postfix[i] == 'P' {
charClassInverted := (re_postfix[i] == 'P')
charsInList := []rune{}
var charsInList []rune
i++
if isUnicodeCharClassLetter(re_postfix[i]) {
var err error

View File

@@ -161,6 +161,7 @@ The following features from [regexp] are (currently) NOT supported:
2. Negated POSIX classes
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

View File

@@ -45,11 +45,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.
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
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
}
// Clones the NFA starting from the given state.
@@ -86,6 +86,8 @@ 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 {

View File

@@ -117,6 +117,7 @@ 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}}},
@@ -545,6 +546,22 @@ 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 {

View File

@@ -4,4 +4,5 @@
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