✨ Begin of otter 🦦
This commit is contained in:
5
src/go.mod
Normal file
5
src/go.mod
Normal file
@@ -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
|
||||||
2
src/go.sum
Normal file
2
src/go.sum
Normal file
@@ -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=
|
||||||
282
src/main.go
Normal file
282
src/main.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/maths/main.go
Normal file
15
src/maths/main.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -9,27 +5,15 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"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
|
// historyIdxValue returns an index into a valid range of history
|
||||||
func historyIdxValue(idx int, history [][]byte) int {
|
func historyIdxValue(idx int, history [][]byte) int {
|
||||||
out := idx
|
out := idx
|
||||||
out = min(len(history), out)
|
out = maths.Min(len(history), out)
|
||||||
out = max(0, out)
|
out = maths.Max(0, out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,7 +441,7 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) {
|
|||||||
if t.echo {
|
if t.echo {
|
||||||
t.writeLine(t.line[t.pos-1:])
|
t.writeLine(t.line[t.pos-1:])
|
||||||
}
|
}
|
||||||
t.pos ++
|
t.pos++
|
||||||
t.moveCursorToPos(t.pos)
|
t.moveCursorToPos(t.pos)
|
||||||
t.queue([]byte("\r\n"))
|
t.queue([]byte("\r\n"))
|
||||||
t.line = make([]byte, 0, 0)
|
t.line = make([]byte, 0, 0)
|
||||||
@@ -603,6 +587,13 @@ func (t *Terminal) ReadLine() (line string, err error) {
|
|||||||
return t.readLine()
|
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) {
|
func (t *Terminal) readLine() (line string, err error) {
|
||||||
// t.lock must be held at this point
|
// t.lock must be held at this point
|
||||||
|
|
||||||
@@ -642,10 +633,7 @@ func (t *Terminal) readLine() (line string, err error) {
|
|||||||
if lineOk {
|
if lineOk {
|
||||||
if t.echo { //&& len(line) > 0 {
|
if t.echo { //&& len(line) > 0 {
|
||||||
// don't put passwords into history...
|
// don't put passwords into history...
|
||||||
b := []byte(line)
|
t.AddToHistory(line)
|
||||||
h := make([]byte, len(b))
|
|
||||||
copy(h, b)
|
|
||||||
t.history = append(t.history, h)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -683,22 +671,6 @@ func (t *Terminal) SetSize(width, height int) {
|
|||||||
t.termWidth, t.termHeight = width, height
|
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 {
|
type shell struct {
|
||||||
r io.Reader
|
r io.Reader
|
||||||
w io.Writer
|
w io.Writer
|
||||||
|
|||||||
Reference in New Issue
Block a user