diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..63f180d --- /dev/null +++ b/src/go.mod @@ -0,0 +1,5 @@ +module github.com/k3y0708/otter + +go 1.19 + +require github.com/carmark/pseudo-terminal-go v0.0.0-20151106093136-5a48ae24c6f5 // indirect diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..cec6881 --- /dev/null +++ b/src/go.sum @@ -0,0 +1,2 @@ +github.com/carmark/pseudo-terminal-go v0.0.0-20151106093136-5a48ae24c6f5 h1:WsxAWarPn1PKpBPSzGCH5qLbcioqdxsXuYIwc+RYz9U= +github.com/carmark/pseudo-terminal-go v0.0.0-20151106093136-5a48ae24c6f5/go.mod h1:8Qkync7rscOMM34525Dcy8RQ/LUCtXt5IagpNsEMRKU= diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..87253ea --- /dev/null +++ b/src/main.go @@ -0,0 +1,282 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "os/user" + "strings" + + "github.com/k3y0708/otter/terminal" +) + +const ( + Prompt = "otter> " + OtterHistory = ".otterhistory" + OtterRC = ".otterrc" +) + +func executeCommand(cmd string, args []string) { + // Execute command + switch cmd { + case "exit": + os.Exit(0) + case "source": + if len(args) == 0 { + os.Stderr.WriteString("source: missing file operand\n") + return + } + + // Check if file exists + if _, err := os.Stat(args[0]); os.IsNotExist(err) { + os.Stderr.WriteString("source: file does not exist\n") + return + } + + sourceFile(args[0]) + default: + //Check if command is a file + cmd, err := findCommand(cmd) + if err != nil { + os.Stderr.WriteString(err.Error() + "\n") + return + } + + // If last argument is &, execute command in background + if len(args) > 0 && args[len(args)-1] == "&" { + // Execute command in background + } else { + out, err := exec.Command(cmd, args...).Output() + if err != nil { + os.Stderr.WriteString(err.Error() + "\n") + } + fmt.Printf("%s", out) + } + } +} + +func findCommand(cmd string) (string, error) { + if _, err := os.Stat(cmd); err == nil { + // Execute file + return cmd, nil + } else { + // Search for command in PATH + path := os.Getenv("PATH") + for _, dir := range strings.Split(path, ":") { + // Check if command is in directory + // If it is, execute it + if _, err := os.Stat(dir + "/" + cmd); err == nil { + return dir + "/" + cmd, nil + } + } + return "", fmt.Errorf("command not found") + } +} + +func replaceEnvVars(str string) string { + + // If string is single quote, return string + if len(str) > 1 && str[0] == '\'' && str[len(str)-1] == '\'' { + return str + } + + // Replace with $HOME if last character is ~ + if len(str) > 0 && str[len(str)-1] == '~' { + userhome, err := os.UserHomeDir() + if err == nil { + str = strings.Replace(str, "~", userhome, -1) + } + } + + // Replace ~ with $HOME if ~ is not followed by a character + if len(str) > 1 && str[0] == '~' && (str[1] < 'a' || str[1] > 'z') && (str[1] < 'A' || str[1] > 'Z') { + userhome, err := os.UserHomeDir() + if err == nil { + str = strings.Replace(str, "~", userhome, 1) + } + } + + // Replace ~user with home directory of user + if len(str) > 2 && str[0] == '~' && (str[1] >= 'a' && str[1] <= 'z' || str[1] >= 'A' && str[1] <= 'Z') { + var username string + for i := 1; i < len(str); i++ { + if str[i] == '/' { + break + } else { + username += string(str[i]) + } + } + usr, err := user.Lookup(username) + if err == nil { + str = strings.Replace(str, "~"+username, usr.HomeDir, 1) + } + } + + // Replace $VAR with value of VAR + // (first char is $, second char is [a-zA-Z_] and rest are [a-zA-Z0-9_]) + for i := 0; i < len(str); i++ { + if str[i] == '$' && str[i+1] != '{' { + var varName string + for j := i + 1; j < len(str); j++ { + if (str[j] < 'a' || str[j] > 'z') && (str[j] < 'A' || str[j] > 'Z') && (str[j] < '0' || str[j] > '9') && str[j] != '_' { + break + } else { + varName += string(str[j]) + } + } + varValue := os.Getenv(varName) + str = strings.Replace(str, "$"+varName, varValue, 1) + } + } + + // Replace ${VAR} with value of VAR + // (first char is $, second char is {, third char is [a-zA-Z_] and rest are [a-zA-Z0-9_], last char is }) + for i := 0; i < len(str); i++ { + if str[i] == '$' && str[i+1] == '{' { + var varName string + for j := i + 2; j < len(str); j++ { + if str[j] == '}' { + break + } else { + varName += string(str[j]) + } + } + varValue := os.Getenv(varName) + str = strings.Replace(str, "${"+varName+"}", varValue, 1) + } + } + + return str +} + +func stringToCommandArgs(str string) (string, []string) { + var cmd string + var args []string + var inSingleQuotes bool + var inDoubleQuotes bool + var currentArg string + + for _, char := range str { + if char == '"' && !inSingleQuotes { + inDoubleQuotes = !inDoubleQuotes + } else if char == '\'' && !inDoubleQuotes { + inSingleQuotes = !inSingleQuotes + } else if char == ' ' && !inSingleQuotes && !inDoubleQuotes { + if cmd == "" { + cmd = currentArg + } else { + args = append(args, currentArg) + } + currentArg = "" + } else { + currentArg += string(char) + } + } + + if cmd == "" { + cmd = currentArg + } else { + args = append(args, currentArg) + } + + for i, arg := range args { + arg = replaceEnvVars(arg) + args[i] = arg + } + return cmd, args +} + +func sourceFile(path string) { + // Panic if file does not exist + if _, err := os.Stat(path); os.IsNotExist(err) { + panic(err) + } + + file, err := os.Open(OtterRC) + if err != nil { + panic(err) + } + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + cmd, args := stringToCommandArgs(scanner.Text()) + executeCommand(cmd, args) + } + file.Close() +} + +func main() { + // Get user home directory + userhome, err := os.UserHomeDir() + if err != nil { + panic(err) + } + otterHistory := userhome + OtterHistory + otterRC := userhome + OtterRC + + term, err := terminal.NewWithStdInOut() + if err != nil { + panic(err) + } + + // If .otterhistory file does not exist, create it else read it + if _, err := os.Stat(otterHistory); os.IsNotExist(err) { + file, err := os.Create(otterHistory) + if err != nil { + panic(err) + } + file.Close() + } else { + file, err := os.Open(otterHistory) + if err != nil { + panic(err) + } + scanner := bufio.NewScanner(file) + for scanner.Scan() { + term.AddToHistory(scanner.Text()) + } + file.Close() + } + + // Welcome message + fmt.Println("Welcome to Otter 🦦 Shell!") + term.SetPrompt(Prompt) + + // If .otterrc file does not exist, create it else read it + if _, err := os.Stat(otterRC); os.IsNotExist(err) { + file, err := os.Create(otterRC) + if err != nil { + panic(err) + } + file.Close() + } else { + sourceFile(otterRC) + } + + line, err := term.ReadLine() + + for { + // If command is EOF (but not empty string), exit + if err == io.EOF { + term.Write([]byte(line)) + fmt.Println() + return + } + if !((err != nil && strings.Contains(err.Error(), "control-c break")) || len(line) == 0) { + // Add command to .otterhistory file + file, err := os.OpenFile(otterHistory, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + panic(err) + } + file.WriteString(line + "\n") + file.Close() + + // Parse command + cmd, args := stringToCommandArgs(line) + executeCommand(cmd, args) + } + line, err = term.ReadLine() + } +} diff --git a/src/maths/main.go b/src/maths/main.go new file mode 100644 index 0000000..1c5a397 --- /dev/null +++ b/src/maths/main.go @@ -0,0 +1,15 @@ +package maths + +func Max(i, j int) int { + if i > j { + return i + } + return j +} + +func Min(i, j int) int { + if i < j { + return i + } + return j +} diff --git a/src/terminal/terminal.go b/src/terminal/terminal.go index b99d5a0..787fbc7 100644 --- a/src/terminal/terminal.go +++ b/src/terminal/terminal.go @@ -1,7 +1,3 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - package terminal import ( @@ -9,27 +5,15 @@ import ( "io" "os" "sync" + + "github.com/k3y0708/otter/maths" ) -func max(i, j int) int { - if i > j { - return i - } - return j -} - -func min(i, j int) int { - if i < j { - return i - } - return j -} - // historyIdxValue returns an index into a valid range of history func historyIdxValue(idx int, history [][]byte) int { out := idx - out = min(len(history), out) - out = max(0, out) + out = maths.Min(len(history), out) + out = maths.Max(0, out) return out } @@ -457,7 +441,7 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) { if t.echo { t.writeLine(t.line[t.pos-1:]) } - t.pos ++ + t.pos++ t.moveCursorToPos(t.pos) t.queue([]byte("\r\n")) t.line = make([]byte, 0, 0) @@ -603,6 +587,13 @@ func (t *Terminal) ReadLine() (line string, err error) { return t.readLine() } +func (t *Terminal) AddToHistory(line string) { + b := []byte(line) + h := make([]byte, len(b)) + copy(h, b) + t.history = append(t.history, h) +} + func (t *Terminal) readLine() (line string, err error) { // t.lock must be held at this point @@ -642,10 +633,7 @@ func (t *Terminal) readLine() (line string, err error) { if lineOk { if t.echo { //&& len(line) > 0 { // don't put passwords into history... - b := []byte(line) - h := make([]byte, len(b)) - copy(h, b) - t.history = append(t.history, h) + t.AddToHistory(line) } return } @@ -683,22 +671,6 @@ func (t *Terminal) SetSize(width, height int) { t.termWidth, t.termHeight = width, height } -func (t *Terminal) SetHistory(h []string) { - // t.history = make([][]byte, len(h)) - // for i := range h { - // t.history[i] = []byte(h[i]) - // } - // // t.historyIdx = len(h) -} - -func (t *Terminal) GetHistory() (h []string) { - // h = make([]string, len(t.history)) - // for i := range t.history { - // h[i] = string(t.history[i]) - // } - return -} - type shell struct { r io.Reader w io.Writer