mirror of
https://codeberg.org/scip/tablizer.git
synced 2025-12-17 04:30:56 +01:00
more refactoring, fixed bug in shell mode output, fixed default
Separator and fixed #3
This commit is contained in:
@@ -87,7 +87,7 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().BoolVarP(&lib.ShowVersion, "version", "V", false, "Print program version")
|
rootCmd.PersistentFlags().BoolVarP(&lib.ShowVersion, "version", "V", false, "Print program version")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&lib.InvertMatch, "invert-match", "v", false, "select non-matching rows")
|
rootCmd.PersistentFlags().BoolVarP(&lib.InvertMatch, "invert-match", "v", false, "select non-matching rows")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&ShowManual, "man", "m", false, "Display manual page")
|
rootCmd.PersistentFlags().BoolVarP(&ShowManual, "man", "m", false, "Display manual page")
|
||||||
rootCmd.PersistentFlags().StringVarP(&lib.Separator, "separator", "s", "", "Custom field separator")
|
rootCmd.PersistentFlags().StringVarP(&lib.Separator, "separator", "s", lib.DefaultSeparator, "Custom field separator")
|
||||||
rootCmd.PersistentFlags().StringVarP(&lib.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)")
|
rootCmd.PersistentFlags().StringVarP(&lib.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)")
|
||||||
|
|
||||||
// output flags, only 1 allowed, hidden, since just short cuts
|
// output flags, only 1 allowed, hidden, since just short cuts
|
||||||
|
|||||||
@@ -19,25 +19,26 @@ package lib
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// command line flags
|
// command line flags
|
||||||
Debug bool
|
Debug bool
|
||||||
XtendedOut bool
|
XtendedOut bool
|
||||||
NoNumbering bool
|
NoNumbering bool
|
||||||
ShowVersion bool
|
ShowVersion bool
|
||||||
Columns string
|
Columns string
|
||||||
UseColumns []int
|
UseColumns []int
|
||||||
Separator string = `(\s\s+|\t)`
|
DefaultSeparator string = `(\s\s+|\t)`
|
||||||
OutflagExtended bool
|
Separator string = `(\s\s+|\t)`
|
||||||
OutflagMarkdown bool
|
OutflagExtended bool
|
||||||
OutflagOrgtable bool
|
OutflagMarkdown bool
|
||||||
OutflagShell bool
|
OutflagOrgtable bool
|
||||||
OutputMode string
|
OutflagShell bool
|
||||||
InvertMatch bool
|
OutputMode string
|
||||||
|
InvertMatch bool
|
||||||
|
|
||||||
// used for validation
|
// used for validation
|
||||||
validOutputmodes = "(orgtbl|markdown|extended|ascii)"
|
validOutputmodes = "(orgtbl|markdown|extended|ascii)"
|
||||||
|
|
||||||
// main program version
|
// main program version
|
||||||
Version = "v1.0.5"
|
Version = "v1.0.6"
|
||||||
|
|
||||||
// generated version string, used by -v contains lib.Version on
|
// generated version string, used by -v contains lib.Version on
|
||||||
// main branch, and lib.Version-$branch-$lastcommit-$date on
|
// main branch, and lib.Version-$branch-$lastcommit-$date on
|
||||||
|
|||||||
@@ -48,8 +48,49 @@ func PrepareColumns() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func numberizeHeaders(data *Tabdata) {
|
||||||
|
// prepare headers: add numbers to headers
|
||||||
|
numberedHeaders := []string{}
|
||||||
|
for i, head := range data.headers {
|
||||||
|
if len(Columns) > 0 {
|
||||||
|
// -c specified
|
||||||
|
if !contains(UseColumns, i+1) {
|
||||||
|
// ignore this one
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if NoNumbering {
|
||||||
|
numberedHeaders = append(numberedHeaders, head)
|
||||||
|
} else {
|
||||||
|
numberedHeaders = append(numberedHeaders, fmt.Sprintf("%s(%d)", head, i+1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.headers = numberedHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
func reduceColumns(data *Tabdata) {
|
||||||
|
// exclude columns, if any
|
||||||
|
if len(Columns) > 0 {
|
||||||
|
reducedEntries := [][]string{}
|
||||||
|
reducedEntry := []string{}
|
||||||
|
for _, entry := range data.entries {
|
||||||
|
reducedEntry = nil
|
||||||
|
for i, value := range entry {
|
||||||
|
if !contains(UseColumns, i+1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
reducedEntry = append(reducedEntry, value)
|
||||||
|
}
|
||||||
|
reducedEntries = append(reducedEntries, reducedEntry)
|
||||||
|
}
|
||||||
|
data.entries = reducedEntries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func PrepareModeFlags() error {
|
func PrepareModeFlags() error {
|
||||||
if len(OutputMode) == 0 {
|
if len(OutputMode) == 0 {
|
||||||
|
// associate short flags like -X with mode selector
|
||||||
switch {
|
switch {
|
||||||
case OutflagExtended:
|
case OutflagExtended:
|
||||||
OutputMode = "extended"
|
OutputMode = "extended"
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ func TestPrepareColumns(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"1,2,3", []int{1, 2, 3}, false},
|
{"1,2,3", []int{1, 2, 3}, false},
|
||||||
{"1,2,", []int{}, true},
|
{"1,2,", []int{}, true},
|
||||||
|
{"a,b", []int{}, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -71,3 +72,46 @@ func TestPrepareColumns(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReduceColumns(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
expect [][]string
|
||||||
|
columns []int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
expect: [][]string{[]string{"a", "b"}},
|
||||||
|
columns: []int{1, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expect: [][]string{[]string{"a", "c"}},
|
||||||
|
columns: []int{1, 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expect: [][]string{[]string{"a"}},
|
||||||
|
columns: []int{1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expect: [][]string{nil},
|
||||||
|
columns: []int{4},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
input := [][]string{[]string{"a", "b", "c"}}
|
||||||
|
|
||||||
|
Columns = "y" // used as a flag with len(Columns)...
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
testname := fmt.Sprintf("reduce-columns-by-%+v", tt.columns)
|
||||||
|
t.Run(testname, func(t *testing.T) {
|
||||||
|
UseColumns = tt.columns
|
||||||
|
data := Tabdata{entries: input}
|
||||||
|
reduceColumns(&data)
|
||||||
|
if !reflect.DeepEqual(data.entries, tt.expect) {
|
||||||
|
t.Errorf("reduceColumns returned invalid data:\ngot: %+v\nexp: %+v", data.entries, tt.expect)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Columns = "" // reset for other tests
|
||||||
|
UseColumns = nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ func parseFile(input io.Reader, pattern string) (Tabdata, error) {
|
|||||||
|
|
||||||
hadFirst := false
|
hadFirst := false
|
||||||
separate := regexp.MustCompile(Separator)
|
separate := regexp.MustCompile(Separator)
|
||||||
|
|
||||||
patternR, err := regexp.Compile(pattern)
|
patternR, err := regexp.Compile(pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, errors.Unwrap(fmt.Errorf("Regexp pattern %s is invalid: %w", pattern, err))
|
return data, errors.Unwrap(fmt.Errorf("Regexp pattern %s is invalid: %w", pattern, err))
|
||||||
|
|||||||
@@ -28,26 +28,18 @@ func TestParser(t *testing.T) {
|
|||||||
data := Tabdata{
|
data := Tabdata{
|
||||||
maxwidthHeader: 5,
|
maxwidthHeader: 5,
|
||||||
maxwidthPerCol: []int{
|
maxwidthPerCol: []int{
|
||||||
5,
|
5, 5, 8,
|
||||||
5,
|
|
||||||
8,
|
|
||||||
},
|
},
|
||||||
columns: 3,
|
columns: 3,
|
||||||
headers: []string{
|
headers: []string{
|
||||||
"ONE",
|
"ONE", "TWO", "THREE",
|
||||||
"TWO",
|
|
||||||
"THREE",
|
|
||||||
},
|
},
|
||||||
entries: [][]string{
|
entries: [][]string{
|
||||||
[]string{
|
[]string{
|
||||||
"asd",
|
"asd", "igig", "cxxxncnc",
|
||||||
"igig",
|
|
||||||
"cxxxncnc",
|
|
||||||
},
|
},
|
||||||
[]string{
|
[]string{
|
||||||
"19191",
|
"19191", "EDD 1", "X",
|
||||||
"EDD 1",
|
|
||||||
"X",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -58,6 +50,7 @@ asd igig cxxxncnc
|
|||||||
|
|
||||||
readFd := strings.NewReader(table)
|
readFd := strings.NewReader(table)
|
||||||
gotdata, err := parseFile(readFd, "")
|
gotdata, err := parseFile(readFd, "")
|
||||||
|
Separator = DefaultSeparator
|
||||||
|
|
||||||
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)
|
||||||
@@ -77,9 +70,7 @@ func TestParserPatternmatching(t *testing.T) {
|
|||||||
{
|
{
|
||||||
entries: [][]string{
|
entries: [][]string{
|
||||||
[]string{
|
[]string{
|
||||||
"asd",
|
"asd", "igig", "cxxxncnc",
|
||||||
"igig",
|
|
||||||
"cxxxncnc",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pattern: "ig",
|
pattern: "ig",
|
||||||
@@ -88,9 +79,7 @@ func TestParserPatternmatching(t *testing.T) {
|
|||||||
{
|
{
|
||||||
entries: [][]string{
|
entries: [][]string{
|
||||||
[]string{
|
[]string{
|
||||||
"19191",
|
"19191", "EDD 1", "X",
|
||||||
"EDD 1",
|
|
||||||
"X",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pattern: "ig",
|
pattern: "ig",
|
||||||
|
|||||||
@@ -26,41 +26,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func printData(data *Tabdata) {
|
func printData(data *Tabdata) {
|
||||||
// prepare headers: add numbers to headers
|
if OutputMode != "shell" {
|
||||||
numberedHeaders := []string{}
|
numberizeHeaders(data)
|
||||||
for i, head := range data.headers {
|
|
||||||
if len(Columns) > 0 {
|
|
||||||
// -c specified
|
|
||||||
if !contains(UseColumns, i+1) {
|
|
||||||
// ignore this one
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if NoNumbering {
|
|
||||||
numberedHeaders = append(numberedHeaders, head)
|
|
||||||
} else {
|
|
||||||
numberedHeaders = append(numberedHeaders, fmt.Sprintf("%s(%d)", head, i+1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.headers = numberedHeaders
|
|
||||||
|
|
||||||
// prepare data
|
|
||||||
if len(Columns) > 0 {
|
|
||||||
reducedEntries := [][]string{}
|
|
||||||
reducedEntry := []string{}
|
|
||||||
for _, entry := range data.entries {
|
|
||||||
reducedEntry = nil
|
|
||||||
for i, value := range entry {
|
|
||||||
if !contains(UseColumns, i+1) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
reducedEntry = append(reducedEntry, value)
|
|
||||||
}
|
|
||||||
reducedEntries = append(reducedEntries, reducedEntry)
|
|
||||||
}
|
|
||||||
data.entries = reducedEntries
|
|
||||||
}
|
}
|
||||||
|
reduceColumns(data)
|
||||||
|
|
||||||
switch OutputMode {
|
switch OutputMode {
|
||||||
case "extended":
|
case "extended":
|
||||||
@@ -190,6 +159,7 @@ func printShellData(data *Tabdata) {
|
|||||||
var idx int
|
var idx int
|
||||||
for _, entry := range data.entries {
|
for _, entry := range data.entries {
|
||||||
idx = 0
|
idx = 0
|
||||||
|
shentries := []string{}
|
||||||
for i, value := range entry {
|
for i, value := range entry {
|
||||||
if len(Columns) > 0 {
|
if len(Columns) > 0 {
|
||||||
if !contains(UseColumns, i+1) {
|
if !contains(UseColumns, i+1) {
|
||||||
@@ -197,10 +167,10 @@ func printShellData(data *Tabdata) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s=\"%s\" ", data.headers[idx], value)
|
shentries = append(shentries, fmt.Sprintf("%s=\"%s\"", data.headers[idx], value))
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println(strings.Join(shentries, " "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,33 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
package lib
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPrinter(t *testing.T) {
|
func TestPrinter(t *testing.T) {
|
||||||
table := `ONE TWO THREE
|
startdata := Tabdata{
|
||||||
asd igig cxxxncnc
|
maxwidthHeader: 5,
|
||||||
19191 EDD 1 X`
|
maxwidthPerCol: []int{
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
8,
|
||||||
|
},
|
||||||
|
columns: 3,
|
||||||
|
headers: []string{
|
||||||
|
"ONE", "TWO", "THREE",
|
||||||
|
},
|
||||||
|
entries: [][]string{
|
||||||
|
[]string{
|
||||||
|
"asd", "igig", "cxxxncnc",
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"19191", "EDD 1", "X",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
expects := map[string]string{
|
expects := map[string]string{
|
||||||
"ascii": `ONE(1) TWO(2) THREE(3)
|
"ascii": `ONE(1) TWO(2) THREE(3)
|
||||||
@@ -42,6 +60,8 @@ asd igig cxxxncnc
|
|||||||
|--------|--------|----------|
|
|--------|--------|----------|
|
||||||
| asd | igig | cxxxncnc |
|
| asd | igig | cxxxncnc |
|
||||||
| 19191 | EDD 1 | X |`,
|
| 19191 | EDD 1 | X |`,
|
||||||
|
"shell": `ONE="asd" TWO="igig" THREE="cxxxncnc"
|
||||||
|
ONE="19191" TWO="EDD 1" THREE="X"`,
|
||||||
}
|
}
|
||||||
|
|
||||||
r, w, err := os.Pipe()
|
r, w, err := os.Pipe()
|
||||||
@@ -52,27 +72,25 @@ asd igig cxxxncnc
|
|||||||
os.Stdout = w
|
os.Stdout = w
|
||||||
|
|
||||||
for mode, expect := range expects {
|
for mode, expect := range expects {
|
||||||
OutputMode = mode
|
testname := fmt.Sprintf("print-%s", mode)
|
||||||
fd := strings.NewReader(table)
|
t.Run(testname, func(t *testing.T) {
|
||||||
data, err := parseFile(fd, "")
|
|
||||||
|
|
||||||
if err != nil {
|
OutputMode = mode
|
||||||
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, data)
|
data := startdata // we need to reset our mock data, since it's being modified in printData()
|
||||||
}
|
printData(&data)
|
||||||
|
|
||||||
printData(&data)
|
buf := make([]byte, 1024)
|
||||||
|
n, err := r.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
buf = buf[:n]
|
||||||
|
output := strings.TrimSpace(string(buf))
|
||||||
|
|
||||||
buf := make([]byte, 1024)
|
if output != expect {
|
||||||
n, err := r.Read(buf)
|
t.Errorf("output mode: %s, got:\n%s\nwant:\n%s\n (%d <=> %d)", mode, output, expect, len(output), len(expect))
|
||||||
if err != nil {
|
}
|
||||||
t.Fatal(err)
|
})
|
||||||
}
|
|
||||||
buf = buf[:n]
|
|
||||||
output := strings.TrimSpace(string(buf))
|
|
||||||
|
|
||||||
if output != expect {
|
|
||||||
t.Errorf("output mode: %s, got:\n%s\nwant:\n%s\n (%d <=> %d)", mode, output, expect, len(output), len(expect))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore
|
// Restore
|
||||||
|
|||||||
38
tablizer.pod
38
tablizer.pod
@@ -24,12 +24,12 @@ tablizer - Manipulate tabular output of other programs
|
|||||||
|
|
||||||
=head1 DESCRIPTION
|
=head1 DESCRIPTION
|
||||||
|
|
||||||
Many programs generate tabular output. But sometimes you need to
|
Many programs generate tabular output. But sometimes you need to
|
||||||
post-process these tables, you may need to remove one or more columns
|
post-process these tables, you may need to remove one or more columns
|
||||||
or you may want to filter for some pattern or you may need the output
|
or you may want to filter for some pattern (See L<PATTERNS>) or you
|
||||||
in another program and need to parse it somehow. Standard unix tools
|
may need the output in another program and need to parse it somehow.
|
||||||
such as awk(1), grep(1) or column(1) may help, but sometimes it's a
|
Standard unix tools such as awk(1), grep(1) or column(1) may help, but
|
||||||
tedious business.
|
sometimes it's a tedious business.
|
||||||
|
|
||||||
Let's take the output of the tool kubectl. It contains cells with
|
Let's take the output of the tool kubectl. It contains cells with
|
||||||
withespace and they do not separate columns by TAB characters. This is
|
withespace and they do not separate columns by TAB characters. This is
|
||||||
@@ -77,6 +77,34 @@ The numbering can be suppressed by using the B<-n> option.
|
|||||||
Finally the B<-d> option enables debugging output which is mostly
|
Finally the B<-d> option enables debugging output which is mostly
|
||||||
usefull for the developer.
|
usefull for the developer.
|
||||||
|
|
||||||
|
=head2 PATTERNS
|
||||||
|
|
||||||
|
You can reduce the rows being displayed by using a regular expression
|
||||||
|
pattern. The regexp is PCRE compatible, refer to the syntax cheat
|
||||||
|
sheet here: L<https://github.com/google/re2/wiki/Syntax>. If you want
|
||||||
|
to read a more comprehensive documentation about the topic and have
|
||||||
|
perl installed you can read it with:
|
||||||
|
|
||||||
|
perldoc perlre
|
||||||
|
|
||||||
|
Or read it online: L<https://perldoc.perl.org/perlre>.
|
||||||
|
|
||||||
|
A note on modifiers: the regexp engine used in tablizer uses another
|
||||||
|
modifier syntax:
|
||||||
|
|
||||||
|
(?MODIFIER)
|
||||||
|
|
||||||
|
The most important modifiers are:
|
||||||
|
|
||||||
|
C<i> ignore case
|
||||||
|
C<m> multiline mode
|
||||||
|
C<s> single line mode
|
||||||
|
|
||||||
|
Example for a case insensitve search:
|
||||||
|
|
||||||
|
kubectl get pods -A | tablizer "(?i)account"
|
||||||
|
|
||||||
|
|
||||||
=head2 OUTPUT MODES
|
=head2 OUTPUT MODES
|
||||||
|
|
||||||
There might be cases when the tabular output of a program is way too
|
There might be cases when the tabular output of a program is way too
|
||||||
|
|||||||
Reference in New Issue
Block a user