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] \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) } }