Compare commits

..

9 Commits

17 changed files with 528 additions and 276 deletions

View File

@@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org). The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org).
## [v1.0.12](https://github.com/TLINDEN/tablizer/tree/v1.0.12) - 2022-10-25
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.11...v1.0.12)
### Added
- Added support to parse CSV input
- Added CSV output support
- Added support for environment variables
### Changed
- We do not use the generated help message anymore, instead we use the
usage from the manpage, which we have to maintain anyway. It looks
better and has flag groups, which cobra is still lacking as of this
writing.
- More refactoring and re-organization, runtime configuration now
lives in the cfg module.
### Fixed
- Fixed [Bug #5](https://github.com/TLINDEN/tablizer/issues/5), where
matches have not been highlighted correctly in some rare cases.
## [v1.0.11](https://github.com/TLINDEN/tablizer/tree/v1.0.11) - 2022-10-19 ## [v1.0.11](https://github.com/TLINDEN/tablizer/tree/v1.0.11) - 2022-10-19
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.10...v1.0.11) [Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.10...v1.0.11)

View File

@@ -41,6 +41,10 @@ cmd/%.go: %.pod
pod2text $*.pod >> cmd/$*.go pod2text $*.pod >> cmd/$*.go
echo "\`" >> cmd/$*.go echo "\`" >> cmd/$*.go
echo "var usage = \`" >> cmd/$*.go
awk '/SYNOPS/{f=1;next} /DESCR/{f=0} f' $*.pod | sed 's/^ //' >> cmd/$*.go
echo "\`" >> cmd/$*.go
buildlocal: buildlocal:
go build -ldflags "-X 'github.com/tlinden/tablizer/cfg.VERSION=$(VERSION)'" go build -ldflags "-X 'github.com/tlinden/tablizer/cfg.VERSION=$(VERSION)'"

View File

@@ -1,15 +1,9 @@
## Fixes to be implemented ## Fixes to be implemented
- rm printYamlData() log.Fatal(), maybe return error on all printers?
- printShellData() checks Columns unnecessarily (compare to yaml or extended)
## Features to be implemented ## Features to be implemented
- add output mode csv - add comment support (csf.NewReader().Comment = '#')
- add --no-headers option - add --no-headers option
- add input parsing support for CSV including unquoting of stuff
like: `"xxx","1919 b"` etc, maybe an extra option for unquoting

View File

