6 Commits

Author SHA1 Message Date
6996e4634f New make recipe called 'release' 2022-02-23 22:15:03 +08:00
5dcccb9880 Bump version number 2022-02-23 22:05:35 +08:00
cf2b11fe1a Merge pull request #3 from anthonyaxenov/develop
New features and refactorings
2022-02-23 22:04:43 +08:00
a01edbac7d New features and refactorings
- added support of h1 md header (-h)
- added support of columns aligning (-a)
- some comments and small fixes
2022-02-23 22:04:07 +08:00
60b2f676bd Merge pull request #2 from anthonyaxenov/develop
New v1.2.0: tsv support
2022-02-23 12:29:08 +08:00
13dedbac3b New v1.2.0: tsv support 2022-02-23 12:25:40 +08:00
6 changed files with 246 additions and 98 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
/bin /bin
# /.vscode # /.vscode
*.csv *.csv
*.tsv
*.md *.md
!example.csv !example.csv

71
.vscode/launch.json vendored
View File

@@ -5,14 +5,71 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Debug csv2md", "name": "csv",
"type": "go", "type": "go",
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"program": "main.go" "program": "main.go",
// "args": [ "args": [
// "example.csv", "-f=example.csv",
// ] ]
} },
{
"name": "csv aligned",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": [
"-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"
]
},
] ]
} }

View File

@@ -12,29 +12,35 @@ LINUX_FILE="${LINUX_PATH}/${BINARY_NAME}"
WINDOWS_FILE="${WINDOWS_PATH}/${BINARY_NAME}.exe" WINDOWS_FILE="${WINDOWS_PATH}/${BINARY_NAME}.exe"
DARWIN_FILE="${DARWIN_PATH}/${BINARY_NAME}" DARWIN_FILE="${DARWIN_PATH}/${BINARY_NAME}"
## clean: Removes all compiled binaries ## clean: Remove all compiled binaries
clean: clean:
@go clean @go clean
@rm -rf bin/ @rm -rf bin/
## linux: Builds new binaries for linux (x64) ## linux: Build new binaries for linux (x64)
linux: linux:
@rm -rf ${LINUX_PATH} @rm -rf ${LINUX_PATH}
@GOARCH=${ARCH} GOOS=linux go build -o ${LINUX_FILE} . && echo "Compiled: ${LINUX_FILE}" @GOARCH=${ARCH} GOOS=linux go build -o ${LINUX_FILE} . && echo "Compiled: ${LINUX_FILE}"
## win: Builds new binaries for windows (x64) ## win: Build new binaries for windows (x64)
win: win:
@rm -rf ${WINDOWS_PATH} @rm -rf ${WINDOWS_PATH}
@GOARCH=${ARCH} GOOS=windows go build -o ${WINDOWS_FILE} . && echo "Compiled: ${WINDOWS_FILE}" @GOARCH=${ARCH} GOOS=windows go build -o ${WINDOWS_FILE} . && echo "Compiled: ${WINDOWS_FILE}"
## darwin: Builds new binaries for darwin (x64) ## darwin: Build new binaries for darwin (x64)
darwin: darwin:
@rm -rf ${DARWIN_PATH} @rm -rf ${DARWIN_PATH}
@GOARCH=${ARCH} GOOS=darwin go build -o ${DARWIN_FILE} . && echo "Compiled: ${DARWIN_FILE}" @GOARCH=${ARCH} GOOS=darwin go build -o ${DARWIN_FILE} . && echo "Compiled: ${DARWIN_FILE}"
## build: Builds new binaries for linux, windows and darwin (x64) ## build: Build new binaries for linux, windows and darwin (x64)
all: clean linux win darwin all: clean linux win darwin
## release: Build all binaries and zip them
release: clean darwin linux win
@zip -j ${LINUX_PATH}.zip ${LINUX_FILE}
@zip -j ${DARWIN_PATH}.zip ${DARWIN_FILE}
@zip -j ${WINDOWS_PATH}.zip ${WINDOWS_FILE}
## compile: This message ## compile: This message
help: Makefile help: Makefile
@echo "Choose a command run:" @echo "Choose a command run:"

View File

