github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/project/expander.go (about) 1 package project 2 3 import ( 4 "path/filepath" 5 "regexp" 6 "runtime" 7 "strings" 8 9 "github.com/ActiveState/cli/internal/constants" 10 "github.com/ActiveState/cli/internal/constraints" 11 "github.com/ActiveState/cli/internal/errs" 12 "github.com/ActiveState/cli/internal/language" 13 "github.com/ActiveState/cli/internal/locale" 14 "github.com/ActiveState/cli/internal/osutils" 15 "github.com/ActiveState/cli/internal/rxutils" 16 "github.com/ActiveState/cli/internal/scriptfile" 17 "github.com/ActiveState/cli/pkg/platform/authentication" 18 "github.com/ActiveState/cli/pkg/projectfile" 19 ) 20 21 type Expansion struct { 22 Project *Project 23 Script *Script 24 BashifyPaths bool 25 } 26 27 func NewExpansion(p *Project) *Expansion { 28 return &Expansion{Project: p} 29 } 30 31 // ApplyWithMaxDepth limits the depth of an expansion to avoid infinite expansion of a value. 32 func (ctx *Expansion) ApplyWithMaxDepth(s string, depth int) (string, error) { 33 if depth > constants.ExpanderMaxDepth { 34 return "", locale.NewExternalError("err_expand_recursion", "Infinite recursion trying to expand variable '{{.V0}}'", s) 35 } 36 37 regex := regexp.MustCompile(`\${?(\w+)\.?([\w-]+)?\.?([\w\.-]+)?(\(\))?}?`) 38 var err error 39 expanded := rxutils.ReplaceAllStringSubmatchFunc(regex, s, func(groups []string) string { 40 if err != nil { 41 return "" 42 } 43 var variable, category, name, meta string 44 var isFunction bool 45 variable = groups[0] 46 47 if len(groups) == 2 { 48 category = "toplevel" 49 name = groups[1] 50 } 51 if len(groups) > 2 { 52 category = groups[1] 53 name = groups[2] 54 } 55 if len(groups) > 3 { 56 meta = groups[3] 57 } 58 lastGroup := groups[len(groups)-1] 59 if strings.HasPrefix(lastGroup, "(") && strings.HasSuffix(lastGroup, ")") { 60 isFunction = true 61 } 62 63 var value string 64 65 if expanderFn, foundExpander := expanderRegistry[category]; foundExpander { 66 var err2 error 67 if value, err2 = expanderFn(variable, name, meta, isFunction, ctx); err2 != nil { 68 err = errs.Wrap(err2, "Could not expand %s.%s", category, name) 69 return "" 70 } 71 } else { 72 return variable // we don't control this variable, so leave it as is 73 } 74 75 if value != "" && value != variable { 76 value, err = ctx.ApplyWithMaxDepth(value, depth+1) 77 } 78 return value 79 }) 80 81 return expanded, err 82 } 83 84 // ExpandFromProject searches for $category.name-style variables in the given 85 // string and substitutes them with their contents, derived from the given 86 // project, and subject to the given constraints (if any). 87 func ExpandFromProject(s string, p *Project) (string, error) { 88 return NewExpansion(p).ApplyWithMaxDepth(s, 0) 89 } 90 91 // ExpandFromProjectBashifyPaths is like ExpandFromProject, but bashifies all instances of 92 // $script.name.path(). 93 func ExpandFromProjectBashifyPaths(s string, p *Project) (string, error) { 94 expansion := &Expansion{Project: p, BashifyPaths: true} 95 return expansion.ApplyWithMaxDepth(s, 0) 96 } 97 98 func ExpandFromScript(s string, script *Script) (string, error) { 99 expansion := &Expansion{ 100 Project: script.project, 101 Script: script, 102 BashifyPaths: runtime.GOOS == "windows" && (script.LanguageSafe()[0] == language.Bash || script.LanguageSafe()[0] == language.Sh), 103 } 104 return expansion.ApplyWithMaxDepth(s, 0) 105 } 106 107 // ExpanderFunc defines an Expander function which can expand the name for a category. An Expander expects the name 108 // to be expanded along with the project-file definition. It will return the expanded value of the name 109 // or a Failure if expansion was unsuccessful. 110 type ExpanderFunc func(variable, name, meta string, isFunction bool, ctx *Expansion) (string, error) 111 112 // EventExpander expands events defined in the project-file. 113 func EventExpander(_ string, name string, meta string, isFunction bool, ctx *Expansion) (string, error) { 114 projectFile := ctx.Project.Source() 115 constrained, err := constraints.FilterUnconstrained(pConditional, projectFile.Events.AsConstrainedEntities()) 116 if err != nil { 117 return "", err 118 } 119 for _, v := range constrained { 120 if v.ID() == name { 121 return projectfile.MakeEventsFromConstrainedEntities([]projectfile.ConstrainedEntity{v})[0].Value, nil 122 } 123 } 124 return "", nil 125 } 126 127 // ScriptExpander expands scripts defined in the project-file. 128 func ScriptExpander(_ string, name string, meta string, isFunction bool, ctx *Expansion) (string, error) { 129 script := ctx.Project.ScriptByName(name) 130 if script == nil { 131 return "", nil 132 } 133 134 if !isFunction { 135 return script.Raw(), nil 136 } 137 138 if meta == "path" || meta == "path._posix" { 139 path, err := expandPath(name, script) 140 if err != nil { 141 return "", err 142 } 143 144 if ctx.BashifyPaths || meta == "path._posix" { 145 return osutils.BashifyPath(path) 146 } 147 148 return path, nil 149 } 150 151 return script.Raw(), nil 152 } 153 154 func expandPath(name string, script *Script) (string, error) { 155 if script.cachedFile() != "" { 156 return script.cachedFile(), nil 157 } 158 159 languages := script.LanguageSafe() 160 if len(languages) == 0 { 161 languages = DefaultScriptLanguage() 162 } 163 164 sf, err := scriptfile.NewEmpty(languages[0], name) 165 if err != nil { 166 return "", err 167 } 168 script.setCachedFile(sf.Filename()) 169 170 v, err := script.Value() 171 if err != nil { 172 return "", err 173 } 174 err = sf.Write(v) 175 if err != nil { 176 return "", err 177 } 178 179 return sf.Filename(), nil 180 } 181 182 // userExpander 183 func userExpander(auth *authentication.Auth, element string) string { 184 if element == "name" { 185 return auth.WhoAmI() 186 } 187 if element == "email" { 188 return auth.Email() 189 } 190 if element == "jwt" { 191 return auth.BearerToken() 192 } 193 return "" 194 } 195 196 // Mixin provides expansions that are not sourced from a project file 197 type Mixin struct { 198 auth *authentication.Auth 199 } 200 201 // NewMixin creates a Mixin object providing extra expansions 202 func NewMixin(auth *authentication.Auth) *Mixin { 203 return &Mixin{auth} 204 } 205 206 // Expander expands mixin variables 207 func (m *Mixin) Expander(_ string, name string, meta string, _ bool, _ *Expansion) (string, error) { 208 if name == "user" { 209 return userExpander(m.auth, meta), nil 210 } 211 return "", nil 212 } 213 214 // ConstantExpander expands constants defined in the project-file. 215 func ConstantExpander(_ string, name string, meta string, isFunction bool, ctx *Expansion) (string, error) { 216 projectFile := ctx.Project.Source() 217 constrained, err := constraints.FilterUnconstrained(pConditional, projectFile.Constants.AsConstrainedEntities()) 218 if err != nil { 219 return "", err 220 } 221 for _, v := range constrained { 222 if v.ID() == name { 223 return projectfile.MakeConstantsFromConstrainedEntities([]projectfile.ConstrainedEntity{v})[0].Value, nil 224 } 225 } 226 return "", nil 227 } 228 229 // ProjectExpander expands constants defined in the project-file. 230 func ProjectExpander(_ string, name string, _ string, isFunction bool, ctx *Expansion) (string, error) { 231 if !isFunction { 232 return "", nil 233 } 234 235 project := ctx.Project 236 switch name { 237 case "url": 238 return project.URL(), nil 239 case "commit": 240 commitID := project.LegacyCommitID() // Not using localcommit due to import cycle. See anti-pattern comment in localcommit pkg. 241 return commitID, nil 242 case "branch": 243 return project.BranchName(), nil 244 case "owner": 245 return project.Namespace().Owner, nil 246 case "name": 247 return project.Namespace().Project, nil 248 case "namespace": 249 return project.Namespace().String(), nil 250 case "path": 251 path := project.Source().Path() 252 if path == "" { 253 return path, nil 254 } 255 dir := filepath.Dir(path) 256 if ctx.BashifyPaths { 257 return osutils.BashifyPath(dir) 258 } 259 return dir, nil 260 } 261 262 return "", nil 263 } 264 265 func TopLevelExpander(variable string, name string, _ string, _ bool, ctx *Expansion) (string, error) { 266 projectFile := ctx.Project.Source() 267 switch name { 268 case "project": 269 return projectFile.Project, nil 270 case "lock": 271 return projectFile.Lock, nil 272 } 273 return variable, nil 274 }