github.com/clusterize-io/tusk@v0.6.3-0.20211001020217-cfe8a8cd0d4a/runner/option.go (about) 1 package runner 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "strings" 8 9 "github.com/clusterize-io/tusk/marshal" 10 yaml "gopkg.in/yaml.v2" 11 ) 12 13 // Option represents an abstract command line option. 14 type Option struct { 15 ValueWithList `yaml:",inline"` 16 17 Short string 18 Type string 19 Usage string 20 Private bool 21 Required bool 22 23 // Used to determine value 24 Environment string 25 DefaultValues ValueList `yaml:"default"` 26 27 // Computed members not specified in yaml file 28 Name string `yaml:"-"` 29 Passed string `yaml:"-"` 30 cacheValue string `yaml:"-"` 31 isCacheSet bool `yaml:"-"` 32 } 33 34 // Dependencies returns a list of options that are required explicitly. 35 // This does not include interpolations. 36 func (o *Option) Dependencies() []string { 37 options := make([]string, 0, len(o.DefaultValues)) 38 for _, value := range o.DefaultValues { 39 options = append(options, value.When.Dependencies()...) 40 } 41 42 return options 43 } 44 45 // UnmarshalYAML ensures that the option definition is valid. 46 func (o *Option) UnmarshalYAML(unmarshal func(interface{}) error) error { 47 type optionType Option // Use new type to avoid recursion 48 if err := unmarshal((*optionType)(o)); err != nil { 49 return err 50 } 51 52 if len(o.Short) > 1 { 53 return fmt.Errorf( 54 "option short name %q cannot exceed one character", 55 o.Short, 56 ) 57 } 58 59 if o.Private { 60 if o.Required { 61 return errors.New("option cannot be both private and required") 62 } 63 64 if o.Environment != "" { 65 return fmt.Errorf( 66 "environment variable %q defined for private option", 67 o.Environment, 68 ) 69 } 70 71 if len(o.ValuesAllowed) != 0 { 72 return errors.New("option cannot be private and specify values") 73 } 74 } 75 76 if o.Required && len(o.DefaultValues) > 0 { 77 return errors.New("default value defined for required option") 78 } 79 80 return nil 81 } 82 83 // Evaluate determines an option's value. 84 // 85 // The order of priority is: 86 // 1. Command-line option passed 87 // 2. Environment variable set 88 // 3. The first item in the default value list with a valid when clause 89 // 90 // Values may also be cached to avoid re-running commands. 91 func (o *Option) Evaluate(ctx Context, vars map[string]string) (string, error) { 92 if o == nil { 93 return "", nil 94 } 95 96 value, err := o.getValue(ctx, vars) 97 if err != nil { 98 return "", err 99 } 100 101 o.cache(value) 102 103 return value, nil 104 } 105 106 func (o *Option) getValue(ctx Context, vars map[string]string) (string, error) { 107 if o.isCacheSet { 108 return o.cacheValue, nil 109 } 110 111 if !o.Private { 112 if value, found := o.getSpecified(); found { 113 if err := o.validateSpecified(value, "option "+o.Name); err != nil { 114 return "", err 115 } 116 117 return value, nil 118 } 119 } 120 121 if o.Required { 122 return "", fmt.Errorf("no value passed for required option: %s", o.Name) 123 } 124 125 return o.getDefaultValue(ctx, vars) 126 } 127 128 func (o *Option) getSpecified() (value string, found bool) { 129 if o.Passed != "" { 130 return o.Passed, true 131 } 132 133 envValue := os.Getenv(o.Environment) 134 if envValue != "" { 135 return envValue, true 136 } 137 138 return "", false 139 } 140 141 func (o *Option) getDefaultValue(ctx Context, vars map[string]string) (string, error) { 142 for _, candidate := range o.DefaultValues { 143 if err := candidate.When.Validate(ctx, vars); err != nil { 144 if !IsFailedCondition(err) { 145 return "", err 146 } 147 continue 148 } 149 150 value, err := candidate.commandValueOrDefault(ctx) 151 if err != nil { 152 return "", fmt.Errorf("could not compute value for option %q: %w", o.Name, err) 153 } 154 155 return value, nil 156 } 157 158 if o.isNumeric() { 159 return "0", nil 160 } 161 162 if o.isBoolean() { 163 return "false", nil 164 } 165 166 return "", nil 167 } 168 169 func (o *Option) cache(value string) { 170 o.isCacheSet = true 171 o.cacheValue = value 172 } 173 174 func (o *Option) isNumeric() bool { 175 switch strings.ToLower(o.Type) { 176 case "int", "integer", "float", "float64", "double": 177 return true 178 default: 179 return false 180 } 181 } 182 183 func (o *Option) isBoolean() bool { 184 switch strings.ToLower(o.Type) { 185 case "bool", "boolean": 186 return true 187 default: 188 return false 189 } 190 } 191 192 // Options represents an ordered set of options as specified in the config. 193 type Options []*Option 194 195 // UnmarshalYAML unmarshals an ordered set of options and assigns names. 196 func (o *Options) UnmarshalYAML(unmarshal func(interface{}) error) error { 197 var ms yaml.MapSlice 198 if err := unmarshal(&ms); err != nil { 199 return err 200 } 201 202 options, err := getOptionsWithOrder(ms) 203 if err != nil { 204 return err 205 } 206 207 *o = options 208 209 return nil 210 } 211 212 // Lookup finds an Option by name. 213 func (o *Options) Lookup(name string) (*Option, bool) { 214 for _, opt := range *o { 215 if opt.Name == name { 216 return opt, true 217 } 218 } 219 220 return nil, false 221 } 222 223 // getOptionsWithOrder returns both the option map and the ordered names. 224 func getOptionsWithOrder(ms yaml.MapSlice) ([]*Option, error) { 225 options := make([]*Option, 0, len(ms)) 226 assign := func(name string, text []byte) error { 227 var opt Option 228 if err := yaml.UnmarshalStrict(text, &opt); err != nil { 229 return err 230 } 231 opt.Name = name 232 233 options = append(options, &opt) 234 235 return nil 236 } 237 238 _, err := marshal.ParseOrderedMap(ms, assign) 239 return options, err 240 }