A simple shell with simple goals
https://git.rawtext.club/sloum/slosh
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
486 lines
12 KiB
486 lines
12 KiB
package main |
|
|
|
import ( |
|
"bytes" |
|
"fmt" |
|
"os" |
|
"os/exec" |
|
"path/filepath" |
|
"strconv" |
|
"strings" |
|
"unicode" |
|
) |
|
|
|
const ( |
|
singleString int = iota |
|
doubleString |
|
comElement |
|
sysPath |
|
pipeLine |
|
andJoin |
|
fileWrite |
|
fileAppend |
|
) |
|
|
|
var CurrentProcess *exec.Cmd = nil |
|
|
|
type CommandSession struct { |
|
CommandList []CommandGroup |
|
} |
|
|
|
func (cs CommandSession) Execute() { |
|
var err error |
|
for i, cg := range cs.CommandList { |
|
if len(cg.commands) > 1 { |
|
err = cg.ExecutePipes() |
|
} else { |
|
err = cg.Execute() |
|
} |
|
if i < len(cs.CommandList) && (err != nil || os.Getenv("?") != "0") { |
|
break |
|
} |
|
} |
|
slosh_vars = map[string]string{} |
|
if err != nil { |
|
fmt.Fprintf(os.Stderr, "%s\n", strings.TrimSpace(err.Error())) |
|
} |
|
} |
|
|
|
type CommandGroup struct { |
|
hasFileRedirect bool |
|
redirectType int |
|
RedirectTo string |
|
commands []*exec.Cmd |
|
} |
|
|
|
func (cg *CommandGroup) AddCommand(c *exec.Cmd) { |
|
if c == nil { |
|
return |
|
} |
|
cg.commands = append(cg.commands, c) |
|
} |
|
|
|
func (cg CommandGroup) ExecutePipes() error { |
|
var out bytes.Buffer |
|
var errOut bytes.Buffer |
|
|
|
environ := os.Environ() |
|
// Add any local vars |
|
for i := range cg.commands { |
|
cg.commands[i].Env = append(cg.commands[0].Env, environ...) |
|
for k, v := range slosh_vars { |
|
cg.commands[i].Env = append(cg.commands[0].Env, fmt.Sprintf("%s=%s", k, v)) |
|
} |
|
} |
|
|
|
if initialTerm != nil { |
|
initialTerm.ApplyMode() |
|
} |
|
err := Execute(&out, &errOut, cg.hasFileRedirect, cg.commands...) |
|
if linerTerm != nil { |
|
linerTerm.ApplyMode() |
|
} |
|
if cg.hasFileRedirect { |
|
var outfile *os.File |
|
if cg.redirectType == fileAppend { |
|
outfile, err = os.OpenFile(cg.RedirectTo, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) |
|
} else { |
|
outfile, err = os.Create(cg.RedirectTo) |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
defer outfile.Close() |
|
if out.Len() > 0 { |
|
outfile.Write(out.Bytes()) |
|
} |
|
if errOut.Len() > 0 { |
|
outfile.Write(errOut.Bytes()) |
|
} |
|
} else { |
|
fmt.Print(out.String()) |
|
e := errOut.String() |
|
if len(e) > 0 { |
|
fmt.Print(e) |
|
} else if err != nil { |
|
fmt.Print(err.Error()) |
|
} |
|
} |
|
|
|
return err |
|
} |
|
|
|
func (cg CommandGroup) Execute() error { |
|
if len(cg.commands) == 0 { |
|
return fmt.Errorf("Shell error") |
|
} |
|
|
|
var err error |
|
|
|
if cg.commands[0].Path == "slosh-special" { |
|
if len(cg.commands[0].Args) > 1 { |
|
switch cg.commands[0].Args[1] { |
|
case "cd": |
|
var p string |
|
if len(cg.commands[0].Args) <= 2 { |
|
p, _ = os.UserHomeDir() |
|
} else { |
|
p = cg.commands[0].Args[2] |
|
} |
|
err = Dir(p) |
|
case "up": |
|
if len(cg.commands[0].Args) <= 2 { |
|
err = nil |
|
} else { |
|
err = Up(cg.commands[0].Args[1:]) |
|
} |
|
case "alias": |
|
err = MakeAlias(cg.commands[0].Args[1:]) |
|
case "set": |
|
err = SetEnv(cg.commands[0].Args[1:]) |
|
case "let": |
|
err = SetLocal(cg.commands[0].Args[1:]) |
|
case "unset": |
|
if len(cg.commands[0].Args) == 2 { |
|
err = fmt.Errorf("No arguments provided") |
|
} else { |
|
err = UnsetEnv(cg.commands[0].Args[2]) |
|
} |
|
case "unalias": |
|
if len(cg.commands[0].Args) == 2 { |
|
err = fmt.Errorf("No arguments provided") |
|
} else { |
|
err = RemoveAlias(cg.commands[0].Args[2]) |
|
} |
|
case "lsf": |
|
err = LoadSloshFiles() |
|
default: |
|
err = fmt.Errorf("Unknown builtin call\n") |
|
} |
|
if err == nil { |
|
os.Setenv("?", "0") |
|
} else { |
|
os.Setenv("?", "1") |
|
} |
|
return err |
|
} else { |
|
return fmt.Errorf("Invalid call to builtin\n") |
|
} |
|
} |
|
|
|
if initialTerm != nil { |
|
initialTerm.ApplyMode() |
|
} |
|
|
|
cg.commands[0].Stdin = os.Stdin |
|
if cg.hasFileRedirect && cg.RedirectTo != "" { |
|
var outfile *os.File |
|
if cg.redirectType == fileAppend { |
|
outfile, err = os.OpenFile(cg.RedirectTo, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) |
|
} else { |
|
outfile, err = os.Create(cg.RedirectTo) |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
defer outfile.Close() |
|
cg.commands[0].Stdout = outfile |
|
cg.commands[0].Stderr = outfile |
|
} else { |
|
cg.commands[0].Stdout = os.Stdout |
|
cg.commands[0].Stderr = os.Stderr |
|
} |
|
|
|
// Add any local vars |
|
cg.commands[0].Env = append(cg.commands[0].Env, os.Environ()...) |
|
for k, v := range slosh_vars { |
|
cg.commands[0].Env = append(cg.commands[0].Env, fmt.Sprintf("%s=%s", k, v)) |
|
} |
|
|
|
CurrentProcess = cg.commands[0] |
|
err = cg.commands[0].Run() |
|
code := cg.commands[0].ProcessState.ExitCode() |
|
os.Setenv("?", strconv.Itoa(code)) |
|
CurrentProcess = nil |
|
|
|
if linerTerm != nil { |
|
linerTerm.ApplyMode() |
|
} |
|
return err |
|
} |
|
|
|
type CommandLine struct { |
|
value []CommandElement |
|
} |
|
|
|
func (cl CommandLine) String() string { |
|
var out strings.Builder |
|
for i, c := range cl.value { |
|
if c.kind == singleString { |
|
out.WriteString(fmt.Sprintf("'%s'", c.value)) |
|
} else if c.kind == doubleString { |
|
out.WriteString(fmt.Sprintf("%q", c.value)) |
|
} else { |
|
out.WriteString(c.value) |
|
} |
|
if i < len(cl.value) - 1 { |
|
out.WriteRune(' ') |
|
} |
|
} |
|
return out.String() |
|
} |
|
|
|
func (cl *CommandLine) ExpandVars() { |
|
for i, c := range cl.value { |
|
if c.kind != singleString { |
|
cl.value[i].value = os.ExpandEnv(c.value) |
|
} |
|
} |
|
} |
|
|
|
func (cl CommandLine) Command() *exec.Cmd { |
|
// Expand all variables |
|
cl.ExpandVars() |
|
|
|
out := make([]string, 0, len(cl.value)) |
|
for _, c := range cl.value { |
|
out = append(out, c.value) |
|
} |
|
if len(out) == 0 { |
|
return nil |
|
} |
|
if cl.value[0].kind == sysPath { |
|
f, err := os.Stat(cl.value[0].value) |
|
if err != nil || f.IsDir() || f.Mode()&0111 == 0 { |
|
return exec.Command("slosh-special", "cd", out[0]) |
|
} |
|
} else if (cl.value[0].value == "cd" || cl.value[0].value == "dir") && len(cl.value[0].value) > 1 { |
|
return exec.Command("slosh-special", out...) |
|
} else if cl.value[0].value == "up" { |
|
arg := "" |
|
if len(out) > 1 { |
|
arg = out[1] |
|
} |
|
return exec.Command("slosh-special", "up", arg) |
|
} else if cl.value[0].value == "set" || cl.value[0].value == "unset" || cl.value[0].value == "let" { |
|
return exec.Command("slosh-special", out...) |
|
} else if cl.value[0].value == "alias" || cl.value[0].value == "unalias" || cl.value[0].value == "lsf" { |
|
return exec.Command("slosh-special", out...) |
|
} |
|
|
|
return exec.Command(out[0], out[1:]...) |
|
} |
|
|
|
func ParseCommandLine(ln string) (CommandSession, []string) { |
|
rawOut := make([]string, 0, 5) |
|
|
|
comSession := CommandSession{} |
|
comGroup := CommandGroup{} |
|
comLine := CommandLine{} |
|
|
|
var prev rune = 0 |
|
var reader strings.Builder |
|
skippingSpace := false |
|
inDoubleString := false |
|
inSingleString := false |
|
expectingFile := false |
|
|
|
for _, ch := range ln { |
|
if ch == '\\' { |
|
prev = ch |
|
continue |
|
} |
|
|
|
if !skippingSpace && unicode.IsSpace(ch) && (!inSingleString && !inDoubleString) { |
|
skippingSpace = true |
|
if reader.Len() > 0 { |
|
val := reader.String() |
|
ce := CommandElement{value: val} |
|
kind := ce.InferKind() |
|
switch kind { |
|
case andJoin: |
|
if len(comLine.value) > 0 { |
|
comGroup.AddCommand(comLine.Command()) |
|
} |
|
comSession.CommandList = append(comSession.CommandList, comGroup) |
|
comGroup = CommandGroup{} |
|
comLine = CommandLine{} |
|
case pipeLine: |
|
comGroup.AddCommand(comLine.Command()) |
|
comLine = CommandLine{} |
|
case fileWrite, fileAppend: |
|
comGroup.AddCommand(comLine.Command()) |
|
expectingFile = true |
|
comGroup.hasFileRedirect = true |
|
comGroup.redirectType = kind |
|
comLine = CommandLine{} |
|
default: |
|
if expectingFile { |
|
expectingFile = false |
|
comGroup.RedirectTo = val |
|
} else { |
|
if val, ok := alias[ce.value]; ok && ce.kind == comElement { |
|
// if len(comLine.value) < 2 && len(comLine.value) > 0 && (comLine.value[0].value == "alias" || comLine.value[0].value == "unalias" || comLine.value[0].value == "unset" || comLine.value[0].value == "set" || comLine.value[0].value == "let") { |
|
if len(comLine.value) > 0 { |
|
comLine.value = append(comLine.value, ce) |
|
} else { |
|
comLine.value = append(comLine.value, val.value...) |
|
} |
|
} else if ce.kind == sysPath { |
|
ce.value = ExpandedAbsFilepath(ce.value) |
|
ce.value = os.ExpandEnv(ce.value) |
|
globs, e := filepath.Glob(ce.value) |
|
if e != nil || len(globs) == 0 { |
|
comLine.value = append(comLine.value, ce) |
|
} |
|
globCESlice := make([]CommandElement, len(globs)) |
|
for i := range globs { |
|
globCESlice[i] = CommandElement{sysPath, globs[i]} |
|
} |
|
comLine.value = append(comLine.value, globCESlice...) |
|
} else { |
|
comLine.value = append(comLine.value, ce) |
|
} |
|
} |
|
} |
|
rawOut = append(rawOut, val) |
|
reader.Reset() |
|
} |
|
prev = ch |
|
continue |
|
} |
|
|
|
if skippingSpace && unicode.IsSpace(ch) && (!inSingleString && !inDoubleString) { |
|
continue |
|
} |
|
|
|
if unicode.IsSpace(ch) && (!inSingleString && !inDoubleString) && prev != '\\' { |
|
continue |
|
} |
|
|
|
if unicode.IsSpace(ch) && (!inSingleString && !inDoubleString) && prev == '\\' { |
|
reader.WriteRune(' ') |
|
} else if ch == '"' && (!inSingleString && !inDoubleString) { |
|
inDoubleString = true |
|
} else if ch == '\'' && (!inSingleString && !inDoubleString) { |
|
inSingleString = true |
|
} else if ch =='"' && prev == '\\' { |
|
reader.WriteRune('"') |
|
} else if ch =='\'' && prev == '\\' { |
|
reader.WriteRune('\'') |
|
} else if inDoubleString && ch == '"' { |
|
s := reader.String() |
|
rawOut = append(rawOut, s) |
|
inDoubleString = false |
|
comLine.value = append(comLine.value, CommandElement{doubleString, s}) |
|
reader.Reset() |
|
} else if inSingleString && ch == '\'' { |
|
s := reader.String() |
|
rawOut = append(rawOut, s) |
|
inSingleString = false |
|
comLine.value = append(comLine.value, CommandElement{singleString, s}) |
|
reader.Reset() |
|
} else { |
|
skippingSpace = false |
|
reader.WriteRune(ch) |
|
} |
|
|
|
prev = ch |
|
} |
|
if reader.Len() > 0 { |
|
val := reader.String() |
|
ce := CommandElement{value: val} |
|
var kind int |
|
if inDoubleString { |
|
kind = doubleString |
|
} else if inSingleString { |
|
kind = singleString |
|
} else { |
|
kind = ce.InferKind() |
|
} |
|
switch kind { |
|
case andJoin: |
|
if len(comLine.value) > 0 { |
|
comGroup.AddCommand(comLine.Command()) |
|
} |
|
comSession.CommandList = append(comSession.CommandList, comGroup) |
|
comGroup = CommandGroup{} |
|
comLine = CommandLine{} |
|
case pipeLine: |
|
comGroup.AddCommand(comLine.Command()) |
|
comLine = CommandLine{} |
|
case fileWrite, fileAppend: |
|
comGroup.AddCommand(comLine.Command()) |
|
expectingFile = true |
|
comGroup.hasFileRedirect = true |
|
comGroup.redirectType = kind |
|
comLine = CommandLine{} |
|
default: |
|
if expectingFile { |
|
expectingFile = false |
|
comGroup.RedirectTo = val |
|
} else { |
|
if val, ok := alias[ce.value]; ok && ce.kind == comElement { |
|
if len(comLine.value) > 0 { |
|
comLine.value = append(comLine.value, ce) |
|
} else { |
|
comLine.value = append(comLine.value, val.value...) |
|
} |
|
} else if ce.kind == sysPath { |
|
ce.value = ExpandedAbsFilepath(ce.value) |
|
ce.value = os.ExpandEnv(ce.value) |
|
globs, e := filepath.Glob(ce.value) |
|
if e != nil || len(globs) == 0 { |
|
comLine.value = append(comLine.value, ce) |
|
} |
|
globCESlice := make([]CommandElement, len(globs)) |
|
for i := range globs { |
|
globCESlice[i] = CommandElement{sysPath, globs[i]} |
|
} |
|
comLine.value = append(comLine.value, globCESlice...) |
|
} else { |
|
comLine.value = append(comLine.value, ce) |
|
} |
|
} |
|
comGroup.AddCommand(comLine.Command()) |
|
} |
|
rawOut = append(rawOut, val) |
|
} else if len(comLine.value) > 0 { |
|
comGroup.AddCommand(comLine.Command()) |
|
} |
|
reader.Reset() |
|
|
|
comSession.CommandList = append(comSession.CommandList, comGroup) |
|
|
|
return comSession, rawOut |
|
} |
|
|
|
type CommandElement struct { |
|
kind int |
|
value string |
|
} |
|
|
|
func (ce CommandElement) String() string { |
|
return ce.value |
|
} |
|
|
|
func (ce *CommandElement) InferKind() int { |
|
if isFilepath(ce.value) { |
|
ce.kind = sysPath |
|
return ce.kind |
|
} |
|
switch ce.value { |
|
case ">": |
|
ce.kind = fileWrite |
|
case ">>": |
|
ce.kind = fileAppend |
|
case "&&": |
|
ce.kind = andJoin |
|
case "|": |
|
ce.kind = pipeLine |
|
default: |
|
ce.kind = comElement |
|
} |
|
return ce.kind |
|
} |
|
|
|
|