diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..5ab61d8 --- /dev/null +++ b/.cargo/config.toml @@ -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" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af80efe --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +*.exe +*.o +tmp/ diff --git a/README.md b/README.md index 25dbde2..6ac1acf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,62 @@ # bash-for-windows -Fully functional bash for windows \ No newline at end of file +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 diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..70ffc13 --- /dev/null +++ b/build.sh @@ -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/" diff --git a/cmd/bash/main.go b/cmd/bash/main.go new file mode 100644 index 0000000..b01505b --- /dev/null +++ b/cmd/bash/main.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4d79f81 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/cametendo/bash-for-windows + +go 1.26.2 diff --git a/internal/shell/builtins.go b/internal/shell/builtins.go new file mode 100644 index 0000000..35b451c --- /dev/null +++ b/internal/shell/builtins.go @@ -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 +} diff --git a/internal/shell/shell.go b/internal/shell/shell.go new file mode 100644 index 0000000..0e5cfb1 --- /dev/null +++ b/internal/shell/shell.go @@ -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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..2f123b5 --- /dev/null +++ b/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + "os" + "github.com/cametendo/bash-for-windows/cmd/bash" +) + +func main() { + if err := bash.Run(); err != nil { + fmt.Fprintf(os.Stderr, "bash: %v\n", err) + os.Exit(1) + } +}