Compare commits
6 Commits
c7fd26e7b4
...
v1.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
6996e4634f
|
|||
|
5dcccb9880
|
|||
| cf2b11fe1a | |||
|
a01edbac7d
|
|||
| 60b2f676bd | |||
|
13dedbac3b
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
/bin
|
/bin
|
||||||
# /.vscode
|
# /.vscode
|
||||||
*.csv
|
*.csv
|
||||||
|
*.tsv
|
||||||
*.md
|
*.md
|
||||||
!example.csv
|
!example.csv
|
||||||
|
|||||||
71
.vscode/launch.json
vendored
71
.vscode/launch.json
vendored
@@ -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"
|
||||||
|
]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
16
Makefile
16
Makefile
@@ -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:"
|
||||||
|
|||||||
67
README.md
67
README.md
@@ -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
6
example.tsv
Normal 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
|
||||||
|
183
main.go
183
main.go
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user