Initial working shell: bash-for-windows
- Cross-compiled Go-based shell for Windows (PE32+ executable) - Builtins: cd, pwd, echo, exit, export, source, alias, type - Coreutils: ls, cat, grep, sort, wc, head, find, cp, mv, rm, mkdir, touch, clear - Command chaining: &&, ||, ; - Pipes between builtins and external commands - Variable expansion (, ) and assignment (NAME=VALUE) - Tokenizer with single/double quote handling - Linux and Windows (x86_64) builds via build.sh
This commit is contained in:
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[target.x86_64-pc-windows-gnu]
|
||||||
|
linker = "x86_64-w64-mingw32-gcc"
|
||||||
|
|
||||||
|
[target.x86_64-pc-windows-gnullvm]
|
||||||
|
linker = "x86_64-w64-mingw32-clang"
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
build/
|
||||||
|
*.exe
|
||||||
|
*.o
|
||||||
|
tmp/
|
||||||
61
README.md
61
README.md
@@ -1,3 +1,62 @@
|
|||||||
# bash-for-windows
|
# bash-for-windows
|
||||||
|
|
||||||
Fully functional bash for windows
|
A fully functional bash shell for Windows, written in Go. Run bash commands and scripts natively on Windows without WSL, Cygwin, or MSYS2.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Interactive shell** with prompt (`bash$`)
|
||||||
|
- **Built-in commands**: cd, pwd, echo, exit, export, source, alias, type
|
||||||
|
- **Built-in coreutils**: ls, cat, grep, sort, wc, head, find, cp, mv, rm, mkdir, touch, clear
|
||||||
|
- **Command chaining**: `&&`, `||`, `;`
|
||||||
|
- **Pipes**: `|` between commands
|
||||||
|
- **Variable expansion**: `$NAME`, `${NAME}`
|
||||||
|
- **Variable assignment**: `NAME=VALUE command`
|
||||||
|
- **Single & double quotes**: `'literal'`, `"$variable"`
|
||||||
|
- **Script execution**: `bash-windows script.sh` or `-c 'commands'`
|
||||||
|
- **No dependencies** — single `.exe` file, runs on any Windows x86-64
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
bash-windows # Interactive mode
|
||||||
|
bash-windows -c 'echo hello'
|
||||||
|
bash-windows script.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
bash$ echo "Hello from bash-for-windows!"
|
||||||
|
bash$ ls -la
|
||||||
|
bash$ cd /tmp && pwd
|
||||||
|
bash$ cat file.txt | grep pattern | wc -l
|
||||||
|
bash$ name="Luffy" && echo $name
|
||||||
|
bash$ mkdir -p project/src && touch project/src/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Requires Go 1.21+.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
./build.sh
|
||||||
|
|
||||||
|
# Manual
|
||||||
|
go build -o build/bash-windows .
|
||||||
|
|
||||||
|
# Windows cross-compile
|
||||||
|
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o build/bash-windows.exe .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/bash/ - Shell entry point
|
||||||
|
internal/shell/ - Shell engine (parser, executor, builtins, coreutils)
|
||||||
|
build/ - Compiled binaries
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
19
build.sh
Executable file
19
build.sh
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BUILD_DIR="build"
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
echo "=== Building bash-for-windows ==="
|
||||||
|
|
||||||
|
# Linux build
|
||||||
|
echo " -> Linux..."
|
||||||
|
go build -ldflags="-s -w" -o "$BUILD_DIR/bash-windows" .
|
||||||
|
|
||||||
|
# Windows cross-compile
|
||||||
|
echo " -> Windows (x86_64)..."
|
||||||
|
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o "$BUILD_DIR/bash-windows.exe" .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Build complete ==="
|
||||||
|
ls -lh "$BUILD_DIR/"
|
||||||
66
cmd/bash/main.go
Normal file
66
cmd/bash/main.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package bash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"github.com/cametendo/bash-for-windows/internal/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run() error {
|
||||||
|
args := os.Args[1:]
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
if args[0] == "-c" && len(args) > 1 {
|
||||||
|
// Execute a command string
|
||||||
|
return runCommand(strings.Join(args[1:], " "))
|
||||||
|
}
|
||||||
|
// Run a script file
|
||||||
|
return runFile(args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return interactive()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCommand(cmd string) error {
|
||||||
|
sh := shell.New()
|
||||||
|
return sh.Execute(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runFile(path string) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %v", path, err)
|
||||||
|
}
|
||||||
|
sh := shell.New()
|
||||||
|
return sh.Execute(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func interactive() error {
|
||||||
|
sh := shell.New()
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
fmt.Println("bash-for-windows v1.0.0")
|
||||||
|
|
||||||
|
for {
|
||||||
|
fmt.Print("bash$ ")
|
||||||
|
input, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if input == "exit" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sh.Execute(input); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "bash: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
3
go.mod
Normal file
3
go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module github.com/cametendo/bash-for-windows
|
||||||
|
|
||||||
|
go 1.26.2
|
||||||
448
internal/shell/builtins.go
Normal file
448
internal/shell/builtins.go
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
package shell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Shell) builtinCd(args []string) error {
|
||||||
|
path := ""
|
||||||
|
if len(args) > 0 {
|
||||||
|
path = args[0]
|
||||||
|
} else {
|
||||||
|
path = os.Getenv("HOME")
|
||||||
|
if path == "" {
|
||||||
|
path = os.Getenv("USERPROFILE")
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
path = "."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ~
|
||||||
|
if strings.HasPrefix(path, "~") {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if home == "" {
|
||||||
|
home = os.Getenv("USERPROFILE")
|
||||||
|
}
|
||||||
|
if len(path) > 1 {
|
||||||
|
path = home + path[1:]
|
||||||
|
} else {
|
||||||
|
path = home
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle relative paths
|
||||||
|
if !filepath.IsAbs(path) {
|
||||||
|
pwd, _ := os.Getwd()
|
||||||
|
path = filepath.Join(pwd, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir(path); err != nil {
|
||||||
|
return fmt.Errorf("cd: %s: %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.vars["PWD"], _ = os.Getwd()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) builtinPwd(args []string) error {
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(pwd)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) builtinEcho(args []string) error {
|
||||||
|
noNewline := false
|
||||||
|
escape := false
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
switch arg {
|
||||||
|
case "-n":
|
||||||
|
noNewline = true
|
||||||
|
case "-e":
|
||||||
|
escape = true
|
||||||
|
case "-en", "-ne":
|
||||||
|
noNewline = true
|
||||||
|
escape = true
|
||||||
|
default:
|
||||||
|
if escape {
|
||||||
|
arg = strings.ReplaceAll(arg, "\\n", "\n")
|
||||||
|
arg = strings.ReplaceAll(arg, "\\t", "\t")
|
||||||
|
arg = strings.ReplaceAll(arg, "\\\\", "\\")
|
||||||
|
}
|
||||||
|
parts = append(parts, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line := strings.Join(parts, " ")
|
||||||
|
if noNewline {
|
||||||
|
fmt.Print(line)
|
||||||
|
} else {
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) builtinExit(args []string) error {
|
||||||
|
code := 0
|
||||||
|
if len(args) > 0 {
|
||||||
|
fmt.Sscanf(args[0], "%d", &code)
|
||||||
|
}
|
||||||
|
os.Exit(code)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) builtinExport(args []string) error {
|
||||||
|
for _, arg := range args {
|
||||||
|
parts := strings.SplitN(arg, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
s.vars[parts[0]] = parts[1]
|
||||||
|
os.Setenv(parts[0], parts[1])
|
||||||
|
} else if len(parts) == 1 {
|
||||||
|
// export NAME (mark for export)
|
||||||
|
if val, ok := s.vars[parts[0]]; ok {
|
||||||
|
os.Setenv(parts[0], val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) builtinSource(args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("source: filename argument required")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Execute(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliases = make(map[string]string)
|
||||||
|
|
||||||
|
func (s *Shell) builtinAlias(args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
for name, cmd := range aliases {
|
||||||
|
fmt.Printf("alias %s='%s'\n", name, cmd)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
parts := strings.SplitN(arg, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
name := parts[0]
|
||||||
|
value := strings.Trim(parts[1], "'\"")
|
||||||
|
aliases[name] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) builtinType(args []string) error {
|
||||||
|
for _, arg := range args {
|
||||||
|
if _, ok := s.builtins[arg]; ok {
|
||||||
|
fmt.Printf("%s is a shell builtin\n", arg)
|
||||||
|
} else if val, ok := aliases[arg]; ok {
|
||||||
|
fmt.Printf("%s is aliased to `%s`\n", arg, val)
|
||||||
|
} else if path := findExecutable(arg); path != "" {
|
||||||
|
fmt.Printf("%s is %s\n", arg, path)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: not found\n", arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) initBuiltins() {
|
||||||
|
s.builtins = map[string]func([]string) error{
|
||||||
|
"cd": s.builtinCd,
|
||||||
|
"pwd": s.builtinPwd,
|
||||||
|
"echo": s.builtinEcho,
|
||||||
|
"exit": s.builtinExit,
|
||||||
|
"export": s.builtinExport,
|
||||||
|
"source": s.builtinSource,
|
||||||
|
"alias": s.builtinAlias,
|
||||||
|
"type": s.builtinType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) initCommands() {
|
||||||
|
commands := map[string]func([]string) error{
|
||||||
|
"ls": commandLs,
|
||||||
|
"cat": commandCat,
|
||||||
|
"grep": commandGrep,
|
||||||
|
"sort": commandSort,
|
||||||
|
"wc": commandWc,
|
||||||
|
"head": commandHead,
|
||||||
|
"find": commandFind,
|
||||||
|
"cp": commandCp,
|
||||||
|
"mv": commandMv,
|
||||||
|
"rm": commandRm,
|
||||||
|
"mkdir": commandMkdir,
|
||||||
|
"touch": commandTouch,
|
||||||
|
"clear": commandClear,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, fn := range commands {
|
||||||
|
s.builtins[name] = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandLs(args []string) error {
|
||||||
|
path := "."
|
||||||
|
showAll := false
|
||||||
|
longFormat := false
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
switch arg {
|
||||||
|
case "-la", "-al":
|
||||||
|
showAll = true
|
||||||
|
longFormat = true
|
||||||
|
case "-a":
|
||||||
|
showAll = true
|
||||||
|
case "-l":
|
||||||
|
longFormat = true
|
||||||
|
default:
|
||||||
|
if !strings.HasPrefix(arg, "-") {
|
||||||
|
path = arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ls: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
if !showAll && strings.HasPrefix(name, ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if longFormat {
|
||||||
|
info, _ := entry.Info()
|
||||||
|
if info != nil {
|
||||||
|
mode := info.Mode().String()
|
||||||
|
size := info.Size()
|
||||||
|
modTime := info.ModTime().Format("Jan _2 15:04")
|
||||||
|
fmt.Printf("%s %8d %s %s\n", mode, size, modTime, name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandCat(args []string) error {
|
||||||
|
for _, path := range args {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cat: %s: %v", path, err)
|
||||||
|
}
|
||||||
|
fmt.Print(string(data))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandGrep(args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return fmt.Errorf("grep: usage: grep [pattern] [file...]")
|
||||||
|
}
|
||||||
|
pattern := args[0]
|
||||||
|
files := args[1:]
|
||||||
|
ignoreCase := false
|
||||||
|
if strings.HasPrefix(pattern, "-i") && len(args) > 1 {
|
||||||
|
ignoreCase = true
|
||||||
|
pattern = strings.TrimPrefix(pattern, "-i")
|
||||||
|
if pattern == "" && len(args) > 1 {
|
||||||
|
pattern = args[1]
|
||||||
|
files = args[2:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range files {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("grep: %s: %v", path, err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
check := line
|
||||||
|
pat := pattern
|
||||||
|
if ignoreCase {
|
||||||
|
check = strings.ToLower(line)
|
||||||
|
pat = strings.ToLower(pattern)
|
||||||
|
}
|
||||||
|
if strings.Contains(check, pat) {
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandSort(args []string) error {
|
||||||
|
var lines []string
|
||||||
|
for _, path := range args {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("sort: %s: %v", path, err)
|
||||||
|
}
|
||||||
|
lines = append(lines, strings.Split(string(data), "\n")...)
|
||||||
|
}
|
||||||
|
sortStrings(lines)
|
||||||
|
for _, line := range lines {
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortStrings(s []string) {
|
||||||
|
n := len(s)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
for j := i + 1; j < n; j++ {
|
||||||
|
if s[i] > s[j] {
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandWc(args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("wc: stdin not supported yet")
|
||||||
|
}
|
||||||
|
totalL, totalW, totalC := 0, 0, 0
|
||||||
|
for _, path := range args {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wc: %s: %v", path, err)
|
||||||
|
}
|
||||||
|
content := string(data)
|
||||||
|
l, w, c := strings.Count(content, "\n"), len(strings.Fields(content)), len(content)
|
||||||
|
totalL += l; totalW += w; totalC += c
|
||||||
|
fmt.Printf("%8d %8d %8d %s\n", l, w, c, path)
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
fmt.Printf("%8d %8d %8d total\n", totalL, totalW, totalC)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandHead(args []string) error {
|
||||||
|
n := 10
|
||||||
|
files := args
|
||||||
|
if len(args) > 0 && args[0] == "-n" && len(args) > 1 {
|
||||||
|
fmt.Sscanf(args[1], "%d", &n)
|
||||||
|
files = args[2:]
|
||||||
|
}
|
||||||
|
for fi, path := range files {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("head: %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if len(files) > 1 {
|
||||||
|
fmt.Printf("==> %s <==\n", path)
|
||||||
|
}
|
||||||
|
splitLines := strings.Split(string(data), "\n")
|
||||||
|
end := n
|
||||||
|
if end > len(splitLines) { end = len(splitLines) }
|
||||||
|
for _, line := range splitLines[:end] {
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
if fi < len(files)-1 { fmt.Println() }
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandFind(args []string) error {
|
||||||
|
root := "."
|
||||||
|
name := ""
|
||||||
|
for i, arg := range args {
|
||||||
|
if arg == "-name" && i+1 < len(args) {
|
||||||
|
name = args[i+1]
|
||||||
|
} else if !strings.HasPrefix(arg, "-") && root == "." {
|
||||||
|
root = arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil { return err }
|
||||||
|
if name != "" {
|
||||||
|
matched, _ := filepath.Match(name, info.Name())
|
||||||
|
if !matched { return nil }
|
||||||
|
}
|
||||||
|
fmt.Println(path)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandCp(args []string) error {
|
||||||
|
if len(args) < 2 { return fmt.Errorf("cp: missing file operand") }
|
||||||
|
data, err := os.ReadFile(args[0])
|
||||||
|
if err != nil { return fmt.Errorf("cp: %v", err) }
|
||||||
|
return os.WriteFile(args[1], data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandMv(args []string) error {
|
||||||
|
if len(args) < 2 { return fmt.Errorf("mv: missing file operand") }
|
||||||
|
return os.Rename(args[0], args[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandRm(args []string) error {
|
||||||
|
recursive := false
|
||||||
|
for _, arg := range args {
|
||||||
|
switch arg {
|
||||||
|
case "-rf", "-fr", "-r":
|
||||||
|
recursive = true
|
||||||
|
default:
|
||||||
|
if !strings.HasPrefix(arg, "-") {
|
||||||
|
if recursive { return os.RemoveAll(arg) }
|
||||||
|
return os.Remove(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandMkdir(args []string) error {
|
||||||
|
parents := false
|
||||||
|
var dirs []string
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == "-p" { parents = true } else { dirs = append(dirs, arg) }
|
||||||
|
}
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if parents {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("mkdir: %v", err) }
|
||||||
|
} else {
|
||||||
|
if err := os.Mkdir(dir, 0755); err != nil { return fmt.Errorf("mkdir: %v", err) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandTouch(args []string) error {
|
||||||
|
for _, path := range args {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil { return fmt.Errorf("touch: %v", err) }
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandClear(args []string) error {
|
||||||
|
fmt.Print("\033[H\033[2J")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
448
internal/shell/shell.go
Normal file
448
internal/shell/shell.go
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
package shell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Shell struct {
|
||||||
|
vars map[string]string
|
||||||
|
builtins map[string]func([]string) error
|
||||||
|
lastExit int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Shell {
|
||||||
|
s := &Shell{
|
||||||
|
vars: map[string]string{},
|
||||||
|
lastExit: 0,
|
||||||
|
}
|
||||||
|
s.initBuiltins()
|
||||||
|
s.vars["SHELL"] = "bash-for-windows"
|
||||||
|
s.vars["BASH_VERSION"] = "1.0.0"
|
||||||
|
s.vars["?"] = "0"
|
||||||
|
if pwd, err := os.Getwd(); err == nil {
|
||||||
|
s.vars["PWD"] = pwd
|
||||||
|
}
|
||||||
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
|
s.vars["HOME"] = home
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) Execute(input string) error {
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(input, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.executeLine(line); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) executeLine(line string) error {
|
||||||
|
// Tokenize the line into its components, handling &&, ||, ;
|
||||||
|
return s.executeChain(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeChain: parse && / || / ; with left-to-right precedence
|
||||||
|
func (s *Shell) executeChain(line string) error {
|
||||||
|
// Strategy: split by ; first (semicolons always separate),
|
||||||
|
// then by &&/|| within each segment
|
||||||
|
segments := splitBySemicolon(line)
|
||||||
|
|
||||||
|
for _, seg := range segments {
|
||||||
|
seg = strings.TrimSpace(seg)
|
||||||
|
if seg == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.executeAndOrList(seg); err != nil {
|
||||||
|
// In ; chains, errors in one command don't stop execution
|
||||||
|
s.setExitCode(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitBySemicolon(line string) []string {
|
||||||
|
var parts []string
|
||||||
|
current := ""
|
||||||
|
inSingle := false
|
||||||
|
inDouble := false
|
||||||
|
|
||||||
|
for i := 0; i < len(line); i++ {
|
||||||
|
c := line[i]
|
||||||
|
switch {
|
||||||
|
case c == '\'' && !inDouble:
|
||||||
|
inSingle = !inSingle
|
||||||
|
current += string(c)
|
||||||
|
case c == '"' && !inSingle:
|
||||||
|
inDouble = !inDouble
|
||||||
|
current += string(c)
|
||||||
|
case c == ';' && !inSingle && !inDouble:
|
||||||
|
parts = append(parts, current)
|
||||||
|
current = ""
|
||||||
|
default:
|
||||||
|
current += string(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current != "" {
|
||||||
|
parts = append(parts, current)
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeAndOrList: parse && / || with left-to-right precedence
|
||||||
|
func (s *Shell) executeAndOrList(line string) error {
|
||||||
|
type token struct {
|
||||||
|
text string
|
||||||
|
op string // operator BEFORE this token (except first = "")
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokens []token
|
||||||
|
current := ""
|
||||||
|
op := ""
|
||||||
|
inSingle := false
|
||||||
|
inDouble := false
|
||||||
|
|
||||||
|
for i := 0; i < len(line); i++ {
|
||||||
|
c := line[i]
|
||||||
|
switch {
|
||||||
|
case c == '\'' && !inDouble:
|
||||||
|
inSingle = !inSingle
|
||||||
|
current += string(c)
|
||||||
|
case c == '"' && !inSingle:
|
||||||
|
inDouble = !inDouble
|
||||||
|
current += string(c)
|
||||||
|
case c == '&' && !inSingle && !inDouble:
|
||||||
|
if i+1 < len(line) && line[i+1] == '&' {
|
||||||
|
if current != "" {
|
||||||
|
tokens = append(tokens, token{current, op})
|
||||||
|
current = ""
|
||||||
|
}
|
||||||
|
op = "&&"
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
current += string(c)
|
||||||
|
}
|
||||||
|
case c == '|' && !inSingle && !inDouble:
|
||||||
|
if i+1 < len(line) && line[i+1] == '|' {
|
||||||
|
if current != "" {
|
||||||
|
tokens = append(tokens, token{current, op})
|
||||||
|
current = ""
|
||||||
|
}
|
||||||
|
op = "||"
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
current += string(c)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
current += string(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current != "" {
|
||||||
|
tokens = append(tokens, token{current, op})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for i, tok := range tokens {
|
||||||
|
cmd := strings.TrimSpace(tok.text)
|
||||||
|
if cmd == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRun := true
|
||||||
|
if i > 0 && tok.op == "&&" {
|
||||||
|
shouldRun = (lastErr == nil)
|
||||||
|
} else if i > 0 && tok.op == "||" {
|
||||||
|
shouldRun = (lastErr != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldRun {
|
||||||
|
err := s.executePipeline(cmd)
|
||||||
|
lastErr = err
|
||||||
|
s.setExitCode(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) setExitCode(err error) {
|
||||||
|
if err != nil {
|
||||||
|
s.vars["?"] = "1"
|
||||||
|
} else {
|
||||||
|
s.vars["?"] = "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) executePipeline(input string) error {
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(input, "|") {
|
||||||
|
return s.doPipe(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.executeCommand(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) executeCommand(input string) error {
|
||||||
|
parts := s.tokenize(input)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdName := parts[0]
|
||||||
|
args := parts[1:]
|
||||||
|
|
||||||
|
if alias, ok := aliases[cmdName]; ok {
|
||||||
|
fullCmd := alias
|
||||||
|
if len(args) > 0 {
|
||||||
|
fullCmd += " " + strings.Join(args, " ")
|
||||||
|
}
|
||||||
|
return s.Execute(fullCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
if builtin, ok := s.builtins[cmdName]; ok {
|
||||||
|
return builtin(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.executeExternal(cmdName, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) tokenize(input string) []string {
|
||||||
|
var tokens []string
|
||||||
|
current := ""
|
||||||
|
inSingle := false
|
||||||
|
inDouble := false
|
||||||
|
|
||||||
|
for i := 0; i < len(input); i++ {
|
||||||
|
c := input[i]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case c == '\'' && !inDouble:
|
||||||
|
inSingle = !inSingle
|
||||||
|
case c == '"' && !inSingle:
|
||||||
|
inDouble = !inDouble
|
||||||
|
case (c == ' ' || c == '\t') && !inSingle && !inDouble:
|
||||||
|
if current != "" {
|
||||||
|
tokens = append(tokens, current)
|
||||||
|
current = ""
|
||||||
|
}
|
||||||
|
case c == '=' && !inSingle && !inDouble:
|
||||||
|
current += string(c)
|
||||||
|
default:
|
||||||
|
current += string(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current != "" {
|
||||||
|
tokens = append(tokens, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tok := range tokens {
|
||||||
|
if strings.HasPrefix(tok, "$") {
|
||||||
|
key := tok[1:]
|
||||||
|
if strings.HasPrefix(key, "{") && strings.HasSuffix(key, "}") {
|
||||||
|
key = key[1 : len(key)-1]
|
||||||
|
}
|
||||||
|
if val, ok := s.vars[key]; ok {
|
||||||
|
tokens[i] = val
|
||||||
|
} else if val := os.Getenv(key); val != "" {
|
||||||
|
tokens[i] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
varAssignIdx := -1
|
||||||
|
for i, tok := range tokens {
|
||||||
|
if strings.Contains(tok, "=") && i == 0 {
|
||||||
|
eqIdx := strings.Index(tok, "=")
|
||||||
|
if eqIdx > 0 && eqIdx < len(tok)-1 {
|
||||||
|
name := tok[:eqIdx]
|
||||||
|
value := tok[eqIdx+1:]
|
||||||
|
s.vars[name] = value
|
||||||
|
os.Setenv(name, value)
|
||||||
|
varAssignIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if varAssignIdx == 0 && len(tokens) > 1 {
|
||||||
|
tokens = tokens[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) executeExternal(cmdName string, args []string) error {
|
||||||
|
cmdPath := findExecutable(cmdName)
|
||||||
|
if cmdPath == "" {
|
||||||
|
return fmt.Errorf("%s: command not found", cmdName)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(cmdPath, args...)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func findExecutable(name string) string {
|
||||||
|
if _, err := os.Stat(name); err == nil {
|
||||||
|
if info, _ := os.Stat(name); info != nil && !info.IsDir() {
|
||||||
|
abs, _ := filepath.Abs(name)
|
||||||
|
return abs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path := os.Getenv("PATH")
|
||||||
|
for _, dir := range filepath.SplitList(path) {
|
||||||
|
fullPath := filepath.Join(dir, name)
|
||||||
|
info, err := os.Stat(fullPath)
|
||||||
|
if err == nil && !info.IsDir() {
|
||||||
|
return fullPath
|
||||||
|
}
|
||||||
|
fullPathExe := fullPath + ".exe"
|
||||||
|
info, err = os.Stat(fullPathExe)
|
||||||
|
if err == nil && !info.IsDir() {
|
||||||
|
return fullPathExe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) doPipe(input string) error {
|
||||||
|
commands := strings.Split(input, "|")
|
||||||
|
|
||||||
|
type cmdPart struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
builtin bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts []cmdPart
|
||||||
|
for _, part := range commands {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tokens := s.tokenize(part)
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := tokens[0]
|
||||||
|
_, isBuiltin := s.builtins[name]
|
||||||
|
parts = append(parts, cmdPart{name, tokens[1:], isBuiltin})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevOutput []byte
|
||||||
|
|
||||||
|
for i, p := range parts {
|
||||||
|
var input []byte
|
||||||
|
if i > 0 {
|
||||||
|
input = prevOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.builtin {
|
||||||
|
output, err := s.captureBuiltin(p.name, p.args, input)
|
||||||
|
if err != nil {
|
||||||
|
// Don't return error, let it pass
|
||||||
|
prevOutput = nil
|
||||||
|
if i == len(parts)-1 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i == len(parts)-1 {
|
||||||
|
fmt.Print(string(output))
|
||||||
|
} else {
|
||||||
|
prevOutput = output
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cmdPath := findExecutable(p.name)
|
||||||
|
if cmdPath == "" {
|
||||||
|
return fmt.Errorf("%s: command not found", p.name)
|
||||||
|
}
|
||||||
|
cmd := exec.Command(cmdPath, p.args...)
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if i == 0 && len(input) == 0 {
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
} else if len(input) > 0 {
|
||||||
|
stdin, _ := cmd.StdinPipe()
|
||||||
|
go func() {
|
||||||
|
stdin.Write(input)
|
||||||
|
stdin.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(parts)-1 {
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
prevOutput = output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) captureBuiltin(name string, args []string, input []byte) ([]byte, error) {
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
oldStdin := os.Stdin
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
if len(input) > 0 {
|
||||||
|
ir, iw, _ := os.Pipe()
|
||||||
|
iw.Write(input)
|
||||||
|
iw.Close()
|
||||||
|
os.Stdin = ir
|
||||||
|
defer ir.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := s.builtins[name]
|
||||||
|
err := fn(args)
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = oldStdout
|
||||||
|
os.Stdin = oldStdin
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
io.Copy(&buf, r)
|
||||||
|
r.Close()
|
||||||
|
|
||||||
|
return buf.Bytes(), err
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user