mirror of
				https://github.com/therootcompany/golib.git
				synced 2025-10-30 20:52:53 +00:00 
			
		
		
		
	feat(gsheet2env): add tool to convert csv to .env
This commit is contained in:
		
							parent
							
								
									24ec3f021d
								
							
						
					
					
						commit
						f882bfc139
					
				| @ -123,3 +123,57 @@ gsheet2csv --strip-comments ./gsheet.csv > ./sheet.csv | |||||||
| \f  form feed (also ^L) | \f  form feed (also ^L) | ||||||
| \v  vertical tab (also ^K) | \v  vertical tab (also ^K) | ||||||
| ``` | ``` | ||||||
|  | 
 | ||||||
|  | ## gsheet2env | ||||||
|  | 
 | ||||||
|  | Converts a Google Sheet to an ENV file. | ||||||
|  | 
 | ||||||
|  | ### Installation | ||||||
|  | 
 | ||||||
|  | ```sh | ||||||
|  | go get github.com/therootcompany/golib/io/transform/gsheet2env | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Usage | ||||||
|  | 
 | ||||||
|  | ```sh | ||||||
|  | gsheet2env --no-shebang --no-header --no-export ./fixtures/gsheet-env-readme.csv | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Example | ||||||
|  | 
 | ||||||
|  | ```csv | ||||||
|  | # DO NOT put SECRETS here. Anyone with a link can see this. Use for config ONLY! | ||||||
|  | LOCAL_TIMEZONE,America/Denver,"Seatle PT, Denver MT, Arizona MST, Chicago CT, Detroit ET" | ||||||
|  | 
 | ||||||
|  | # to show that escapes are handled | ||||||
|  | FIRST_LETTER,A, | ||||||
|  | MONEY_SYMBOL,$, | ||||||
|  | SINGLE_QUOTE,', | ||||||
|  | DOUBLE_QUOTE,"""", | ||||||
|  | 
 | ||||||
|  | # to show that newlines are handled | ||||||
|  | TRIFORCE,"wisdom | ||||||
|  | courage | ||||||
|  | power","Zelda | ||||||
|  | Link | ||||||
|  | Ganon" | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ```sh | ||||||
|  | # DO NOT put SECRETS here. Anyone with a link can see this. Use for config ONLY! | ||||||
|  | # Seatle PT, Denver MT, Arizona MST, Chicago CT, Detroit ET | ||||||
|  | LOCAL_TIMEZONE='America/Denver' | ||||||
|  | # to show that escapes are handled | ||||||
|  | FIRST_LETTER='A' | ||||||
|  | MONEY_SYMBOL='$' | ||||||
|  | SINGLE_QUOTE=''"'"'' | ||||||
|  | DOUBLE_QUOTE='"' | ||||||
|  | # to show that newlines are handled | ||||||
|  | # Zelda | ||||||
|  | # Link | ||||||
|  | # Ganon | ||||||
|  | TRIFORCE='wisdom | ||||||
|  | courage | ||||||
|  | power' | ||||||
|  | ``` | ||||||
|  | |||||||
							
								
								
									
										169
									
								
								io/transform/gsheet2csv/cmd/gsheet2env/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								io/transform/gsheet2csv/cmd/gsheet2env/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,169 @@ | |||||||
|  | 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() { | ||||||
|  | 	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() | ||||||
|  | 
 | ||||||
|  | 	// 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.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 | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								io/transform/gsheet2csv/fixtures/gsheet-env-readme.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								io/transform/gsheet2csv/fixtures/gsheet-env-readme.csv
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | # DO NOT put SECRETS here. Anyone with a link can see this. Use for config ONLY! | ||||||
|  | LOCAL_TIMEZONE,America/Denver,"Seatle PT, Denver MT, Arizona MST, Chicago CT, Detroit ET" | ||||||
|  | # | ||||||
|  | # to show that escapes are handled | ||||||
|  | FIRST_LETTER,A, | ||||||
|  | MONEY_SYMBOL,$, | ||||||
|  | SINGLE_QUOTE,', | ||||||
|  | DOUBLE_QUOTE,"""", | ||||||
|  | # | ||||||
|  | # to show that newlines are handled | ||||||
|  | TRIFORCE,"wisdom | ||||||
|  | courage | ||||||
|  | power","Zelda | ||||||
|  | Link | ||||||
|  | Ganon" | ||||||
| Can't render this file because it has a wrong number of fields in line 2. | 
							
								
								
									
										14
									
								
								io/transform/gsheet2csv/fixtures/gsheet-env.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								io/transform/gsheet2csv/fixtures/gsheet-env.csv
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | # DO NOT put SECRETS here. These are used when the Ai program runs to set certain parameters,, | ||||||
|  | ENV,Value,Comment | ||||||
|  | LOCAL_TIMEZONE,America/Denver,"Seatle PT, Denver MT, Arizona MST, Chicago CT, Detroit ET" | ||||||
|  | LITERAL_VAR,$SHELL, | ||||||
|  | ,, | ||||||
|  | # separated,, | ||||||
|  | EMPTY_KEY,, | ||||||
|  | MULTI,"a nice | ||||||
|  | multi-line | ||||||
|  | value block","a nice | ||||||
|  | multi-line | ||||||
|  | comment block" | ||||||
|  | ,no key, | ||||||
|  | ${FOO},,this will break things | ||||||
| 
 | 
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user