mirror of
https://github.com/therootcompany/golib.git
synced 2025-10-30 12:42:51 +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)
|
||||
\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