cli/azd/pkg/cmdsubst/cmdsubst.go (54 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package cmdsubst
import (
"context"
"fmt"
"regexp"
"strings"
)
type CommandExecutor interface {
// Returns true + replacement string if a command is recognized and runs successfully.
// Returns false and no error if the command was not recognized by the executor, or in other "no-op" cases.
Run(ctx context.Context, commandName string, args []string) (bool, string, error)
}
// This package is designed to be used in the context of ARM parameter file templates,
// which are relatively simple, JSON-format documents.
// The package relies on regular expressions and does not include a full parser.
//
// The command invocation is dollar sign, followed by open parenthesis and optional whitespace
// followed by command name (word chars), followed by optional argument list
// (word chars, whitespace, and dashes), followed by closing parenthesis.
var commandInvocationRegex = regexp.MustCompile(`\$\(\s*(\w+)([\w\s-]*)\)`)
// Similar to commandInvocationRegex, this format string is used to construct a regular expression
// that tests whether specific command is being invoked by a given document.
// We are looking for dollar sign, followed by open parenthesis and optional whitespace
// followed by command name (which will be filled in by the fmt.Sprintf() call),
// followed by optional argument list (word chars, whitespace, and dashes), followed by closing parenthesis.
const commandInvocationFmt = "\\$\\(\\s*%s[\\w\\s-]*\\)"
const (
wholeMatchStart = 0
wholeMatchEnd = 1
commandNameStart = 2
commandNameEnd = 3
argsStart = 4
argsEnd = 5
)
// Eval replaces all occurrences of bash-like command output substitution $(command arg1 arg2 ...)
// with the result provided by the command executor.
// Any error from the command executor will result in an error reported from Eval().
func Eval(ctx context.Context, input string, cmd CommandExecutor) (string, error) {
var sb strings.Builder
allMatches := commandInvocationRegex.FindAllStringSubmatchIndex(input, -1)
if len(allMatches) == 0 {
return input, nil // No substitution necessary
}
contentBeforeMatchIndex := 0
for _, match := range allMatches {
// Write "content before match"
sb.WriteString(input[contentBeforeMatchIndex:match[wholeMatchStart]])
contentBeforeMatchIndex = match[wholeMatchEnd]
// Extract invocation data and call evaluator
commandName := input[match[commandNameStart]:match[commandNameEnd]]
argumentStr := input[match[argsStart]:match[argsEnd]]
args := strings.Fields(strings.TrimSpace(argumentStr))
ran, result, err := cmd.Run(ctx, commandName, args)
if err != nil {
return "", err
} else if ran {
// Successful substitution
sb.WriteString(result)
} else {
// Unrecognized command--write original content in and continue
sb.WriteString(input[match[wholeMatchStart]:match[wholeMatchEnd]])
}
}
// Write content after last match
restStartIndex := allMatches[len(allMatches)-1][wholeMatchEnd]
sb.WriteString(input[restStartIndex:])
return sb.String(), nil
}
// Returns true if the document 'doc' contains an invocation of a command.
func ContainsCommandInvocation(doc, commandName string) bool {
if len(commandName) == 0 || len(doc) == 0 {
return false
}
regexStr := fmt.Sprintf(commandInvocationFmt, commandName)
commandInvocationRegex := regexp.MustCompile(regexStr)
return commandInvocationRegex.MatchString(doc)
}