WIP: Waifufetch by JGH0 working in windows-bash

This commit is contained in:
Cametendo
2026-05-28 21:00:34 +02:00
parent 114cbf43bd
commit 7b3a101946
8 changed files with 1354 additions and 105 deletions

View File

@@ -60,7 +60,17 @@ func (s *Shell) expandWord(word string) string {
i += 2
}
} else {
result.WriteByte(next)
// Only consume the backslash when escaping a shell
// metacharacter. Before regular path characters (letters,
// digits, etc.) keep it literal so Windows paths like
// C:\workspace work unquoted.
const metachars = " \t\n$*?[\"'\\|&;()<>{}!#~`"
if strings.ContainsRune(metachars, rune(next)) {
result.WriteByte(next)
} else {
result.WriteByte('\\')
result.WriteByte(next)
}
i += 2
}
} else {
@@ -72,6 +82,82 @@ func (s *Shell) expandWord(word string) string {
result.WriteByte('$')
break
}
// $'...' ANSI C string
if word[i] == '\'' {
i++ // skip opening '
for i < len(word) && word[i] != '\'' {
if word[i] == '\\' && i+1 < len(word) {
i++ // skip backslash, now at escape char
switch word[i] {
case 'n':
result.WriteByte('\n')
case 't':
result.WriteByte('\t')
case 'r':
result.WriteByte('\r')
case '\\':
result.WriteByte('\\')
case '\'':
result.WriteByte('\'')
case '"':
result.WriteByte('"')
case 'a':
result.WriteByte('\a')
case 'b':
result.WriteByte('\b')
case 'f':
result.WriteByte('\f')
case 'v':
result.WriteByte('\v')
case 'e', 'E':
result.WriteByte(0x1b)
case '0', '1', '2', '3', '4', '5', '6', '7':
// Octal \NNN — up to 3 digits
oct := 0
for k := 0; k < 3 && i < len(word) && word[i] >= '0' && word[i] <= '7'; k++ {
oct = oct*8 + int(word[i]-'0')
i++
}
result.WriteByte(byte(oct))
continue
case 'x':
// Hex \xNN — up to 2 digits
i++ // skip 'x'
hexv := 0
for k := 0; k < 2 && i < len(word); k++ {
d := word[i]
if d >= '0' && d <= '9' {
hexv = hexv*16 + int(d-'0')
i++
} else if d >= 'a' && d <= 'f' {
hexv = hexv*16 + int(d-'a'+10)
i++
} else if d >= 'A' && d <= 'F' {
hexv = hexv*16 + int(d-'A'+10)
i++
} else {
break
}
}
result.WriteByte(byte(hexv))
continue
default:
result.WriteByte('\\')
result.WriteByte(word[i])
}
i++
} else {
result.WriteByte(word[i])
i++
}
}
if i < len(word) {
i++ // skip closing '
}
continue
}
switch word[i] {
case '(':
if i+1 < len(word) && word[i+1] == '(' {
@@ -146,7 +232,7 @@ func (s *Shell) expandWord(word string) string {
result.WriteString(s.vars["#"])
i++
case '@':
result.WriteString(s.vars["@"])
result.WriteString(strings.Join(s.args, "\x01"))
i++
case '*':
result.WriteString(s.vars["*"])
@@ -172,17 +258,52 @@ func (s *Shell) expandWord(word string) string {
}
func (s *Shell) getVar(name string) string {
if v, ok := s.vars[name]; ok {
// Resolve nameref chain
resolved := s.resolveNR(name)
if v, ok := s.vars[resolved]; ok {
return v
}
return os.Getenv(name)
return os.Getenv(resolved)
}
func (s *Shell) evalVarExpr(expr string) string {
// ${#VAR} — string length
// ${#arr[@]} or ${#arr[*]} → length of array
if strings.HasPrefix(expr, "#") {
return strconv.Itoa(len(s.getVar(expr[1:])))
rest := expr[1:]
if strings.HasSuffix(rest, "[@]") || strings.HasSuffix(rest, "[*]") {
arrName := rest[:len(rest)-3]
return strconv.Itoa(len(s.getArray(arrName)))
}
// ${#VAR} — string length
return strconv.Itoa(len(s.getVar(rest)))
}
// Array indexing: ${arr[@]}, ${arr[*]}, ${arr[N]}
if bracketIdx := strings.Index(expr, "["); bracketIdx >= 0 && strings.HasSuffix(expr, "]") {
// Make sure there's no operator before the bracket
prefix := expr[:bracketIdx]
hasOp := strings.ContainsAny(prefix, ":-:=:+%#")
if !hasOp {
arrName := prefix
idx := expr[bracketIdx+1 : len(expr)-1]
if idx == "@" {
arr := s.getArray(arrName)
return strings.Join(arr, "\x01")
}
if idx == "*" {
arr := s.getArray(arrName)
return strings.Join(arr, " ")
}
// Numeric index
n := s.evalArith(idx)
arr := s.getArray(arrName)
if n >= 0 && n < len(arr) {
return arr[n]
}
return ""
}
}
// ${VAR:-default}
if idx := strings.Index(expr, ":-"); idx >= 0 {
varName := expr[:idx]
@@ -285,7 +406,48 @@ func evalArithExpr(expr string) int {
if strings.HasPrefix(expr, "(") && strings.HasSuffix(expr, ")") {
return evalArithExpr(expr[1 : len(expr)-1])
}
// Operators in precedence order (lowest first so we split on last occurrence)
// Comparison operators (lowest precedence) — multi-char first
for _, op := range []string{"<=", ">=", "==", "!=", "<", ">"} {
if idx := findBinaryOpStr(expr, op); idx >= 0 {
left := evalArithExpr(expr[:idx])
right := evalArithExpr(expr[idx+len(op):])
switch op {
case "<":
if left < right {
return 1
}
return 0
case ">":
if left > right {
return 1
}
return 0
case "<=":
if left <= right {
return 1
}
return 0
case ">=":
if left >= right {
return 1
}
return 0
case "==":
if left == right {
return 1
}
return 0
case "!=":
if left != right {
return 1
}
return 0
}
}
}
// Arithmetic operators in precedence order (lowest first so we split on last occurrence)
for _, op := range []string{"+", "-", "*", "/", "%"} {
if idx := findBinaryOp(expr, op); idx >= 0 {
left := evalArithExpr(expr[:idx])
@@ -313,6 +475,39 @@ func evalArithExpr(expr string) int {
return 0
}
// findBinaryOpStr finds the rightmost occurrence of a multi-character binary operator.
func findBinaryOpStr(expr, op string) int {
depth := 0
// Search right-to-left
for i := len(expr) - len(op); i >= 0; i-- {
switch expr[i] {
case ')':
depth++
case '(':
depth--
}
if depth != 0 {
continue
}
if expr[i:i+len(op)] == op {
// Make sure it's not part of a longer operator
// e.g. don't match < in <=
if len(op) == 1 {
// For < and >, make sure next char is not =
if i+1 < len(expr) && (expr[i+1] == '=' || (op == "<" && expr[i+1] == '<') || (op == ">" && expr[i+1] == '>')) {
continue
}
}
// For single char ops, make sure previous char is not the same op (e.g. << or >>)
if len(op) == 1 && i > 0 && expr[i-1] == expr[i] {
continue
}
return i
}
}
return -1
}
func findBinaryOp(expr, op string) int {
depth := 0
for i := len(expr) - 1; i >= 0; i-- {
@@ -353,7 +548,7 @@ func (s *Shell) tokenize(input string) []string {
current := strings.Builder{}
inSingle := false
inDouble := false
parenDepth := 0 // nesting depth inside $(...) or $((...))
parenDepth := 0 // nesting depth inside $(...) or $((...))
pendingDollar := false // true after $ when next char is (
wasQuoted := false
@@ -420,7 +615,7 @@ doneTokenizing:
if eqIdx := strings.Index(clean, "="); eqIdx > 0 {
name := clean[:eqIdx]
if isValidIdentifier(name) && !strings.Contains(clean[:eqIdx], "$") {
value := s.expandWord(clean[eqIdx+1:])
value := strings.ReplaceAll(s.expandWord(clean[eqIdx+1:]), "\x01", " ")
s.vars[name] = value
os.Setenv(name, value)
rawTokens = rawTokens[1:]
@@ -438,6 +633,16 @@ doneTokenizing:
tok = tok[2:]
}
expanded := s.expandWord(tok)
// Handle multi-word expansion from $@ and ${arr[@]}
if strings.Contains(expanded, "\x01") {
parts := strings.Split(expanded, "\x01")
for _, p := range parts {
if p != "" {
result = append(result, p)
}
}
continue
}
if !quoted && strings.ContainsAny(expanded, "*?[") {
result = append(result, s.expandGlob(expanded)...)
} else {