2
3
Fork 0
Browse Source

Initial commit of mostly working shell

master
sloum 1 year ago
commit
1c168b6c51
  1. 2
      .gitignore
  2. 54
      builtins.go
  3. 410
      commandRunner.go
  4. 5
      go.mod
  5. 4
      go.sum
  6. 188
      main.go
  7. 34
      rc.go
  8. 129
      utils.go

2
.gitignore vendored

@ -0,0 +1,2 @@
slosh
roadmap.md

54
builtins.go

@ -0,0 +1,54 @@
package main
import (
"fmt"
"os"
"strconv"
"strings"
)
func Dir(in string) error {
p := ExpandedAbsFilepath(in)
return os.Chdir(p)
}
func Up(in []string) error {
switch len(in) {
case 0:
return nil
case 1:
return os.Chdir("..")
case 2:
num, err := strconv.Atoi(in[1])
if err != nil || num < 0 || num > 999 {
return fmt.Errorf("Invalid argument %q", in[1])
}
return os.Chdir(strings.Repeat("../", num))
default:
return fmt.Errorf("Too many arguments. Expected 1, got %d", len(in)-1)
}
}
func MakeAlias(a []string) error {
if len(a) < 2 {
return fmt.Errorf("Invalida arguments, expected 2 got %d", len(a))
}
cl := CommandLine{}
for _, i := range a[1:] {
ce := CommandElement{value: i}
ce.InferKind()
cl.value = append(cl.value, ce)
}
alias[a[0]] = cl
return nil
}
func SetEnv(items []string) error {
return os.Setenv(items[0], strings.Join(items[1:], " "))
}
func UnsetEnv(item string) error {
return os.Unsetenv(item)
}

410
commandRunner.go

@ -0,0 +1,410 @@
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
"unicode"
)
const (
singleString int = iota
doubleString
comElement
sysPath
pipeLine
andJoin
fileWrite
fileAppend
)
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 err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
}
}
if i < len(cs.CommandList) && err != nil {
break
}
}
}
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
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("Empty command group")
}
var err error
if cg.commands[0].Path == "slosh-special" {
if len(cg.commands[0].Args) > 2 {
switch cg.commands[0].Args[1] {
case "cd":
err = Dir(cg.commands[0].Args[2])
case "up":
err = Dir(cg.commands[0].Args[2])
case "alias":
err = MakeAlias(cg.commands[0].Args[2:])
case "set":
err = SetEnv(cg.commands[0].Args[2:])
case "unset":
err = UnsetEnv(cg.commands[0].Args[2])
default:
err = fmt.Errorf("Unknown builtin call\n")
}
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
}
err = cg.commands[0].Run()
if err != nil {
last_exit_code = 1
} else {
last_exit_code = cg.commands[0].ProcessState.ExitCode()
}
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 {
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) ExpandPaths() {
for i := range cl.value {
if cl.value[i].kind == sysPath {
cl.value[i].value = ExpandedAbsFilepath(cl.value[i].value)
}
}
}
func (cl CommandLine) Command() *exec.Cmd {
// Expand all of the paths before calling
cl.ExpandPaths()
// 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", "cd", out[1])
} 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" {
return exec.Command("slosh-special", out...)
} else if cl.value[0].value == "unset" {
return exec.Command("slosh-special", out...)
} else if cl.value[0].value == "alias" {
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 {
comLine.value = append(comLine.value, val.value...)
fmt.Println(comLine)
} 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 {
comLine.value = append(comLine.value, val.value...)
fmt.Println(comLine)
} 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
}

5
go.mod

@ -0,0 +1,5 @@
module git.rawtext.club/slosh
go 1.16
require github.com/peterh/liner v1.2.1

4
go.sum

@ -0,0 +1,4 @@
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg=
github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=

188
main.go

