diff --git a/cfg/config.go b/cfg/config.go index 610d32c..08c7920 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -28,7 +28,7 @@ import ( ) const ( - Version = "v1.5.9" + Version = "v1.5.10" MAXPARTS = 2 ) @@ -93,6 +93,8 @@ type Config struct { UseHighlight bool Interactive bool InputJSON bool + AutoHeaders bool + CustomHeaders []string SortMode string SortDescending bool @@ -413,6 +415,12 @@ func (conf *Config) PreparePattern(patterns []*Pattern) error { return nil } +func (conf *Config) PrepareCustomHeaders(custom string) { + if len(custom) > 0 { + conf.CustomHeaders = strings.Split(custom, ",") + } +} + // Parse config file. Ignore if the file doesn't exist but return an // error if it exists but fails to read or parse func (conf *Config) ParseConfigfile() error { diff --git a/cmd/root.go b/cmd/root.go index 24d12ef..5e6e8e0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -59,6 +59,7 @@ func Execute() { ShowCompletion string modeflag cfg.Modeflag sortmode cfg.Sortmode + headers string ) var rootCmd = &cobra.Command{ @@ -91,6 +92,7 @@ func Execute() { conf.CheckEnv() conf.PrepareModeFlags(modeflag) conf.PrepareSortFlags(sortmode) + conf.PrepareCustomHeaders(headers) wrapE(conf.PrepareFilters()) @@ -137,6 +139,10 @@ func Execute() { "Output field separator (' ' for ascii table, ',' for CSV)") rootCmd.PersistentFlags().BoolVarP(&conf.InputJSON, "json", "j", false, "JSON input mode") + rootCmd.PersistentFlags().BoolVarP(&conf.AutoHeaders, "auto-headers", "", false, + "Generate headers automatically") + rootCmd.PersistentFlags().StringVarP(&headers, "custom-headers", "", "", + "Custom headers") // sort options rootCmd.PersistentFlags().StringVarP(&conf.SortByColumn, "sort-by", "k", "", diff --git a/cmd/tablizer.go b/cmd/tablizer.go index 0a84dc5..9d8f8c9 100644 --- a/cmd/tablizer.go +++ b/cmd/tablizer.go @@ -22,6 +22,8 @@ SYNOPSIS -R, --regex-transposer Apply /search/replace/ regexp to fields given in -T -j, --json Read JSON input (must be array of hashes) -I, --interactive Interactively filter and select rows + --auto-headers Generate headers if there are none present in input + --custom-headers a,b,... Use custom headers, separated by comma Output Flags (mutually exclusive): -X, --extended Enable extended output @@ -517,6 +519,8 @@ Operational Flags: -R, --regex-transposer Apply /search/replace/ regexp to fields given in -T -j, --json Read JSON input (must be array of hashes) -I, --interactive Interactively filter and select rows + --auto-headers Generate headers if there are none present in input + --custom-headers a,b,... Use custom headers, separated by comma Output Flags (mutually exclusive): -X, --extended Enable extended output diff --git a/lib/parser.go b/lib/parser.go index ef77148..195babd 100644 --- a/lib/parser.go +++ b/lib/parser.go @@ -66,6 +66,43 @@ func Parse(conf cfg.Config, input io.Reader) (Tabdata, error) { return data, err } +/* + * Setup headers, given headers might be usable headers or just the + * first row, which we use to determine how many headers to generate, + * if enabled. + */ +func SetHeaders(conf cfg.Config, headers []string) []string { + if !conf.AutoHeaders && len(conf.CustomHeaders) == 0 { + return headers + } + + if conf.AutoHeaders { + heads := make([]string, len(headers)) + for idx := range headers { + heads[idx] = fmt.Sprintf("%d", idx+1) + } + + return heads + } + + if len(conf.CustomHeaders) == len(headers) { + return conf.CustomHeaders + } + + // use as much custom ones we have, generate the remainder + heads := make([]string, len(headers)) + + for idx := range headers { + if idx < len(conf.CustomHeaders) { + heads[idx] = conf.CustomHeaders[idx] + } else { + heads[idx] = fmt.Sprintf("%d", idx+1) + } + } + + return heads +} + /* Parse CSV input. */ @@ -87,7 +124,7 @@ func parseCSV(conf cfg.Config, input io.Reader) (Tabdata, error) { } if len(records) >= 1 { - data.headers = records[0] + data.headers = SetHeaders(conf, records[0]) data.columns = len(records) for _, head := range data.headers { @@ -98,9 +135,14 @@ func parseCSV(conf cfg.Config, input io.Reader) (Tabdata, error) { } } - if len(records) > 1 { - data.entries = records[1:] + if len(records) >= 1 { + if conf.AutoHeaders || len(conf.CustomHeaders) > 0 { + data.entries = records + } else { + data.entries = records[1:] + } } + } return data, nil @@ -128,7 +170,9 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) { data.columns = len(parts) // process all header fields - for _, part := range parts { + firstrow := make([]string, len(parts)) + + for idx, part := range parts { // register widest header field headerlen := len(part) if headerlen > data.maxwidthHeader { @@ -136,11 +180,22 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) { } // register fields data - data.headers = append(data.headers, strings.TrimSpace(part)) + firstrow[idx] = strings.TrimSpace(part) // done hadFirst = true } + + data.headers = SetHeaders(conf, firstrow) + + if conf.AutoHeaders || len(conf.CustomHeaders) > 0 { + // we do not use generated headers, consider as row + if matchPattern(conf, line) == conf.InvertMatch { + continue + } + + data.entries = append(data.entries, firstrow) + } } else { // data processing if matchPattern(conf, line) == conf.InvertMatch { diff --git a/lib/parser_test.go b/lib/parser_test.go index 74fe685..5d7ad63 100644 --- a/lib/parser_test.go +++ b/lib/parser_test.go @@ -366,6 +366,56 @@ func TestParserSeparators(t *testing.T) { } } +func TestParserSetHeaders(t *testing.T) { + row := []string{"c", "b", "c", "d", "e"} + + tests := []struct { + name string + custom []string + expect []string + auto bool + }{ + { + name: "default", + expect: row, + }, + { + name: "auto", + expect: strings.Split("1 2 3 4 5", " "), + auto: true, + }, + { + name: "custom-complete", + custom: strings.Split("A B C D E", " "), + expect: strings.Split("A B C D E", " "), + }, + { + name: "custom-too-short", + custom: strings.Split("A B", " "), + expect: strings.Split("A B 3 4 5", " "), + }, + { + name: "custom-too-long", + custom: strings.Split("A B C D E F G", " "), + expect: strings.Split("A B C D E", " "), + }, + } + + for _, testdata := range tests { + testname := fmt.Sprintf("parse-%s", testdata.name) + t.Run(testname, func(t *testing.T) { + conf := cfg.Config{ + AutoHeaders: testdata.auto, + CustomHeaders: testdata.custom, + } + headers := SetHeaders(conf, row) + + assert.NotNil(t, headers) + assert.EqualValues(t, testdata.expect, headers) + }) + } +} + func wrapValidateParser(conf cfg.Config, input io.Reader) (Tabdata, error) { data, err := Parse(conf, input) diff --git a/tablizer.1 b/tablizer.1 index 6927aa5..549b2d4 100644 --- a/tablizer.1 +++ b/tablizer.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "TABLIZER 1" -.TH TABLIZER 1 "2025-10-09" "1" "User Commands" +.TH TABLIZER 1 "2025-10-10" "1" "User Commands" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -160,6 +160,8 @@ tablizer \- Manipulate tabular output of other programs \& \-R, \-\-regex\-transposer Apply /search/replace/ regexp to fields given in \-T \& \-j, \-\-json Read JSON input (must be array of hashes) \& \-I, \-\-interactive Interactively filter and select rows +\& \-\-auto\-headers Generate headers if there are none present in input +\& \-\-custom\-headers a,b,... Use custom headers, separated by comma \& \& Output Flags (mutually exclusive): \& \-X, \-\-extended Enable extended output diff --git a/tablizer.pod b/tablizer.pod index e731a94..e3791f9 100644 --- a/tablizer.pod +++ b/tablizer.pod @@ -21,6 +21,8 @@ tablizer - Manipulate tabular output of other programs -R, --regex-transposer Apply /search/replace/ regexp to fields given in -T -j, --json Read JSON input (must be array of hashes) -I, --interactive Interactively filter and select rows + --auto-headers Generate headers if there are none present in input + --custom-headers a,b,... Use custom headers, separated by comma Output Flags (mutually exclusive): -X, --extended Enable extended output