@@ -1,37 +1,64 @@
# csv2md # csv2md
Stupidly simple tool to convert csv-file to [markdown](https://spec-md.com/) table. Stupidly simple tool to convert csv/tsv to [markdown](https://spec-md.com/) table.
Outputs result in stdout. Prints result in stdout.
## Usage ```
Usage:
csv2md [-help|--help] [-f=<FILE>] [-h=<HEADER>] [-t] [-a]
```shell Available arguments:
csv2md (-h|-help|--help|help) # to get this help -help|--help - show this help
csv2md example.csv # convert data from file and write result in stdout -f=<FILE> - convert specified FILE
csv2md < example.csv # convert data from stdin and write result in stdout -h=<HEADER> - add main header (h1) to result
cat example.csv | csv2md # convert data from stdin and write result in stdout -t - convert input as tsv
csv2md example.csv > example.md # convert data from file and write result in new file -a - align columns width
csv2md example.csv | less # convert data from file and write result in stdout using pager
csv2md # paste or type data to stdin by hands
# press Ctrl+D to view result in stdout
csv2md > example.md # paste or type data to stdin by hands
# press Ctrl+D to write result in new file
...anything is possible with redirection and piping FILE formats supported:
- csv (default)
- tsv (with -t flag)
Path to FILE may be presented as:
- absolute
- relative to current working directory
- 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 >>).
``` ```
> **IMPORTANT:** **IMPORTANT:**
> * input must be valid csv 1. Input data must be valid csv/tsv
> * whitespaces allowed only between double-quotes 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 can be found here: https://people.sc.fsu.edu/~jburkardt/data/csv/csv.html ## Examples
```
csv2md - paste or type csv to stdin and then
press Ctrl+D to view result in stdout
csv2md -t > example.md - paste or type tsv to stdin and then
press Ctrl+D to write result in new file
csv2md -f example.csv - convert csv from file and view result in stdout
csv2md -t < example.tsv - convert tsv from stdin and view result in stdout
csv2md -t < example.tsv > example.md - convert tsv from stdin and write result in new file
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, 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)
## Compilation ## Compilation
1) [Install go](https://go.dev/learn/). 1) [Install go](https://go.dev/learn/).
2) Download this repo via zip or `git clone`. 2) Download this repo via zip or `git clone`.
3) Run `make help` to get help or `go run . <csv_path>` to build and run temporary binary. 3) Run `make help` to get help about compilation or `go run . [ARGS...]` to build and run temporary binary.
## License ## License

6
example.tsv Normal file
View File

@@ -0,0 +1,6 @@
impossible political hand larger upward during
shade tip opinion star keep aside
wrong heat line pool song just
she slowly gain snow ourselves six
race thrown get yard nearest swam
though than teacher away dirt escape
1 impossible political hand larger upward during
2 shade tip opinion star keep aside
3 wrong heat line pool song just
4 she slowly gain snow ourselves six
5 race thrown get yard nearest swam
6 though than teacher away dirt escape

183
main.go
View File

