From 0d98f9ff44029d43673d48dff31ac27acf714887 Mon Sep 17 00:00:00 2001 From: Raphael 'kena' Poss Date: Fri, 4 Dec 2020 23:25:30 +0100 Subject: [PATCH] util/log: replace gen.sh by a go template generator This prevents build problems due to folk not using a `bash` that's modern enough. Release note: None --- Makefile | 19 +- docs/generated/logging.md | 3 +- pkg/util/log/channel/channel_generated.go | 2 +- pkg/util/log/channels.go | 8 +- pkg/util/log/gen.go | 429 ++++++++++++++++++++ pkg/util/log/gen.sh | 404 ------------------ pkg/util/log/log_channels_generated.go | 80 ++-- pkg/util/log/severity/severity_generated.go | 22 +- 8 files changed, 497 insertions(+), 470 deletions(-) create mode 100644 pkg/util/log/gen.go delete mode 100755 pkg/util/log/gen.sh diff --git a/Makefile b/Makefile index c1109c7156d8..ead9d4655405 100644 --- a/Makefile +++ b/Makefile @@ -1544,24 +1544,21 @@ docs/generated/redact_safe.md: sed -E -e 's/^([^:]*):[0-9]+:.*redact\.RegisterSafeType\((.*)\).*/\1 | \`\2\`/g' >>$@.tmp || { rm -f $@.tmp; exit 1; } @mv -f $@.tmp $@ -docs/generated/logging.md: pkg/util/log/gen.sh pkg/util/log/logpb/log.proto - bash $< logging.md >$@.tmp || { rm -f $@.tmp; exit 1; } +docs/generated/logging.md: pkg/util/log/gen.go pkg/util/log/logpb/log.proto + $(GO) run $(GOFLAGS) $(GOMODVENDORFLAGS) $^ logging.md $@.tmp || { rm -f $@.tmp; exit 1; } mv -f $@.tmp $@ -pkg/util/log/severity/severity_generated.go: pkg/util/log/gen.sh pkg/util/log/logpb/log.proto - bash $< severity.go >$@.tmp || { rm -f $@.tmp; exit 1; } +pkg/util/log/severity/severity_generated.go: pkg/util/log/gen.go pkg/util/log/logpb/log.proto + $(GO) run $(GOFLAGS) $(GOMODVENDORFLAGS) $^ severity.go $@.tmp || { rm -f $@.tmp; exit 1; } mv -f $@.tmp $@ - gofmt -s -w $@ -pkg/util/log/channel/channel_generated.go: pkg/util/log/gen.sh pkg/util/log/logpb/log.proto - bash $< channel.go >$@.tmp || { rm -f $@.tmp; exit 1; } +pkg/util/log/channel/channel_generated.go: pkg/util/log/gen.go pkg/util/log/logpb/log.proto + $(GO) run $(GOFLAGS) $(GOMODVENDORFLAGS) $^ channel.go $@.tmp || { rm -f $@.tmp; exit 1; } mv -f $@.tmp $@ - gofmt -s -w $@ -pkg/util/log/log_channels_generated.go: pkg/util/log/gen.sh pkg/util/log/logpb/log.proto - bash $< log_channels.go >$@.tmp || { rm -f $@.tmp; exit 1; } +pkg/util/log/log_channels_generated.go: pkg/util/log/gen.go pkg/util/log/logpb/log.proto + $(GO) run $(GOFLAGS) $(GOMODVENDORFLAGS) $^ log_channels.go $@.tmp || { rm -f $@.tmp; exit 1; } mv -f $@.tmp $@ - gofmt -s -w $@ settings-doc-gen := $(if $(filter buildshort,$(MAKECMDGOALS)),$(COCKROACHSHORT),$(COCKROACH)) diff --git a/docs/generated/logging.md b/docs/generated/logging.md index 7ab607fcef21..30fb234c23e0 100644 --- a/docs/generated/logging.md +++ b/docs/generated/logging.md @@ -22,6 +22,7 @@ The FATAL severity is used for situations that require an immedate, hard server shutdown. A report is also sent to telemetry if telemetry is enabled. + # Logging channels ## DEV @@ -33,7 +34,7 @@ CockroachDB, when the caller does not indicate a channel. This channel is special in that there are no constraints as to what may or may not be logged on it. Conversely, users in -production deployments are invited to not collect The DEV channel logs in +production deployments are invited to not collect DEV logs in centralized logging facilities, because they likely contain sensitive operational data. diff --git a/pkg/util/log/channel/channel_generated.go b/pkg/util/log/channel/channel_generated.go index 7e0f63660ce9..b80a30451774 100644 --- a/pkg/util/log/channel/channel_generated.go +++ b/pkg/util/log/channel/channel_generated.go @@ -1,4 +1,4 @@ -// Code generated by gen.sh. DO NOT EDIT. +// Code generated by gen.go. DO NOT EDIT. package channel diff --git a/pkg/util/log/channels.go b/pkg/util/log/channels.go index 6ab1bd6709cc..2a419236f34a 100644 --- a/pkg/util/log/channels.go +++ b/pkg/util/log/channels.go @@ -22,10 +22,10 @@ import ( "github.com/cockroachdb/errors" ) -//go:generate ./gen.sh ../../../docs/generated/logging.md logging.md -//go:generate ./gen.sh severity/severity_generated.go severity.go -//go:generate ./gen.sh channel/channel_generated channel.go -//go:generate ./gen.sh log_channels_generated.go log_channels.go +//go:generate go run ./gen.go logpb/log.proto logging.md ../../../docs/generated/logging.md +//go:generate go run ./gen.go logpb/log.proto severity.go severity/severity_generated.go +//go:generate go run ./gen.go logpb/log.proto channel.go channel/channel_generated.go +//go:generate go run ./gen.go logpb/log.proto log_channels.go log_channels_generated.go // Channel aliases a type. type Channel = logpb.Channel diff --git a/pkg/util/log/gen.go b/pkg/util/log/gen.go new file mode 100644 index 000000000000..43671936ff43 --- /dev/null +++ b/pkg/util/log/gen.go @@ -0,0 +1,429 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +// +build ignore + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "strings" + "text/template" + + "github.com/cockroachdb/cockroach/pkg/cli/exit" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/gostdlib/go/format" +) + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, "ERROR:", err) + exit.WithCode(exit.UnspecifiedError()) + } +} + +func run() error { + if len(os.Args) < 3 { + return errors.Newf("usage: %s template>\n", os.Args[0]) + } + + // Which template are we running? + tmplName := os.Args[2] + tmplSrc, ok := templates[tmplName] + if !ok { + return errors.Newf("unknown template: %q", tmplName) + } + tmpl, err := template.New(tmplName).Parse(tmplSrc) + if err != nil { + return errors.Wrap(err, tmplName) + } + + // Read the input .proto file. + chans, sevs, err := readInput(os.Args[1]) + if err != nil { + return err + } + + // Render the template. + var src bytes.Buffer + if err := tmpl.Execute(&src, struct { + Severities []info + Channels []info + }{sevs, chans}); err != nil { + return err + } + + // If we are generating a .go file, do a pass of gofmt. + newBytes := src.Bytes() + if strings.HasSuffix(tmplName, ".go") { + newBytes, err = format.Source(newBytes) + if err != nil { + return errors.Wrap(err, "gofmt") + } + } + + // Write the output file. + w := os.Stdout + if len(os.Args) > 3 { + f, err := os.OpenFile(os.Args[3], os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + w = f + } + if _, err := w.Write(newBytes); err != nil { + return err + } + + return nil +} + +type info struct { + RawComment string + Comment string + PComment string + Name string + NAME string + NameLower string +} + +func readInput(protoName string) (chans []info, sevs []info, err error) { + protoData, err := ioutil.ReadFile(protoName) + if err != nil { + return nil, nil, err + } + inSevs := false + inChans := false + rawComment := "" + for _, line := range strings.Split(string(protoData), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + switch { + case !inSevs && !inChans && strings.HasPrefix(line, "enum Severity"): + inSevs = true + continue + case inSevs && !inChans && strings.HasPrefix(line, "}"): + inSevs = false + continue + case !inSevs && !inChans && strings.HasPrefix(line, "enum Channel"): + inChans = true + continue + case !inSevs && inChans && strings.HasPrefix(line, "}"): + inChans = false + continue + } + if !inSevs && !inChans { + continue + } + + if strings.HasPrefix(line, "//") { + rawComment += line + "\n" + continue + } + if strings.HasPrefix(line, "reserved") { + rawComment = "" + continue + } + key := strings.Split(line, " ")[0] + title := strings.ReplaceAll(strings.Title(strings.ReplaceAll(strings.ToLower(key), "_", " ")), " ", "") + if inSevs { + comment := "// The " + key + " severity" + strings.TrimPrefix(rawComment, "// "+key) + sevs = append(sevs, info{ + RawComment: rawComment, + Comment: comment, + PComment: strings.ReplaceAll(strings.ReplaceAll(comment, "// ", ""), "//", ""), + Name: title, + NAME: strings.ToUpper(key), + NameLower: strings.ToLower(key), + }) + } + if inChans { + comment := "// The " + key + " channel" + strings.TrimPrefix(rawComment, "// "+key) + chans = append(chans, info{ + RawComment: rawComment, + Comment: comment, + PComment: strings.ReplaceAll(strings.ReplaceAll(comment, "// ", ""), "//", ""), + Name: title, + NAME: strings.ToUpper(key), + NameLower: strings.ToLower(key), + }) + } + rawComment = "" + } + + return chans, sevs, nil +} + +var templates = map[string]string{ + "logging.md": `# Logging levels (severities) +{{range .Severities}}{{if eq .NAME "NONE" "UNKNOWN" "DEFAULT"|not}} +## {{.NAME}} + +{{.PComment}} +{{- end}}{{- end}} + +# Logging channels +{{range .Channels}} +## {{.NAME}} + +{{.PComment}} +{{- end}} +`, + + "severity.go": `// Code generated by gen.go. DO NOT EDIT. + +package severity + +import "github.com/cockroachdb/cockroach/pkg/util/log/logpb" +{{range .Severities}} + +{{ .RawComment -}} +const {{.NAME}} = logpb.Severity_{{.NAME}} +{{end}} +`, + + "channel.go": `// Code generated by gen.go. DO NOT EDIT. + +package channel + +import "github.com/cockroachdb/cockroach/pkg/util/log/logpb" + +{{range .Channels}} + +{{ .RawComment -}} +const {{.NAME}} = logpb.Channel_{{.NAME}} +{{end}} +`, + + "log_channels.go": `// Code generated by gen.go. DO NOT EDIT. + +package log + +import ( + "context" + + "github.com/cockroachdb/cockroach/pkg/util/log/channel" + "github.com/cockroachdb/cockroach/pkg/util/log/severity" +) + +// ChannelLogger is a helper interface to ease the run-time selection +// of channels. We do not force use of ChannelLogger when +// instantiating the logger objects below (e.g. by giving them the +// interface type), to ensure the calls remain inlinable in the common +// case. +// +// Note that casting a channel logger to the interface +// type yields a heap allocation: it may be useful for performance to +// pre-allocate interface references in the global scope. +type ChannelLogger interface { + {{range .Severities}}{{if eq .NAME "NONE" "UNKNOWN" "DEFAULT"|not -}} + // {{.Name}}f logs to the channel with severity {{.NAME}}. + // It extracts log tags from the context and logs them along with the given + // message. Arguments are handled in the manner of fmt.Printf. + {{.Name}}f(ctx context.Context, format string, args ...interface{}) + + // V{{.Name}}f logs to the channel with severity {{.NAME}}, + // if logging has been enabled for the source file where the call is + // performed at the provided verbosity level, via the vmodule setting. + // It extracts log tags from the context and logs them along with the given + // message. Arguments are handled in the manner of fmt.Printf. + V{{.Name}}f(ctx context.Context, level Level, format string, args ...interface{}) + + // {{.Name}} logs to the channel with severity {{.NAME}}. + // It extracts log tags from the context and logs them along with the given + // message. + {{.Name}}(ctx context.Context, msg string) + + // {{.Name}}fDepth logs to the channel with severity {{.NAME}}, + // offsetting the caller's stack frame by 'depth'. + // It extracts log tags from the context and logs them along with the given + // message. Arguments are handled in the manner of fmt.Printf. + {{.Name}}fDepth(ctx context.Context, depth int, format string, args ...interface{}) + + {{end}}{{end}}{{- /* end range severities */ -}} + + // Shout logs to the channel, and also to the real stderr if logging + // is currently redirected to a file. + Shout(ctx context.Context, sev Severity, msg string) + + // Shoutf logs to the channel, and also to the real stderr if + // logging is currently redirected to a file. Arguments are handled in + // the manner of fmt.Printf. + Shoutf(ctx context.Context, sev Severity, format string, args ...interface{}) +} + +{{$sevs := .Severities}} +{{range $unused, $chan := .Channels}} +// logger{{.Name}} is the logger type for the {{.NAME}} channel. +type logger{{.Name}} struct{} + +// {{.Name}} is a logger that logs to the {{.NAME}} channel. +// +{{.Comment -}} +var {{.Name}} logger{{.Name}} + +// {{.Name}} and logger{{.Name}} implement ChannelLogger. +// +// We do not force use of ChannelLogger when instantiating the logger +// object above (e.g. by giving it the interface type), to ensure +// the calls to the API methods remain inlinable in the common case. +var _ ChannelLogger = {{.Name}} + +{{range $sevi, $sev := $sevs}}{{if eq .NAME "NONE" "UNKNOWN" "DEFAULT"|not}}{{with $chan}} +// {{with $sev}}{{.Name}}{{end}}f logs to the {{.NAME}} channel with severity {{with $sev}}{{.NAME}}{{end}}. +// It extracts log tags from the context and logs them along with the given +// message. Arguments are handled in the manner of fmt.Printf. +// +{{.Comment -}} +// +{{with $sev}}{{.Comment}}{{end -}} +func (logger{{.Name}}) {{with $sev}}{{.Name}}{{end}}f(ctx context.Context, format string, args ...interface{}) { + logfDepth(ctx, 1, severity.{{with $sev}}{{.NAME}}{{end}}, channel.{{.NAME}}, format, args...) +} + +// V{{with $sev}}{{.Name}}{{end}}f logs to the {{.NAME}} channel with severity {{with $sev}}{{.NAME}}{{end}}, +// if logging has been enabled for the source file where the call is +// performed at the provided verbosity level, via the vmodule setting. +// It extracts log tags from the context and logs them along with the given +// message. Arguments are handled in the manner of fmt.Printf. +// +{{.Comment -}} +// +{{with $sev}}{{.Comment}}{{end -}} +func (logger{{.Name}}) V{{with $sev}}{{.Name}}{{end}}f(ctx context.Context, level Level, format string, args ...interface{}) { + if VDepth(level, 1) { + logfDepth(ctx, 1, severity.{{with $sev}}{{.NAME}}{{end}}, channel.{{.NAME}}, format, args...) + } +} + +// {{with $sev}}{{.Name}}{{end}} logs to the {{.NAME}} channel with severity {{with $sev}}{{.NAME}}{{end}}. +// It extracts log tags from the context and logs them along with the given +// message. +// +{{.Comment -}} +// +{{with $sev}}{{.Comment}}{{end -}} +func (logger{{.Name}}) {{with $sev}}{{.Name}}{{end}}(ctx context.Context, msg string) { + logfDepth(ctx, 1, severity.{{with $sev}}{{.NAME}}{{end}}, channel.{{.NAME}}, msg) +} + +// {{with $sev}}{{.Name}}{{end}}fDepth logs to the {{.NAME}} channel with severity {{with $sev}}{{.NAME}}{{end}}, +// offsetting the caller's stack frame by 'depth'. +// It extracts log tags from the context and logs them along with the given +// message. Arguments are handled in the manner of fmt.Printf. +// +{{.Comment -}} +// +{{with $sev}}{{.Comment}}{{end -}} +func (logger{{.Name}}) {{with $sev}}{{.Name}}{{end}}fDepth(ctx context.Context, depth int, format string, args ...interface{}) { + logfDepth(ctx, depth+1, severity.{{with $sev}}{{.NAME}}{{end}}, channel.{{.NAME}}, format, args...) +} + +{{if .NAME|eq "DEV"}} +// {{with $sev}}{{.Name}}{{end}}f logs to the {{.NAME}} channel with severity {{with $sev}}{{.NAME}}{{end}}, +// if logging has been enabled for the source file where the call is +// performed at the provided verbosity level, via the vmodule setting. +// It extracts log tags from the context and logs them along with the given +// message. Arguments are handled in the manner of fmt.Printf. +// +{{.Comment -}} +// +{{with $sev}}{{.Comment}}{{end -}} +func {{with $sev}}{{.Name}}{{end}}f(ctx context.Context, format string, args ...interface{}) { + logfDepth(ctx, 1, severity.{{with $sev}}{{.NAME}}{{end}}, channel.{{.NAME}}, format, args...) +} + +// V{{with $sev}}{{.Name}}{{end}}f logs to the {{.NAME}} channel with severity {{with $sev}}{{.NAME}}{{end}}. +// It extracts log tags from the context and logs them along with the given +// message. Arguments are handled in the manner of fmt.Printf. +// +{{.Comment -}} +// +{{with $sev}}{{.Comment}}{{end -}} +func V{{with $sev}}{{.Name}}{{end}}f(ctx context.Context, level Level, format string, args ...interface{}) { + if VDepth(level, 1) { + logfDepth(ctx, 1, severity.{{with $sev}}{{.NAME}}{{end}}, channel.{{.NAME}}, format, args...) + } +} + +// {{with $sev}}{{.Name}}{{end}} logs to the {{.NAME}} channel with severity {{with $sev}}{{.NAME}}{{end}}. +// It extracts log tags from the context and logs them along with the given +// message. +// +{{.Comment -}} +// +{{with $sev}}{{.Comment}}{{end -}} +func {{with $sev}}{{.Name}}{{end}}(ctx context.Context, msg string) { + logfDepth(ctx, 1, severity.{{with $sev}}{{.NAME}}{{end}}, channel.{{.NAME}}, msg) +} + +// {{with $sev}}{{.Name}}{{end}}fDepth logs to the {{.NAME}} channel with severity {{with $sev}}{{.NAME}}{{end}}, +// offsetting the caller's stack frame by 'depth'. +// It extracts log tags from the context and logs them along with the given +// message. Arguments are handled in the manner of fmt.Printf. +// +{{.Comment -}} +// +{{with $sev}}{{.Comment}}{{end -}} +func {{with $sev}}{{.Name}}{{end}}fDepth(ctx context.Context, depth int, format string, args ...interface{}) { + logfDepth(ctx, depth+1, severity.{{with $sev}}{{.NAME}}{{end}}, channel.{{.NAME}}, format, args...) +} +{{end}}{{- /* end channel name = DEV */ -}} + +{{end}}{{end}}{{end}}{{- /* end range severities */ -}} + +// Shout logs to channel {{.NAME}}, and also to the real stderr if logging +// is currently redirected to a file. +// +{{.Comment -}} +func (logger{{.Name}}) Shout(ctx context.Context, sev Severity, msg string) { + shoutfDepth(ctx, 1, sev, channel.{{.NAME}}, msg) +} + +// Shoutf logs to channel {{.NAME}}, and also to the real stderr if +// logging is currently redirected to a file. Arguments are handled in +// the manner of fmt.Printf. +// +{{.Comment -}} +func (logger{{.Name}}) Shoutf(ctx context.Context, sev Severity, format string, args ...interface{}) { + shoutfDepth(ctx, 1, sev, channel.{{.NAME}}, format, args...) +} + +{{if .NAME|eq "DEV"}} + +// Shout logs to channel {{.NAME}}, and also to the real stderr if logging +// is currently redirected to a file. +// +{{.Comment -}} +func Shout(ctx context.Context, sev Severity, msg string) { + shoutfDepth(ctx, 1, sev, channel.{{.NAME}}, msg) +} + +// Shoutf logs to channel {{.NAME}}, and also to the real stderr if +// logging is currently redirected to a file. Arguments are handled in +// the manner of fmt.Printf. +// +{{.Comment -}} +func Shoutf(ctx context.Context, sev Severity, format string, args ...interface{}) { + shoutfDepth(ctx, 1, sev, channel.{{.NAME}}, format, args...) +} + +{{end}}{{- /* end channel name = DEV */ -}} + +{{end}}{{- /* end range channels */ -}} +`, +} diff --git a/pkg/util/log/gen.sh b/pkg/util/log/gen.sh deleted file mode 100755 index 7ef251ead220..000000000000 --- a/pkg/util/log/gen.sh +++ /dev/null @@ -1,404 +0,0 @@ -#!/usr/bin/env bash - -set -eu -o pipefail - -pkg=$(dirname "$0") - -all_severities=$(awk ' -/enum Severity/ { active=1; next } -/}/ { active=0; next } -/^[ \t]*[A-Z][A-Z]*/ { if (active) { print $1 } } -' <$pkg/logpb/log.proto) - -severities=() -severities_comments=() -severities_raw_comments=() -for s in $all_severities; do - raw_comment=$(awk ' -/\/\/ '$s'/ { active=1; print $0; next } -/^[ \t]*[A-Z][A-Z]*/ { active=0; next } -{ if (active) { print $0 } } -' <$pkg/logpb/log.proto | sed -e "s/^ *//g") - - if test $s = NONE -o $s = UNKNOWN -o $s = DEFAULT; then - eval ${s}_COMMENT="\$raw_comment" - continue - fi - - comment=$(echo "$raw_comment" | sed -e "s/$s/The $s severity/g") - set +u # builder image bash is too old and balks at expanding an empty array - severities=(${severities[*]} $s) - severities_raw_comments=("${severities_raw_comments[@]}" "$raw_comment") - severities_comments=("${severities_comments[@]}" "$comment") - set -u -done - -all_channels=$(awk ' -/enum Channel/ { active=1; next } -/}/ { active=0; next } -/^[ \t]*[A-Z][A-Z]*/ { if (active) { print $1 } } -' <$pkg/logpb/log.proto) - -channels=() -channels_comments=() -channels_raw_comments=() -for s in $all_channels; do - raw_comment=$(awk ' -/\/\/ '$s'/ { active=1; print $0; next } -/^[ \t]*[A-Z][A-Z]*/ { active=0; next } -{ if (active) { print $0 } } -' <$pkg/logpb/log.proto | sed -e "s/^ *//g") - comment=$(echo "$raw_comment" | sed -e "s/$s/The $s channel/g") - set +u # builder image bash is too old and balks at expanding an empty array - channels=(${channels[*]} $s) - channels_comments=("${channels_comments[@]}" "$comment") - channels_raw_comments=("${channels_raw_comments[@]}" "$raw_comment") - set -u -done - -if test $1 = log_channels.go; then - cat < foo - severityw=(${severity//_/ }) # foo_bar -> (foo bar) - Severityt=${severityw[*]^} # (foo bar) -> (Foo Bar) - Severityt=${Severityt[*]} # (Foo Bar) -> "Foo Bar" - Severity=${Severityt// /} # "Foo Bar" -> "FooBar" - SeverityComment=${severities_comments[$sidx]} - cat < foo - channelw=(${channel//_/ }) # foo_bar -> (foo bar) - Channelt=${channelw[*]^} # (foo bar) -> (Foo Bar) - Channelt=${Channelt[*]} # (Foo Bar) -> "Foo Bar" - Channel=${Channelt// /} # "Foo Bar" -> "FooBar" - ChannelComment=${channels_comments[$cidx]} - - cat < foo - severityw=(${severity//_/ }) # foo_bar -> (foo bar) - Severityt=${severityw[*]^} # (foo bar) -> (Foo Bar) - Severityt=${Severityt[*]} # (Foo Bar) -> "Foo Bar" - Severity=${Severityt// /} # "Foo Bar" -> "FooBar" - SeverityComment=${severities_comments[$sidx]} - - cat < foo - severityw=(${severity//_/ }) # foo_bar -> (foo bar) - Severityt=${severityw[*]^} # (foo bar) -> (Foo Bar) - Severityt=${Severityt[*]} # (Foo Bar) -> "Foo Bar" - Severity=${Severityt// /} # "Foo Bar" -> "FooBar" - SeverityComment=${severities_raw_comments[$sidx]} - cat < foo - channelw=(${channel//_/ }) # foo_bar -> (foo bar) - Channelt=${channelw[*]^} # (foo bar) -> (Foo Bar) - Channelt=${Channelt[*]} # (Foo Bar) -> "Foo Bar" - Channel=${Channelt// /} # "Foo Bar" -> "FooBar" - ChannelComment=${channels_raw_comments[$cidx]} - - cat <