mirror of
https://github.com/therootcompany/golib.git
synced 2025-12-23 22:08:46 +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