2
3
Fork 0
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

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
}