/ components / execd / pkg / util / pathutil / path.go
path.go
  1  // Copyright 2026 Alibaba Group Holding Ltd.
  2  //
  3  // Licensed under the Apache License, Version 2.0 (the "License");
  4  // you may not use this file except in compliance with the License.
  5  // You may obtain a copy of the License at
  6  //
  7  //     http://www.apache.org/licenses/LICENSE-2.0
  8  //
  9  // Unless required by applicable law or agreed to in writing, software
 10  // distributed under the License is distributed on an "AS IS" BASIS,
 11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  // See the License for the specific language governing permissions and
 13  // limitations under the License.
 14  
 15  package pathutil
 16  
 17  import (
 18  	"fmt"
 19  	"os"
 20  	"path/filepath"
 21  	"regexp"
 22  	"sort"
 23  	"strings"
 24  )
 25  
 26  var envVarPattern = regexp.MustCompile(`\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)`)
 27  
 28  func envMapFromProcessAndOverrides(envOverrides map[string]string) map[string]string {
 29  	out := make(map[string]string, len(envOverrides)+16)
 30  	for _, kv := range os.Environ() {
 31  		parts := strings.SplitN(kv, "=", 2)
 32  		if len(parts) != 2 {
 33  			continue
 34  		}
 35  		out[parts[0]] = parts[1]
 36  	}
 37  	for k, v := range envOverrides {
 38  		out[k] = v
 39  	}
 40  	return out
 41  }
 42  
 43  func validateEnvVars(path string, env map[string]string) error {
 44  	matches := envVarPattern.FindAllStringSubmatch(path, -1)
 45  	if len(matches) == 0 {
 46  		return nil
 47  	}
 48  
 49  	missingSet := make(map[string]struct{})
 50  	for _, m := range matches {
 51  		name := m[1]
 52  		if name == "" {
 53  			name = m[2]
 54  		}
 55  		if _, ok := env[name]; !ok {
 56  			missingSet[name] = struct{}{}
 57  		}
 58  	}
 59  	if len(missingSet) == 0 {
 60  		return nil
 61  	}
 62  
 63  	missing := make([]string, 0, len(missingSet))
 64  	for name := range missingSet {
 65  		missing = append(missing, name)
 66  	}
 67  	sort.Strings(missing)
 68  	return fmt.Errorf("path references undefined environment variables: %s", strings.Join(missing, ","))
 69  }
 70  
 71  // ExpandPathWithEnv expands environment variables and a leading "~" to user home.
 72  // Environment resolution uses process env overlaid by envOverrides.
 73  func ExpandPathWithEnv(path string, envOverrides map[string]string) (string, error) {
 74  	if path == "" {
 75  		return "", nil
 76  	}
 77  	env := envMapFromProcessAndOverrides(envOverrides)
 78  	if err := validateEnvVars(path, env); err != nil {
 79  		return "", err
 80  	}
 81  
 82  	expanded := os.Expand(path, func(key string) string {
 83  		return env[key]
 84  	})
 85  	if expanded == "~" || strings.HasPrefix(expanded, "~/") || strings.HasPrefix(expanded, `~\`) {
 86  		home, err := os.UserHomeDir()
 87  		if err != nil {
 88  			return "", err
 89  		}
 90  		if expanded == "~" {
 91  			return home, nil
 92  		}
 93  		return filepath.Join(home, expanded[2:]), nil
 94  	}
 95  
 96  	return expanded, nil
 97  }
 98  
 99  // ExpandPath expands environment variables and a leading "~" to user home.
100  // It supports "~", "~/" and "~\" prefixes.
101  func ExpandPath(path string) (string, error) {
102  	return ExpandPathWithEnv(path, nil)
103  }
104  
105  func ExpandAbsPath(path string) (string, error) {
106  	expanded, err := ExpandPathWithEnv(path, nil)
107  	if err != nil {
108  		return "", err
109  	}
110  	return filepath.Abs(expanded)
111  }