WIP: Waifufetch by JGH0 working in windows-bash
This commit is contained in:
@@ -24,6 +24,8 @@ func (e exitCodeErr) Error() string { return "" }
|
||||
|
||||
type Shell struct {
|
||||
vars map[string]string
|
||||
arrays map[string][]string
|
||||
namerefs map[string]string
|
||||
builtins map[string]func([]string) error
|
||||
funcs map[string]string // function name → body
|
||||
lastExit int
|
||||
@@ -31,15 +33,20 @@ type Shell struct {
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
args []string
|
||||
errexit bool
|
||||
nounset bool
|
||||
pipefail bool
|
||||
}
|
||||
|
||||
func New() *Shell {
|
||||
s := &Shell{
|
||||
vars: map[string]string{},
|
||||
funcs: map[string]string{},
|
||||
Stdin: os.Stdin,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
vars: map[string]string{},
|
||||
arrays: map[string][]string{},
|
||||
namerefs: map[string]string{},
|
||||
funcs: map[string]string{},
|
||||
Stdin: os.Stdin,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
}
|
||||
s.initBuiltins()
|
||||
s.vars["SHELL"] = "bash-for-windows"
|
||||
@@ -58,6 +65,38 @@ func New() *Shell {
|
||||
return s
|
||||
}
|
||||
|
||||
// resolveNR resolves a variable name through the nameref chain (circular-ref safe).
|
||||
func (s *Shell) resolveNR(name string) string {
|
||||
seen := map[string]bool{}
|
||||
for {
|
||||
if seen[name] {
|
||||
return name
|
||||
}
|
||||
seen[name] = true
|
||||
target, ok := s.namerefs[name]
|
||||
if !ok {
|
||||
return name
|
||||
}
|
||||
name = target
|
||||
}
|
||||
}
|
||||
|
||||
// getArray returns array (resolving namerefs).
|
||||
func (s *Shell) getArray(name string) []string {
|
||||
return s.arrays[s.resolveNR(name)]
|
||||
}
|
||||
|
||||
// setArray sets array (resolving namerefs).
|
||||
func (s *Shell) setArray(name string, vals []string) {
|
||||
s.arrays[s.resolveNR(name)] = vals
|
||||
}
|
||||
|
||||
// appendArray appends to array (resolving namerefs).
|
||||
func (s *Shell) appendArray(name string, vals []string) {
|
||||
n := s.resolveNR(name)
|
||||
s.arrays[n] = append(s.arrays[n], vals...)
|
||||
}
|
||||
|
||||
func (s *Shell) SetArgs(args []string) {
|
||||
s.args = args
|
||||
s.vars["#"] = fmt.Sprintf("%d", len(args))
|
||||
@@ -81,7 +120,12 @@ func (s *Shell) SetVar(name, value string) {
|
||||
|
||||
// Execute runs commands from the given input string.
|
||||
func (s *Shell) Execute(input string) error {
|
||||
// Normalize CRLF to LF
|
||||
input = strings.ReplaceAll(input, "\r\n", "\n")
|
||||
input = strings.ReplaceAll(input, "\r", "\n")
|
||||
input = strings.ReplaceAll(input, "\\\n", " ")
|
||||
// Pre-process heredocs
|
||||
input = preprocessHeredocs(input)
|
||||
blocks := parseBlocks(input)
|
||||
for _, block := range blocks {
|
||||
if err := s.executeBlock(block); err != nil {
|
||||
@@ -119,7 +163,7 @@ func IsIncomplete(input string) bool {
|
||||
for _, stmt := range stmts {
|
||||
w := firstWord(stmt)
|
||||
switch w {
|
||||
case "if", "for", "while", "until":
|
||||
case "if", "for", "while", "until", "case":
|
||||
depth++
|
||||
case "fi", "done", "esac":
|
||||
depth--
|
||||
@@ -162,7 +206,7 @@ func parseBlocks(input string) []string {
|
||||
funcKwDepth = 0
|
||||
for _, p := range splitStatements(stmt[braceIdx+1:]) {
|
||||
switch firstWord(p) {
|
||||
case "if", "for", "while", "until":
|
||||
case "if", "for", "while", "until", "case":
|
||||
funcKwDepth++
|
||||
case "fi", "done", "esac":
|
||||
funcKwDepth--
|
||||
@@ -182,7 +226,7 @@ func parseBlocks(input string) []string {
|
||||
}
|
||||
|
||||
switch w {
|
||||
case "if", "for", "while", "until":
|
||||
case "if", "for", "while", "until", "case":
|
||||
kwDepth++
|
||||
}
|
||||
kwDepth += embeddedKwDepth(stmt)
|
||||
@@ -211,7 +255,7 @@ func parseBlocks(input string) []string {
|
||||
continue
|
||||
}
|
||||
switch w {
|
||||
case "if", "for", "while", "until":
|
||||
case "if", "for", "while", "until", "case":
|
||||
funcKwDepth++
|
||||
case "fi", "done", "esac":
|
||||
funcKwDepth--
|
||||
@@ -246,23 +290,178 @@ func embeddedKwDepth(stmt string) int {
|
||||
return delta
|
||||
}
|
||||
|
||||
// splitStatements splits input on semicolons and newlines, respecting quotes.
|
||||
// preprocessHeredocs converts heredoc syntax (<<MARKER) to temp-file redirects.
|
||||
// It processes the input line by line, detecting <<MARKER, collecting the body,
|
||||
// writing it to a temp file, and replacing <<MARKER with < /tmp/file.
|
||||
func preprocessHeredocs(input string) string {
|
||||
lines := strings.Split(input, "\n")
|
||||
var result []string
|
||||
i := 0
|
||||
for i < len(lines) {
|
||||
line := lines[i]
|
||||
// Check for heredoc marker in this line: <<MARKER or <<-MARKER or <<"MARKER" or <<'MARKER'
|
||||
// Find all heredoc markers on this line (there could be multiple)
|
||||
processed, markers := parseHeredocMarkers(line)
|
||||
if len(markers) == 0 {
|
||||
result = append(result, line)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// Collect heredoc bodies for each marker
|
||||
i++
|
||||
newLine := processed
|
||||
for _, marker := range markers {
|
||||
stripTabs := strings.HasPrefix(marker, "-")
|
||||
if stripTabs {
|
||||
marker = marker[1:]
|
||||
}
|
||||
// Strip quotes from marker
|
||||
quotedMarker := marker
|
||||
if len(marker) >= 2 && ((marker[0] == '\'' && marker[len(marker)-1] == '\'') ||
|
||||
(marker[0] == '"' && marker[len(marker)-1] == '"')) {
|
||||
marker = marker[1 : len(marker)-1]
|
||||
}
|
||||
_ = quotedMarker
|
||||
// Collect body until marker
|
||||
var bodyLines []string
|
||||
for i < len(lines) {
|
||||
bodyLine := lines[i]
|
||||
check := bodyLine
|
||||
if stripTabs {
|
||||
check = strings.TrimLeft(check, "\t")
|
||||
}
|
||||
if strings.TrimRight(check, "\r") == marker {
|
||||
i++
|
||||
break
|
||||
}
|
||||
bodyLines = append(bodyLines, bodyLine)
|
||||
i++
|
||||
}
|
||||
// Write to temp file
|
||||
content := strings.Join(bodyLines, "\n")
|
||||
if len(bodyLines) > 0 {
|
||||
content += "\n"
|
||||
}
|
||||
f, err := os.CreateTemp("", "heredoc*")
|
||||
if err == nil {
|
||||
f.WriteString(content)
|
||||
f.Close()
|
||||
newLine += " < " + f.Name()
|
||||
}
|
||||
}
|
||||
result = append(result, newLine)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
// parseHeredocMarkers finds <<MARKER patterns in a line and returns the line with
|
||||
// the heredoc redirects removed (replaced), plus the list of markers found.
|
||||
func parseHeredocMarkers(line string) (string, []string) {
|
||||
var markers []string
|
||||
var out strings.Builder
|
||||
i := 0
|
||||
inSingle := false
|
||||
inDouble := false
|
||||
|
||||
for i < len(line) {
|
||||
c := line[i]
|
||||
switch {
|
||||
case c == '\'' && !inDouble:
|
||||
inSingle = !inSingle
|
||||
out.WriteByte(c)
|
||||
i++
|
||||
case c == '"' && !inSingle:
|
||||
inDouble = !inDouble
|
||||
out.WriteByte(c)
|
||||
i++
|
||||
case c == '<' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '<':
|
||||
// Possible heredoc
|
||||
i += 2 // skip <<
|
||||
stripTabs := false
|
||||
if i < len(line) && line[i] == '-' {
|
||||
stripTabs = true
|
||||
i++
|
||||
}
|
||||
// Skip spaces after <<
|
||||
for i < len(line) && line[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
// Read the marker (may be quoted)
|
||||
marker := ""
|
||||
if i < len(line) && (line[i] == '\'' || line[i] == '"') {
|
||||
quote := line[i]
|
||||
i++
|
||||
for i < len(line) && line[i] != quote {
|
||||
marker += string(line[i])
|
||||
i++
|
||||
}
|
||||
if i < len(line) {
|
||||
i++ // skip closing quote
|
||||
}
|
||||
} else {
|
||||
// Unquoted marker
|
||||
for i < len(line) && line[i] != ' ' && line[i] != '\t' && line[i] != ';' && line[i] != '|' && line[i] != '&' && line[i] != '>' && line[i] != '<' {
|
||||
marker += string(line[i])
|
||||
i++
|
||||
}
|
||||
}
|
||||
if marker != "" {
|
||||
if stripTabs {
|
||||
markers = append(markers, "-"+marker)
|
||||
} else {
|
||||
markers = append(markers, marker)
|
||||
}
|
||||
// Don't write the <<MARKER to output; it will be replaced by < file
|
||||
}
|
||||
default:
|
||||
out.WriteByte(c)
|
||||
i++
|
||||
}
|
||||
}
|
||||
return out.String(), markers
|
||||
}
|
||||
|
||||
// splitStatements splits input on semicolons and newlines, respecting quotes and ((...)).
|
||||
// Double semicolons ;; are preserved as a single token (for case statements).
|
||||
func splitStatements(input string) []string {
|
||||
var result []string
|
||||
current := strings.Builder{}
|
||||
inSingle := false
|
||||
inDouble := false
|
||||
parenDepth := 0
|
||||
|
||||
for i := 0; i < len(input); i++ {
|
||||
c := input[i]
|
||||
switch {
|
||||
case c == '\'' && !inDouble:
|
||||
case c == '\'' && !inDouble && parenDepth == 0:
|
||||
inSingle = !inSingle
|
||||
current.WriteByte(c)
|
||||
case c == '"' && !inSingle:
|
||||
case c == '"' && !inSingle && parenDepth == 0:
|
||||
inDouble = !inDouble
|
||||
current.WriteByte(c)
|
||||
case (c == ';' || c == '\n') && !inSingle && !inDouble:
|
||||
case c == '(' && !inSingle && !inDouble:
|
||||
parenDepth++
|
||||
current.WriteByte(c)
|
||||
case c == ')' && !inSingle && !inDouble && parenDepth > 0:
|
||||
parenDepth--
|
||||
current.WriteByte(c)
|
||||
case c == ';' && !inSingle && !inDouble && parenDepth == 0:
|
||||
if i+1 < len(input) && input[i+1] == ';' {
|
||||
// Double semicolon — flush current token, then emit ";;" as a token
|
||||
if s := strings.TrimSpace(current.String()); s != "" {
|
||||
result = append(result, s)
|
||||
}
|
||||
current.Reset()
|
||||
result = append(result, ";;")
|
||||
i++ // skip second ;
|
||||
} else {
|
||||
// Single semicolon — just a statement separator
|
||||
if s := strings.TrimSpace(current.String()); s != "" {
|
||||
result = append(result, s)
|
||||
}
|
||||
current.Reset()
|
||||
}
|
||||
case c == '\n' && !inSingle && !inDouble && parenDepth == 0:
|
||||
if s := strings.TrimSpace(current.String()); s != "" {
|
||||
result = append(result, s)
|
||||
}
|
||||
@@ -338,11 +537,18 @@ func (s *Shell) executeBlock(block string) error {
|
||||
case "if":
|
||||
return s.executeIf(block)
|
||||
case "for":
|
||||
// Check for C-style for (( ... ))
|
||||
trimmed := strings.TrimSpace(block)
|
||||
if strings.HasPrefix(trimmed, "for ((") || strings.HasPrefix(trimmed, "for((") {
|
||||
return s.executeForC(block)
|
||||
}
|
||||
return s.executeFor(block)
|
||||
case "while":
|
||||
return s.executeWhileUntil(block, false)
|
||||
case "until":
|
||||
return s.executeWhileUntil(block, true)
|
||||
case "case":
|
||||
return s.executeCase(block)
|
||||
}
|
||||
if isFuncDefStart(block) {
|
||||
return s.defineFunction(block)
|
||||
@@ -418,6 +624,7 @@ func (s *Shell) executeAndOrList(line string) error {
|
||||
op := ""
|
||||
inSingle := false
|
||||
inDouble := false
|
||||
dbDepth := 0 // double-bracket [[ depth
|
||||
|
||||
for i := 0; i < len(line); i++ {
|
||||
c := line[i]
|
||||
@@ -428,12 +635,22 @@ func (s *Shell) executeAndOrList(line string) error {
|
||||
case c == '"' && !inSingle:
|
||||
inDouble = !inDouble
|
||||
current.WriteByte(c)
|
||||
case c == '&' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '&':
|
||||
case c == '[' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '[':
|
||||
dbDepth++
|
||||
current.WriteByte(c)
|
||||
current.WriteByte(line[i+1])
|
||||
i++
|
||||
case c == ']' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == ']' && dbDepth > 0:
|
||||
dbDepth--
|
||||
current.WriteByte(c)
|
||||
current.WriteByte(line[i+1])
|
||||
i++
|
||||
case c == '&' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '&' && dbDepth == 0:
|
||||
tokens = append(tokens, tok{current.String(), op})
|
||||
current.Reset()
|
||||
op = "&&"
|
||||
i++
|
||||
case c == '|' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '|':
|
||||
case c == '|' && !inSingle && !inDouble && i+1 < len(line) && line[i+1] == '|' && dbDepth == 0:
|
||||
tokens = append(tokens, tok{current.String(), op})
|
||||
current.Reset()
|
||||
op = "||"
|
||||
|
||||
Reference in New Issue
Block a user