diff --git a/Makefile b/Makefile index 8741c42..42d1ad8 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION = 5.0.3 +VERSION = 6.0.0 PACKAGES := $(shell go list -f {{.Dir}} ./...) GOFILES := $(addsuffix /*.go,$(PACKAGES)) diff --git a/README.md b/README.md index 317f353..d113a15 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,14 @@ - [Time format](#time-format) - [Template output](#template-output) - [Color output](#color-output) - - [Stopwatch regex](#stopwatch-regex) - [JSON input](#json-input) + - [Stopwatch regex](#stopwatch-regex) + - [Stopwatch regex template](#stopwatch-regex-template) + - [Stopwatch condition](#stopwatch-condition) - [Example](#example) - [Comments](https://github.com/sgreben/tj/issues/1) + ## Get it Using go get: @@ -35,25 +38,47 @@ docker pull quay.io/sergey_grebenshchikov/tj ```text Usage of tj: - -timeformat string - either a go time format string or one of the predefined format names (https://golang.org/pkg/time/#pkg-constants) -template string - either a go template (https://golang.org/pkg/text/template) or one of the predefined template names - -start string - a regex pattern. if given, only lines matching it (re)start the stopwatch - -readjson - parse each stdin line as JSON - -jsontemplate string - go template, used to extract text from json input. implies -readjson + either a go template (https://golang.org/pkg/text/template) or one of the predefined template names + -time-format string + either a go time format string or one of the predefined format names (https://golang.org/pkg/time/#pkg-constants) + -time-zone string + time zone to use (default "Local") + -match-regex string + a regex pattern. if given, only tokens matching it (re)start the stopwatch + -match-template string + go template, used to extract text used for -match-regex + -match-condition string + go template. if given, only tokens that result in 'true' (re)start the stopwatch + -match-buffer + buffer lines between matches of -match-regex / -match-condition, copy delta values from final line to buffered lines + -match string + alias for -match-template + -condition string + alias for -match-condition + -regex string + alias for -match-regex + -read-json + parse a sequence of JSON objects from stdin -scale string - either a sequence of hex colors or one of the predefined color scale names (colors go from fast to slow) - (default "BlueToRed") + either a sequence of hex colors or one of the predefined color scale names (colors go from fast to slow) + (default "BlueToRed") -scale-fast duration - the lower bound for the color scale (default 100ms) + the lower bound for the color scale (default 100ms) -scale-slow duration - the upper bound for the color scale (default 2s) - -delta-buffer - buffer lines between -start matches, copy delta values from final line to buffered lines + the upper bound for the color scale (default 2s) + -scale-linear + use linear scale (default true) + -scale-cube + use cubic scale + -scale-cubert + use cubic root scale + -scale-sqr + use quadratic scale + -scale-sqrt + use quadratic root scale + -version + print version and exit ``` ### JSON output @@ -65,24 +90,24 @@ $ (echo Hello; echo World) | tj ``` ```json -{"timeSecs":1516648762,"timeNanos":1516648762008900882,"time":"2018-01-22T20:19:22+01:00","deltaSecs":0.000015003,"deltaNanos":15003,"delta":"15.003µs","totalSecs":0.000015003,"totalNanos":15003,"total":"15.003µs","text":"Hello"} -{"timeSecs":1516648762,"timeNanos":1516648762009093926,"time":"2018-01-22T20:19:22+01:00","deltaSecs":0.000193044,"deltaNanos":193044,"delta":"193.044µs","totalSecs":0.000208047,"totalNanos":208047,"total":"208.047µs","text":"World"} +{"timeSecs":1517592179,"timeNanos":1517592179895262811,"time":"2018-02-02T18:22:59+01:00","deltaSecs":0.000016485,"deltaNanos":16485,"delta":"16.485µs","totalSecs":0.000016485,"totalNanos":16485,"total":"16.485µs","text":"Hello"} +{"timeSecs":1517592179,"timeNanos":1517592179895451948,"time":"2018-02-02T18:22:59+01:00","deltaSecs":0.000189137,"deltaNanos":189137,"delta":"189.137µs","totalSecs":0.000205622,"totalNanos":205622,"total":"205.622µs","text":"World"} ``` ### Time format -You can set the format of the `time` field using the `-timeformat` parameter: +You can set the format of the `time` field using the `-time-format` parameter: ```bash -$ (echo Hello; echo World) | tj -timeformat Kitchen +$ (echo Hello; echo World) | tj -time-format Kitchen ``` ```json -{"timeSecs":1516648899,"timeNanos":1516648899954888290,"time":"8:21PM","deltaSecs":0.000012913,"deltaNanos":12913,"delta":"12.913µs","totalSecs":0.000012913,"totalNanos":12913,"total":"12.913µs","text":"Hello"} -{"timeSecs":1516648899,"timeNanos":1516648899955092012,"time":"8:21PM","deltaSecs":0.000203722,"deltaNanos":203722,"delta":"203.722µs","totalSecs":0.000216635,"totalNanos":216635,"total":"216.635µs","text":"World"} +{"timeSecs":1517592194,"timeNanos":1517592194875016639,"time":"6:23PM","deltaSecs":0.000017142,"deltaNanos":17142,"delta":"17.142µs","totalSecs":0.000017142,"totalNanos":17142,"total":"17.142µs","text":"Hello"} +{"timeSecs":1517592194,"timeNanos":1517592194875197515,"time":"6:23PM","deltaSecs":0.000180876,"deltaNanos":180876,"delta":"180.876µs","totalSecs":0.000198018,"totalNanos":198018,"total":"198.018µs","text":"World"} ``` -The [constant names from pkg/time](https://golang.org/pkg/time/#pkg-constants) as well as regular go time layouts are admissible values for `-timeformat`: +The [constant names from pkg/time](https://golang.org/pkg/time/#pkg-constants) as well as regular go time layouts are admissible values for `-time-format`: | Name | Format | |------------|-------------------------------------| @@ -115,17 +140,19 @@ $ (echo Hello; echo World) | tj -template '{{ .I }} {{.TimeSecs}} {{.Text}}' 1 1516649679 World ``` -The fields available to the template are specified in the [`line` struct](cmd/tj/main.go#L19). +The fields available to the template are specified in the [`token` struct](cmd/tj/main.go#L18). Some templates are pre-defined and can be used via `-template NAME`: -| Name | Template | -|------------|----------------------------------------------| -| Color | `{{color .}}█{{reset}} {{.Text}}` | -| ColorText | `{{color .}}{{.Text}}{{reset}}` | -| Delta | `{{.DeltaNanos}} {{.Text}}` | -| Time | `{{.TimeString}} {{.Text}}` | -| TimeDelta | `{{.TimeString}} +{{.DeltaNanos}} {{.Text}}` | +| Name | Template | +|------------|--------------------------------------------------| +| Color | `{{color .}}█{{reset}} {{.Text}}` | +| ColorText | `{{color .}}{{.Text}}{{reset}}` | +| Delta | `{{.DeltaNanos}} {{.Text}}` | +| Text | `{{.Text}}` | +| Time | `{{.TimeString}} {{.Text}}` | +| TimeDelta | `{{.TimeString}} +{{.DeltaNanos}} {{.Text}}` | +| TimeColor | `{{.TimeString}} {{color .}}█{{reset}} {{.Text}}`| ### Color output @@ -149,6 +176,11 @@ The color scale can be set using the parameters `-scale`, `-scale-fast`, and `- - The `-scale` parameter defines the colors used in the scale. - The `-scale-fast` and `-scale-slow` parameters define the boundaries of the scale: durations shorter than the value of `-scale-fast` are mapped to the leftmost color, durations longer than the value of `-scale-slow` are mapped to the rightmost color. +The scale is linear by default, but can be transformed: + +- `-scale-sqr`, `-scale-sqrt` yields a quadratic (root) scale +- `-scale-cube`, `-scale-cubert` yields a cubic (root) scale + There are several pre-defined color scales: | Name | Scale | @@ -165,33 +197,44 @@ There are several pre-defined color scales: You can also provide your own color scale using the same syntax as the pre-defined ones. -### Stopwatch regex - -Sometimes you need to measure the duration between certain *tokens* in the input. +### JSON input -To help with this, `tj` can match each line against a regular expression and only reset the stopwatch (`delta`, `deltaSecs`, `deltaNanos`) when a line matches. +Using `-read-json`, you can tell `tj` to parse stdin as a sequence of JSON objects. The parsed object can be referred to via `.Object`, like this: -The regular expression can be specified via the `-start` parameter. +```bash +$ echo '{"hello": "World"}' | tj -read-json -template "{{.TimeString}} {{.Object.hello}}" +``` -### JSON input +``` +2018-01-25T21:55:06+01:00 World +``` -Using `-readjson`, you can tell `tj` to parse each input line as a separate JSON object. Fields of this object can be referred to via `.Object` in the `line` struct, like this: +The exact JSON string that was parsed can be recovered using `.Text`: ```bash -$ echo '{"hello": "World"}' | tj -readjson -template "{{.TimeString}} {{.Object.hello}}" +$ echo '{"hello" : "World"} { }' | tj -read-json -template "{{.TimeString}} {{.Text}}" ``` ``` -2018-01-25T21:55:06+01:00 World +2018-01-25T21:55:06+01:00 {"hello" : "World"} +2018-01-25T21:55:06+01:00 { } ``` -Additionally, you can also specify a template `-jsontemplate` to extract text from the object. The output of this template is matched against the stopwatch regex. +### Stopwatch regex + +Sometimes you need to measure the duration between certain *tokens* in the input. + +To help with this, `tj` can match each line against a regular expression and only reset the stopwatch (`delta`, `deltaSecs`, `deltaNanos`) when a line matches. The regular expression can be specified via the `-match-regex` (alias `-regex`) parameter. + +### Stopwatch regex template -This allows you to use only specific fields of the object as stopwatch reset triggers. For example: +When using `-match-regex`, you can also specify a template `-match-template` (alias `-match`) to extract text from the current token. The output of this template is matched against the stopwatch regex. + +This allows you to use only specific fields of JSON objects as stopwatch reset triggers. For example: ```bash $ (echo {}; sleep 1; echo {}; sleep 1; echo '{"reset": "yes"}'; echo {}) | - tj -jsontemplate "{{.reset}}" -start yes -template "{{.I}} {{.DeltaNanos}}" + tj -read-json -match .reset -regex yes -template "{{.I}} {{.DeltaNanos}}" ``` ``` @@ -201,16 +244,20 @@ $ (echo {}; sleep 1; echo {}; sleep 1; echo '{"reset": "yes"}'; echo {}) | 3 79099 ``` -The output of the JSON template is stored in the field `.JSONText` of the `line` struct: +The output of the match template is stored in the field `.MatchText` of the `token` struct: ```bash -$ echo '{"message":"hello"}' | tj -jsontemplate "{{.message}}" -template "{{.TimeString}} {{.JSONText}}" +$ echo '{"message":"hello"}' | tj -read-json -match-template .message -template "{{.TimeString}} {{.MatchText}}" ``` ``` 2018-01-25T22:20:59+01:00 hello ``` +### Stopwatch condition + +Additionally to `-match-regex`, you can specify a `-match-condition` go template. If this template produces the literal string `true`, the stopwatch is reset - "matches" of the `-match-condition` are treated like matches of the `-match-regex`. + ## Example Finding the slowest step in a `docker build` (using `jq`): @@ -225,8 +272,8 @@ RUN echo Done being slow ```bash $ docker build . | - tj -start ^Step | - jq -s 'max_by(.deltaNanos) | {step:.startText, duration:.delta}' + tj -regex ^Step | + jq -s 'max_by(.deltaNanos) | {step:.start.text, duration:.delta}' ``` ```json @@ -237,7 +284,7 @@ Alternatively, using color output and buffering: ```bash $ docker build . | - tj -start ^Step -template Color -scale GreenToGreenToRed -delta-buffer + tj -regex ^Step -match-buffer -template Color -scale-cube ``` ![Docker build with color output](docs/images/docker.png) diff --git a/cmd/tj/json.go b/cmd/tj/json.go new file mode 100644 index 0000000..2a51efc --- /dev/null +++ b/cmd/tj/json.go @@ -0,0 +1,98 @@ +package main + +import ( + "bytes" + "encoding/json" + "io" + "os" + "strings" +) + +const jsonStreamScratchBufferBytes = 4096 + +type jsonStream struct { + token + Text string `json:"-"` // the original text that Object was parsed from + Object interface{} `json:"object,omitempty"` + + textBuffer *bytes.Buffer // intercepts bytes read by decoder + scratchBuffer []byte // determines size of decoder.Buffered() + buffer *tokenBuffer + decoder *json.Decoder + decodeError error + done bool +} + +func newJSONStream() *jsonStream { + textBuffer := bytes.NewBuffer(nil) + tee := io.TeeReader(os.Stdin, textBuffer) + return &jsonStream{ + decoder: json.NewDecoder(tee), + textBuffer: textBuffer, + scratchBuffer: make([]byte, jsonStreamScratchBufferBytes), + buffer: &tokenBuffer{}, + } +} + +func (j *jsonStream) Token() *token { + return &j.token +} + +func (j *jsonStream) CopyCurrent() tokenStream { + return &jsonStream{ + token: j.token, + Object: j.Object, + } +} + +func (j *jsonStream) AppendCurrentToBuffer() { + *j.buffer = append(*j.buffer, j.CopyCurrent()) +} + +func (j *jsonStream) FlushBuffer() { + j.buffer.flush(j) +} + +func (j *jsonStream) CurrentMatchText() string { + if matchTemplate != nil { + return matchTemplate.execute(j.Object) + } + return j.Text +} + +func (j *jsonStream) Err() error { + if j.decodeError == io.EOF { + return nil + } + return j.decodeError +} + +func (j *jsonStream) readerSize(r io.Reader) int { + total := 0 + var err error + var n int + for err == nil { + n, err = r.Read(j.scratchBuffer) + total += n + } + return total +} + +func (j *jsonStream) Scan() bool { + j.Object = new(interface{}) + err := j.decoder.Decode(&j.Object) + numBytesNotParsedByJSON := j.readerSize(j.decoder.Buffered()) // "{..} XYZ" -> len("XYZ") + bytesUnreadByUs := j.textBuffer.Bytes() // "{..} XYZ" -> "{..} XYZ" + numBytesUnreadByUs := len(bytesUnreadByUs) + numBytesParsedByJSON := numBytesUnreadByUs - numBytesNotParsedByJSON // len("{..}") + bytesReadByJSON := bytesUnreadByUs[:numBytesParsedByJSON] // "{..} XYZ" -> "{..}" + j.Text = strings.TrimSpace(string(bytesReadByJSON)) + j.textBuffer.Next(numBytesParsedByJSON) // "*{..} XYZ" -> "*XYZ" + if err != nil { + if j.decodeError == nil || j.decodeError == io.EOF { + j.decodeError = err + } + return false + } + return true +} diff --git a/cmd/tj/lines.go b/cmd/tj/lines.go new file mode 100644 index 0000000..ac00f6b --- /dev/null +++ b/cmd/tj/lines.go @@ -0,0 +1,60 @@ +package main + +import ( + "bufio" + "os" +) + +type lineStream struct { + token + Text string `json:"text,omitempty"` + + buffer *tokenBuffer + scanner *bufio.Scanner +} + +func newLineStream() *lineStream { + return &lineStream{ + scanner: bufio.NewScanner(os.Stdin), + buffer: &tokenBuffer{}, + } +} + +func (l *lineStream) Token() *token { + return &l.token +} + +func (l *lineStream) CopyCurrent() tokenStream { + return &lineStream{ + token: l.token, + Text: l.Text, + } +} + +func (l *lineStream) AppendCurrentToBuffer() { + *l.buffer = append(*l.buffer, l.CopyCurrent()) +} + +func (l *lineStream) FlushBuffer() { + l.buffer.flush(l) +} + +func (l *lineStream) CurrentMatchText() string { + if matchTemplate != nil { + return matchTemplate.execute(l.Text) + } + return l.Text +} + +func (l *lineStream) Err() error { + return l.scanner.Err() +} + +func (l *lineStream) Scan() bool { + l.Text = "" + ok := l.scanner.Scan() + if ok { + l.Text = l.scanner.Text() + } + return ok +} diff --git a/cmd/tj/main.go b/cmd/tj/main.go index d772136..e297840 100644 --- a/cmd/tj/main.go +++ b/cmd/tj/main.go @@ -1,7 +1,6 @@ package main import ( - "bufio" "bytes" "encoding/json" "flag" @@ -16,8 +15,8 @@ import ( "github.com/sgreben/tj/pkg/color" ) -type line struct { - I uint64 `json:"-"` // line number +type token struct { + I uint64 `json:"-"` // token index TimeSecs int64 `json:"timeSecs"` TimeNanos int64 `json:"timeNanos"` TimeString string `json:"time,omitempty"` @@ -30,37 +29,57 @@ type line struct { TotalNanos int64 `json:"totalNanos"` TotalString string `json:"total,omitempty"` Total time.Duration `json:"-"` - Text string `json:"text,omitempty"` - StartText string `json:"startText,omitempty"` - JSONText string `json:"jsonText,omitempty"` - Object interface{} `json:"object,omitempty"` - StartObject interface{} `json:"startObject,omitempty"` + MatchText string `json:"-"` + Start interface{} `json:"start,omitempty"` +} + +func (t *token) copyDeltasFrom(token *token) { + t.DeltaSecs = token.DeltaSecs + t.DeltaNanos = token.DeltaNanos + t.DeltaString = token.DeltaString + t.Delta = token.Delta } type configuration struct { - timeFormat string // -timeformat="..." + timeFormat string // -time-format="..." + timeZone string // -time-zone="..." template string // -template="..." - start string // -start="..." - readJSON bool // -readjson - jsonTemplate string // -jsontemplate="..." - colorScale string // -scale="..." - fast time.Duration // -scale-fast="..." - slow time.Duration // -scale-slow="..." - buffer bool // -delta-buffer + matchRegex string // -match-regex="..." + matchTemplate string // -match-template="..." + matchCondition string // -match-condition="..." + buffer bool // -match-buffer + readJSON bool // -read-json + scaleText string // -scale="..." + scaleFast time.Duration // -scale-fast="..." + scaleSlow time.Duration // -scale-slow="..." + scaleCube bool // -scale-cube + scaleSqr bool // -scale-sqr + scaleLinear bool // -scale-linear + scaleSqrt bool // -scale-sqrt + scaleCubert bool // -scale-cubert printVersionAndExit bool // -version } -type printerFunc func(line *line) error +type printerFunc func(interface{}) error var ( - version string - config configuration - printer printerFunc - start *regexp.Regexp - jsonTemplate *templateWithBuffer - scale color.Scale + version string + config configuration + printer printerFunc + matchRegex *regexp.Regexp + matchCondition *templateWithBuffer + matchTemplate *templateWithBuffer + scale color.Scale + tokens tokenStream + location *time.Location ) +func print(data interface{}) { + if err := printer(data); err != nil { + fmt.Fprintln(os.Stderr, "output error:", err) + } +} + var timeFormats = map[string]string{ "ANSIC": time.ANSIC, "UnixDate": time.UnixDate, @@ -80,11 +99,13 @@ var timeFormats = map[string]string{ } var templates = map[string]string{ + "Text": "{{.Text}}", "Time": "{{.TimeString}} {{.Text}}", "TimeDelta": "{{.TimeString}} +{{.DeltaNanos}} {{.Text}}", "Delta": "{{.DeltaNanos}} {{.Text}}", "ColorText": "{{color .}}{{.Text}}{{reset}}", "Color": "{{color .}}█{{reset}} {{.Text}}", + "TimeColor": "{{.TimeString}} {{color .}}█{{reset}} {{.Text}}", } var colorScales = map[string]string{ @@ -104,23 +125,22 @@ var templateFuncs = template.FuncMap{ "reset": func() string { return color.Reset }, } -func foregroundColor(line *line) string { - c := float64(line.DeltaNanos-int64(config.fast)) / float64(config.slow-config.fast) +func foregroundColor(o tokenOwner) string { + token := o.Token() + c := float64(token.DeltaNanos-int64(config.scaleFast)) / float64(config.scaleSlow-config.scaleFast) return color.Foreground(scale(c)) } func jsonPrinter() printerFunc { enc := json.NewEncoder(os.Stdout) - return func(line *line) error { - return enc.Encode(line) - } + return enc.Encode } func templatePrinter(t string) printerFunc { template := template.Must(template.New("-template").Funcs(templateFuncs).Option("missingkey=zero").Parse(t)) newline := []byte("\n") - return func(line *line) error { - err := template.Execute(os.Stdout, line) + return func(data interface{}) error { + err := template.Execute(os.Stdout, data) os.Stdout.Write(newline) return err } @@ -153,75 +173,98 @@ func colorScalesHelp() string { return "either a sequence of hex colors or one of the predefined color scale names (colors go from fast to slow)\n" + strings.Join(help, "\n") } +func addTemplateDelimitersIfLiteral(t string) string { + if !strings.Contains(t, "{{") { + return "{{" + t + "}}" + } + return t +} + func init() { flag.StringVar(&config.template, "template", "", templatesHelp()) - flag.StringVar(&config.timeFormat, "timeformat", "RFC3339", timeFormatsHelp()) - flag.StringVar(&config.start, "start", "", "a regex pattern. if given, only lines matching it (re)start the stopwatch") - flag.BoolVar(&config.readJSON, "readjson", false, "parse each stdin line as JSON") - flag.StringVar(&config.jsonTemplate, "jsontemplate", "", "go template, used to extract text from json input. implies -readjson") - flag.StringVar(&config.colorScale, "scale", "BlueToRed", colorScalesHelp()) - flag.DurationVar(&config.fast, "scale-fast", 100*time.Millisecond, "the lower bound for the color scale") - flag.DurationVar(&config.slow, "scale-slow", 2*time.Second, "the upper bound for the color scale") - flag.BoolVar(&config.buffer, "delta-buffer", false, "buffer lines between -start matches, copy delta values from final line to buffered lines") + flag.StringVar(&config.timeFormat, "time-format", "RFC3339", timeFormatsHelp()) + flag.StringVar(&config.timeZone, "time-zone", "Local", "time zone to use") + flag.StringVar(&config.matchRegex, "regex", "", "alias for -match-regex") + flag.StringVar(&config.matchRegex, "match-regex", "", "a regex pattern. if given, only tokens matching it (re)start the stopwatch") + flag.StringVar(&config.matchCondition, "condition", "", "alias for -match-condition") + flag.StringVar(&config.matchCondition, "match-condition", "", "go template. if given, only tokens that result in 'true' (re)start the stopwatch") + flag.StringVar(&config.matchTemplate, "match", "", "alias for -match-template") + flag.StringVar(&config.matchTemplate, "match-template", "", "go template, used to extract text used for -match-regex") + flag.BoolVar(&config.buffer, "match-buffer", false, "buffer lines between matches of -match-regex / -match-condition, copy delta values from final line to buffered lines") + flag.BoolVar(&config.readJSON, "read-json", false, "parse a sequence of JSON objects from stdin") + flag.StringVar(&config.scaleText, "scale", "BlueToRed", colorScalesHelp()) + flag.DurationVar(&config.scaleFast, "scale-fast", 100*time.Millisecond, "the lower bound for the color scale") + flag.DurationVar(&config.scaleSlow, "scale-slow", 2*time.Second, "the upper bound for the color scale") + flag.BoolVar(&config.scaleCube, "scale-cube", false, "use cubic scale") + flag.BoolVar(&config.scaleSqr, "scale-sqr", false, "use quadratic scale") + flag.BoolVar(&config.scaleLinear, "scale-linear", true, "use linear scale") + flag.BoolVar(&config.scaleSqrt, "scale-sqrt", false, "use quadratic root scale") + flag.BoolVar(&config.scaleCubert, "scale-cubert", false, "use cubic root scale") flag.BoolVar(&config.printVersionAndExit, "version", false, "print version and exit") flag.Parse() if config.printVersionAndExit { fmt.Println(version) os.Exit(0) } + var err error + location, err = time.LoadLocation(config.timeZone) + if err != nil { + fmt.Fprintln(os.Stderr, "time zone parse error:", err) + os.Exit(1) + } if knownFormat, ok := timeFormats[config.timeFormat]; ok { config.timeFormat = knownFormat } if knownTemplate, ok := templates[config.template]; ok { config.template = knownTemplate } - if knownScale, ok := colorScales[config.colorScale]; ok { - config.colorScale = knownScale + if knownScale, ok := colorScales[config.scaleText]; ok { + config.scaleText = knownScale } - if config.colorScale != "" { - scale = color.ParseScale(config.colorScale) + if config.scaleText != "" { + scale = color.ParseScale(config.scaleText) + } + if config.scaleLinear { + // do nothing + } + if config.scaleSqrt { + scale = color.Sqrt(scale) + } + if config.scaleCubert { + scale = color.Cubert(scale) + } + if config.scaleSqr { + scale = color.Sqr(scale) + } + if config.scaleCube { + scale = color.Cube(scale) } if config.template != "" { printer = templatePrinter(config.template) } else { printer = jsonPrinter() } - if config.start != "" { - start = regexp.MustCompile(config.start) + if config.matchRegex != "" { + matchRegex = regexp.MustCompile(config.matchRegex) } - if config.jsonTemplate != "" { - config.readJSON = true - jsonTemplate = &templateWithBuffer{ - template: template.Must(template.New("-jsontemplate").Option("missingkey=zero").Parse(config.jsonTemplate)), + if config.readJSON { + tokens = newJSONStream() + } else { + tokens = newLineStream() + } + if config.matchTemplate != "" { + config.matchTemplate = addTemplateDelimitersIfLiteral(config.matchTemplate) + matchTemplate = &templateWithBuffer{ + template: template.Must(template.New("-match-template").Option("missingkey=zero").Parse(config.matchTemplate)), buffer: bytes.NewBuffer(nil), } } -} - -type lineBuffer []*line - -func (b *lineBuffer) flush(line *line) { - for i, oldLine := range *b { - oldLine.DeltaSecs = line.DeltaSecs - oldLine.DeltaNanos = line.DeltaNanos - oldLine.DeltaString = line.DeltaString - oldLine.Delta = line.Delta - oldLine.print() - (*b)[i] = nil - } - *b = (*b)[:0] -} - -func (l *line) print() { - if err := printer(l); err != nil { - fmt.Fprintln(os.Stderr, "output error:", err) - } -} - -func (l *line) parseJSON() { - l.Object = new(interface{}) - if err := json.Unmarshal([]byte(l.Text), &l.Object); err != nil { - fmt.Fprintln(os.Stderr, "JSON parse error:", err) + if config.matchCondition != "" { + config.matchCondition = addTemplateDelimitersIfLiteral(config.matchCondition) + matchCondition = &templateWithBuffer{ + template: template.Must(template.New("-match-condition").Option("missingkey=zero").Funcs(templateFuncs).Parse(config.matchCondition)), + buffer: bytes.NewBuffer(nil), + } } } @@ -230,66 +273,90 @@ type templateWithBuffer struct { buffer *bytes.Buffer } -func (t *templateWithBuffer) execute(data interface{}) string { +func (t *templateWithBuffer) executeSilent(data interface{}) (string, error) { t.buffer.Reset() - if err := t.template.Execute(t.buffer, data); err != nil { + err := t.template.Execute(t.buffer, data) + return t.buffer.String(), err +} + +func (t *templateWithBuffer) execute(data interface{}) string { + s, err := t.executeSilent(data) + if err != nil { fmt.Fprintln(os.Stderr, "template error:", err) } - return t.buffer.String() + return s +} + +type tokenOwner interface { + Token() *token +} + +type tokenStream interface { + tokenOwner + AppendCurrentToBuffer() + FlushBuffer() + CurrentMatchText() string + CopyCurrent() tokenStream + Err() error + Scan() bool } func main() { - scanner := bufio.NewScanner(os.Stdin) - lineBuffer := lineBuffer{} - line := line{Time: time.Now()} - first := line.Time - last := line.Time + token := tokens.Token() + first := time.Now().In(location) + last := first i := uint64(0) - for scanner.Scan() { - now := time.Now() + + for tokens.Scan() { + now := time.Now().In(location) delta := now.Sub(last) total := now.Sub(first) - line.DeltaSecs = delta.Seconds() - line.DeltaNanos = delta.Nanoseconds() - line.DeltaString = delta.String() - line.Delta = delta - line.TotalSecs = total.Seconds() - line.TotalNanos = total.Nanoseconds() - line.TotalString = total.String() - line.Total = total - line.TimeSecs = now.Unix() - line.TimeNanos = now.UnixNano() - line.TimeString = now.Format(config.timeFormat) - line.Time = now - line.Text = scanner.Text() - line.I = i - match := &line.Text - if config.readJSON { - line.parseJSON() - if jsonTemplate != nil { - line.JSONText = jsonTemplate.execute(line.Object) - match = &line.JSONText - } + + token.DeltaSecs = delta.Seconds() + token.DeltaNanos = delta.Nanoseconds() + token.DeltaString = delta.String() + token.Delta = delta + token.TotalSecs = total.Seconds() + token.TotalNanos = total.Nanoseconds() + token.TotalString = total.String() + token.Total = total + token.TimeSecs = now.Unix() + token.TimeNanos = now.UnixNano() + token.TimeString = now.Format(config.timeFormat) + token.Time = now + + token.I = i + token.MatchText = tokens.CurrentMatchText() + + matchRegexDefined := matchRegex != nil + matchConditionDefined := matchCondition != nil + matchDefined := matchRegexDefined || matchConditionDefined + printToken := !matchDefined || !config.buffer + + matches := matchDefined + if matchRegexDefined { + matches = matches && matchRegex.MatchString(token.MatchText) + } + if matchConditionDefined { + result, _ := matchCondition.executeSilent(tokens) + matches = matches && strings.TrimSpace(result) == "true" } - startDefined := start != nil - printLine := !startDefined || !config.buffer - startMatches := startDefined && start.MatchString(*match) - resetStopwatch := !startDefined || startMatches + resetStopwatch := !matchDefined || matches - if printLine { - line.print() + if printToken { + print(tokens) } - if startMatches { - line.StartText = line.Text - line.StartObject = line.Object + if matches { + currentCopy := tokens.CopyCurrent() + currentCopy.Token().Start = nil // Prevent nested .start.start.start blow-up + token.Start = currentCopy if config.buffer { - lineBuffer.flush(&line) + tokens.FlushBuffer() } } - if !printLine { - lineCopy := line - lineBuffer = append(lineBuffer, &lineCopy) + if !printToken { + tokens.AppendCurrentToBuffer() } if resetStopwatch { last = now @@ -298,10 +365,10 @@ func main() { } if config.buffer { - lineBuffer.flush(&line) + tokens.FlushBuffer() } - if err := scanner.Err(); err != nil { + if err := tokens.Err(); err != nil { fmt.Fprintln(os.Stderr, "input error:", err) os.Exit(1) } diff --git a/cmd/tj/tokenBuffer.go b/cmd/tj/tokenBuffer.go new file mode 100644 index 0000000..e37d28b --- /dev/null +++ b/cmd/tj/tokenBuffer.go @@ -0,0 +1,12 @@ +package main + +type tokenBuffer []tokenStream + +func (b *tokenBuffer) flush(token tokenStream) { + for i, oldToken := range *b { + oldToken.Token().copyDeltasFrom(token.Token()) + print(oldToken) + (*b)[i] = nil + } + *b = (*b)[:0] +} diff --git a/pkg/color/color.go b/pkg/color/color.go index 7083573..96a8272 100644 --- a/pkg/color/color.go +++ b/pkg/color/color.go @@ -2,6 +2,7 @@ package color import ( "fmt" + "math" "regexp" "strconv" "strings" @@ -19,6 +20,34 @@ func index(r, g, b uint8) int { return 36*ri + 6*gi + bi + 16 } +func Cube(scale Scale) Scale { + return func(c float64) (r, g, b uint8) { + c = clamp(c) + return scale(c * c * c) + } +} + +func Sqr(scale Scale) Scale { + return func(c float64) (r, g, b uint8) { + c = clamp(c) + return scale(c * c) + } +} + +func Sqrt(scale Scale) Scale { + return func(c float64) (r, g, b uint8) { + c = clamp(c) + return scale(math.Sqrt(c)) + } +} + +func Cubert(scale Scale) Scale { + return func(c float64) (r, g, b uint8) { + c = clamp(c) + return scale(math.Pow(c, 1.0/3.0)) + } +} + func clamp(c float64) float64 { if c < 0 { c = 0 @@ -71,7 +100,6 @@ func ParseScale(scale string) Scale { } func interpolate2(c float64, r1, g1, b1, r2, g2, b2 uint8) (r, g, b uint8) { - c = clamp(c) r = uint8(float64(r1)*(1-c) + float64(r2)*c) g = uint8(float64(g1)*(1-c) + float64(g2)*c) b = uint8(float64(b1)*(1-c) + float64(b2)*c)