Compare commits
13 Commits
v0.3.0
...
e79c19a929
Author | SHA1 | Date | |
---|---|---|---|
e79c19a929 | |||
d2bce37935 | |||
bb3b866b77 | |||
e07f27dc78 | |||
65d2317f79 | |||
a631fc289c | |||
d62a429cce | |||
7b31031553 | |||
38c842cb07 | |||
9f9af36be8 | |||
8217b67122 | |||
1f06dcef64 | |||
119475b41b |
@@ -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.
|
||||
|
32
cmd/main.go
32
cmd/main.go
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -36,3 +36,7 @@ func (s uniq_arr[T]) values() []T {
|
||||
}
|
||||
return toRet
|
||||
}
|
||||
|
||||
func (s uniq_arr[T]) len() int {
|
||||
return len(s.backingMap)
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
10
regex/nfa.go
10
regex/nfa.go
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user