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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user