2025-10-12 01:43:50 -06:00

184 lines
4.7 KiB
Go

package main
import (
"errors"
"flag"
"fmt"
"io"
"os"
"strings"
"unicode/utf8"
"github.com/therootcompany/golib/io/transform/gsheet2csv"
)
const (
fileSeparator = "\x1c"
groupSeparator = "\x1d"
recordSeparator = "\x1e"
unitSeparator = "\x1f"
)
func main() {
var commentArg string
format := "CSV"
delim := ','
if strings.Contains(os.Args[0], "tsv") {
delim = '\t'
format = "TSV"
}
// Parse command-line flags
flag.StringVar(&commentArg, "comment", "#", "treat lines beginning with this rune as comments")
outputFile := flag.String("o", "", "Output "+format+" file (default: stdout)")
delimString := flag.String("d", string(delim), "field delimiter to use for output file ('\\t' for tab, '^_' for Unit Separator, etc)")
useCRLF := flag.Bool("crlf", false, "use CRLF (\\r\\n) as record separator")
urlOnly := flag.Bool("print-url", false, "don't download, just print the Google Sheet URL")
parseOnly := flag.Bool("print-ids", false, "don't download, just print the Doc ID and Sheet ID (gid)")
rawOnly := flag.Bool("raw", false, "don't parse, just download")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [flags] <google-sheet-url-or-file-path>\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Converts a Google Sheet to %s format.\n\n", format)
fmt.Fprintf(os.Stderr, "Flags:\n")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nExample:\n")
fmt.Fprintf(os.Stderr, " %s -o output.tsv 'https://docs.google.com/spreadsheets/d/1KdNsc63pk0QRerWDPcIL9cMnGQlG-9Ue9Jlf0PAAA34/edit?gid=559037238#gid=559037238'\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s -o output.tsv 'file://gsheet.csv'\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s -o output.tsv './gsheet.csv'\n", os.Args[0])
}
flag.Parse()
// Check for URL argument
if len(flag.Args()) != 1 {
fmt.Fprintf(os.Stderr, "Error: exactly one Google Sheet URL is required\n")
flag.Usage()
os.Exit(1)
}
url := flag.Args()[0]
// Prepare output writer
var out *os.File
if *outputFile != "" {
var err error
out, err = os.Create(*outputFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating output file: %v\n", err)
os.Exit(1)
}
defer func() { _ = out.Close() }()
} else {
out = os.Stdout
}
switch *delimString {
case "^_", "\\x1f":
*delimString = unitSeparator
case "^^", "\\x1e":
*delimString = recordSeparator
case "^]", "\\x1d":
*delimString = groupSeparator
case "^\\", "\\x1c":
*delimString = fileSeparator
case "^L", "\\f":
*delimString = "\f"
case "^K", "\\v":
*delimString = "\v"
case "^I", "\\t":
*delimString = "\t"
}
delim, _ = utf8.DecodeRuneInString(*delimString)
var rc io.ReadCloser
if strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") {
docid, gid := gsheet2csv.ParseIDs(url)
if *parseOnly {
fmt.Printf("docid=%s\ngid=%s\n", docid, gid)
} else {
fmt.Fprintf(os.Stderr, "docid=%s\ngid=%s\n", docid, gid)
}
sheetURL := gsheet2csv.ToCSVURL(docid, gid)
if *urlOnly {
fmt.Printf("%s\n", sheetURL)
} else {
fmt.Fprintf(os.Stderr, "downloading %s\n", sheetURL)
}
if !*urlOnly {
resp, err := gsheet2csv.GetSheet(docid, gid)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting url: %v\n", err)
os.Exit(1)
}
defer func() { _ = resp.Body.Close() }()
rc = resp.Body
}
} else {
url = strings.TrimPrefix(url, "file://")
fmt.Fprintf(os.Stderr, "opening %s\n", url)
f, err := os.Open(url)
if err != nil {
fmt.Fprintf(os.Stderr, "Error opening file: %v\n", err)
os.Exit(1)
}
rc = f
}
if out == os.Stdout {
fmt.Fprintf(os.Stderr, "\n")
}
if *urlOnly || *parseOnly {
os.Exit(0)
return
}
if *rawOnly {
if _, err := io.Copy(out, rc); err != nil {
fmt.Fprintf(os.Stderr, "Error getting url body: %v\n", err)
os.Exit(1)
}
return
}
comment, _ := utf8.DecodeRuneInString(commentArg)
// Create a reader for the Google Sheet
gsr := gsheet2csv.NewReader(rc)
gsr.QuotedComments = false
gsr.Comment = 0
gsr.ReuseRecord = true
// Create CSV writer
csvw := gsheet2csv.NewWriter(out)
csvw.Comma = delim // Set delimiter to tab for TSV
csvw.Comment = comment
csvw.UseCRLF = *useCRLF
for {
// Convert each record
record, err := gsr.Read()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
fmt.Fprintf(os.Stderr, "Error reading "+format+": %v\n", err)
os.Exit(1)
}
if err := csvw.Write(record); err != nil {
fmt.Fprintf(os.Stderr, "Error writing "+format+": %v\n", err)
os.Exit(1)
}
}
// Flush the writer to ensure all data is written
csvw.Flush()
if err := csvw.Error(); err != nil {
fmt.Fprintf(os.Stderr, "Error flushing "+format+" writer: %v\n", err)
os.Exit(1)
}
if out != os.Stdout {
fmt.Fprintf(os.Stderr, "wrote %s\n", *outputFile)
}
}