Skip to content

Commit

Permalink
WIP: flag type
Browse files Browse the repository at this point in the history
  • Loading branch information
thockin committed Sep 21, 2023
1 parent 7740013 commit c25f833
Showing 1 changed file with 138 additions and 58 deletions.
196 changes: 138 additions & 58 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,145 @@ const (

const defaultDirMode = os.FileMode(0775) // subject to umask

// FIXME: should this carry SSH keys? if so, sub-structs?
type credential struct {
URL string `json:"url"`
Username string `json:"username"`
Password string `json:"password"`
PasswordFile string `json:"password-file"`
Password string `json:"password,omitempty"`
PasswordFile string `json:"password-file,omitempty"`
}

func (c credential) String() string {
jb, err := json.Marshal(c)
if err != nil {
return fmt.Sprintf("<encoding error: %v>", err)
}
return string(jb)
}

// credentialSliceValue is for flags.
type credentialSliceValue struct {
value []credential
changed bool
}

var _ pflag.Value = &credentialSliceValue{}
var _ pflag.SliceValue = &credentialSliceValue{}

// pflagCredentialSlice is like pflag.StringSlice()
func pflagCredentialSlice(name, def, usage string) *[]credential {
p := &credentialSliceValue{}
p.Set(def)
pflag.Var(p, name, usage)
return &p.value
}

// unmarshal is like json.Unmarshal, but fails on unknown fields.
func (cs credentialSliceValue) unmarshal(val string, out any) error {
dec := json.NewDecoder(strings.NewReader(val))
dec.DisallowUnknownFields()
return dec.Decode(out)
}

// decodeList handles a string-encoded JSON object.
func (cs credentialSliceValue) decodeObject(val string) (credential, error) {
var cred credential
if err := cs.unmarshal(val, &cred); err != nil {
return credential{}, err
}
return cred, nil
}

// decodeList handles a string-encoded JSON list.
func (cs credentialSliceValue) decodeList(val string) ([]credential, error) {
var creds []credential
if err := cs.unmarshal(val, &creds); err != nil {
return nil, err
}
return creds, nil
}

// decode handles a string-encoded JSON object or list.
func (cs credentialSliceValue) decode(val string) ([]credential, error) {
s := strings.TrimSpace(val)
if s == "" {
return nil, nil
}
// If it tastes like an object...
if s[0] == '{' {
cred, err := cs.decodeObject(s)
return []credential{cred}, err
}
// If it tastes like a list...
if s[0] == '[' {
return cs.decodeList(s)
}
// Otherwise, bad
return nil, fmt.Errorf("not a JSON object or list")
}

func (cs *credentialSliceValue) Set(val string) error {
v, err := cs.decode(val)
if err != nil {
return err
}

if !cs.changed {
cs.value = v
} else {
cs.value = append(cs.value, v...)
}
cs.changed = true

return nil
}

func (cs credentialSliceValue) Type() string {
return "credentialSlice"
}

func (cs credentialSliceValue) String() string {
if len(cs.value) == 0 {
return "[]"
}
jb, err := json.Marshal(cs.value)
if err != nil {
return fmt.Sprintf("<encoding error: %v>", err)
}
return string(jb)
}

func (cs *credentialSliceValue) Append(val string) error {
v, err := cs.decodeObject(val)
if err != nil {
return err
}
cs.value = append(cs.value, v)
return nil
}

func (cs *credentialSliceValue) Replace(val []string) error {
creds := []credential{}
for _, s := range val {
v, err := cs.decodeObject(s)
if err != nil {
return err
}
creds = append(creds, v)
}
cs.value = creds
return nil
}

func (cs credentialSliceValue) GetSlice() []string {
if len(cs.value) == 0 {
return nil
}
ret := []string{}
for _, cred := range cs.value {
ret = append(ret, cred.String())
}
return ret
}

func envString(def string, key string, alts ...string) string {
Expand Down Expand Up @@ -143,54 +277,6 @@ func envStringArray(def string, key string, alts ...string) []string {
return parse(def)
}

func envStringArrayJSONOrError(def string, key string, alts ...string) ([]string, error) {
parse := func(key, val string) ([]string, error) {
s := strings.TrimSpace(val)
if s == "" {
return nil, nil
}
// If it tastes like an object...
if s[0] == '{' {
return []string{s}, nil
}
// If it tastes like an array...
if s[0] == '[' {
// Parse into an array of "stuff to decode later".
var arr []json.RawMessage
if err := json.Unmarshal([]byte(s), &arr); err != nil {
return nil, fmt.Errorf("ERROR: invalid JSON list env %s=%q: %w", key, val, err)
}
// Re-encode as []string
ret := []string{}
for _, rm := range arr {
ret = append(ret, string(rm))
}
return ret, nil
}
return nil, fmt.Errorf("ERROR: invalid JSON env %s=%q: not a list or object", key, val)
}

if val := os.Getenv(key); val != "" {
return parse(key, val)
}
for _, alt := range alts {
if val := os.Getenv(alt); val != "" {
fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key)
return parse(alt, val)
}
}
return parse("", def)
}
func envStringArrayJSON(def string, key string, alts ...string) []string {
val, err := envStringArrayJSONOrError(def, key, alts...)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
return nil
}
return val
}

func envBoolOrError(def bool, key string, alts ...string) (bool, error) {
parse := func(key, val string) (bool, error) {
parsed, err := strconv.ParseBool(val)
Expand Down Expand Up @@ -506,9 +592,7 @@ func main() {
flPasswordFile := pflag.String("password-file",
envString("", "GITSYNC_PASSWORD_FILE", "GIT_SYNC_PASSWORD_FILE"),
"the file from which the password or personal access token for git auth will be sourced")
flCredentials := pflag.StringArray("credential",
envStringArrayJSON("", "GITSYNC_CREDENTIAL"),
"one or more credentials (see --man for details) available for authentication")
flCredentials := pflagCredentialSlice("credential", envString("", "GITSYNC_CREDENTIAL"), "one or more credentials (see --man for details) available for authentication")

flSSH := pflag.Bool("ssh",
envBool(false, "GITSYNC_SSH", "GIT_SYNC_SSH"),
Expand Down Expand Up @@ -760,11 +844,7 @@ func main() {

credentials := []credential{}
if len(*flCredentials) > 0 {
for _, s := range *flCredentials {
cred := credential{}
if err := json.Unmarshal([]byte(s), &cred); err != nil {
handleConfigError(log, true, "ERROR: can't parse --credential payload: %v", err)
}
for _, cred := range *flCredentials {
if cred.URL == "" {
//FIXME: can it default to --repo?
handleConfigError(log, true, "ERROR: --credential URL must be specified")
Expand Down

0 comments on commit c25f833

Please sign in to comment.