171 lines
4.5 KiB
Go
171 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"git.milar.in/milarin/buildinfo"
|
|
)
|
|
|
|
var ( //flags
|
|
// regex with sub groups
|
|
input = flag.String("i", `^(.|\n)*?$`, "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")
|
|
|
|
showVersion = flag.Bool("v", false, "show version and exit")
|
|
|
|
OutputNullByte = flag.Bool("0", false, "use nullbyte instead of newline as line separator for printing output")
|
|
)
|
|
|
|
var ( // globals
|
|
LineSeparator string = "\n"
|
|
|
|
replacePattern = regexp.MustCompile(`\{(\d+)(?::(.*?))?(?::(.*?))?(?::(.*?))?\}`)
|
|
numMutationPattern = regexp.MustCompile(`([+\-*/])(\d+|\((\d+)\))`)
|
|
)
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
if *showVersion {
|
|
buildinfo.Print(buildinfo.Options{})
|
|
return
|
|
}
|
|
|
|
if *OutputNullByte {
|
|
LineSeparator = string(rune(0))
|
|
}
|
|
|
|
pattern, err := regexp.Compile(*input)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
escapedOutput := EscSeqReplacer.Replace(*output)
|
|
|
|
for line := range readLines(os.Stdin) {
|
|
matches := pattern.FindStringSubmatch(line)
|
|
|
|
if len(matches) == 0 {
|
|
if *keepUnmatched {
|
|
fmt.Print(line, LineSeparator)
|
|
}
|
|
continue
|
|
}
|
|
|
|
fmt.Print(replaceVars(escapedOutput, matches...), LineSeparator)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
line, err := ReadLine(r)
|
|
|
|
// 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 && line != "" {
|
|
out <- line
|
|
}
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}(ch, r)
|
|
|
|
return ch
|
|
}
|
|
|
|
func ReadLine(r *bufio.Reader) (string, error) {
|
|
lines := make([]string, 0, *lineParseAmount)
|
|
|
|
var line string
|
|
var err error
|
|
for line, err = r.ReadString('\n'); ; line, err = r.ReadString('\n') {
|
|
if rn, size := utf8.DecodeLastRuneInString(line); rn == '\n' {
|
|
line = line[:len(line)-size]
|
|
}
|
|
|
|
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 {
|
|
break
|
|
}
|
|
}
|
|
|
|
linesCombined := strings.Join(lines, "\n")
|
|
return linesCombined, err
|
|
}
|
|
|
|
func replaceVars(format string, vars ...string) string {
|
|
replacements := replacePattern.FindAllStringSubmatch(format, -1) // TODO arguments do not change in outer loop (can be moved to main method)
|
|
|
|
for _, replacement := range replacements {
|
|
rplStr := replacement[0]
|
|
varIndex, _ := strconv.Atoi(replacement[1])
|
|
rplColor := makeColor(replacement[2])
|
|
rplFmt := replacement[3]
|
|
|
|
// 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[4])
|
|
format = strings.Replace(format, rplStr, rplColor.Sprintf(rplFmt, mutate(value, vars)), 1)
|
|
} else if strings.HasSuffix(rplFmt, "f") || strings.HasSuffix(rplFmt, "g") { // replace floats
|
|
value, _ := strconv.ParseFloat(vars[varIndex], 64)
|
|
mutate := numMut2func[float64](replacement[4])
|
|
format = strings.Replace(format, rplStr, rplColor.Sprintf(rplFmt, mutate(value, vars)), 1)
|
|
} else { // replace strings
|
|
format = strings.Replace(format, rplStr, rplColor.Sprintf(rplFmt, vars[varIndex]), 1)
|
|
}
|
|
}
|
|
|
|
return format
|
|
}
|