package main import ( "bufio" "flag" "fmt" "io" "os" "slices" "strconv" "unicode" "github.com/fatih/color" ) const CONCAT rune = '~' var notDotChars []rune var caseInsensitiveFlag *bool // Whether we are running in case-insensitive mode func isOperator(c rune) bool { if c == '+' || c == '?' || c == '*' || c == '|' || c == CONCAT { return true } return false } /* priority returns the priority of the given operator */ func priority(op rune) int { precedence := []rune{'|', CONCAT, '+', '*', '?'} return slices.Index(precedence, op) } /* The Shunting-Yard algorithm is used to convert the given infix (regeular) expression to postfix. The primary benefit of this is getting rid of parentheses. It also inserts explicit concatenation operators to make parsing easier in Thompson's algorithm. See: https://blog.cernera.me/converting-regular-expressions-to-postfix-notation-with-the-shunting-yard-algorithm/ */ func shuntingYard(re string) []postfixNode { re_postfix := make([]rune, 0) // Convert the string to a slice of runes to allow iteration through it re_runes_orig := []rune(re) // This is the rune slice before the first parsing loop (which detects and replaces numeric ranges) re_runes := make([]rune, 0) // Check for numeric range. If we are at the start of a numeric range, // skip to end and construct the equivalent regex for the range. // The reason this is outside the loop below, is that it actually modifies // the given regex (we 'cut' the numeric range and 'paste' an equivalent regex). // It also makes the overall parsing easier, since I don't have to worry about the numeric range // anymore. // Eventually, I might be able to add it into the main parsing loop, to reduce the time // complexity. // A numeric range has the syntax: . Ir matches all numbers in this range. 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] != '?')) { i++ // Step over opening angle bracket tmpStr := "" hyphenFound := false for i < len(re_runes_orig) && re_runes_orig[i] != '>' { if !unicode.IsDigit(re_runes_orig[i]) { if re_runes_orig[i] != '-' || (hyphenFound) { panic("ERROR: Invalid numeric range.") } } if re_runes_orig[i] == '-' { hyphenFound = true } tmpStr += string(re_runes_orig[i]) i++ } // End of string reached and last character doesn't close the range if i == len(re_runes_orig) && re_runes_orig[len(re_runes_orig)-1] != '>' { panic("ERROR: Numeric range not closed.") } if len(tmpStr) == 0 { panic("ERROR: Empty numeric range.") } // Closing bracket will be skipped when the loop variable increments var rangeStart int var rangeEnd int fmt.Sscanf(tmpStr, "%d-%d", &rangeStart, &rangeEnd) regex := range2regex(rangeStart, rangeEnd) re_runes = append(re_runes, []rune(regex)...) } else { re_runes = append(re_runes, c) } } /* Add concatenation operators. Only add a concatenation operator between two characters if both the following conditions are met: 1. The first character isn't an opening parantheses or alteration operator (or an escape character) a. This makes sense, because these operators can't be _concatenated_ with anything else. 2. The second character isn't a 'closing operator' - one that applies to something before it a. Again, these operators can'be concatenated _to_. They can, however, be concatenated _from_. Caveats: 1. Don't mess with anything inside brackets - character class 2. Don't mess with anything inside braces - numeric repetition 3. Don't mess with any lookarounds. */ i := 0 for i < len(re_runes) { re_postfix = append(re_postfix, re_runes[i]) if re_runes[i] == '[' && (i == 0 || re_runes[i-1] != '\\') { // We do not touch things inside brackets, unless they are escaped. Inside this block, the only task is to expand character ranges into their constituent characters. re_postfix[len(re_postfix)-1] = LBRACKET // Replace the '[' character with LBRACKET. This allows for easier parsing of all characters (including opening and closing brackets) within the character class toAppend := make([]rune, 0) // Holds all the runes in the current character class if i < len(re_runes)-1 && re_runes[i+1] == '^' { // Inverting class - match everything NOT in brackets re_postfix = append(re_postfix, '^') i++ // Skip opening bracket and caret } if i < len(re_runes)-1 && re_runes[i+1] == ']' { // Nothing inside brackets - panic. panic("Empty character class.") } for re_runes[i] != ']' { i++ // Skip all characters inside brackets // TODO: Check for escaped characters // Check ahead for character range if i < len(re_runes)-2 && re_runes[i+1] == '-' { rangeStart := re_runes[i] rangeEnd := re_runes[i+2] if int(rangeEnd) < int(rangeStart) { panic("Range is out of order.") } for i := rangeStart; i <= rangeEnd; i++ { toAppend = append(toAppend, i) } i += 2 // Skip start and hyphen (end will automatically be skipped on next iteration of loop) continue } toAppend = append(toAppend, re_runes[i]) } // Replace the last character (which should have been ']', with RBRACKET toAppend[len(toAppend)-1] = RBRACKET re_postfix = append(re_postfix, toAppend...) } if i < len(re_runes) && re_runes[i] == '{' && (i > 0 && re_runes[i-1] != '\\') { // We don't touch things inside braces, either i++ // Skip opening brace for i < len(re_runes) && re_runes[i] != '}' { re_postfix = append(re_postfix, re_runes[i]) i++ } if i == len(re_runes) { panic("Invalid numeric specifier.") } re_postfix = append(re_postfix, re_runes[i]) // Append closing brace } if i < len(re_runes) && re_runes[i] == '(' && (i == 0 || re_runes[i-1] != '\\') && (i < len(re_runes)-1 && re_runes[i+1] == '?') { // Unescaped open parentheses followed by question mark = lokaround. Don't mess with it. i++ // Step inside if i == len(re_runes)-1 || (re_runes[i+1] != '=' && re_runes[i+1] != '!' && re_runes[i+1] != '<') { panic("Invalid regex. Lookaround intended?") } re_postfix = append(re_postfix, re_runes[i]) i++ numOpenParens := 1 for numOpenParens != 0 { if i >= len(re_runes) { panic("Unclosed lookaround.") } if re_runes[i] == '(' { numOpenParens++ } if re_runes[i] == ')' { numOpenParens-- if numOpenParens == 0 { break } } re_postfix = append(re_postfix, re_runes[i]) i++ } continue } if i < len(re_runes) && (re_runes[i] != '(' && 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] != '+' && re_runes[i+1] != '?' && re_runes[i+1] != ')' && re_runes[i+1] != '{' { re_postfix = append(re_postfix, CONCAT) } } } i++ } opStack := make([]rune, 0) // Operator stack outQueue := make([]postfixNode, 0) // Output queue // Actual algorithm numOpenParens := 0 // Number of open parentheses for i := 0; i < len(re_postfix); i++ { /* Two cases: 1. Current character is alphanumeric - send to output queue 2. Current character is operator - do the following: a. If current character has greater priority than top of opStack, push to opStack. b. If not, keep popping from opStack (and appending to outQueue) until: i. opStack is empty, OR ii. current character has greater priority than top of opStack 3. If current character is '(', push to opStack 4. If current character is ')', pop from opStack (and append to outQueue) until '(' is found. Discard parantheses. 5. If current character is '[', find all the characters until ']', then create a postfixNode containing all these contents. Add this node to outQueue. 6. If current character is '{', find the appropriate numeric specifier (range start, range end). Apply the range to the postfixNode at the end of outQueue. */ c := re_postfix[i] if isNormalChar(c) { if caseInsensitiveFlag != nil && *caseInsensitiveFlag { outQueue = append(outQueue, newPostfixNode(allCases(c)...)) } else { outQueue = append(outQueue, newPostfixNode(c)) } continue } // Escape character if c == '\\' { // Escape character - invert special and non-special characters eg. \( is treated as a literal parentheses, \b is treated as word boundary if i == len(re_postfix)-1 { // End of string - panic, because backslash is an escape character (something needs to come after it) panic("ERROR: Backslash with no escape character.") } i++ outQueue = append(outQueue, newEscapedNode(re_postfix[i])) continue // Escaped character will automatically be skipped when loop variable increments } if c == '.' { // Dot metacharacter - represents 'any' character, but I am only adding Unicode 0020-007E outQueue = append(outQueue, newPostfixDotNode()) continue } if c == '^' { // Start-of-string assertion outQueue = append(outQueue, newPostfixNode(c)) } if c == '$' { // End-of-string assertion outQueue = append(outQueue, newPostfixNode(c)) } // Check if we're at the start of a lookaround if c == '(' && i < len(re_postfix)-1 && re_postfix[i+1] == '?' { i += 2 // Skip opening paren and question mark regex := "" // Stores lookaround regex numOpenParens := 1 for numOpenParens != 0 { if i >= len(re_postfix) { panic("Unclosed lookaround.") } if re_postfix[i] == '(' { numOpenParens++ } if re_postfix[i] == ')' { numOpenParens-- if numOpenParens == 0 { break } } regex += string(re_postfix[i]) i++ } if len(regex) <= 1 { // Nothing in regex - panic panic("Invalid lookaround. (too short?)") } // 'regex' should now contain the lookaround regex, plus the characters at the start (which indicate pos/neg, ahead/behind) // Now we should filter that out. toAppend := postfixNode{nodetype: ASSERTION, startReps: 1, endReps: 1} if regex[0] == '<' { // Lookbehind toAppend.lookaroundDir = LOOKBEHIND regex = regex[1:] } else if regex[0] == '=' || regex[0] == '!' { toAppend.lookaroundDir = LOOKAHEAD } else { panic("Invalid lookaround.") } // Positive or negative if regex[0] == '=' { // Positive toAppend.lookaroundSign = POSITIVE toAppend.contents = []rune(regex[1:]) } else if regex[0] == '!' { // Negative toAppend.lookaroundSign = NEGATIVE toAppend.contents = []rune(regex[1:]) } else { panic("Invalid lookaround.") } outQueue = append(outQueue, toAppend) continue } if isOperator(c) { if len(opStack) == 0 { opStack = append(opStack, c) } else { topStack, err := peek(opStack) if err != nil { panic("ERROR: Operator without operand.") } if priority(c) > priority(topStack) { // 2a opStack = append(opStack, c) } else { for priority(c) <= priority(topStack) { // 2b to_append := mustPop(&opStack) outQueue = append(outQueue, newPostfixNode(to_append)) topStack, _ = peek(opStack) } opStack = append(opStack, c) } } } if c == LBRACKET { // Used for character classes i++ // Step forward so we can look at the character class var invertMatch bool if re_postfix[i] == '^' { invertMatch = true i++ } chars := make([]rune, 0) // List of characters - used only for character classes for i < len(re_postfix) { if re_postfix[i] == RBRACKET { break } chars = append(chars, re_postfix[i]) i++ } if i == len(re_postfix) { // We have reached the end of the string, so we didn't encounter a closing brakcet. Panic. panic("ERROR: Opening bracket without closing bracket.") } if !invertMatch { outQueue = append(outQueue, newPostfixCharNode(chars...)) } else { // Invert match - create an allChars postfixNode, then add the given states to its 'except' list. toAdd := newPostfixDotNode() toAdd.except = chars outQueue = append(outQueue, toAdd) } continue } if c == '{' { i++ // Skip opening brace // Three possibilities: // 1. Single number - {5} // 2. Range - {3,5} // 3. Start with no end, {3,} startRange := make([]rune, 0) startRangeNum := 0 endRange := make([]rune, 0) endRangeNum := 0 for i < len(re_postfix) && unicode.IsDigit(re_postfix[i]) { startRange = append(startRange, re_postfix[i]) i++ } if len(startRange) == 0 { // {} is not valid, neither is {,5} panic("ERROR: Invalid numeric specifier.") } if i == len(re_postfix) { panic("ERROR: Brace not closed.") } startRangeNum, err := strconv.Atoi(string(startRange)) if err != nil { panic(err) } if re_postfix[i] == '}' { // Case 1 above endRangeNum = startRangeNum } else { if re_postfix[i] != ',' { panic("ERROR: Invalid numeric specifier.") } i++ // Skip comma for i < len(re_postfix) && unicode.IsDigit(re_postfix[i]) { endRange = append(endRange, re_postfix[i]) i++ } if i == len(re_postfix) { panic("ERROR: Brace not closed.") } if re_postfix[i] != '}' { panic("ERROR: Invalid numeric specifier.") } if len(endRange) == 0 { // Case 3 above endRangeNum = INFINITE_REPS } else { // Case 2 above var err error endRangeNum, err = strconv.Atoi(string(endRange)) if err != nil { panic(err) } } } idx := len(outQueue) - 1 // Get the most recently added non-paren node for node := outQueue[idx]; idx >= 0 && (node.nodetype == RPAREN || node.nodetype == LPAREN); node = outQueue[idx] { idx-- } if idx < 0 { panic("Numeric specifier with no content.") } outQueue[idx].startReps = startRangeNum outQueue[idx].endReps = endRangeNum } if c == '(' { opStack = append(opStack, c) outQueue = append(outQueue, newPostfixNode(c)) numOpenParens++ } if c == ')' { // Keep popping from opStack until we encounter an opening parantheses. Panic if we reach the end of the stack. for val, err := peek(opStack); val != '('; val, err = peek(opStack) { if err != nil { panic("ERROR: Imbalanced parantheses.") } to_append := mustPop(&opStack) outQueue = append(outQueue, newPostfixNode(to_append)) } _ = mustPop(&opStack) // Get rid of opening parentheses outQueue = append(outQueue, newPostfixNode(')')) // Add closing parentheses numOpenParens-- } } // Pop all remaining operators (and append to outQueue) for len(opStack) > 0 { to_append := mustPop(&opStack) outQueue = append(outQueue, newPostfixNode(to_append)) } if numOpenParens != 0 { panic("ERROR: Imbalanced parantheses.") } return outQueue } // Thompson's algorithm. Constructs Finite-State Automaton from given string. // Returns start state and number of groups in regex. func thompson(re []postfixNode) (*State, int) { nfa := make([]*State, 0) // Stack of states numGroups := 0 // Number of capturing groups for _, c := range re { if c.nodetype == CHARACTER || c.nodetype == ASSERTION { state := State{} state.transitions = make(map[int][]*State) if c.allChars { state.allChars = true if len(c.except) != 0 { state.except = append([]rune{}, c.except...) } } state.content = rune2Contents(c.contents) state.output = make([]*State, 0) state.output = append(state.output, &state) state.isEmpty = false if c.nodetype == ASSERTION { state.isEmpty = true // This is a little weird. A lookaround has the 'isEmpty' flag set, even though it _isn't_ empty (the contents are the regex). But, there's so much error-checking that relies on this flag that it's better to keep it this way. state.content = newContents(EPSILON) // Ideally, an assertion shouldn't have any content, since it doesn't say anything about the content of string if c.lookaroundDir == 0 || c.lookaroundSign == 0 { switch c.contents[0] { case '^': state.assert = SOS case '$': state.assert = EOS case 'b': state.assert = WBOUND case 'B': state.assert = NONWBOUND } } else { // Lookaround state.lookaroundRegex = string(c.contents) if c.lookaroundDir == LOOKAHEAD { if c.lookaroundSign == POSITIVE { state.assert = PLA } if c.lookaroundSign == NEGATIVE { state.assert = NLA } } if c.lookaroundDir == LOOKBEHIND { if c.lookaroundSign == POSITIVE { state.assert = PLB } if c.lookaroundSign == NEGATIVE { state.assert = NLB } } tmpRe := shuntingYard(state.lookaroundRegex) var numGroupsLookaround int state.lookaroundNFA, numGroupsLookaround = thompson(tmpRe) state.lookaroundNumCaptureGroups = numGroupsLookaround } } nfa = append(nfa, &state) } if c.nodetype == LPAREN || c.nodetype == RPAREN { s := &State{} s.assert = NONE s.content = newContents(EPSILON) s.isEmpty = true s.output = make([]*State, 0) s.output = append(s.output, s) s.transitions = make(map[int][]*State) // LPAREN nodes are just added normally if c.nodetype == LPAREN { numGroups++ s.groupBegin = true s.groupNum = numGroups nfa = append(nfa, s) continue } // For RPAREN nodes, I assume that the last two nodes in the list are an LPAREN, // and then some other node. // These three nodes (LPAREN, the middle node and RPAREN) are extracted together, concatenated // and added back in. if c.nodetype == RPAREN { s.groupEnd = true middleNode := mustPop(&nfa) lparenNode := mustPop(&nfa) s.groupNum = lparenNode.groupNum tmp := concatenate(lparenNode, middleNode) to_add := concatenate(tmp, s) nfa = append(nfa, to_add) } } // Must be an operator if it isn't a character switch c.nodetype { case CONCATENATE: s2 := mustPop(&nfa) s1 := mustPop(&nfa) s1 = concatenate(s1, s2) nfa = append(nfa, s1) case KLEENE: // Create a 0-state, concat the popped state after it, concat the 0-state after the popped state s1 := mustPop(&nfa) stateToAdd := kleene(*s1) nfa = append(nfa, stateToAdd) case PLUS: // a+ is equivalent to aa* s1 := mustPop(&nfa) s2 := kleene(*s1) s1 = concatenate(s1, s2) nfa = append(nfa, s1) case QUESTION: // ab? is equivalent to a(b|) s1 := mustPop(&nfa) s2 := question(s1) nfa = append(nfa, s2) case PIPE: s1 := mustPop(&nfa) s2 := mustPop(&nfa) s3 := alternate(s1, s2) nfa = append(nfa, s3) } if c.startReps != 1 || c.endReps != 1 { // Must have a numeric specifier attached to it if c.endReps != -1 && c.endReps < c.startReps { panic("ERROR: Numeric specifier - start greater than end.") } state := mustPop(&nfa) var stateToAdd *State = nil // Take advantage of the following facts: // a{5} == aaaaa // a{3,5} == aaaa?a? // a{5,} == aaaaa+ // Nov. 3 2024 - I have two choices on how I want to implement numeric // specifiers. // a. Encode the logic while creating the states. I will have to create a function // that creates a deep-copy of a given state / NFA, so that I can concatenate them to // each other (concatenating them with the 'concatenate' method - which takes addresses - does // not work). Creating this function might be a lot of work. // b. Encode the logic while parsing the string (shunting-yard). If I can expand the numeric specifier // at this point, I can leave thompson untouched. for i := 0; i < c.startReps; i++ { // Case 1 stateToAdd = concatenate(stateToAdd, cloneState(state)) } if c.endReps == INFINITE_REPS { // Case 3 s2 := kleene(*state) stateToAdd = concatenate(stateToAdd, s2) } else { // Case 2 for i := c.startReps; i < c.endReps; i++ { stateToAdd = concatenate(stateToAdd, question(state)) } } nfa = append(nfa, stateToAdd) } } if len(nfa) != 1 { panic("ERROR: Invalid Regex.") } verifyLastStates(nfa) return nfa[0], numGroups } func main() { 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.") substituteText := flag.String("s", "", "Substitute the contents of each match with the given string. Overrides -o and -v") flag.Parse() // In multi-line mode, 'dot' metacharacter also matches newline if !(*multiLineFlag) { notDotChars = []rune{'\n'} } else { notDotChars = []rune{} } // -l and -o are mutually exclusive: -o overrides -l if *onlyFlag { *lineFlag = false } // Check if substitute text has been enabled substituteFlagEnabled := false flag.Visit(func(f *flag.Flag) { if f.Name == "s" { substituteFlagEnabled = 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.Println("ERROR: Missing cmdline args") os.Exit(22) } var re string re = flag.Args()[0] var test_str string var test_runes []rune // Rune-slice representation of test_str 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) re_postfix := shuntingYard(re) startState, numGroups := thompson(re_postfix) 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) } } test_runes = []rune(test_str) matchIndices := findAllMatches(startState, test_runes, numGroups) 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 _, idx := range matchIndices { fmt.Fprintf(out, "%s\n", idx.toString()) } 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) 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_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_runes { inMatchIndex := false for _, idx := range matchIndices { if i == idx[0].startIdx { fmt.Fprintf(out, "%s", *substituteText) i = idx[0].endIdx inMatchIndex = true break } } if !inMatchIndex { fmt.Fprintf(out, "%c", test_runes[i]) } } } else { for i, c := range test_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() } }