go-shout/parse.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
}