// Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information package syntax import ( "bytes" "fmt" "regexp" "strings" ) func charClass(s string) (string, error) { if strings.HasPrefix(s, "[[.") || strings.HasPrefix(s, "[[=") { return "", fmt.Errorf("collating features not available") } if !strings.HasPrefix(s, "[[:") { return "", nil } name := s[3:] end := strings.Index(name, ":]]") if end < 0 { return "", fmt.Errorf("[[: was not matched with a closing :]]") } name = name[:end] switch name { case "alnum", "alpha", "ascii", "blank", "cntrl", "digit", "graph", "lower", "print", "punct", "space", "upper", "word", "xdigit": default: return "", fmt.Errorf("invalid character class: %q", name) } return s[:len(name)+6], nil } // TranslatePattern turns a shell pattern expression into a regular // expression that can be used with regexp.Compile. It will return an // error if the input pattern was incorrect. Otherwise, the returned // expression can be passed to regexp.MustCompile. // // For example, TranslatePattern(`foo*bar?`, true) returns `foo.*bar.`. // // Note that this function (and QuotePattern) should not be directly // used with file paths if Windows is supported, as the path separator // on that platform is the same character as the escaping character for // shell patterns. func TranslatePattern(pattern string, greedy bool) (string, error) { any := false loop: for _, r := range pattern { switch r { // including those that need escaping since they are // special chars in regexes case '*', '?', '[', '\\', '.', '+', '(', ')', '|', ']', '{', '}', '^', '$': any = true break loop } } if !any { // short-cut without a string copy return pattern, nil } var buf bytes.Buffer for i := 0; i < len(pattern); i++ { switch c := pattern[i]; c { case '*': buf.WriteString(".*") if !greedy { buf.WriteByte('?') } case '?': buf.WriteString(".") case '\\': if i++; i >= len(pattern) { return "", fmt.Errorf(`\ at end of pattern`) } buf.WriteString(regexp.QuoteMeta(string(pattern[i]))) case '[': name, err := charClass(pattern[i:]) if err != nil { return "", err } if name != "" { buf.WriteString(name) i += len(name) - 1 break } buf.WriteByte(c) if i++; i >= len(pattern) { return "", fmt.Errorf("[ was not matched with a closing ]") } switch c = pattern[i]; c { case '!', '^': buf.WriteByte('^') i++ c = pattern[i] } buf.WriteByte(c) last := c rangeStart := byte(0) for { if i++; i >= len(pattern) { return "", fmt.Errorf("[ was not matched with a closing ]") } last, c = c, pattern[i] buf.WriteByte(c) if c == ']' { break } if rangeStart != 0 && rangeStart > c { return "", fmt.Errorf("invalid range: %c-%c", rangeStart, c) } if c == '-' { rangeStart = last } else { rangeStart = 0 } } default: buf.WriteString(regexp.QuoteMeta(string(c))) } } return buf.String(), nil } // QuotePattern returns a string that quotes all special characters in // the given pattern. The returned string is a pattern that matches the // literal string. // // For example, QuotePattern(`foo*bar?`) returns `foo\*bar\?`. func QuotePattern(pattern string) string { any := false loop: for _, r := range pattern { switch r { case '*', '?', '[', '\\': any = true break loop } } if !any { // short-cut without a string copy return pattern } var buf bytes.Buffer for _, r := range pattern { switch r { case '*', '?', '[', '\\': buf.WriteByte('\\') } buf.WriteRune(r) } return buf.String() }