245 lines
6.5 KiB
Go
245 lines
6.5 KiB
Go
package executor
|
||
|
||
import (
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// evalWhen вычисляет when-выражение над hostVars. Грамматика намеренно
|
||
// минимальна и совпадает с тем, что разрешает lint.WhenGrammarRule:
|
||
// сравнения (== != < > <= >=), and/or/not, переменные и литералы.
|
||
// Никаких вызовов функций — если выражение прошло линтер, сюда оно
|
||
// попадает уже гарантированно "чистым".
|
||
//
|
||
// Переменные резолвятся через hostVars сразу в момент разбора атома
|
||
// (parseAtom), а не отложенно — hostVars известен целиком заранее, так
|
||
// что откладывать резолюцию нет причины, а более ранняя версия из-за
|
||
// этого имела баг: compare() сравнивал имя переменной как литерал,
|
||
// а не её значение.
|
||
func evalWhen(expr string, hostVars map[string]interface{}) bool {
|
||
p := &condParser{tokens: tokenize(expr), vars: hostVars}
|
||
v, err := p.parseOr()
|
||
if err != nil {
|
||
// невалидное when — считаем условие ложным и не роняем весь плейбук:
|
||
// синтаксическая проверка уже была на этапе lint, здесь это
|
||
// подстраховка на случай --force поверх ошибки линтера.
|
||
return false
|
||
}
|
||
return toBool(v)
|
||
}
|
||
|
||
type condParser struct {
|
||
tokens []string
|
||
pos int
|
||
vars map[string]interface{}
|
||
}
|
||
|
||
func (p *condParser) peek() string {
|
||
if p.pos >= len(p.tokens) {
|
||
return ""
|
||
}
|
||
return p.tokens[p.pos]
|
||
}
|
||
|
||
func (p *condParser) next() string {
|
||
t := p.peek()
|
||
p.pos++
|
||
return t
|
||
}
|
||
|
||
func (p *condParser) parseOr() (interface{}, error) {
|
||
left, err := p.parseAnd()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for p.peek() == "or" {
|
||
p.next()
|
||
right, err := p.parseAnd()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
left = toBool(left) || toBool(right)
|
||
}
|
||
return left, nil
|
||
}
|
||
|
||
func (p *condParser) parseAnd() (interface{}, error) {
|
||
left, err := p.parseNot()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for p.peek() == "and" {
|
||
p.next()
|
||
right, err := p.parseNot()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
left = toBool(left) && toBool(right)
|
||
}
|
||
return left, nil
|
||
}
|
||
|
||
func (p *condParser) parseNot() (interface{}, error) {
|
||
if p.peek() == "not" {
|
||
p.next()
|
||
v, err := p.parseNot()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return !toBool(v), nil
|
||
}
|
||
return p.parseComparison()
|
||
}
|
||
|
||
func (p *condParser) parseComparison() (interface{}, error) {
|
||
left, err := p.parseAtom()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
op := p.peek()
|
||
switch op {
|
||
case "==", "!=", "<", ">", "<=", ">=":
|
||
p.next()
|
||
right, err := p.parseAtom()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return compare(op, left, right), nil
|
||
}
|
||
return left, nil
|
||
}
|
||
|
||
// parseAtom резолвит переменные сразу через p.vars — идентификатор,
|
||
// не являющийся числом/строкой/true/false, ищется в hostVars и
|
||
// возвращается уже как реальное значение (или nil, если переменной нет).
|
||
func (p *condParser) parseAtom() (interface{}, error) {
|
||
tok := p.next()
|
||
switch {
|
||
case tok == "(":
|
||
v, err := p.parseOr()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if p.next() != ")" {
|
||
return nil, fmt.Errorf("ожидалась закрывающая скобка")
|
||
}
|
||
return v, nil
|
||
case tok == "":
|
||
return nil, fmt.Errorf("неожиданный конец выражения")
|
||
case strings.HasPrefix(tok, `"`) || strings.HasPrefix(tok, "'"):
|
||
return strings.Trim(tok, `"'`), nil
|
||
case isNumber(tok):
|
||
f, _ := strconv.ParseFloat(tok, 64)
|
||
return f, nil
|
||
case tok == "true":
|
||
return true, nil
|
||
case tok == "false":
|
||
return false, nil
|
||
default:
|
||
return p.vars[tok], nil // отсутствующая переменная — nil, не ошибка
|
||
}
|
||
}
|
||
|
||
func compare(op string, left, right interface{}) bool {
|
||
// числовое сравнение, если оба значения — float64
|
||
lf, lok := left.(float64)
|
||
rf, rok := right.(float64)
|
||
if lok && rok {
|
||
switch op {
|
||
case "==":
|
||
return lf == rf
|
||
case "!=":
|
||
return lf != rf
|
||
case "<":
|
||
return lf < rf
|
||
case ">":
|
||
return lf > rf
|
||
case "<=":
|
||
return lf <= rf
|
||
case ">=":
|
||
return lf >= rf
|
||
}
|
||
}
|
||
// булево сравнение, если оба значения — bool (частый случай:
|
||
// `when: clear_cache == true`, где true уже распознан как bool атом)
|
||
lb, lbok := left.(bool)
|
||
rb, rbok := right.(bool)
|
||
if lbok && rbok {
|
||
switch op {
|
||
case "==":
|
||
return lb == rb
|
||
case "!=":
|
||
return lb != rb
|
||
}
|
||
}
|
||
ls := fmt.Sprintf("%v", left)
|
||
rs := fmt.Sprintf("%v", right)
|
||
switch op {
|
||
case "==":
|
||
return ls == rs
|
||
case "!=":
|
||
return ls != rs
|
||
default:
|
||
return ls < rs // строковое < / > — редкий случай, оставлен для полноты
|
||
}
|
||
}
|
||
|
||
// toBool приводит уже резолвленное значение (не varRef — резолюция
|
||
// происходит раньше, в parseAtom) к bool.
|
||
func toBool(v interface{}) bool {
|
||
switch vv := v.(type) {
|
||
case bool:
|
||
return vv
|
||
case string:
|
||
return vv != ""
|
||
case float64:
|
||
return vv != 0
|
||
default:
|
||
return v != nil
|
||
}
|
||
}
|
||
|
||
func isNumber(s string) bool {
|
||
_, err := strconv.ParseFloat(s, 64)
|
||
return err == nil
|
||
}
|
||
|
||
// tokenize — примитивный токенизатор: числа/строки/идентификаторы через
|
||
// пробелы, операторы сравнения и скобки — отдельными токенами.
|
||
func tokenize(expr string) []string {
|
||
var tokens []string
|
||
i := 0
|
||
for i < len(expr) {
|
||
c := expr[i]
|
||
switch {
|
||
case c == ' ' || c == '\t':
|
||
i++
|
||
case c == '(' || c == ')':
|
||
tokens = append(tokens, string(c))
|
||
i++
|
||
case strings.HasPrefix(expr[i:], "=="), strings.HasPrefix(expr[i:], "!="),
|
||
strings.HasPrefix(expr[i:], "<="), strings.HasPrefix(expr[i:], ">="):
|
||
tokens = append(tokens, expr[i:i+2])
|
||
i += 2
|
||
case c == '<' || c == '>':
|
||
tokens = append(tokens, string(c))
|
||
i++
|
||
case c == '"' || c == '\'':
|
||
j := i + 1
|
||
for j < len(expr) && expr[j] != c {
|
||
j++
|
||
}
|
||
tokens = append(tokens, expr[i:j+1])
|
||
i = j + 1
|
||
default:
|
||
j := i
|
||
for j < len(expr) && expr[j] != ' ' && expr[j] != '(' && expr[j] != ')' {
|
||
j++
|
||
}
|
||
tokens = append(tokens, expr[i:j])
|
||
i = j
|
||
}
|
||
}
|
||
return tokens
|
||
}
|