Files
dataxl/cmd/dataxl/main.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)
}