131 lines
3.3 KiB
Go
131 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func stripComment(line string) string {
|
|
inSingle := false
|
|
inDouble := false
|
|
for i := 0; i < len(line); i++ {
|
|
ch := line[i]
|
|
switch {
|
|
case ch == '\\' && !inSingle:
|
|
i++ // skip escaped character (backslash escapes in double-quoted and unquoted contexts)
|
|
case ch == '\'' && !inDouble:
|
|
inSingle = !inSingle
|
|
case ch == '"' && !inSingle:
|
|
inDouble = !inDouble
|
|
case ch == '#' && !inSingle && !inDouble:
|
|
return strings.TrimRight(line[:i], " \t")
|
|
}
|
|
}
|
|
return line
|
|
}
|
|
|
|
func parseExitCode(lines []string) ([]string, ExitCodeType, int) {
|
|
if len(lines) == 0 {
|
|
return lines, ExitCodeNone, 0
|
|
}
|
|
|
|
last := lines[len(lines)-1]
|
|
if len(last) < 3 || last[0] != '[' || last[len(last)-1] != ']' {
|
|
return lines, ExitCodeNone, 0
|
|
}
|
|
|
|
inner := last[1 : len(last)-1]
|
|
if inner == "*" {
|
|
return lines[:len(lines)-1], ExitCodeWildcard, 0
|
|
}
|
|
|
|
code, err := strconv.Atoi(inner)
|
|
if err != nil {
|
|
return lines, ExitCodeNone, 0
|
|
}
|
|
return lines[:len(lines)-1], ExitCodeExact, code
|
|
}
|
|
|
|
func trimTrailingEmpty(lines []string) []string {
|
|
end := len(lines)
|
|
for end > 0 && lines[end-1] == "" {
|
|
end--
|
|
}
|
|
return lines[:end]
|
|
}
|
|
|
|
func parse(path, content string) (ShoutFile, error) {
|
|
rawLines := strings.Split(content, "\n")
|
|
|
|
// Remove trailing newline
|
|
if len(rawLines) > 0 && rawLines[len(rawLines)-1] == "" {
|
|
rawLines = rawLines[:len(rawLines)-1]
|
|
}
|
|
|
|
var commands []Command
|
|
var directives []Directive
|
|
var current *Command
|
|
seenCommand := false
|
|
|
|
for i, line := range rawLines {
|
|
lineNum := i + 1
|
|
|
|
if !seenCommand && strings.HasPrefix(line, "@") {
|
|
if strings.HasPrefix(line, "@setup ") {
|
|
setupPath := strings.TrimSpace(line[7:])
|
|
if setupPath == "" {
|
|
return ShoutFile{}, fmt.Errorf("%s:%d: @setup requires a file path", path, lineNum)
|
|
}
|
|
directives = append(directives, Directive{
|
|
Type: "setup", Path: setupPath, Line: lineNum,
|
|
})
|
|
} else if strings.HasPrefix(line, "@env ") {
|
|
rest := strings.TrimSpace(line[5:])
|
|
eq := strings.Index(rest, "=")
|
|
if eq <= 0 {
|
|
return ShoutFile{}, fmt.Errorf("%s:%d: malformed @env directive (expected KEY=VALUE): %s", path, lineNum, line)
|
|
}
|
|
directives = append(directives, Directive{
|
|
Type: "env", Key: rest[:eq], Value: rest[eq+1:], Line: lineNum,
|
|
})
|
|
} else {
|
|
return ShoutFile{}, fmt.Errorf("%s:%d: unknown directive: %s", path, lineNum, line)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(line, "$ ") {
|
|
seenCommand = true
|
|
if current != nil {
|
|
trimmed := trimTrailingEmpty(current.Expected)
|
|
remaining, ecType, ecVal := parseExitCode(trimmed)
|
|
current.Expected = trimTrailingEmpty(remaining)
|
|
current.ExitCodeType = ecType
|
|
current.ExitCodeValue = ecVal
|
|
commands = append(commands, *current)
|
|
}
|
|
|
|
current = &Command{
|
|
Line: lineNum,
|
|
Raw: line,
|
|
Cmd: stripComment(line[2:]),
|
|
Expected: nil,
|
|
}
|
|
} else if current != nil {
|
|
current.Expected = append(current.Expected, line)
|
|
}
|
|
}
|
|
|
|
if current != nil {
|
|
trimmed := trimTrailingEmpty(current.Expected)
|
|
remaining, ecType, ecVal := parseExitCode(trimmed)
|
|
current.Expected = trimTrailingEmpty(remaining)
|
|
current.ExitCodeType = ecType
|
|
current.ExitCodeValue = ecVal
|
|
commands = append(commands, *current)
|
|
}
|
|
|
|
return ShoutFile{Path: path, Commands: commands, Directives: directives}, nil
|
|
}
|