@ -0,0 +1,188 @@
package main
import (
"fmt"
"io/fs"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strings"
ln "github.com/peterh/liner"
)
const (
History_Filename string = "~/.slosh_history"
)
var (
completion_names = map[string]bool{"cd":true,"exit":true,"up":true}
slosh_vars = map[string]string{}
alias = map[string]CommandLine{}
slosh_prompt string
last_exit_code int = 0
initialTerm ln.ModeApplier = nil
linerTerm ln.ModeApplier = nil
)
func setUpLineInput() *ln.State {
var err error
initialTerm, err = ln.TerminalMode()
if err != nil {
initialTerm = nil
}
l := ln.NewLiner()
linerTerm, err = ln.TerminalMode()
if err != nil {
linerTerm = nil
}
l.SetCtrlCAborts(true)
l.SetTabCompletionStyle(ln.TabPrints)
l.SetCompleter(func(line string) []string {
c := make([]string, 0, 5)
if line == "" {
return c
}
// TODO add a search for local files here
_, l := ParseCommandLine(line)
current := l[len(l)-1]
if isFilepath(current) {
p := current
if !strings.HasPrefix(p, "/") && !strings.HasPrefix(p, "~") {
wd, _ := os.Getwd()
p = filepath.Join(wd, current)
}
// TODO fix this to cycle through files in a folder
// Currently the `/` isnt getting added to folders
// on tab press, but otherwise it works
var rootPath string
var match string
if strings.HasSuffix(p, "/") {
rootPath = ExpandedAbsFilepath(p)
match = ""
} else {
match = filepath.Base(p)
rootPath = ExpandedAbsFilepath(filepath.Dir(p))
}
fInfo, err := ioutil.ReadDir(rootPath)
if err == nil {
for _, f := range fInfo {
if strings.HasPrefix(f.Name(), match) {
out := f.Name()
suffix := ""
if f.IsDir() {
suffix = "/"
}
suggestion := fmt.Sprintf(
"%s %s%s",
strings.TrimSpace(strings.Join(l[:len(l)-1], " ")),
filepath.Join(rootPath, out),
suffix,
)
c = append(c, suggestion)
}
}
}
}
for key, _ := range completion_names {
if strings.HasPrefix(key, strings.ToLower(line)) {
c = append(c, key)
}
}
return c
})
return l
}
func setCompletionNames() {
pathvar := os.Getenv("PATH")
elmts := strings.Split(pathvar, ":")
for i := range elmts {
filepath.Walk(elmts[i], func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
b := filepath.Base(path)
if b == "." {
return nil
}
completion_names[b] = true
return nil
},
)
}
}
func prompt(ln *ln.State) string {
val, err := ln.Prompt(get_slosh_prompt())
if err != nil {
panic("Input read error")
}
ln.AppendHistory(val)
return val
}
func get_slosh_prompt() string {
p := os.Getenv("SLOSH_PROMPT")
wd, _ := os.Getwd()
if p == "" {
return wd + "# "
}
short := wd
if strings.Contains(p, "%d") {
s := strings.Split(short, "/")
if len(s) > 3 {
short = fmt.Sprintf("...%s", filepath.Join(s[len(s)-3:]...))
}
}
p = strings.ReplaceAll(p, "%D", wd)
p = strings.ReplaceAll(p, "%d", short)
p = strings.ReplaceAll(p, "%c", filepath.Base(wd))
u, err := user.Current()
if err != nil {
p = strings.ReplaceAll(p, "%u", "???")
} else {
p = strings.ReplaceAll(p, "%u", u.Name)
}
p = strings.ReplaceAll(p, "\\n", "\n")
p = strings.ReplaceAll(p, "\\r", "\r")
return p
}
func initShell() *ln.State {
setCompletionNames()
ParseSloshFile()
return setUpLineInput()
}
func main() {
ln := initShell()
defer ln.Close()
if f, e := os.Open(ExpandedAbsFilepath(History_Filename)); e == nil {
ln.ReadHistory(f)
f.Close()
}
// var err error
for {
in := prompt(ln)
if len(in) == 0 {
continue
} else if in == "exit" {
break
}
entry, _ := ParseCommandLine(in)
entry.Execute()
}
}

34
rc.go

@ -0,0 +1,34 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
func ParseSloshFile() {
parseRcLines(getRcLines())
}
func getRcLines() []string {
fdata, err := ioutil.ReadFile(filepath.Join(HomeDir(), ".slosh"))
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to read ~/.slosh\n")
return []string{}
}
return strings.Split(string(fdata), "\n")
}
func parseRcLines(lines []string) {
for _, line := range lines {
line = strings.TrimSpace(line)
if len(line) == 0 {
continue
}
command, _ := ParseCommandLine(line)
command.Execute()
}
}

129
utils.go

@ -0,0 +1,129 @@
package main
import (
"bytes"
"io"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
)
const (
str int = iota
path
command
pipe
redirect
)
type component struct {
value string
kind int
}
func HomeDir() string {
usr, _ := user.Current()
return usr.HomeDir
}
func isFilepath(p string) bool {
if strings.HasPrefix(p, "./") || strings.HasPrefix(p, "~") || strings.HasPrefix(p, "../") || strings.HasPrefix(p, "/") {
return true
}
wd, _ := os.Getwd()
_, err := os.Stat(filepath.Join(wd, p))
if err != nil {
return false
}
return true
}
func ExpandedAbsFilepath(p string) string {
if strings.HasPrefix(p, "~") {
if p == "~" || strings.HasPrefix(p, "~/") {
homedir, _ := os.UserHomeDir()
if len(p) <= 2 {
p = homedir
} else if len(p) > 2 {
p = filepath.Join(homedir, p[2:])
}
} else {
i := strings.IndexRune(p, '/')
var u string
if i < 0 {
u = p[1:]
} else {
u = p[1:i]
}
usr, err := user.Lookup(u)
if err != nil {
p = filepath.Join("/home", u, p[i:])
} else {
p = filepath.Join(usr.HomeDir, p[i:])
}
}
} else if !strings.HasPrefix(p, "/") {
wd, _ := os.Getwd()
p = filepath.Join(wd, p)
}
path, _ := filepath.Abs(p)
return path
}
func Contains(needle string, haystack []string) bool {
var out bool
for _, x := range haystack {
if x == needle {
out = true
break
}
}
return out
}
// Make stderr work properly. It gets eaten up...
func Execute(output_buffer, error_buffer *bytes.Buffer, capture_output bool, stack ...*exec.Cmd) (err error) {
pipe_stack := make([]*io.PipeWriter, len(stack)-1)
i := 0
for ; i < len(stack)-1; i++ {
stdin_pipe, stdout_pipe := io.Pipe()
stack[i].Stdout = stdout_pipe
stack[i].Stderr = error_buffer
stack[i+1].Stdin = stdin_pipe
pipe_stack[i] = stdout_pipe
}
if capture_output {
stack[i].Stdout = output_buffer
stack[i].Stderr = error_buffer
} else {
stack[i].Stdout = os.Stdout
stack[i].Stderr = os.Stderr
}
return call(stack, pipe_stack)
}
func call(stack []*exec.Cmd, pipes []*io.PipeWriter) (err error) {
if stack[0].Process == nil {
if err = stack[0].Start(); err != nil {
return err
}
}
if len(stack) > 1 {
if err = stack[1].Start(); err != nil {
return err
}
defer func() {
if err == nil {
pipes[0].Close()
err = call(stack[1:], pipes[1:])
}
}()
}
return stack[0].Wait()
}
Loading…
Cancel
Save