@@ -20,12 +20,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/gookit/color" "github.com/gookit/color"
"os"
"regexp" "regexp"
) )
const DefaultSeparator string = `(\s\s+|\t)` const DefaultSeparator string = `(\s\s+|\t)`
const ValidOutputModes string = "(orgtbl|markdown|extended|ascii|yaml|shell)" const Version string = "v1.0.12"
const Version string = "v1.0.11"
var VERSION string // maintained by -x var VERSION string // maintained by -x
@@ -35,9 +35,10 @@ type Config struct {
Columns string Columns string
UseColumns []int UseColumns []int
Separator string Separator string
OutputMode string OutputMode int
InvertMatch bool InvertMatch bool
Pattern string Pattern string
PatternR *regexp.Regexp
SortMode string SortMode string
SortDescending bool SortDescending bool
@@ -45,12 +46,10 @@ type Config struct {
/* /*
FIXME: make configurable somehow, config file or ENV FIXME: make configurable somehow, config file or ENV
see https://github.com/gookit/color will be set by see https://github.com/gookit/color.
io.ProcessFiles() according to currently supported
color mode.
*/ */
MatchFG string ColorStyle color.Style
MatchBG string
NoColor bool NoColor bool
} }
@@ -62,8 +61,20 @@ type Modeflag struct {
S bool S bool
Y bool Y bool
A bool A bool
C bool
} }
// used for switching printers
const (
Extended = iota + 1
Orgtbl
Markdown
Shell
Yaml
CSV
Ascii
)
// various sort types // various sort types
type Sortmode struct { type Sortmode struct {
Numeric bool Numeric bool
@@ -71,22 +82,43 @@ type Sortmode struct {
Age bool Age bool
} }
func Colors() map[color.Level]map[string]string {
// default color schemes // default color schemes
return map[color.Level]map[string]string{ func Colors() map[color.Level]map[string]color.Color {
return map[color.Level]map[string]color.Color{
color.Level16: { color.Level16: {
"bg": "green", "fg": "black", "bg": color.BgGreen, "fg": color.FgBlack,
}, },
color.Level256: { color.Level256: {
"bg": "lightGreen", "fg": "black", "bg": color.BgLightGreen, "fg": color.FgBlack,
}, },
color.LevelRgb: { color.LevelRgb: {
// FIXME: maybe use something nicer // FIXME: maybe use something nicer
"bg": "lightGreen", "fg": "black", "bg": color.BgLightGreen, "fg": color.FgBlack,
}, },
} }
} }
// find supported color mode, modifies config based on constants
func (c *Config) DetermineColormode() {
if !isTerminal(os.Stdout) {
color.Disable()
} else {
level := color.TermColorLevel()
colors := Colors()
c.ColorStyle = color.New(colors[level]["bg"], colors[level]["fg"])
}
}
// Return true if current terminal is interactive
func isTerminal(f *os.File) bool {
o, _ := f.Stat()
if (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice {
return true
} else {
return false
}
}
func Getversion() string { func Getversion() string {
// main program version // main program version
@@ -97,39 +129,6 @@ func Getversion() string {
return fmt.Sprintf("This is tablizer version %s", VERSION) return fmt.Sprintf("This is tablizer version %s", VERSION)
} }
func (conf *Config) PrepareModeFlags(flag Modeflag, mode string) error {
if len(mode) == 0 {
// associate short flags like -X with mode selector
switch {
case flag.X:
conf.OutputMode = "extended"
case flag.M:
conf.OutputMode = "markdown"
case flag.O:
conf.OutputMode = "orgtbl"
case flag.S:
conf.OutputMode = "shell"
conf.NoNumbering = true
case flag.Y:
conf.OutputMode = "yaml"
conf.NoNumbering = true
default:
conf.OutputMode = "ascii"
}
} else {
r, _ := regexp.Compile(ValidOutputModes) // hardcoded, no fail expected
match := r.MatchString(mode)
if !match {
return errors.New("Invalid output mode!")
}
conf.OutputMode = mode
}
return nil
}
func (conf *Config) PrepareSortFlags(flag Sortmode) { func (conf *Config) PrepareSortFlags(flag Sortmode) {
switch { switch {
case flag.Numeric: case flag.Numeric:
@@ -142,3 +141,60 @@ func (conf *Config) PrepareSortFlags(flag Sortmode) {
conf.SortMode = "string" conf.SortMode = "string"
} }
} }
func (conf *Config) PrepareModeFlags(flag Modeflag) {
switch {
case flag.X:
conf.OutputMode = Extended
case flag.O:
conf.OutputMode = Orgtbl
case flag.M:
conf.OutputMode = Markdown
case flag.S:
conf.OutputMode = Shell
case flag.Y:
conf.OutputMode = Yaml
case flag.C:
conf.OutputMode = CSV
default:
conf.OutputMode = Ascii
}
}
func (c *Config) CheckEnv() {
// check for environment vars, command line flags have precedence,
// NO_COLOR is being checked by the color module itself.
if !c.NoNumbering {
_, set := os.LookupEnv("T_NO_HEADER_NUMBERING")
if set {
c.NoNumbering = true
}
}
if len(c.Columns) == 0 {
cols := os.Getenv("T_COLUMNS")
if len(cols) > 1 {
c.Columns = cols
}
}
}
func (c *Config) ApplyDefaults() {
// mode specific defaults
if c.OutputMode == Yaml || c.OutputMode == CSV {
c.NoNumbering = true
}
}
func (c *Config) PreparePattern(pattern string) error {
PatternR, err := regexp.Compile(pattern)
if err != nil {
return errors.Unwrap(fmt.Errorf("Regexp pattern %s is invalid: %w", c.Pattern, err))
}
c.PatternR = PatternR
c.Pattern = pattern
return nil
}

View File

@@ -26,46 +26,26 @@ import (
func TestPrepareModeFlags(t *testing.T) { func TestPrepareModeFlags(t *testing.T) {
var tests = []struct { var tests = []struct {
flag Modeflag flag Modeflag
mode string // input, if any expect int // output (constant enum)
expect string // output
want bool
}{ }{
// short commandline flags like -M // short commandline flags like -M
{Modeflag{X: true}, "", "extended", false}, {Modeflag{X: true}, Extended},
{Modeflag{S: true}, "", "shell", false}, {Modeflag{S: true}, Shell},
{Modeflag{O: true}, "", "orgtbl", false}, {Modeflag{O: true}, Orgtbl},
{Modeflag{Y: true}, "", "yaml", false}, {Modeflag{Y: true}, Yaml},
{Modeflag{M: true}, "", "markdown", false}, {Modeflag{M: true}, Markdown},
{Modeflag{}, "", "ascii", false}, {Modeflag{}, Ascii},
// long flags like -o yaml
{Modeflag{}, "extended", "extended", false},
{Modeflag{}, "shell", "shell", false},
{Modeflag{}, "orgtbl", "orgtbl", false},
{Modeflag{}, "yaml", "yaml", false},
{Modeflag{}, "markdown", "markdown", false},
// failing
{Modeflag{}, "blah", "", true},
} }
// FIXME: use a map for easier printing
for _, tt := range tests { for _, tt := range tests {
testname := fmt.Sprintf("PrepareModeFlags-flags-mode-%s-expect-%s-want-%t", testname := fmt.Sprintf("PrepareModeFlags-expect-%d", tt.expect)
tt.mode, tt.expect, tt.want)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
c := Config{OutputMode: tt.mode} c := Config{}
// check either flag or pre filled mode, whatever is defined in tt c.PrepareModeFlags(tt.flag)
err := c.PrepareModeFlags(tt.flag, tt.mode)
if err != nil {
if !tt.want {
// expect to fail
t.Fatalf("PrepareModeFlags returned unexpected error: %s", err)
}
} else {
if c.OutputMode != tt.expect { if c.OutputMode != tt.expect {
t.Errorf("got: %s, expect: %s", c.OutputMode, tt.expect) t.Errorf("got: %d, expect: %d", c.OutputMode, tt.expect)
}
} }
}) })
} }
@@ -96,3 +76,28 @@ func TestPrepareSortFlags(t *testing.T) {
}) })
} }
} }
func TestPreparePattern(t *testing.T) {
var tests = []struct {
pattern string
wanterr bool
}{
{"[A-Z]+", false},
{"[a-z", true},
}
for _, tt := range tests {
testname := fmt.Sprintf("PreparePattern-pattern-%s-wanterr-%t", tt.pattern, tt.wanterr)
t.Run(testname, func(t *testing.T) {
c := Config{}
err := c.PreparePattern(tt.pattern)
if err != nil {
if !tt.wanterr {
t.Errorf("PreparePattern returned error: %s", err)
}
}
})
}
}

View File

