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  }