mirror of
				https://github.com/therootcompany/golib.git
				synced 2025-10-30 20:52:53 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			178 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			178 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package main
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"flag"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/therootcompany/golib/io/transform/gsheet2csv"
 | |
| )
 | |
| 
 | |
| func isValidKey(key string) bool {
 | |
| 	for _, c := range key {
 | |
| 		isUpperWord := (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
 | |
| 		if !isUpperWord {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| func main() {
 | |
| 	delimString := flag.String("d", ",", "field delimiter to use for output file ('\\t' for tab, '^_' for Unit Separator, etc)")
 | |
| 	noShebang := flag.Bool("no-shebang", false, "don't begin the file with #!/bin/sh")
 | |
| 	noHeader := flag.Bool("no-header", false, "treat all non-comment rows as ENVs - don't expect a header")
 | |
| 	noExport := flag.Bool("no-export", false, "disable export prefix")
 | |
| 	outputFile := flag.String("o", "-", "path to output env file (default: stdout)")
 | |
| 	flag.Parse()
 | |
| 
 | |
| 	delim, err := gsheet2csv.DecodeDelimiter(*delimString)
 | |
| 	if err != nil {
 | |
| 		fmt.Fprintf(os.Stderr, "Error decoding output delimiter: %v\n", err)
 | |
| 		os.Exit(1)
 | |
| 	}
 | |
| 
 | |
| 	// Require Google Sheet URL as argument
 | |
| 	if len(flag.Args()) != 1 {
 | |
| 		fmt.Fprintf(os.Stderr, "Error: exactly one Google Sheet URL or path is required\n")
 | |
| 		flag.Usage()
 | |
| 		os.Exit(1)
 | |
| 	}
 | |
| 	gsheetURLOrPath := flag.Args()[0]
 | |
| 
 | |
| 	// Prepare output writer
 | |
| 	var out *os.File
 | |
| 	if len(*outputFile) > 0 && *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)
 | |
| 			return
 | |
| 		}
 | |
| 		defer func() { _ = out.Close() }()
 | |
| 	} else {
 | |
| 		out = os.Stdout
 | |
| 	}
 | |
| 
 | |
| 	gsr := gsheet2csv.NewReaderFrom(gsheetURLOrPath)
 | |
| 	// preserves comment-looking data (and actual comments)
 | |
| 	gsr.Comment = 0
 | |
| 	gsr.Comma = delim
 | |
| 	gsr.FieldsPerRecord = -1
 | |
| 
 | |
| 	if !*noShebang {
 | |
| 		if _, err := out.Write([]byte("#!/bin/sh\n\n")); err != nil {
 | |
| 			fmt.Fprintf(os.Stderr, "Error creating output file: %v\n", err)
 | |
| 			os.Exit(1)
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if err := convert(gsr, out, *noHeader, *noExport); err != nil {
 | |
| 		fmt.Fprintf(os.Stderr, "Error converting CSV to ENV: %v\n", err)
 | |
| 		os.Exit(1)
 | |
| 	}
 | |
| 
 | |
| 	if out != os.Stdout {
 | |
| 		fmt.Fprintf(os.Stderr, "wrote %s\n", *outputFile)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func convert(gsr *gsheet2csv.Reader, out io.Writer, noHeader bool, noExport bool) error {
 | |
| 	consumedHeader := noHeader
 | |
| 	for {
 | |
| 		row, err := gsr.Read()
 | |
| 		if err != nil {
 | |
| 			if errors.Is(err, io.EOF) {
 | |
| 				break
 | |
| 			}
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		var key string
 | |
| 		if len(row) >= 1 {
 | |
| 			key = strings.TrimSpace(row[0])
 | |
| 			if len(key) == 0 {
 | |
| 				if _, err := fmt.Fprintln(out); err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			// Preserve but ignore proper comments
 | |
| 			if keyComment, exists := strings.CutPrefix(key, "#"); exists {
 | |
| 				keyComment = strings.TrimSpace(keyComment)
 | |
| 				if len(keyComment) == 0 {
 | |
| 					if _, err := fmt.Fprintln(out, "#"); err != nil {
 | |
| 						return err
 | |
| 					}
 | |
| 					continue
 | |
| 				}
 | |
| 
 | |
| 				saniComment := sanitizeComment(keyComment)
 | |
| 				if _, err := fmt.Fprintf(out, "%s", saniComment); err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		var value string
 | |
| 		if len(row) >= 2 {
 | |
| 			value = strings.TrimSpace(row[1])
 | |
| 		}
 | |
| 
 | |
| 		var saniComment string
 | |
| 		if len(row) >= 3 {
 | |
| 			saniComment = sanitizeComment(row[2])
 | |
| 		}
 | |
| 
 | |
| 		if !consumedHeader {
 | |
| 			consumedHeader = true
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Error on invalid keys
 | |
| 		if !isValidKey(key) {
 | |
| 			return fmt.Errorf("invalid key in record %s", strings.Join(row, ","))
 | |
| 		}
 | |
| 
 | |
| 		// Escape single quotes in value for shell compatibility
 | |
| 		value = strings.ReplaceAll(value, "'", "'\"'\"'")
 | |
| 
 | |
| 		// Output the ENV line
 | |
| 		prefix := ""
 | |
| 		if !noExport {
 | |
| 			prefix = "export "
 | |
| 		}
 | |
| 		if _, err := fmt.Fprintf(out, "%s%s%s='%s'\n", saniComment, prefix, key, value); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func sanitizeComment(comment string) string {
 | |
| 	var formatted []string
 | |
| 
 | |
| 	comment = strings.TrimSpace(comment)
 | |
| 	lines := strings.FieldsFuncSeq(comment, func(r rune) bool {
 | |
| 		return r == '\n' || r == '\r'
 | |
| 	})
 | |
| 	for line := range lines {
 | |
| 		trimmed := strings.TrimSpace(line)
 | |
| 		formatted = append(formatted, "# "+trimmed)
 | |
| 	}
 | |
| 
 | |
| 	comment = strings.Join(formatted, "\n")
 | |
| 	if len(comment) > 0 {
 | |
| 		comment += "\n"
 | |
| 	}
 | |
| 	return comment
 | |
| }
 |