pkg/common/flag/flag.go (148 lines of code) (raw):
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package flag
import (
"context"
"flag"
"fmt"
"log/slog"
"os"
"strconv"
"strings"
)
// This package provides helper functions to parse command line arguments and environment variables.
// It provides functions similar to the standard `flag` package, but with added functionality to handle environment variables as a fallback. The package also provides a mechanism to dump all parsed flag values.
type FlagParser func() error
type FlagValueDumper func() string
var flagParsed bool = false
var flagParsers []FlagParser = []FlagParser{}
var flagValueDumper []FlagValueDumper = []FlagValueDumper{}
var envFalsyValues map[string]struct{} = map[string]struct{}{
"false": {},
}
func Parse() error {
flag.Parse()
for _, parser := range flagParsers {
err := parser()
if err != nil {
return err
}
}
flagParsed = true
return nil
}
func Parsed() bool {
return flagParsed
}
func Reset() {
flagParsed = false
flagParsers = []FlagParser{}
flagValueDumper = []FlagValueDumper{}
}
func DumpAll(ctx context.Context) {
setting := ""
for _, dumper := range flagValueDumper {
setting += dumper() + "\n"
}
slog.InfoContext(ctx, setting)
}
func PrintDefaults() {
flag.PrintDefaults()
}
// String is a similar to flag.String but it also parse value from specified environment variable when the parameter was not provided explicitly on command line argument.
// Environment variables are ignored when the given envKey is an empty string.
func String(name string, value string, usage string, envKey string) *string {
result := value
resultPtr := &result
if envKey != "" {
usage = fmt.Sprintf("%s [environment variable key: \"%s\"]", usage, envKey)
}
fromCmdArgs := flag.String(name, value, usage)
flagParsers = append(flagParsers, func() error {
providedFromCmdArgs, err := isProvidedFromCommandlineArgs(name)
if err != nil {
return err
}
if providedFromCmdArgs {
*resultPtr = *fromCmdArgs
} else if isProvidedFromEnvironmentVariable(envKey) {
*resultPtr = os.Getenv(envKey)
}
return nil
})
flagValueDumper = append(flagValueDumper, func() string {
return fmt.Sprintf("%s: %v", name, *resultPtr)
})
return resultPtr
}
// Bool is a similar to flag.Bool but it also parse value from specified environment variable when the parameter was not provided explicitly on command line argument.
// Environment variables are ignored when the given envKey is an empty string.
func Bool(name string, value bool, usage string, envKey string) *bool {
result := value
resultPtr := &result
if envKey != "" {
usage = fmt.Sprintf("%s [environment variable key: \"%s\"]", usage, envKey)
}
fromCmdArgs := flag.Bool(name, value, usage)
flagParsers = append(flagParsers, func() error {
providedFromCmdArgs, err := isProvidedFromCommandlineArgs(name)
if err != nil {
return err
}
if providedFromCmdArgs {
*resultPtr = *fromCmdArgs
} else if isProvidedFromEnvironmentVariable(envKey) {
value := os.Getenv(envKey)
if _, found := envFalsyValues[strings.ToLower(value)]; found {
*resultPtr = false
} else {
*resultPtr = true
}
}
return nil
})
flagValueDumper = append(flagValueDumper, func() string {
return fmt.Sprintf("%s: %v", name, *resultPtr)
})
return resultPtr
}
// Int is a similar to flag.Int but it also parse value from specified environment variable when the parameter was not provided explicitly on command line argument.
// Environment variables are ignored when the given envKey is an empty string.
func Int(name string, value int, usage string, envKey string) *int {
result := value
resultPtr := &result
if envKey != "" {
usage = fmt.Sprintf("%s [environment variable key: \"%s\"]", usage, envKey)
}
fromCmdArgs := flag.Int(name, value, usage)
flagParsers = append(flagParsers, func() error {
providedFromCmdArgs, err := isProvidedFromCommandlineArgs(name)
if err != nil {
return err
}
if providedFromCmdArgs {
*resultPtr = *fromCmdArgs
} else if isProvidedFromEnvironmentVariable(envKey) {
envValue := os.Getenv(envKey)
intEnv, err := strconv.ParseInt(envValue, 10, 64)
if err != nil {
return err
}
*resultPtr = int(intEnv)
}
return nil
})
flagValueDumper = append(flagValueDumper, func() string {
return fmt.Sprintf("%s: %v", name, *resultPtr)
})
return resultPtr
}
func isProvidedFromCommandlineArgs(key string) (bool, error) {
if !flag.Parsed() {
return false, fmt.Errorf("command line arguments are not yet parsed")
}
found := false
flag.Visit(func(f *flag.Flag) {
if f.Name == key {
found = true
}
})
return found, nil
}
func isProvidedFromEnvironmentVariable(key string) bool {
if key == "" {
return false
}
_, found := os.LookupEnv(key)
return found
}