github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/language/language.go (about) 1 package language 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/ActiveState/cli/internal/constants" 8 "github.com/ActiveState/cli/internal/locale" 9 "github.com/ActiveState/cli/internal/osutils" 10 "github.com/thoas/go-funk" 11 ) 12 13 // Language tracks the languages potentially used. 14 type Language int 15 16 // Language constants are provided for safety/reference. 17 const ( 18 Unset Language = iota 19 Unknown 20 Bash 21 Sh 22 Batch 23 PowerShell 24 Perl 25 Python3 26 Python2 27 Ruby 28 ) 29 30 // UnrecognizedLanguageError simplifies construction of LocalizedError for an unrecognized language. 31 func UnrecognizedLanguageError(name string, options []string) *locale.LocalizedError { 32 opts := locale.T("language_unknown_options") 33 if len(options) > 0 { 34 opts = strings.Join(options, ", ") 35 } 36 return locale.NewInputError("err_invalid_language", "", name, opts) 37 } 38 39 const ( 40 filePatternPrefix = "script-*" 41 ) 42 43 type languageData struct { 44 name string 45 text string 46 ext string 47 hdr bool 48 require string 49 version string 50 exec Executable 51 } 52 53 var lookup = [...]languageData{ 54 {}, 55 { 56 "unknown", locale.T("language_name_unknown"), ".tmp", false, "", "", 57 Executable{"", false}, 58 }, 59 { 60 "bash", "Bash", ".sh", true, "", "", 61 Executable{"bash" + osutils.ExeExtension, true}, 62 }, 63 { 64 "sh", "Shell", ".sh", true, "", "", 65 Executable{"sh" + osutils.ExeExtension, true}, 66 }, 67 { 68 "batch", "Batch", ".bat", false, "", "", 69 Executable{"cmd.exe", true}, 70 }, 71 { 72 "powershell", "PowerShell", ".ps1", false, "", "", 73 Executable{"powershell.exe", true}, 74 }, 75 { 76 "perl", "Perl", ".pl", true, "perl", "5.36.0", 77 Executable{constants.ActivePerlExecutable, false}, 78 }, 79 { 80 "python3", "Python 3", ".py", true, "python", "3.10.8", 81 Executable{constants.ActivePython3Executable, false}, 82 }, 83 { 84 "python2", "Python 2", ".py", true, "python", "2.7.18.5", 85 Executable{constants.ActivePython2Executable, false}, 86 }, 87 { 88 "ruby", "Ruby", ".rb", true, "ruby", "3.2.2", 89 Executable{constants.RubyExecutable, false}, 90 }, 91 } 92 93 // MakeByShell returns either bash or cmd based on whether the provided 94 // shell name contains "cmd". This should be taken to mean that bash is a sort 95 // of default. 96 func MakeByShell(shell string) Language { 97 shell = strings.ToLower(shell) 98 99 if strings.Contains(shell, "cmd") { 100 return Batch 101 } 102 103 return Bash 104 } 105 106 // MakeByName will retrieve a language by a given name after lower-casing. 107 func MakeByName(name string) Language { 108 if len(name) == 0 { 109 return Unset 110 } 111 112 nameParts := strings.Split(name, "@") 113 for i, data := range lookup { 114 if strings.ToLower(nameParts[0]) == data.name { 115 return Language(i) 116 } 117 } 118 119 return Unknown 120 } 121 122 // MakeByNameAndVersion will retrieve a language by a given name and version. 123 func MakeByNameAndVersion(name, version string) Language { 124 if strings.ToLower(name) == Python3.Requirement() { 125 name = Python3.String() 126 // Disambiguate python, preferring Python3. 127 major, _, _ := strings.Cut(version, ".") 128 major = strings.TrimLeft(major, ">=<") // constraint characters (e.g. ">3.9") 129 if major == "2" { 130 name = Python2.String() 131 } 132 } 133 return MakeByName(name) 134 } 135 136 // MakeByText will retrieve a language by a given text 137 func MakeByText(text string) Language { 138 for i, data := range lookup { 139 if text == data.text { 140 return Language(i) 141 } 142 } 143 144 return Unknown 145 } 146 147 func (l Language) data() languageData { 148 i := int(l) 149 if i < 0 || i > len(lookup)-1 { 150 i = 1 151 } 152 return lookup[i] 153 } 154 155 // String implements the fmt.Stringer interface. 156 func (l Language) String() string { 157 return l.data().name 158 } 159 160 // Text returns the human-readable value. 161 func (l Language) Text() string { 162 return l.data().text 163 } 164 165 // Recognized returns whether the language is a known useful value. 166 func (l *Language) Recognized() bool { 167 return l != nil && *l != Unset && *l != Unknown 168 } 169 170 // Validate ensures that the current language is recognized 171 func (l *Language) Validate() error { 172 if !l.Recognized() { 173 return UnrecognizedLanguageError(l.String(), RecognizedSupportedsNames()) 174 } 175 return nil 176 } 177 178 // Ext return the file extension for the language. 179 func (l Language) Ext() string { 180 return l.data().ext 181 } 182 183 // Header returns the interpreter directive. 184 func (l Language) Header() string { 185 ld := l.data() 186 if ld.hdr { 187 return fmt.Sprintf("#!/usr/bin/env %s\n", ld.name) 188 } 189 return "" 190 } 191 192 // TempPattern returns the os.CreateTemp pattern to be used to form the temp 193 // file name. 194 func (l Language) TempPattern() string { 195 return filePatternPrefix + l.data().ext 196 } 197 198 // Requirement returns the platform-level string representation. 199 func (l Language) Requirement() string { 200 return l.data().require 201 } 202 203 // RecommendedVersion returns the string representation of the recommended 204 // version. 205 func (l Language) RecommendedVersion() string { 206 return l.data().version 207 } 208 209 // Executable provides details about the executable related to the Language. 210 func (l Language) Executable() Executable { 211 return l.data().exec 212 } 213 214 // UnmarshalYAML implements the go-yaml/yaml.Unmarshaler interface. 215 func (l *Language) UnmarshalYAML(applyPayload func(interface{}) error) error { 216 var payload string 217 if err := applyPayload(&payload); err != nil { 218 return err 219 } 220 221 return l.Set(payload) 222 } 223 224 // MarshalYAML implements the go-yaml/yaml.Marshaler interface. 225 func (l Language) MarshalYAML() (interface{}, error) { 226 return l.String(), nil 227 } 228 229 // Set implements the captain marshaler interfaces. 230 func (l *Language) Set(v string) error { 231 lang := MakeByName(v) 232 if !lang.Recognized() { 233 return UnrecognizedLanguageError(v, RecognizedNames()) 234 } 235 236 *l = lang 237 return nil 238 } 239 240 // Type implements the captain.FlagMarshaler interface. 241 func (l *Language) Type() string { 242 return "language" 243 } 244 245 // Executable contains details about an executable program used to interpret a 246 // Language. 247 type Executable struct { 248 name string 249 allowThirdParty bool 250 } 251 252 // Name returns the executables file's name. 253 func (e Executable) Name() string { 254 // We don't want to generate as.yaml code that uses the full filename for the language name 255 // https://www.pivotaltracker.com/story/show/177845386 256 return strings.TrimSuffix(e.name, ".exe") 257 } 258 259 // Filename returns the executables file's full name. 260 func (e Executable) Filename() string { 261 return e.name 262 } 263 264 // CanUseThirdParty expresses whether the executable is expected to be provided by the 265 // shell environment. 266 func (e Executable) CanUseThirdParty() bool { 267 return e.allowThirdParty 268 } 269 270 // Available returns whether the executable is not "builtin" and also has a 271 // defined name. 272 func (e Executable) Available() bool { 273 return !e.allowThirdParty && e.name != "" 274 } 275 276 // Recognized returns all languages that are supported. 277 func Recognized() []Language { 278 var langs []Language 279 for i := range lookup { 280 if l := Language(i); l.Recognized() { 281 langs = append(langs, l) 282 } 283 } 284 return langs 285 } 286 287 // RecognizedNames returns all language names that are supported. 288 func RecognizedNames() []string { 289 var langs []string 290 for i, data := range lookup { 291 if l := Language(i); l.Recognized() { 292 langs = append(langs, data.name) 293 } 294 } 295 return langs 296 } 297 298 // Supported tracks the languages potentially used for projects. 299 type Supported struct { 300 Language 301 } 302 303 // Recognized returns whether the supported language is a known useful value. 304 func (l *Supported) Recognized() bool { 305 return l != nil && l.Language.Recognized() && l.Executable().Available() 306 } 307 308 // UnmarshalYAML implements the go-yaml/yaml.Unmarshaler interface. 309 func (l *Supported) UnmarshalYAML(applyPayload func(interface{}) error) error { 310 var payload string 311 if err := applyPayload(&payload); err != nil { 312 return err 313 } 314 315 return l.Set(payload) 316 } 317 318 // Set implements the captain marshaler interfaces. 319 func (l *Supported) Set(v string) error { 320 supported := Supported{MakeByName(v)} 321 if !supported.Recognized() { 322 return UnrecognizedLanguageError(v, RecognizedSupportedsNames()) 323 } 324 325 *l = supported 326 return nil 327 } 328 329 // RecognizedSupporteds returns all languages that are not "builtin" 330 // and also have a defined executable name. 331 func RecognizedSupporteds() []Supported { 332 var supporteds []Supported 333 for i := range lookup { 334 l := Supported{Language(i)} 335 if l.Recognized() { 336 supporteds = append(supporteds, l) 337 } 338 } 339 return supporteds 340 } 341 342 // RecognizedSupportedsNames returns all languages that are not 343 // "builtin" and also have a defined executable name. 344 func RecognizedSupportedsNames() []string { 345 var supporteds []string 346 for i, data := range lookup { 347 l := Supported{Language(i)} 348 if l.Recognized() && !funk.Contains(supporteds, data.require) { 349 supporteds = append(supporteds, data.require) 350 } 351 } 352 return supporteds 353 }