package main import ( "bufio" "flag" "fmt" "io" "os" "reflect" "regexp" "strconv" "strings" ) var ( // regex with sub groups input = flag.String("i", "^.*?$", "input pattern") // format string with {0} as placeholders // {0} always matches the whole line // {1} and onwards match their respective sub groups // // You can optionally specify a printf-syntax for formatting like this: {1:%s} or {1:%02d} // printf-syntax is currently supported for: strings, floats, integers. // // Addtionally mutators can be provided // to further manipulate the value using the given syntax: {1:%d:+1} // // The value mutated by can also be a back reference to another group // using round brackets like this: {1:%d:+(2)}} // // Multiple mutators can be used at once: {1:%d:+2*3-(2)} // Be aware that they will be applied strictly from left to right! // // The following number mutators (integers and floats) are allowed: // + - * / ^ % output = flag.String("o", "{0}", "output pattern") // don't ignore lines which do not match against input. // they will be copied without any changes keepUnmatched = flag.Bool("k", false, "keep unmatched lines") // if the amount of lines in stdin are not divisible by lineParseAmount, // the last lines will be ignored completely. // it may be useful to have a boolean flag for this behavior lineParseAmount = flag.Int("n", 1, "amount of lines to feed into input pattern") replacePattern = regexp.MustCompile(`\{(\d+)(?::(.*?))?(?::(.*?))?\}`) numMutationPattern = regexp.MustCompile(`([+\-*/])(\d+|\((\d+)\))`) ) func main() { flag.Parse() pattern, err := regexp.Compile(*input) if err != nil { panic(err) } for line := range readLines(os.Stdin) { line = line[:len(line)-1] matches := pattern.FindStringSubmatch(line) if len(matches) == 0 { if *keepUnmatched { fmt.Println(line) } continue } fmt.Println(replaceVars(*output, matches...)) } } func readLines(r io.Reader) <-chan string { ch := make(chan string, 10) go func(out chan<- string, source io.Reader) { defer close(out) r := bufio.NewReader(source) for { var line string var err error lines := make([]string, 0, *lineParseAmount) for line, err = r.ReadString('\n'); err == nil; line, err = r.ReadString('\n') { lines = append(lines, line) if len(lines) == cap(lines) { break } } if err != nil { return } out <- strings.Join(lines, "") } }(ch, r) return ch } func replaceVars(format string, vars ...string) string { replacements := replacePattern.FindAllStringSubmatch(format, -1) for _, replacement := range replacements { rplStr := replacement[0] varIndex, _ := strconv.Atoi(replacement[1]) rplFmt := replacement[2] // default format if not specified by user if rplFmt == "" { rplFmt = "%s" } if strings.HasSuffix(rplFmt, "d") { // replace integers value, _ := strconv.ParseInt(vars[varIndex], 10, 64) mutate := numMut2func[int64](replacement[3]) format = strings.Replace(format, rplStr, fmt.Sprintf(rplFmt, mutate(value, vars)), 1) } else if strings.HasSuffix(rplFmt, "f") { // replace floats value, _ := strconv.ParseFloat(vars[varIndex], 64) mutate := numMut2func[float64](replacement[3]) format = strings.Replace(format, rplStr, fmt.Sprintf(rplFmt, mutate(value, vars)), 1) } else { // replace strings format = strings.Replace(format, rplStr, fmt.Sprintf(rplFmt, vars[varIndex]), 1) } } return format } var numMutatorCache = map[string]interface{}{} func numMut2func[T int64 | float64](mutation string) (f func(value T, vars []string) T) { if mutation == "" { return func(value T, vars []string) T { return value } } // caching if v, ok := numMutatorCache[mutation]; ok { return v.(func(value T, vars []string) T) } defer func() { numMutatorCache[mutation] = f }() matches := numMutationPattern.FindAllStringSubmatch(mutation, -1) mutators := make([]NumMutator, 0, len(matches)) var err error for _, match := range matches { mut := NumMutator{Op: NewNumOperatorFromString(match[1])} if match[3] == "" { mut.Value, err = strconv.Atoi(match[2]) mut.Var = false if err != nil { panic("invalid number in number mutator: " + match[2]) } } else { mut.Var = true mut.Value, err = strconv.Atoi(match[3]) if err != nil { panic("invalid back reference group in number mutator: " + match[2]) } } mutators = append(mutators, mut) } numberParser := number_parser[T]() return func(value T, vars []string) T { for _, mutator := range mutators { var otherValue T if mutator.Var { other := numberParser(vars[mutator.Value]) otherValue = T(other) } else { otherValue = T(mutator.Value) } switch mutator.Op { case NumOperatorAdd: value += otherValue case NumOperatorSub: value -= otherValue case NumOperatorMul: value *= otherValue case NumOperatorDiv: value /= otherValue default: } } return value } } func number_parser[T int64 | float64]() func(str string) T { typeOfT := reflect.TypeOf(new(T)).Elem() typeOfInt64 := reflect.TypeOf(new(int64)).Elem() typeOfFloat64 := reflect.TypeOf(new(float64)).Elem() if typeOfT == typeOfInt64 { return func(str string) T { num, err := strconv.Atoi(str) if err != nil { panic("expected integer but found " + str) } return T(num) } } else if typeOfT == typeOfFloat64 { return func(str string) T { num, err := strconv.ParseFloat(str, 64) if err != nil { panic("expected float but found " + str) } return T(num) } } panic("invalid number type") }