From a01edbac7d4102fa5330d50dc8d7b800f1339ee1 Mon Sep 17 00:00:00 2001 From: AnthonyAxenov Date: Wed, 23 Feb 2022 22:04:07 +0800 Subject: [PATCH] New features and refactorings - added support of h1 md header (-h) - added support of columns aligning (-a) - some comments and small fixes --- .vscode/launch.json | 56 +++++++++++++++++++++--- README.md | 51 +++++++++++----------- main.go | 104 +++++++++++++++++++++++++++++++------------- 3 files changed, 151 insertions(+), 60 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 27bf946..3fec797 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Debug csv2md", + "name": "csv", "type": "go", "request": "launch", "mode": "auto", @@ -15,15 +15,61 @@ ] }, { - "name": "Debug csv2md (tsv)", + "name": "csv aligned", "type": "go", "request": "launch", "mode": "auto", "program": "main.go", "args": [ - "-t", - "-f=example.tsv", + "-f=example.csv", + "-a" ] - } + }, + { + "name": "csv header", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "main.go", + "args": [ + "-f=example.csv", + "-h=\"My Header\"", + ] + }, + { + "name": "tsv", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "main.go", + "args": [ + "-f=example.tsv", + "-t" + ] + }, + { + "name": "tsv aligned", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "main.go", + "args": [ + "-f=example.tsv", + "-t", + "-a" + ] + }, + { + "name": "tsv header", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "main.go", + "args": [ + "-f=example.tsv", + "-h=\"My Header\"", + "-t" + ] + }, ] } diff --git a/README.md b/README.md index f61c16e..3b2080d 100644 --- a/README.md +++ b/README.md @@ -2,39 +2,39 @@ Stupidly simple tool to convert csv/tsv to [markdown](https://spec-md.com/) table. -Outputs result in stdout. +Prints result in stdout. -## Usage - -```shell -csv2md [-help|--help] [-t] [-f ] ``` +Usage: + csv2md [-help|--help] [-f=] [-h=
] [-t] [-a] Available arguments: -* `-help` or `--help` - get help -* `-f=` or `-f ` - convert specified `FILE` -* `-t` - convert input as tsv + -help|--help - show this help + -f= - convert specified FILE + -h=
- add main header (h1) to result + -t - convert input as tsv + -a - align columns width -Available `FILE` formats: -* csv (default); -* tsv (with `-t` argument). +FILE formats supported: + - csv (default) + - tsv (with -t flag) -Path to `FILE` may be presented as: -* absolute path; -* path relative to current working directory; -* path relative to user home directory (~). +Path to FILE may be presented as: + - absolute + - relative to current working directory + - relative to user home directory (~) -Also if `PATH` contains whitespaces then you should double-quote it. +Both HEADER and FILE path with whitespaces must be double-quoted. +To save result as separate file you should use redirection operators (> or >>). +``` -To save result as separate file you can use piping. +**IMPORTANT:** +1. Input data must be valid csv/tsv +2. Whitespaces allowed only between double-quotes +3. Due to markdown spec first line of result table will always be presented as header. + So if your raw data hasn't one you'll should add it before conversion or edit later in ready md. -> **IMPORTANT:** -> 1. Input data must be valid csv/tsv -> 2. Whitespaces allowed only between double-quotes -> 3. Due to markdown spec first line of result table will always be presented as header. -> So if your raw data hasn't one you'll should add it before conversion or edit later in ready md. - -### Examples +## Examples ``` csv2md - paste or type csv to stdin and then @@ -47,8 +47,9 @@ csv2md -t < example.tsv > example.md - convert tsv from stdin and write result cat example.csv | csv2md - convert csv from stdin and view result in stdout csv2md -t -f=example.tsv > example.md - convert tsv from file and write result in new file csv2md -f example.csv | less - convert csv from file and view result in stdout using pager +csv2md -f example.csv | code -n - - convert csv from file and open result in vscode -...anything is possible with redirection and piping, e.g. grep, sed, awk, etc. +...anything is possible with redirection and piping, inc. grep, sed, awk, etc. ``` You can generate some examples here: [csv](https://onlinerandomtools.com/generate-random-csv), [tsv](https://onlinerandomtools.com/generate-random-tsv) diff --git a/main.go b/main.go index ec9b26b..e051201 100644 --- a/main.go +++ b/main.go @@ -15,23 +15,27 @@ const VERSION = "1.2.0" func main() { log.SetFlags(0) - filepath := flag.String("f", "", "File path") is_help := flag.Bool("help", false, "Get help") - as_tsv := flag.Bool("t", false, "Parse as tsv") + filepath := flag.String("f", "", "File path") + header := flag.String("h", "", "Add main header (h1)") + as_tsv := flag.Bool("t", false, "Parse input as tsv") + aligned := flag.Bool("a", false, "Align columns width") flag.Parse() + // show help if *is_help { usage(os.Stdout) os.Exit(0) } - if *filepath == "" { - data, err := readRawCsv(*as_tsv) + // if filepath is not provided then convert data from stdin + if len(*filepath) == 0 { + data, err := ReadStdin(*as_tsv) if err != nil { log.Fatal(err) } - print(data) - } else { + Print(Convert(*header, data, *aligned)) + } else { // otherwise convert data from file src, err := ExpandPath(*filepath) if err != nil { log.Fatal(err) @@ -40,26 +44,26 @@ func main() { log.Fatal(err) } - data, err := readCsvFile(src, *as_tsv) + data, err := ReadFile(src, *as_tsv) if err != nil { log.Fatal(err) } - print(data) + Print(Convert(*header, data, *aligned)) } } -// print write converted data to stdout -func print(data [][]string) { +// Print writes converted data to stdout +func Print(data []string) { if len(data) == 0 { usage(os.Stderr) } - result := convert(data) - for _, row := range result { + + for _, row := range data { fmt.Println(row) } } -// ExpandPath return absolute path to file replacing ~ to user's home folder +// ExpandPath returns absolute path to file replacing ~ to user's home folder func ExpandPath(path string) (string, error) { homepath, err := os.UserHomeDir() if err != nil { @@ -69,8 +73,8 @@ func ExpandPath(path string) (string, error) { return newpath, err } -// readRawCsv read data from file -func readCsvFile(filePath string, as_tsv bool) ([][]string, error) { +// ReadFile reads data from file +func ReadFile(filePath string, as_tsv bool) ([][]string, error) { f, err := os.Open(filePath) if err != nil { return nil, errors.New("Failed to open file '" + filePath + "': " + err.Error()) @@ -90,8 +94,8 @@ func readCsvFile(filePath string, as_tsv bool) ([][]string, error) { return records, nil } -// readRawCsv read data from stdin -func readRawCsv(as_tsv bool) ([][]string, error) { +// ReadStdin reads data from stdin +func ReadStdin(as_tsv bool) ([][]string, error) { csvReader := csv.NewReader(os.Stdin) if as_tsv { csvReader.Comma = '\t' @@ -105,19 +109,54 @@ func readRawCsv(as_tsv bool) ([][]string, error) { return records, nil } -// convert format data from file or stdin as markdown -func convert(records [][]string) []string { +// Convert formats data from file or stdin as markdown +func Convert(header string, records [][]string, aligned bool) []string { var result []string - for idx, row := range records { + + // add h1 if passed + header = strings.Trim(header, "\t\r\n ") + if len(header) != 0 { + result = append(result, "# " + header) + result = append(result, "") + } + + // if user wants aligned columns width then we + // count max length of every value in every column + widths := make(map[int]int) + if aligned { + for _, row := range records { + for col_idx, col := range row { + length := len(col) + if len(widths) == 0 || widths[col_idx] < length { + widths[col_idx] = length + } + } + } + } + + // build markdown table + for row_idx, row := range records { + + // table content str := "| " - for _, col := range row { - str += col + " | " + for col_idx, col := range row { + if aligned { + str += fmt.Sprintf("%-*s | ", widths[col_idx], col) + } else { + str += col + " | " + } } result = append(result, str) - if idx == 0 { + + // content separator only after first row (header) + if row_idx == 0 { str := "| " - for range row { - str += "--- | " + for col_idx := range row { + if !aligned || widths[col_idx] < 3 { + str += "--- | " + } else { + str += strings.Repeat("-", widths[col_idx]) + " | " + } } result = append(result, str) } @@ -125,7 +164,7 @@ func convert(records [][]string) []string { return result } -// usage print help into writer +// usage Print help into writer func usage(writer *os.File) { usage := []string{ "csv2md v" + VERSION, @@ -133,12 +172,14 @@ func usage(writer *os.File) { "https://github.com/anthonyaxenov/csv2md", "", "Usage:", - "\tcsv2md [-help|--help] [-t] [-f ]", + "\tcsv2md [-help|--help] [-f=] [-h=
] [-t] [-a]", "", "Available arguments:", - "\t-help|--help - get this help", - "\t-f=|-f - convert specified FILE", - "\t-t - convert input as TSV", + "\t-help|--help - show this help", + "\t-f= - Convert specified FILE", + "\t-h=
- add main header (h1) to result", + "\t-t - Convert input as tsv", + "\t-a - align columns width", "", "FILE formats supported:", "\t- csv (default)", @@ -148,6 +189,9 @@ func usage(writer *os.File) { "\t- absolute", "\t- relative to current working directory", "\t- relative to user home directory (~)", + "", + "Both HEADER and FILE path with whitespaces must be double-quoted.", + "To save result as separate file you should use redirection operators (> or >>).", } for _, str := range usage { fmt.Fprintln(writer, str)