@@ -25,6 +25,7 @@ import (
"log" "log"
"os" "os"
"os/exec" "os/exec"
"strings"
) )
func man() { func man() {
@@ -48,7 +49,6 @@ func Execute() {
var ( var (
conf cfg.Config conf cfg.Config
ShowManual bool ShowManual bool
Outputmode string
ShowVersion bool ShowVersion bool
modeflag cfg.Modeflag modeflag cfg.Modeflag
sortmode cfg.Sortmode sortmode cfg.Sortmode
@@ -69,16 +69,15 @@ func Execute() {
return nil return nil
} }
// prepare flags // Setup
err := conf.PrepareModeFlags(modeflag, Outputmode) conf.CheckEnv()
if err != nil { conf.PrepareModeFlags(modeflag)
return err
}
conf.PrepareSortFlags(sortmode) conf.PrepareSortFlags(sortmode)
conf.DetermineColormode()
conf.ApplyDefaults()
// actual execution starts here // actual execution starts here
return lib.ProcessFiles(conf, args) return lib.ProcessFiles(&conf, args)
}, },
} }
@@ -94,26 +93,24 @@ func Execute() {
// sort options // sort options
rootCmd.PersistentFlags().IntVarP(&conf.SortByColumn, "sort-by", "k", 0, "Sort by column (default: 1)") rootCmd.PersistentFlags().IntVarP(&conf.SortByColumn, "sort-by", "k", 0, "Sort by column (default: 1)")
// sort mode, only 1 allowed
rootCmd.PersistentFlags().BoolVarP(&conf.SortDescending, "sort-desc", "D", false, "Sort in descending order (default: ascending)") rootCmd.PersistentFlags().BoolVarP(&conf.SortDescending, "sort-desc", "D", false, "Sort in descending order (default: ascending)")
rootCmd.PersistentFlags().BoolVarP(&sortmode.Numeric, "sort-numeric", "i", false, "sort according to string numerical value") rootCmd.PersistentFlags().BoolVarP(&sortmode.Numeric, "sort-numeric", "i", false, "sort according to string numerical value")
rootCmd.PersistentFlags().BoolVarP(&sortmode.Time, "sort-time", "t", false, "sort according to time string") rootCmd.PersistentFlags().BoolVarP(&sortmode.Time, "sort-time", "t", false, "sort according to time string")
rootCmd.PersistentFlags().BoolVarP(&sortmode.Age, "sort-age", "a", false, "sort according to age (duration) string") rootCmd.PersistentFlags().BoolVarP(&sortmode.Age, "sort-age", "a", false, "sort according to age (duration) string")
rootCmd.MarkFlagsMutuallyExclusive("sort-desc", "sort-numeric", "sort-time", "sort-age")
// output flags, only 1 allowed, hidden, since just short cuts // output flags, only 1 allowed
rootCmd.PersistentFlags().BoolVarP(&modeflag.X, "extended", "X", false, "Enable extended output") rootCmd.PersistentFlags().BoolVarP(&modeflag.X, "extended", "X", false, "Enable extended output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.M, "markdown", "M", false, "Enable markdown table output") rootCmd.PersistentFlags().BoolVarP(&modeflag.M, "markdown", "M", false, "Enable markdown table output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.O, "orgtbl", "O", false, "Enable org-mode table output") rootCmd.PersistentFlags().BoolVarP(&modeflag.O, "orgtbl", "O", false, "Enable org-mode table output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.S, "shell", "S", false, "Enable shell mode output") rootCmd.PersistentFlags().BoolVarP(&modeflag.S, "shell", "S", false, "Enable shell mode output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.Y, "yaml", "Y", false, "Enable yaml output") rootCmd.PersistentFlags().BoolVarP(&modeflag.Y, "yaml", "Y", false, "Enable yaml output")
rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell", "yaml") rootCmd.PersistentFlags().BoolVarP(&modeflag.C, "csv", "C", false, "Enable CSV output")
_ = rootCmd.Flags().MarkHidden("extended") rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell", "yaml", "csv")
_ = rootCmd.Flags().MarkHidden("orgtbl")
_ = rootCmd.Flags().MarkHidden("markdown")
_ = rootCmd.Flags().MarkHidden("shell")
_ = rootCmd.Flags().MarkHidden("yaml")
// same thing but more common, takes precedence over above group rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n")
rootCmd.PersistentFlags().StringVarP(&Outputmode, "output", "o", "", "Output mode - one of: orgtbl, markdown, extended, shell, ascii(default)")
err := rootCmd.Execute() err := rootCmd.Execute()
if err != nil { if err != nil {

View File

@@ -8,24 +8,33 @@ SYNOPSIS
Usage: Usage:
tablizer [regex] [file, ...] [flags] tablizer [regex] [file, ...] [flags]
Flags: Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,) -c, --columns string Only show the speficied columns (separated by ,)
-d, --debug Enable debugging
-h, --help help for tablizer
-v, --invert-match select non-matching rows -v, --invert-match select non-matching rows
-m, --man Display manual page
-n, --no-numbering Disable header numbering -n, --no-numbering Disable header numbering
-N, --no-color Disable pattern highlighting -N, --no-color Disable pattern highlighting
-o, --output string Output mode - one of: orgtbl, markdown, extended, yaml, ascii(default) -s, --separator string Custom field separator
-k, --sort-by int Sort by column (default: 1)
Output Flags (mutually exclusive):
-X, --extended Enable extended output -X, --extended Enable extended output
-M, --markdown Enable markdown table output -M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output -O, --orgtbl Enable org-mode table output
-s, --separator string Custom field separator -S, --shell Enable shell evaluable ouput
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
Sort Mode Flags (mutually exclusive):
-a, --sort-age sort according to age (duration) string -a, --sort-age sort according to age (duration) string
-k, --sort-by int Sort by column (default: 1)
-D, --sort-desc Sort in descending order (default: ascending) -D, --sort-desc Sort in descending order (default: ascending)
-i, --sort-numeric sort according to string numerical value -i, --sort-numeric sort according to string numerical value
-t, --sort-time sort according to time string -t, --sort-time sort according to time string
Other Flags:
-d, --debug Enable debugging
-h, --help help for tablizer
-m, --man Display manual page
-v, --version Print program version -v, --version Print program version
DESCRIPTION DESCRIPTION
@@ -178,8 +187,17 @@ DESCRIPTION
Beside normal ascii mode (the default) and extended mode there are more Beside normal ascii mode (the default) and extended mode there are more
output modes available: orgtbl which prints an Emacs org-mode table and output modes available: orgtbl which prints an Emacs org-mode table and
markdown which prints a Markdown table and yaml, which prints yaml markdown which prints a Markdown table, yaml, which prints yaml encoding
encoding. and CSV mode, which prints a comma separated value file.
ENVIRONMENT VARIABLES
tablizer supports certain environment variables which use can use to
influence program behavior. Commandline flags have always precedence
over environment variables.
<T_NO_HEADER_NUMBERING> - disable numbering of header fields, like -n.
<T_COLUMNS> - comma separated list of columns to output, like -c
<NO_COLORS> - disable colorization of matches, like -N
BUGS BUGS
In order to report a bug, unexpected behavior, feature requests or to In order to report a bug, unexpected behavior, feature requests or to
@@ -205,3 +223,39 @@ AUTHORS
Thomas von Dein tom AT vondein DOT org Thomas von Dein tom AT vondein DOT org
` `
var usage = `
Usage:
tablizer [regex] [file, ...] [flags]
Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows
-n, --no-numbering Disable header numbering
-N, --no-color Disable pattern highlighting
-s, --separator string Custom field separator
-k, --sort-by int Sort by column (default: 1)
Output Flags (mutually exclusive):
-X, --extended Enable extended output
-M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable ouput
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
Sort Mode Flags (mutually exclusive):
-a, --sort-age sort according to age (duration) string
-D, --sort-desc Sort in descending order (default: ascending)
-i, --sort-numeric sort according to string numerical value
-t, --sort-time sort according to time string
Other Flags:
-d, --debug Enable debugging
-h, --help help for tablizer
-m, --man Display manual page
-v, --version Print program version
`

View File

@@ -20,7 +20,6 @@ package lib
// contains a whole parsed table // contains a whole parsed table
type Tabdata struct { type Tabdata struct {
maxwidthHeader int // longest header maxwidthHeader int // longest header
maxwidthPerCol []int // max width per column
columns int // count columns int // count
headers []string // [ "ID", "NAME", ...] headers []string // [ "ID", "NAME", ...]
entries [][]string entries [][]string

View File

@@ -155,17 +155,10 @@ func trimRow(row []string) []string {
func colorizeData(c cfg.Config, output string) string { func colorizeData(c cfg.Config, output string) string {
if len(c.Pattern) > 0 && !c.NoColor && color.IsConsole(os.Stdout) { if len(c.Pattern) > 0 && !c.NoColor && color.IsConsole(os.Stdout) {
r := regexp.MustCompile("(" + c.Pattern + ")") r := regexp.MustCompile("(" + c.Pattern + ")")
return r.ReplaceAllString(output, "<bg="+c.MatchBG+";fg="+c.MatchFG+">$1</>") return r.ReplaceAllStringFunc(output, func(in string) string {
return c.ColorStyle.Sprint(in)
})
} else { } else {
return output return output
} }
} }
func isTerminal(f *os.File) bool {
o, _ := f.Stat()
if (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice {
return true
} else {
return false
}
}

View File

@@ -48,11 +48,6 @@ func TestContains(t *testing.T) {
func TestPrepareColumns(t *testing.T) { func TestPrepareColumns(t *testing.T) {
data := Tabdata{ data := Tabdata{
maxwidthHeader: 5, maxwidthHeader: 5,
maxwidthPerCol: []int{
5,
5,
8,
},
columns: 3, columns: 3,
headers: []string{ headers: []string{
"ONE", "TWO", "THREE", "ONE", "TWO", "THREE",

View File

@@ -19,50 +19,39 @@ package lib
import ( import (
"errors" "errors"
"github.com/gookit/color"
"github.com/tlinden/tablizer/cfg" "github.com/tlinden/tablizer/cfg"
"io" "io"
"os" "os"
) )
func ProcessFiles(c cfg.Config, args []string) error { func ProcessFiles(c *cfg.Config, args []string) error {
fds, pattern, err := determineIO(&c, args) fds, pattern, err := determineIO(c, args)
if err != nil { if err != nil {
return err return err
} }
determineColormode(&c) if err := c.PreparePattern(pattern); err != nil {
return err
}
for _, fd := range fds { for _, fd := range fds {
data, err := parseFile(c, fd, pattern) data, err := Parse(*c, fd)
if err != nil { if err != nil {
return err return err
} }
err = PrepareColumns(&c, &data) err = PrepareColumns(c, &data)
if err != nil { if err != nil {
return err return err
} }
printData(os.Stdout, c, &data) printData(os.Stdout, *c, &data)
} }
return nil return nil
} }
// find supported color mode, modifies config based on constants
func determineColormode(c *cfg.Config) {
if !isTerminal(os.Stdout) {
color.Disable()
} else {
level := color.TermColorLevel()
colors := cfg.Colors()
c.MatchFG = colors[level]["fg"]
c.MatchBG = colors[level]["bg"]
}
}
func determineIO(c *cfg.Config, args []string) ([]io.Reader, string, error) { func determineIO(c *cfg.Config, args []string) ([]io.Reader, string, error) {
var pattern string var pattern string
var fds []io.Reader var fds []io.Reader

View File

@@ -19,6 +19,7 @@ package lib
import ( import (
"bufio" "bufio"
"encoding/csv"
"errors" "errors"
"fmt" "fmt"
"github.com/alecthomas/repr" "github.com/alecthomas/repr"
@@ -28,20 +29,84 @@ import (
"strings" "strings"
) )
/*
Parser switch
*/
func Parse(c cfg.Config, input io.Reader) (Tabdata, error) {
if len(c.Separator) == 1 {
return parseCSV(c, input)
}
return parseTabular(c, input)
}
/*
Parse CSV input.
*/
func parseCSV(c cfg.Config, input io.Reader) (Tabdata, error) {
var content io.Reader = input
data := Tabdata{}
if len(c.Pattern) > 0 {
scanner := bufio.NewScanner(input)
lines := []string{}
hadFirst := false
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if hadFirst {
// don't match 1st line, it's the header
if c.PatternR.MatchString(line) == c.InvertMatch {
// by default -v is false, so if a line does NOT
// match the pattern, we will ignore it. However,
// if the user specified -v, the matching is inverted,
// so we ignore all lines, which DO match.
continue
}
}
lines = append(lines, line)
hadFirst = true
}
content = strings.NewReader(strings.Join(lines, "\n"))
}
csvreader := csv.NewReader(content)
csvreader.Comma = rune(c.Separator[0])
records, err := csvreader.ReadAll()
if err != nil {
return data, errors.Unwrap(fmt.Errorf("Could not parse CSV input: %w", err))
}
if len(records) >= 1 {
data.headers = records[0]
data.columns = len(records)
for _, head := range data.headers {
// register widest header field
headerlen := len(head)
if headerlen > data.maxwidthHeader {
data.maxwidthHeader = headerlen
}
}
if len(records) > 1 {
data.entries = records[1:]
}
}
return data, nil
}
/* /*
Parse tabular input. Parse tabular input.
*/ */
func parseFile(c cfg.Config, input io.Reader, pattern string) (Tabdata, error) { func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) {
data := Tabdata{} data := Tabdata{}
var scanner *bufio.Scanner var scanner *bufio.Scanner
hadFirst := false hadFirst := false
separate := regexp.MustCompile(c.Separator) separate := regexp.MustCompile(c.Separator)
patternR, err := regexp.Compile(pattern)
if err != nil {
return data, errors.Unwrap(fmt.Errorf("Regexp pattern %s is invalid: %w", pattern, err))
}
scanner = bufio.NewScanner(input) scanner = bufio.NewScanner(input)
@@ -76,8 +141,8 @@ func parseFile(c cfg.Config, input io.Reader, pattern string) (Tabdata, error) {
} }
} else { } else {
// data processing // data processing
if len(pattern) > 0 { if len(c.Pattern) > 0 {
if patternR.MatchString(line) == c.InvertMatch { if c.PatternR.MatchString(line) == c.InvertMatch {
// by default -v is false, so if a line does NOT // by default -v is false, so if a line does NOT
// match the pattern, we will ignore it. However, // match the pattern, we will ignore it. However,
// if the user specified -v, the matching is inverted, // if the user specified -v, the matching is inverted,
@@ -89,16 +154,6 @@ func parseFile(c cfg.Config, input io.Reader, pattern string) (Tabdata, error) {
idx := 0 // we cannot use the header index, because we could exclude columns idx := 0 // we cannot use the header index, because we could exclude columns
values := []string{} values := []string{}
for _, part := range parts { for _, part := range parts {
width := len(strings.TrimSpace(part))
if len(data.maxwidthPerCol)-1 < idx {
data.maxwidthPerCol = append(data.maxwidthPerCol, width)
} else {
if width > data.maxwidthPerCol[idx] {
data.maxwidthPerCol[idx] = width
}
}
// if Debug { // if Debug {
// fmt.Printf("<%s> ", value) // fmt.Printf("<%s> ", value)
// } // }

View File

@@ -25,40 +25,58 @@ import (
"testing" "testing"
) )
var input = []struct {
name string
text string
separator string
}{
{
name: "tabular-data",
separator: cfg.DefaultSeparator,
text: `
ONE TWO THREE
asd igig cxxxncnc
19191 EDD 1 X`,
},
{
name: "csv-data",
separator: ",",
text: `
ONE,TWO,THREE
asd,igig,cxxxncnc
19191,"EDD 1",X`,
},
}
func TestParser(t *testing.T) { func TestParser(t *testing.T) {
data := Tabdata{ data := Tabdata{
maxwidthHeader: 5, maxwidthHeader: 5,
maxwidthPerCol: []int{
5, 5, 8,
},
columns: 3, columns: 3,
headers: []string{ headers: []string{
"ONE", "TWO", "THREE", "ONE", "TWO", "THREE",
}, },
entries: [][]string{ entries: [][]string{
{ {"asd", "igig", "cxxxncnc"},
"asd", "igig", "cxxxncnc", {"19191", "EDD 1", "X"},
},
{
"19191", "EDD 1", "X",
},
}, },
} }
table := `ONE TWO THREE for _, in := range input {
asd igig cxxxncnc testname := fmt.Sprintf("parse-%s", in.name)
19191 EDD 1 X` t.Run(testname, func(t *testing.T) {
readFd := strings.NewReader(strings.TrimSpace(in.text))
readFd := strings.NewReader(table) c := cfg.Config{Separator: in.separator}
c := cfg.Config{Separator: cfg.DefaultSeparator} gotdata, err := Parse(c, readFd)
gotdata, err := parseFile(c, readFd, "")
if err != nil { if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)
} }
if !reflect.DeepEqual(data, gotdata) { if !reflect.DeepEqual(data, gotdata) {
t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n", c.Separator, data, gotdata) t.Errorf("Parser returned invalid data\nExp: %+v\nGot: %+v\n",
data, gotdata)
}
})
} }
} }
@@ -71,48 +89,37 @@ func TestParserPatternmatching(t *testing.T) {
}{ }{
{ {
entries: [][]string{ entries: [][]string{
{ {"asd", "igig", "cxxxncnc"},
"asd", "igig", "cxxxncnc",
},
}, },
pattern: "ig", pattern: "ig",
invert: false, invert: false,
}, },
{ {
entries: [][]string{ entries: [][]string{
{ {"19191", "EDD 1", "X"},
"19191", "EDD 1", "X",
},
}, },
pattern: "ig", pattern: "ig",
invert: true, invert: true,
}, },
{
entries: [][]string{
{
"asd", "igig", "cxxxncnc",
},
},
pattern: "[a-z",
want: true,
},
} }
table := `ONE TWO THREE for _, in := range input {
asd igig cxxxncnc
19191 EDD 1 X`
for _, tt := range tests { for _, tt := range tests {
testname := fmt.Sprintf("parse-with-pattern-%s-inverted-%t", tt.pattern, tt.invert) testname := fmt.Sprintf("parse-%s-with-pattern-%s-inverted-%t",
in.name, tt.pattern, tt.invert)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
c := cfg.Config{InvertMatch: tt.invert, Pattern: tt.pattern, Separator: cfg.DefaultSeparator} c := cfg.Config{InvertMatch: tt.invert, Pattern: tt.pattern,
Separator: in.separator}
readFd := strings.NewReader(table) _ = c.PreparePattern(tt.pattern)
gotdata, err := parseFile(c, readFd, tt.pattern)
readFd := strings.NewReader(strings.TrimSpace(in.text))
gotdata, err := Parse(c, readFd)
if err != nil { if err != nil {
if !tt.want { if !tt.want {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) t.Errorf("Parser returned error: %s\nData processed so far: %+v",
err, gotdata)
} }
} else { } else {
if !reflect.DeepEqual(tt.entries, gotdata.entries) { if !reflect.DeepEqual(tt.entries, gotdata.entries) {
@@ -123,34 +130,29 @@ asd igig cxxxncnc
}) })
} }
} }
}
func TestParserIncompleteRows(t *testing.T) { func TestParserIncompleteRows(t *testing.T) {
data := Tabdata{ data := Tabdata{
maxwidthHeader: 5, maxwidthHeader: 5,
maxwidthPerCol: []int{
5, 5, 1,
},
columns: 3, columns: 3,
headers: []string{ headers: []string{
"ONE", "TWO", "THREE", "ONE", "TWO", "THREE",
}, },
entries: [][]string{ entries: [][]string{
{ {"asd", "igig", ""},
"asd", "igig", "", {"19191", "EDD 1", "X"},
},
{
"19191", "EDD 1", "X",
},
}, },
} }
table := `ONE TWO THREE table := `
ONE TWO THREE
asd igig asd igig
19191 EDD 1 X` 19191 EDD 1 X`
readFd := strings.NewReader(table) readFd := strings.NewReader(strings.TrimSpace(table))
c := cfg.Config{Separator: cfg.DefaultSeparator} c := cfg.Config{Separator: cfg.DefaultSeparator}
gotdata, err := parseFile(c, readFd, "") gotdata, err := Parse(c, readFd)
if err != nil { if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib package lib
import ( import (
"encoding/csv"
"fmt" "fmt"
"github.com/gookit/color" "github.com/gookit/color"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
@@ -43,21 +44,24 @@ func printData(w io.Writer, c cfg.Config, data *Tabdata) {
sortTable(c, data) sortTable(c, data)
switch c.OutputMode { switch c.OutputMode {
case "extended": case cfg.Extended:
printExtendedData(w, c, data) printExtendedData(w, c, data)
case "ascii": case cfg.Ascii:
printAsciiData(w, c, data) printAsciiData(w, c, data)
case "orgtbl": case cfg.Orgtbl:
printOrgmodeData(w, c, data) printOrgmodeData(w, c, data)
case "markdown": case cfg.Markdown:
printMarkdownData(w, c, data) printMarkdownData(w, c, data)
case "shell": case cfg.Shell:
printShellData(w, c, data) printShellData(w, c, data)
case "yaml": case cfg.Yaml:
printYamlData(w, c, data) printYamlData(w, c, data)
case cfg.CSV:
printCSVData(w, c, data)
default: default:
printAsciiData(w, c, data) printAsciiData(w, c, data)
} }
} }
func output(w io.Writer, str string) { func output(w io.Writer, str string) {
@@ -185,7 +189,7 @@ func printShellData(w io.Writer, c cfg.Config, data *Tabdata) {
} }
} }
// no colrization here // no colorization here
output(w, out) output(w, out)
} }
@@ -224,3 +228,23 @@ func printYamlData(w io.Writer, c cfg.Config, data *Tabdata) {
output(w, string(yamlstr)) output(w, string(yamlstr))
} }
func printCSVData(w io.Writer, c cfg.Config, data *Tabdata) {
csvout := csv.NewWriter(w)
if err := csvout.Write(data.headers); err != nil {
log.Fatalln("error writing record to csv:", err)
}
for _, entry := range data.entries {
if err := csvout.Write(entry); err != nil {
log.Fatalln("error writing record to csv:", err)
}
}
csvout.Flush()
if err := csvout.Error(); err != nil {
log.Fatal(err)
}
}

View File

@@ -29,12 +29,6 @@ import (
func newData() Tabdata { func newData() Tabdata {
return Tabdata{ return Tabdata{
maxwidthHeader: 8, maxwidthHeader: 8,
maxwidthPerCol: []int{
5,
9,
3,
26,
},
columns: 4, columns: 4,
headers: []string{ headers: []string{
"NAME", "NAME",
@@ -72,24 +66,33 @@ var tests = []struct {
column int // sort by this column, 0 == default first or NO Sort column int // sort by this column, 0 == default first or NO Sort
desc bool // sort in descending order, default == ascending desc bool // sort in descending order, default == ascending
nonum bool // hide numbering nonum bool // hide numbering
mode string // shell, orgtbl, etc. empty == default: ascii mode int // shell, orgtbl, etc. empty == default: ascii
usecol []int // columns to display, empty == display all usecol []int // columns to display, empty == display all
usecolstr string // for testname, must match usecol usecolstr string // for testname, must match usecol
expect string // rendered output we expect expect string // rendered output we expect
}{ }{
// --------------------- Default settings mode tests `` // --------------------- Default settings mode tests ``
{ {
mode: "ascii", mode: cfg.Ascii,
name: "default", name: "default",
expect: ` expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4) NAME(1) DURATION(2) COUNT(3) WHEN(4)
beta 1d10h5m1s 33 3/1/2014 beta 1d10h5m1s 33 3/1/2014
alpha 4h35m 170 2013-Feb-03 alpha 4h35m 170 2013-Feb-03
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`, ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
},
{
mode: cfg.CSV,
name: "csv",
expect: `
NAME,DURATION,COUNT,WHEN
beta,1d10h5m1s,33,3/1/2014
alpha,4h35m,170,2013-Feb-03
ceta,33d12h,9,06/Jan/2008 15:04:05 -0700`,
}, },
{ {
name: "default", name: "default",
mode: "orgtbl", mode: cfg.Orgtbl,
expect: ` expect: `
+---------+-------------+----------+----------------------------+ +---------+-------------+----------+----------------------------+
| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) | | NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) |
@@ -101,7 +104,7 @@ ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
}, },
{ {
name: "default", name: "default",
mode: "markdown", mode: cfg.Markdown,
expect: ` expect: `
| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) | | NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) |
|---------|-------------|----------|----------------------------| |---------|-------------|----------|----------------------------|
@@ -111,7 +114,7 @@ ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
}, },
{ {
name: "default", name: "default",
mode: "shell", mode: cfg.Shell,
nonum: true, nonum: true,
expect: ` expect: `
NAME="beta" DURATION="1d10h5m1s" COUNT="33" WHEN="3/1/2014" NAME="beta" DURATION="1d10h5m1s" COUNT="33" WHEN="3/1/2014"
@@ -120,7 +123,7 @@ NAME="ceta" DURATION="33d12h" COUNT="9" WHEN="06/Jan/2008 15:04:05 -0700"`,
}, },
{ {
name: "default", name: "default",
mode: "yaml", mode: cfg.Yaml,
nonum: true, nonum: true,
expect: ` expect: `
entries: entries:
@@ -139,7 +142,7 @@ entries:
}, },
{ {
name: "default", name: "default",
mode: "extended", mode: cfg.Extended,
expect: ` expect: `
NAME(1): beta NAME(1): beta
DURATION(2): 1d10h5m1s DURATION(2): 1d10h5m1s
@@ -248,7 +251,7 @@ DURATION(2) WHEN(4)
func TestPrinter(t *testing.T) { func TestPrinter(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
testname := fmt.Sprintf("print-sortcol-%d-desc-%t-sortby-%s-mode-%s-usecolumns-%s", testname := fmt.Sprintf("print-sortcol-%d-desc-%t-sortby-%s-mode-%d-usecolumns-%s",
tt.column, tt.desc, tt.sortby, tt.mode, tt.usecolstr) tt.column, tt.desc, tt.sortby, tt.mode, tt.usecolstr)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
// replaces os.Stdout, but we ignore it // replaces os.Stdout, but we ignore it
@@ -265,6 +268,8 @@ func TestPrinter(t *testing.T) {
NoColor: true, NoColor: true,
} }
c.ApplyDefaults()
// the test checks the len! // the test checks the len!
if len(tt.usecol) > 0 { if len(tt.usecol) > 0 {
c.Columns = "yes" c.Columns = "yes"

View File

@@ -133,7 +133,7 @@
.\" ======================================================================== .\" ========================================================================
.\" .\"
.IX Title "TABLIZER 1" .IX Title "TABLIZER 1"
.TH TABLIZER 1 "2022-10-16" "1" "User Commands" .TH TABLIZER 1 "2022-10-23" "1" "User Commands"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents. .\" way too many mistakes in technical documents.
.if n .ad l .if n .ad l
@@ -146,24 +146,33 @@ tablizer \- Manipulate tabular output of other programs
\& Usage: \& Usage:
\& tablizer [regex] [file, ...] [flags] \& tablizer [regex] [file, ...] [flags]
\& \&
\& Flags: \& Operational Flags:
\& \-c, \-\-columns string Only show the speficied columns (separated by ,) \& \-c, \-\-columns string Only show the speficied columns (separated by ,)
\& \-d, \-\-debug Enable debugging
\& \-h, \-\-help help for tablizer
\& \-v, \-\-invert\-match select non\-matching rows \& \-v, \-\-invert\-match select non\-matching rows
\& \-m, \-\-man Display manual page
\& \-n, \-\-no\-numbering Disable header numbering \& \-n, \-\-no\-numbering Disable header numbering
\& \-N, \-\-no\-color Disable pattern highlighting \& \-N, \-\-no\-color Disable pattern highlighting
\& \-o, \-\-output string Output mode \- one of: orgtbl, markdown, extended, yaml, ascii(default) \& \-s, \-\-separator string Custom field separator
\& \-k, \-\-sort\-by int Sort by column (default: 1)
\&
\& Output Flags (mutually exclusive):
\& \-X, \-\-extended Enable extended output \& \-X, \-\-extended Enable extended output
\& \-M, \-\-markdown Enable markdown table output \& \-M, \-\-markdown Enable markdown table output
\& \-O, \-\-orgtbl Enable org\-mode table output \& \-O, \-\-orgtbl Enable org\-mode table output
\& \-s, \-\-separator string Custom field separator \& \-S, \-\-shell Enable shell evaluable ouput
\& \-Y, \-\-yaml Enable yaml output
\& \-C, \-\-csv Enable CSV output
\& \-A, \-\-ascii Default output mode, ascii tabular
\&
\& Sort Mode Flags (mutually exclusive):
\& \-a, \-\-sort\-age sort according to age (duration) string \& \-a, \-\-sort\-age sort according to age (duration) string
\& \-k, \-\-sort\-by int Sort by column (default: 1)
\& \-D, \-\-sort\-desc Sort in descending order (default: ascending) \& \-D, \-\-sort\-desc Sort in descending order (default: ascending)
\& \-i, \-\-sort\-numeric sort according to string numerical value \& \-i, \-\-sort\-numeric sort according to string numerical value
\& \-t, \-\-sort\-time sort according to time string \& \-t, \-\-sort\-time sort according to time string
\&
\& Other Flags:
\& \-d, \-\-debug Enable debugging
\& \-h, \-\-help help for tablizer
\& \-m, \-\-man Display manual page
\& \-v, \-\-version Print program version \& \-v, \-\-version Print program version
.Ve .Ve
.SH "DESCRIPTION" .SH "DESCRIPTION"
@@ -345,8 +354,22 @@ You can use this in an eval loop.
.PP .PP
Beside normal ascii mode (the default) and extended mode there are Beside normal ascii mode (the default) and extended mode there are
more output modes available: \fBorgtbl\fR which prints an Emacs org-mode more output modes available: \fBorgtbl\fR which prints an Emacs org-mode
table and \fBmarkdown\fR which prints a Markdown table and \fByaml\fR, which table and \fBmarkdown\fR which prints a Markdown table, \fByaml\fR, which
prints yaml encoding. prints yaml encoding and \s-1CSV\s0 mode, which prints a comma separated
value file.
.SS "\s-1ENVIRONMENT VARIABLES\s0"
.IX Subsection "ENVIRONMENT VARIABLES"
\&\fBtablizer\fR supports certain environment variables which use can use
to influence program behavior. Commandline flags have always
precedence over environment variables.
.IP "<T_NO_HEADER_NUMBERING> \- disable numbering of header fields, like \fB\-n\fR." 4
.IX Item "<T_NO_HEADER_NUMBERING> - disable numbering of header fields, like -n."
.PD 0
.IP "<T_COLUMNS> \- comma separated list of columns to output, like \fB\-c\fR" 4
.IX Item "<T_COLUMNS> - comma separated list of columns to output, like -c"
.IP "<\s-1NO_COLORS\s0> \- disable colorization of matches, like \fB\-N\fR" 4
.IX Item "<NO_COLORS> - disable colorization of matches, like -N"
.PD
.SH "BUGS" .SH "BUGS"
.IX Header "BUGS" .IX Header "BUGS"
In order to report a bug, unexpected behavior, feature requests In order to report a bug, unexpected behavior, feature requests

View File

@@ -7,24 +7,33 @@ tablizer - Manipulate tabular output of other programs
Usage: Usage:
tablizer [regex] [file, ...] [flags] tablizer [regex] [file, ...] [flags]
Flags: Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,) -c, --columns string Only show the speficied columns (separated by ,)
-d, --debug Enable debugging
-h, --help help for tablizer
-v, --invert-match select non-matching rows -v, --invert-match select non-matching rows
-m, --man Display manual page
-n, --no-numbering Disable header numbering -n, --no-numbering Disable header numbering
-N, --no-color Disable pattern highlighting -N, --no-color Disable pattern highlighting
-o, --output string Output mode - one of: orgtbl, markdown, extended, yaml, ascii(default) -s, --separator string Custom field separator
-k, --sort-by int Sort by column (default: 1)
Output Flags (mutually exclusive):
-X, --extended Enable extended output -X, --extended Enable extended output
-M, --markdown Enable markdown table output -M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output -O, --orgtbl Enable org-mode table output
-s, --separator string Custom field separator -S, --shell Enable shell evaluable ouput
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
Sort Mode Flags (mutually exclusive):
-a, --sort-age sort according to age (duration) string -a, --sort-age sort according to age (duration) string
-k, --sort-by int Sort by column (default: 1)
-D, --sort-desc Sort in descending order (default: ascending) -D, --sort-desc Sort in descending order (default: ascending)
-i, --sort-numeric sort according to string numerical value -i, --sort-numeric sort according to string numerical value
-t, --sort-time sort according to time string -t, --sort-time sort according to time string
Other Flags:
-d, --debug Enable debugging
-h, --help help for tablizer
-m, --man Display manual page
-v, --version Print program version -v, --version Print program version
@@ -198,8 +207,26 @@ You can use this in an eval loop.
Beside normal ascii mode (the default) and extended mode there are Beside normal ascii mode (the default) and extended mode there are
more output modes available: B<orgtbl> which prints an Emacs org-mode more output modes available: B<orgtbl> which prints an Emacs org-mode
table and B<markdown> which prints a Markdown table and B<yaml>, which table and B<markdown> which prints a Markdown table, B<yaml>, which
prints yaml encoding. prints yaml encoding and CSV mode, which prints a comma separated
value file.
=head2 ENVIRONMENT VARIABLES
B<tablizer> supports certain environment variables which use can use
to influence program behavior. Commandline flags have always
precedence over environment variables.
=over
=item <T_NO_HEADER_NUMBERING> - disable numbering of header fields, like B<-n>.
=item <T_COLUMNS> - comma separated list of columns to output, like B<-c>
=item <NO_COLORS> - disable colorization of matches, like B<-N>
=back
=head1 BUGS =head1 BUGS