format/main.go

238 lines
6.0 KiB
Go
Raw Normal View History

2021-08-20 23:37:18 +02:00
package main
import (
"bufio"
"flag"
"fmt"
2022-04-18 13:00:34 +02:00
"io"
2021-08-20 23:37:18 +02:00
"os"
"reflect"
2021-08-20 23:37:18 +02:00
"regexp"
"strconv"
"strings"
"unicode/utf8"
2021-08-20 23:37:18 +02:00
)
var (
2021-08-20 23:47:41 +02:00
// regex with sub groups
input = flag.String("i", `^(.|\n)*?$`, "input pattern")
2021-08-20 23:47:41 +02:00
// format string with {0} as placeholders
// {0} always matches the whole line
// {1} and onwards match their respective sub groups
2022-04-18 17:39:41 +02:00
//
2021-08-20 23:47:57 +02:00
// You can optionally specify a printf-syntax for formatting like this: {1:%s} or {1:%02d}
2022-04-18 17:39:41 +02:00
// 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:
// + - * / ^ %
2021-08-20 23:47:41 +02:00
output = flag.String("o", "{0}", "output pattern")
// don't ignore lines which do not match against input.
// they will be copied without any changes
2021-08-20 23:37:18 +02:00
keepUnmatched = flag.Bool("k", false, "keep unmatched lines")
2022-04-18 13:00:34 +02:00
// 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")
2022-04-18 17:39:41 +02:00
replacePattern = regexp.MustCompile(`\{(\d+)(?::(.*?))?(?::(.*?))?\}`)
numMutationPattern = regexp.MustCompile(`([+\-*/])(\d+|\((\d+)\))`)
2021-08-20 23:37:18 +02:00
)
func main() {
flag.Parse()
pattern, err := regexp.Compile(*input)
if err != nil {
panic(err)
}
escapedOutput := EscSeqReplacer.Replace(*output)
2022-04-18 13:00:34 +02:00
for line := range readLines(os.Stdin) {
2021-08-20 23:37:18 +02:00
matches := pattern.FindStringSubmatch(line)
if len(matches) == 0 {
if *keepUnmatched {
fmt.Println(line)
}
continue
}
fmt.Println(replaceVars(escapedOutput, matches...))
2021-08-20 23:37:18 +02:00
}
}
2022-04-18 13:00:34 +02:00
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'); ; line, err = r.ReadString('\n') {
if rn, size := utf8.DecodeLastRuneInString(line); rn == '\n' {
line = line[:len(line)-size]
}
2022-04-18 13:00:34 +02:00
lines = append(lines, line)
// stop reading as soon as lineParseAmount is reached or an error occured (most likely EOF)
if len(lines) == cap(lines) || err != nil {
2022-04-18 13:00:34 +02:00
break
}
}
linesCombined := strings.Join(lines, "\n")
// use data as line if reading was successfull or EOF has been reached
// in the latter case: only use data if something could be read until EOF
if err == nil || err == io.EOF && linesCombined != "" {
out <- linesCombined
}
2022-04-18 13:00:34 +02:00
if err != nil {
return
}
}
}(ch, r)
return ch
}
2021-08-20 23:37:18 +02:00
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)
2022-04-18 17:39:41 +02:00
mutate := numMut2func[int64](replacement[3])
format = strings.Replace(format, rplStr, fmt.Sprintf(rplFmt, mutate(value, vars)), 1)
2022-04-18 20:59:52 +02:00
} else if strings.HasSuffix(rplFmt, "f") || strings.HasSuffix(rplFmt, "g") { // replace floats
2021-08-20 23:37:18 +02:00
value, _ := strconv.ParseFloat(vars[varIndex], 64)
2022-04-18 17:39:41 +02:00
mutate := numMut2func[float64](replacement[3])
format = strings.Replace(format, rplStr, fmt.Sprintf(rplFmt, mutate(value, vars)), 1)
2021-08-20 23:37:18 +02:00
} else { // replace strings
format = strings.Replace(format, rplStr, fmt.Sprintf(rplFmt, vars[varIndex]), 1)
}
}
return format
}
2022-04-18 17:39:41 +02:00
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]()
2022-04-18 17:39:41 +02:00
return func(value T, vars []string) T {
for _, mutator := range mutators {
var otherValue T
if mutator.Var {
other := numberParser(vars[mutator.Value])
2022-04-18 17:39:41 +02:00
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")
}