9 "github.com/charmbracelet/crush/internal/env"
10 "github.com/charmbracelet/crush/internal/shell"
13type VariableResolver interface {
14 ResolveValue(value string) (string, error)
18 Exec(ctx context.Context, command string) (stdout, stderr string, err error)
21type shellVariableResolver struct {
26func NewShellVariableResolver(env env.Env) VariableResolver {
27 return &shellVariableResolver{
29 shell: shell.NewShell(
37// ResolveValue is a method for resolving values, such as environment variables.
38// it will resolve shell-like variable substitution anywhere in the string, including:
39// - $(command) for command substitution
40// - $VAR or ${VAR} for environment variables
41func (r *shellVariableResolver) ResolveValue(value string) (string, error) {
42 // Special case: lone $ is an error (backward compatibility)
44 return "", fmt.Errorf("invalid value format: %s", value)
47 // If no $ found, return as-is
48 if !strings.Contains(value, "$") {
54 // Handle command substitution: $(command)
56 start := strings.Index(result, "$(")
61 // Find matching closing parenthesis
64 for i := start + 2; i < len(result); i++ {
67 } else if result[i] == ')' {
77 return "", fmt.Errorf("unmatched $( in value: %s", value)
80 command := result[start+2 : end]
81 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
83 stdout, _, err := r.shell.Exec(ctx, command)
86 return "", fmt.Errorf("command execution failed for '%s': %w", command, err)
89 // Replace the $(command) with the output
90 replacement := strings.TrimSpace(stdout)
91 result = result[:start] + replacement + result[end+1:]
94 // Handle environment variables: $VAR and ${VAR}
97 start := strings.Index(result[searchStart:], "$")
101 start += searchStart // Adjust for the offset
103 // Skip if this is part of $( which we already handled
104 if start+1 < len(result) && result[start+1] == '(' {
105 // Skip past this $(...)
106 searchStart = start + 1
112 if start+1 < len(result) && result[start+1] == '{' {
113 // Handle ${VAR} format
114 closeIdx := strings.Index(result[start+2:], "}")
116 return "", fmt.Errorf("unmatched ${ in value: %s", value)
118 varName = result[start+2 : start+2+closeIdx]
119 end = start + 2 + closeIdx + 1
121 // Handle $VAR format - variable names must start with letter or underscore
122 if start+1 >= len(result) {
123 return "", fmt.Errorf("incomplete variable reference at end of string: %s", value)
126 if result[start+1] != '_' &&
127 (result[start+1] < 'a' || result[start+1] > 'z') &&
128 (result[start+1] < 'A' || result[start+1] > 'Z') {
129 return "", fmt.Errorf("invalid variable name starting with '%c' in: %s", result[start+1], value)
133 for end < len(result) && (result[end] == '_' ||
134 (result[end] >= 'a' && result[end] <= 'z') ||
135 (result[end] >= 'A' && result[end] <= 'Z') ||
136 (result[end] >= '0' && result[end] <= '9')) {
139 varName = result[start+1 : end]
142 envValue := r.env.Get(varName)
144 return "", fmt.Errorf("environment variable %q not set", varName)
147 result = result[:start] + envValue + result[end:]
148 searchStart = start + len(envValue) // Continue searching after the replacement
154type environmentVariableResolver struct {
158func NewEnvironmentVariableResolver(env env.Env) VariableResolver {
159 return &environmentVariableResolver{
164// ResolveValue resolves environment variables from the provided env.Env.
165func (r *environmentVariableResolver) ResolveValue(value string) (string, error) {
166 if !strings.HasPrefix(value, "$") {
170 varName := strings.TrimPrefix(value, "$")
171 resolvedValue := r.env.Get(varName)
172 if resolvedValue == "" {
173 return "", fmt.Errorf("environment variable %q not set", varName)
175 return resolvedValue, nil
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list