Skip to content
This repository was archived by the owner on Dec 13, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
)

const (
Version = "v1.5.9"
Version = "v1.5.10"
MAXPARTS = 2
)

Expand Down Expand Up @@ -93,6 +93,8 @@ type Config struct {
UseHighlight bool
Interactive bool
InputJSON bool
AutoHeaders bool
CustomHeaders []string

SortMode string
SortDescending bool
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func Execute() {
ShowCompletion string
modeflag cfg.Modeflag
sortmode cfg.Sortmode
headers string
)

var rootCmd = &cobra.Command{
Expand Down Expand Up @@ -91,6 +92,7 @@ func Execute() {
conf.CheckEnv()
conf.PrepareModeFlags(modeflag)
conf.PrepareSortFlags(sortmode)
conf.PrepareCustomHeaders(headers)

wrapE(conf.PrepareFilters())

Expand Down Expand Up @@ -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", "",
Expand Down
4 changes: 4 additions & 0 deletions cmd/tablizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ SYNOPSIS
-R, --regex-transposer </from/to/> 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
Expand Down Expand Up @@ -517,6 +519,8 @@ Operational Flags:
-R, --regex-transposer </from/to/> 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
Expand Down
65 changes: 60 additions & 5 deletions lib/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -128,19 +170,32 @@ 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 {
data.maxwidthHeader = headerlen
}

// 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 {
Expand Down
50 changes: 50 additions & 0 deletions lib/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion tablizer.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -160,6 +160,8 @@ tablizer \- Manipulate tabular output of other programs
\& \-R, \-\-regex\-transposer </from/to/> 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
Expand Down
2 changes: 2 additions & 0 deletions tablizer.pod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ tablizer - Manipulate tabular output of other programs
-R, --regex-transposer </from/to/> 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
Expand Down
Loading