@@ -11,62 +11,59 @@ import (
"strings" "strings"
) )
const VERSION = "1.1.0" const VERSION = "1.3.0"
func main() { func main() {
log.SetFlags(0) log.SetFlags(0)
switch len(os.Args) { is_help := flag.Bool("help", false, "Get help")
case 1: // first we read data from stdin and then convert it filepath := flag.String("f", "", "File path")
data, err := readRawCsv() header := flag.String("h", "", "Add main header (h1)")
if err != nil { as_tsv := flag.Bool("t", false, "Parse input as tsv")
log.Fatal(err) aligned := flag.Bool("a", false, "Align columns width")
} flag.Parse()
print(data)
case 2: // but if 2nd arg is present // show help
// probably user wants to get help if *is_help {
help1 := flag.Bool("h", false, "Get help") usage(os.Stdout)
help2 := flag.Bool("help", false, "Get help") os.Exit(0)
flag.Parse() }
if os.Args[1] == "help" || *help1 || *help2 {
usage(os.Stdout)
os.Exit(0)
}
// ...or to convert data from file // if filepath is not provided then convert data from stdin
src, err := ExpandPath(os.Args[1]) if len(*filepath) == 0 {
if err != nil { data, err := ReadStdin(*as_tsv)
log.Fatal(err) if err != nil {
} log.Fatal(err)
if _, err := os.Stat(src); err != nil { }
log.Fatal(err) Print(Convert(*header, data, *aligned))
} } else { // otherwise convert data from file
src, err := ExpandPath(*filepath)
if err != nil {
log.Fatal(err)
}
if _, err := os.Stat(src); err != nil {
log.Fatal(err)
}
data, err := readCsvFile(src) data, err := ReadFile(src, *as_tsv)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
print(data) Print(Convert(*header, data, *aligned))
// otherwise let's show usage help and exit (probably inaccessible code, but anyway)
default:
usage(os.Stdout)
os.Exit(0)
} }
} }
// print write converted data to stdout // Print writes converted data to stdout
func print(data [][]string) { func Print(data []string) {
if len(data) == 0 { if len(data) == 0 {
usage(os.Stderr) usage(os.Stderr)
} }
result := convert(data)
for _, row := range result { for _, row := range data {
fmt.Println(row) 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) { func ExpandPath(path string) (string, error) {
homepath, err := os.UserHomeDir() homepath, err := os.UserHomeDir()
if err != nil { if err != nil {
@@ -76,8 +73,8 @@ func ExpandPath(path string) (string, error) {
return newpath, err return newpath, err
} }
// readRawCsv read data from file // ReadFile reads data from file
func readCsvFile(filePath string) ([][]string, error) { func ReadFile(filePath string, as_tsv bool) ([][]string, error) {
f, err := os.Open(filePath) f, err := os.Open(filePath)
if err != nil { if err != nil {
return nil, errors.New("Failed to open file '" + filePath + "': " + err.Error()) return nil, errors.New("Failed to open file '" + filePath + "': " + err.Error())
@@ -85,37 +82,81 @@ func readCsvFile(filePath string) ([][]string, error) {
defer f.Close() defer f.Close()
csvReader := csv.NewReader(f) csvReader := csv.NewReader(f)
if as_tsv {
csvReader.Comma = '\t'
}
records, err := csvReader.ReadAll() records, err := csvReader.ReadAll()
if err != nil { if err != nil {
return nil, errors.New("Failed to parse CSV from file '" + filePath + "': " + err.Error()) return nil, errors.New("Failed to parse file '" + filePath + "': " + err.Error())
} }
return records, nil return records, nil
} }
// readRawCsv read data from stdin // ReadStdin reads data from stdin
func readRawCsv() ([][]string, error) { func ReadStdin(as_tsv bool) ([][]string, error) {
csvReader := csv.NewReader(os.Stdin) csvReader := csv.NewReader(os.Stdin)
if as_tsv {
csvReader.Comma = '\t'
}
records, err := csvReader.ReadAll() records, err := csvReader.ReadAll()
if err != nil { if err != nil {
return nil, errors.New("Failed to parse CSV from stdin: " + err.Error()) return nil, errors.New("Failed to parse input from stdin: " + err.Error())
} }
return records, nil return records, nil
} }
// convert format data from file or stdin as markdown // Convert formats data from file or stdin as markdown
func convert(records [][]string) []string { func Convert(header string, records [][]string, aligned bool) []string {
var result []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 := "| " str := "| "
for _, col := range row { for col_idx, col := range row {
str += col + " | " if aligned {
str += fmt.Sprintf("%-*s | ", widths[col_idx], col)
} else {
str += col + " | "
}
} }
result = append(result, str) result = append(result, str)
if idx == 0 {
// content separator only after first row (header)
if row_idx == 0 {
str := "| " str := "| "
for range row { for col_idx := range row {
str += "--- | " if !aligned || widths[col_idx] < 3 {
str += "--- | "
} else {
str += strings.Repeat("-", widths[col_idx]) + " | "
}
} }
result = append(result, str) result = append(result, str)
} }
@@ -123,24 +164,34 @@ func convert(records [][]string) []string {
return result return result
} }
// usage print help into writer // usage Print help into writer
func usage(writer *os.File) { func usage(writer *os.File) {
usage := []string{ usage := []string{
"csv2md v" + VERSION, "csv2md v" + VERSION,
"Anthony Axenov (c) 2022, MIT license", "Anthony Axenov (c) 2022, MIT License",
"https://github.com/anthonyaxenov/csv2md", "https://github.com/anthonyaxenov/csv2md",
"", "",
"Usage:", "Usage:",
"\tcsv2md (-h|-help|--help|help) # to get this help", "\tcsv2md [-help|--help] [-f=<FILE>] [-h=<HEADER>] [-t] [-a]",
"\tcsv2md example.csv # convert data from file and write result in stdout", "",
"\tcsv2md < example.csv # convert data from stdin and write result in stdout", "Available arguments:",
"\tcat example.csv | csv2md # convert data from stdin and write result in stdout", "\t-help|--help - show this help",
"\tcsv2md example.csv > example.md # convert data from file and write result in new file", "\t-f=<FILE> - Convert specified FILE",
"\tcsv2md example.csv | less # convert data from file and write result in stdout using pager", "\t-h=<HEADER> - add main header (h1) to result",
"\tcsv2md # paste or type data to stdin by hands", "\t-t - Convert input as tsv",
"\t # press Ctrl+D to view result in stdout", "\t-a - align columns width",
"\tcsv2md > example.md # paste or type data to stdin by hands", "",
"\t # press Ctrl+D to write result in new file", "FILE formats supported:",
"\t- csv (default)",
"\t- tsv (with -t flag)",
"",
"Path to FILE may be presented as:",
"\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 { for _, str := range usage {
fmt.Fprintln(writer, str) fmt.Fprintln(writer, str)