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 }