134 lines
3.4 KiB
Go
134 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime/debug"
|
|
"strings"
|
|
)
|
|
|
|
var buildVersion = "dev"
|
|
|
|
// options contains CLI concerns only. Conversion functions receive explicit
|
|
// arguments so they can be reused and tested without constructing a FlagSet.
|
|
type options struct {
|
|
inFile string
|
|
outFile string
|
|
from string
|
|
to string
|
|
sheet string
|
|
pretty bool
|
|
version bool
|
|
}
|
|
|
|
func main() {
|
|
if err := run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr); err != nil {
|
|
fmt.Fprintln(os.Stderr, "dataxl:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run(args []string, stdin io.Reader, stdout, stderr io.Writer) error {
|
|
opt, err := parseOptions(args, stderr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if opt.version {
|
|
_, err := fmt.Fprintln(stdout, "dataxl", resolvedVersion())
|
|
return err
|
|
}
|
|
if err := opt.resolveFormats(); err != nil {
|
|
return err
|
|
}
|
|
|
|
input, err := readInput(opt.inFile, stdin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
output, err := convert(input, opt.from, opt.to, opt.sheet, opt.pretty)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeOutput(opt.outFile, stdout, output)
|
|
}
|
|
|
|
func parseOptions(args []string, stderr io.Writer) (options, error) {
|
|
var opt options
|
|
fs := flag.NewFlagSet("dataxl", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
fs.StringVar(&opt.inFile, "i", "", "input file, defaults to stdin")
|
|
fs.StringVar(&opt.outFile, "o", "", "output file, defaults to stdout")
|
|
fs.StringVar(&opt.from, "from", "", "input format: json, yaml, toml, csv, tsv, xlsx")
|
|
fs.StringVar(&opt.to, "to", "", "output format: json, yaml, toml, csv, tsv, xlsx")
|
|
fs.StringVar(&opt.sheet, "sheet", "Sheet1", "worksheet name for xlsx input/output")
|
|
fs.BoolVar(&opt.pretty, "pretty", true, "pretty-print structured output")
|
|
fs.BoolVar(&opt.version, "version", false, "print version and exit")
|
|
fs.Usage = func() {
|
|
_, _ = fmt.Fprintln(stderr, `Usage:
|
|
dataxl -from yaml -to tsv -i input.yaml -o output.tsv
|
|
dataxl -from tsv -to json < paste-from-excel.tsv
|
|
dataxl -from json -to xlsx -i data.json -o data.xlsx
|
|
|
|
Formats:
|
|
json, yaml/yml, toml, csv, tsv, xlsx
|
|
|
|
Notes:
|
|
Structured records are flattened into spreadsheet columns such as user.name
|
|
and items[0].sku. Spreadsheet columns with those names are restored when
|
|
converting back to json/yaml/toml.`)
|
|
}
|
|
if err := fs.Parse(args); err != nil {
|
|
return options{}, err
|
|
}
|
|
if fs.NArg() != 0 {
|
|
return options{}, fmt.Errorf("unexpected arguments: %s", strings.Join(fs.Args(), " "))
|
|
}
|
|
return opt, nil
|
|
}
|
|
|
|
func resolvedVersion() string {
|
|
if buildVersion != "dev" {
|
|
return buildVersion
|
|
}
|
|
info, ok := debug.ReadBuildInfo()
|
|
if ok && info.Main.Version != "" && info.Main.Version != "(devel)" {
|
|
return info.Main.Version
|
|
}
|
|
return buildVersion
|
|
}
|
|
|
|
func (opt *options) resolveFormats() error {
|
|
opt.from = normalizeFormat(opt.from)
|
|
opt.to = normalizeFormat(opt.to)
|
|
if opt.from == "" {
|
|
opt.from = inferFormat(opt.inFile)
|
|
}
|
|
if opt.to == "" {
|
|
opt.to = inferFormat(opt.outFile)
|
|
}
|
|
if opt.from == "" || opt.to == "" {
|
|
return fmt.Errorf("both -from and -to are required when a format cannot be inferred from file names")
|
|
}
|
|
if err := validateFormat(opt.from); err != nil {
|
|
return err
|
|
}
|
|
return validateFormat(opt.to)
|
|
}
|
|
|
|
func readInput(path string, stdin io.Reader) ([]byte, error) {
|
|
if path == "" || path == "-" {
|
|
return io.ReadAll(stdin)
|
|
}
|
|
return os.ReadFile(path)
|
|
}
|
|
|
|
func writeOutput(path string, stdout io.Writer, data []byte) error {
|
|
if path == "" || path == "-" {
|
|
_, err := stdout.Write(data)
|
|
return err
|
|
}
|
|
return os.WriteFile(path, data, 0o644)
|
|
}
|