diff --git a/Readme.md b/Readme.md index e0d032e..63d0b30 100644 --- a/Readme.md +++ b/Readme.md @@ -43,12 +43,17 @@ Use the `func` function from the stdlib: In this case, `(form_to_be_evaluated)` will not be evaluated until the function is called. ### Control flow -See `stdlib/control_flow.go`. We have if and while forms: +See `stdlib/control_flow.go`. We have if, while, and progn forms: `(if (cond) (then) (else))` `(when (cond) (form1)....... (formN))` - +`(progn (form1)..... (formN))` +If and While should be self explanatory. For those new to LISP, the rough idea of progn is to evaluate a sequence of N forms and return the result of the final one. We also have functioning implementations of map and reduce in the stdlib (incomplete) +## Comments +The standard delimiter for comments is ; +any characters after a semicolon will be ignored until end of line + ## How to build ### Compiling/Installation - For now simply run `go install cmd/...` for each utility you wish to use. If you have GOPATH and GOBIN set it should be usable from PATH @@ -69,9 +74,14 @@ We also have functioning implementations of map and reduce in the stdlib (incomp * one can write arbitrary shs script into `.shsrc` including function and variable declarations * of note are the following variables - `SH_LOGGING` Sets the log level (from 0 to 3) - - `SHS_SH_PROMPT` Sets the prompt + - `SHS_STATIC_PROMPT` Sets the prompt - `SH_HIST_FILE` Sets the history file - - `SH_DEBUG_MODE` Adds additional debug output for the lexer + - `SH_DEBUG_MODE` Adds additional debug output for the lexer (high clutter) +* additionally, the repl will evaluate any function you define as `_SH_PROMPT` before the shell prompt + - if defined, the function will be evaluated before printing the prompt + - the function will be given 0 arguments + - if the function does not return a string, its output will be discarded + - afterwards, the repl will print the values in `SHS_STATIC_PROMPT` Here is an example of a shs configuration file: ```lisp (export GOPATH (concat HOME "/go")) @@ -80,6 +90,8 @@ Here is an example of a shs configuration file: (export GIT_TERMINAL_PROMPT 1) (export SH_HIST_FILE (concat HOME "/.shs_hist")) (export SH_LOGGING 0) +(export SHS_STATIC_PROMPT ">") +(func _SH_PROMPT () (concat (?) ($ basename ($ pwd)) "\n")) ``` ## Contributing diff --git a/ast/eval.go b/ast/eval.go index 0f6cc98..e82a89b 100644 --- a/ast/eval.go +++ b/ast/eval.go @@ -19,12 +19,14 @@ package ast import "gitlab.com/whom/shs/log" -/* determines whether or not to execute a system call +/* determines whether or not to execute a system binary * when a function cannot be found in the functable * (use case: shell) - * ExecFunc determines the name of the system call function to fetch */ var ExecWhenFuncUndef = false + +/* name of the command used to execute a system binary + */ var ExecFunc = "l" /* Runs through an AST of tokens @@ -61,6 +63,8 @@ func (in *Token) Eval(funcs FuncTable, vars VarTable, cnvtUndefVars bool) *Token "eval") return nil } + } else { + res.Next = in.Next } case LIST: diff --git a/ast/func_table.go b/ast/func_table.go index b70f758..b939e0c 100644 --- a/ast/func_table.go +++ b/ast/func_table.go @@ -19,18 +19,35 @@ package ast import "gitlab.com/whom/shs/log" +/* expected function header for any stdlib function + */ type Operation func(*Token, VarTable, FuncTable) *Token +/* holds a stdlib function along with relevant metadata + */ type Function struct { + // go function that list of args are passed to Function Operation + + // name of function Name string + + // number of times user has called this function TimesCalled int - Args int // TODO: Make this a list of expected types (TAGs) + + // number of args required + Args int } +/* holds a mapping of key to function + * passed to eval and into function calls + * initialized by repl at startup + */ type FuncTable *map[string]*Function -// TODO: Currently only checks arg list length +/* validates an individual call of a function + * makes sure correct arguments are passed in + */ func (f Function) ParseFunction(args *Token) bool { // handle infinite args if f.Args < 0 { @@ -52,6 +69,9 @@ func (f Function) ParseFunction(args *Token) bool { return true } +/* handles a call to a function + * calls ParseFunction and increments TimesCalled + */ func (f Function) CallFunction(args *Token, vt VarTable, ft FuncTable) *Token { if !f.ParseFunction(args) { log.Log(log.ERR, @@ -64,6 +84,8 @@ func (f Function) CallFunction(args *Token, vt VarTable, ft FuncTable) *Token { return f.Function(args, vt, ft) } +/* searches for function mapped to argument in FuncTable + */ func GetFunction(arg string, table FuncTable) *Function { target, ok := (*table)[arg] if !ok { @@ -75,3 +97,18 @@ func GetFunction(arg string, table FuncTable) *Function { return target } + + +/* returns list of all functions in table + */ +func ListFuncs(ft FuncTable) []string { + keys := make([]string, len(*ft)) + i := 0 + + for k := range *ft { + keys[i] = k + i++ + } + + return keys +} diff --git a/ast/lex.go b/ast/lex.go index 532f32a..3031cab 100644 --- a/ast/lex.go +++ b/ast/lex.go @@ -22,8 +22,12 @@ import ( "unicode" ) +// all delimiters that work on strings const string_delims string = "\"'`" +/* takes a line of user input + * returns an unsimplified tree of tokens + */ func Lex(input string) *Token { ret := lex(input) if ret == nil { @@ -121,6 +125,17 @@ func lex(input string) *Token { return -2 } + // returns the end of the string OR the end of the line + matchLineEnd := func(start int) int { + for i := start; i < len(input); i++ { + if input[i] == '\n' { + return i + } + } + + return len(input) + } + needs_alloc := false start_pos := 0 for i := 0; i < len(input); i++ { @@ -137,13 +152,18 @@ func lex(input string) *Token { is_str = true needs_alloc = true - case ' ': + case ' ', '\n', '\t', '\v', '\f', '\r': if i == start_pos { start_pos += 1 continue } needs_alloc = true + + // comment case + case ';': + start_pos = i + 1 + i = matchLineEnd(start_pos) } if needs_alloc { @@ -185,6 +205,7 @@ error: return nil } +// returns true if a string could contain an int or float func StrIsNumber(arg string) bool { dotCount := 0 diff --git a/ast/print.go b/ast/print.go index 944b698..ea5251c 100644 --- a/ast/print.go +++ b/ast/print.go @@ -18,6 +18,7 @@ package ast import ( + "fmt" "strings" ) @@ -49,7 +50,7 @@ loop: constructor.WriteString(iter.FmtToken()) } - println(constructor.String()) + fmt.Printf(constructor.String() + "\n") goto loop } diff --git a/ast/token.go b/ast/token.go index 367cd42..b9ba0ca 100644 --- a/ast/token.go +++ b/ast/token.go @@ -19,7 +19,11 @@ package ast import "fmt" +/* token_t is a tag that declares the type of the + * datum contained in a token + */ type Token_t int + const ( LIST Token_t = iota STRING Token_t = iota @@ -31,6 +35,9 @@ const ( FALSE string = "F" ) +/* Contains a parsed lexeme + * and a pointer to the next parsed lexeme in the same scope + */ type Token struct { Next *Token Tag Token_t diff --git a/ast/tokenstack.go b/ast/tokenstack.go index 9f7360b..136ccae 100644 --- a/ast/tokenstack.go +++ b/ast/tokenstack.go @@ -17,16 +17,23 @@ package ast +/* primitive stack type for tokens + * useful for iterative algorithms on tokens + */ type TokenStack struct { buffer []*Token capacity int } +/* push token onto stack + */ func (s *TokenStack) Push(v *Token) { s.capacity++ s.buffer = append(s.buffer, v) } +/* pop token off stack + */ func (s *TokenStack) Pop() *Token { if s.capacity <= 0 { return nil diff --git a/ast/var_table.go b/ast/var_table.go index 7037e4a..f597eee 100644 --- a/ast/var_table.go +++ b/ast/var_table.go @@ -26,11 +26,19 @@ import ( "gitlab.com/whom/shs/log" ) -// Trigger this if you are using this for a shell +/* defines whether or not to synchronize tokens wiht os environment vars + * will not sync non stringable tokens + */ var SyncTablesWithOSEnviron = false +/* mapping of key to token. + */ type VarTable *map[string]*Token +/* retrieve the token cooresponding to a given key + * if SyncTablesWithOSEnviron is true and no token exists for a key + * os Environment variables will be searched for the key + */ func GetVar(arg string, vt VarTable) *Token { val, ok := (*vt)[arg] if !ok { @@ -55,8 +63,10 @@ func GetVar(arg string, vt VarTable) *Token { return val } -// TODO: this could be much more optimal -// probably a stdlib thing +/* adds a key->token mapping to the table + * if SyncTablesWithOSEnviron is true, will also add value to os environment + * will not do so for non stringable tokens + */ func SetVar(variable string, value *Token, vt VarTable) { (*vt)[variable] = value if SyncTablesWithOSEnviron && @@ -73,22 +83,21 @@ func SetVar(variable string, value *Token, vt VarTable) { } } -// Library represents variables defined in inner scope -// It is assumed library is ordered from innermost scope to outermost scope -func GetVarFromTables(arg string, library []VarTable) *Token { - var res *Token - res = nil - for i := 0; i < len(library); i += 1 { - res = GetVar(arg, library[i]) - if res != nil { - // TODO: Log scope res was found in? - break - } +/* lists all vars in tables + */ +func ListVars(vt VarTable) []string { + keys := make([]string, len(*vt)) + i := 0 + for k := range *vt { + keys[i] = k } - return res + return keys } +/* if SyncTablesWithOSEnviron is true + * function will put ever environment variable into VarTable + */ func InitVarTable(table VarTable) { if !SyncTablesWithOSEnviron { return @@ -122,6 +131,9 @@ func DeleteVarTable(table VarTable) { } } +/* removes var from vartable + * if SyncTablesWithOSENviron is true, also unsets environment variable + */ func RemoveVar(arg string, table VarTable) { if SyncTablesWithOSEnviron { err := os.Unsetenv(arg) diff --git a/cmd/shs_repl.go b/cmd/shs.go similarity index 60% rename from cmd/shs_repl.go rename to cmd/shs.go index ea6007c..3379c7c 100644 --- a/cmd/shs_repl.go +++ b/cmd/shs.go @@ -18,11 +18,13 @@ package main import ( + "os" "fmt" "strconv" - "github.com/chzyer/readline" + "github.com/peterh/liner" "gitlab.com/whom/shs/ast" "gitlab.com/whom/shs/log" + "gitlab.com/whom/shs/util" "gitlab.com/whom/shs/config" ) @@ -30,6 +32,22 @@ const ( def_prompt string = "λ " ) + +// useful for when input contains escape sequences +// not checking delims cause thats up to the user who defines their prompts +func parseString(in string) string { + in = "\"" + in + "\"" + out, err := strconv.Unquote(in) + if err != nil { + log.Log(log.ERR, + "Couldnt parse (pre?)prompt", + "init") + return "" + } + + return out +} + func setLogLvl(vars ast.VarTable) { var loglvl string @@ -54,6 +72,7 @@ func main() { var prompt string var debug string var hist string + no_hist := false ast.SyncTablesWithOSEnviron = true ast.ExecWhenFuncUndef = true @@ -67,34 +86,62 @@ func main() { hist_t := ast.GetVar("SH_HIST_FILE", vars) if hist_t != nil { hist = hist_t.Value() + } else { + no_hist = true } - prompt_t := ast.GetVar("SHS_SH_PROMPT", vars) + dyn_prompt := ast.GetFunction("_SH_PROMPT", funcs) + if dyn_prompt == nil || dyn_prompt.Args != 0 { + dyn_prompt = nil + } + + prompt_t := ast.GetVar("SHS_STATIC_PROMPT", vars) if prompt_t != nil { - prompt = prompt_t.Value() + prompt = parseString(prompt_t.Value()) } else { prompt = def_prompt } - rl, err := readline.NewEx(&readline.Config{ - Prompt: prompt, - HistoryFile: hist, - InterruptPrompt: "^C", + line := liner.NewLiner() + defer line.Close() + + line.SetCtrlCAborts(true) + line.SetCompleter(func(line string) (c []string) { + return util.ShellCompleter(line, vars, funcs) }) - defer rl.Close() - if err != nil { - log.Log(log.ERR, "Couldnt initialize readline: " + err.Error(), "repl") - return + var histFile *os.File + var err error + if !no_hist { + histFile, err = os.Open(hist) + if err == nil { + line.ReadHistory(histFile) + } else { + log.Log(log.ERR, + "couldnt read history: " + err.Error(), + "repl") + } + defer histFile.Close() } for { setLogLvl(vars) - text, err := rl.Readline() - if err != nil { - log.Log(log.ERR, "couldnt read user input: " + err.Error(), "repl") + var prePrompt string + if dyn_prompt != nil { + p_tok := dyn_prompt.CallFunction(nil, vars, funcs) + if p_tok != nil && p_tok.Tag == ast.STRING { + prePrompt = parseString(p_tok.Value()) + } } + fmt.Printf(prePrompt) + text, err := line.Prompt(prompt) + if err != nil && err != liner.ErrPromptAborted{ + log.Log(log.ERR, "couldnt read user input: " + err.Error(), "repl") + continue + } + + line.AppendHistory(text) userInput := ast.Lex(text) if userInput == nil { // errors handled in Lex diff --git a/config/config.go b/config/config.go index 6ce7d51..fc8eda3 100644 --- a/config/config.go +++ b/config/config.go @@ -18,14 +18,16 @@ package config import ( - "os" - "io" - "bufio" "gitlab.com/whom/shs/log" "gitlab.com/whom/shs/ast" + "gitlab.com/whom/shs/util" "gitlab.com/whom/shs/stdlib" ) +/* creates new VarTable and FuncTable + * reads a configuration file + * executes contents and returns tables + */ func InitFromConfig(configFile string) (ast.VarTable, ast.FuncTable) { funcs := stdlib.GenFuncTable() vars := &map[string]*ast.Token{} @@ -35,32 +37,10 @@ func InitFromConfig(configFile string) (ast.VarTable, ast.FuncTable) { p := ast.GetVar("HOME", vars) configFile = p.Value() + "/" + configFile - cfile, err := os.Open(configFile) - if err != nil { - log.Log(log.DEBUG, - "unable to open config file: " + err.Error(), - "config") - return vars, funcs - } - - r := bufio.NewReader(cfile) - text, err := r.ReadString('\n') - for err != io.EOF { - if err != nil { - log.Log(log.ERR, - "unable to read from config file: " + err.Error(), - "config") - break - } - - // Eval lines in config - ast.Lex(text).Eval(funcs, vars, false) - text, err = r.ReadString('\n') - } + util.LoadScript(configFile, vars, funcs) log.Log(log.DEBUG, "config file fully evaluated", "config") - cfile.Close() return vars, funcs } diff --git a/go.mod b/go.mod index 125370b..2b85407 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module gitlab.com/whom/shs go 1.14 -require github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect +require ( + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/peterh/liner v1.2.0 // indirect +) diff --git a/go.sum b/go.sum index 454fe40..285b298 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +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.0 h1:w/UPXyl5GfahFxcTOz2j9wCIHNI+pUPr2laqpojKNCg= +github.com/peterh/liner v1.2.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= diff --git a/log/logger.go b/log/logger.go index 5f3d773..2d736e6 100644 --- a/log/logger.go +++ b/log/logger.go @@ -29,16 +29,23 @@ const ( var logLevel int64 +/* Set the log level from 0 to 4 + * currently only 0, 1, and 2 are used. + */ func SetLogLvl(lvl int64) { if lvl < 4 && lvl > 0 { logLevel = lvl } } +/* get current log level as int + */ func GetLogLvl() int64 { return logLevel } +/* writes a message to the log + */ func Log(lvl int, msg, context string) { if int64(lvl) > logLevel { return diff --git a/stdlib/arith.go b/stdlib/arith.go index 7a61754..d516551 100644 --- a/stdlib/arith.go +++ b/stdlib/arith.go @@ -29,7 +29,40 @@ import ( // perhaps we simply write out arithmetic routines that operate on the strings // then we need not worry about storage length. -func add(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { +/* Takes 1 argument (must be a string) + * will attempt to cast it to a number. + * will return nil if cast fails + * + * Example: (number "3.4") + */ +func NumCast(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { + in = in.Eval(f, a, false) + if in.Tag != ast.STRING { + log.Log(log.ERR, + "only a string can successfully be cast to a number", + "number_cast") + return nil + } + + if !ast.StrIsNumber(in.Value()) { + log.Log(log.ERR, + "string failed number cast", + "number_cast") + return nil + } + + out := in.Copy() + out.Tag = ast.NUMBER + return out +} + +/* adds N number arguments + * takes N arguments + * returns the sum, or nil if improper arguments were given + * + * Example: (+ 1 2) + */ +func Add(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { var res float64 in = in.Eval(f, a, false) @@ -71,7 +104,12 @@ func add(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { return t } -func sub(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { +/* subtract N args from the final arg + * takes N args, returns nil if improper args given + * + * Example: (- 2 1) + */ +func Sub(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { var res float64 var sub float64 @@ -121,7 +159,12 @@ func sub(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { return t } -func mult(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { +/* multiplies N arguments + * returns nil if an improper argument is given + * + * Example: (* 1 2) + */ +func Mult(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { res := 1.0 in = in.Eval(f, a, false) @@ -163,7 +206,13 @@ func mult(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { return t } -func div(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { +/* divide N arguments + * the first argument is divided by each subsequent argument in order + * returns nil if an improper argument is given + * + * Example (/ 25 5) + */ +func Div(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { var res float64 in = in.Eval(f, a, false) diff --git a/stdlib/bool.go b/stdlib/bool.go index 328c4e7..d6b07e6 100644 --- a/stdlib/bool.go +++ b/stdlib/bool.go @@ -23,7 +23,35 @@ import ( "gitlab.com/whom/shs/ast" ) -func not(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* Takes one argument, must be a string + * attempts to cast to bool (T or F are valid values) + * returns nil on failure + * + * Example: (bool "F") + */ +func BoolCast(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + in = in.Eval(ft, vt, false) + if in.Tag == ast.LIST || in.Tag == ast.NUMBER { + log.Log(log.ERR, + "only strings successfully cast to bool", + "bool cast") + return nil + } + + body := in.Value() + if body != ast.TRUE && body != ast.FALSE { + log.Log(log.ERR, + "cast to bool failed", + "bool cast") + return nil + } + + res := &ast.Token{ Tag: ast.BOOL } + res.Set(body) + return res +} + +func Not(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, false) if in.Tag != ast.BOOL { @@ -41,7 +69,7 @@ func not(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return t } -func eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +func Eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { out := ast.TRUE in = in.Eval(ft, vt, false) @@ -123,7 +151,7 @@ func eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return t } -func lt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +func Lt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { out := ast.TRUE second := in.Next @@ -146,7 +174,7 @@ func lt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return t } -func gt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +func Gt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { out := ast.TRUE second := in.Next @@ -169,14 +197,14 @@ func gt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return t } -func ne(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - return not(eq(in, vt, ft), vt, ft) +func Ne(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + return Not(Eq(in, vt, ft), vt, ft) } -func gte(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - return not(lt(in, vt, ft), vt, ft) +func Gte(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + return Not(Lt(in, vt, ft), vt, ft) } -func lte(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - return not(gt(in, vt, ft), vt, ft) +func Lte(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + return Not(Gt(in, vt, ft), vt, ft) } diff --git a/stdlib/call.go b/stdlib/call.go index 4359503..99e0647 100644 --- a/stdlib/call.go +++ b/stdlib/call.go @@ -30,7 +30,6 @@ import ( ) var bgProcs = make([]*exec.Cmd, 0) -var LastExitCode int var sigs = []os.Signal{ os.Interrupt, syscall.SIGTERM, @@ -40,8 +39,17 @@ var sigs = []os.Signal{ syscall.SIGCONT, } +/* Exit code of last run process + */ +var LastExitCode int -func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* Takes n arguments (list of tokens generated by lexing a shell command) + * Evaluates arguments, but does not err on undefined symbols (note the last arg to Eval(...)) + * Executes shell command and returns nil + * + * Example (l vim file.txt) + */ +func Call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, true) if in == nil { return nil @@ -74,9 +82,10 @@ func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { } else { cmd = exec.Command(path) } + cmd.Env = os.Environ() + cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin signalChan := make(chan os.Signal, 2) signal.Notify(signalChan, sigs...) @@ -99,7 +108,12 @@ func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return nil } -func bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* Starts a call in the background + * Takes n args (a shell command not delimited by string delimiters) + * + * Example: (bg vim file.txt) + */ +func Bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { if in == nil { return nil } @@ -143,7 +157,14 @@ func bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return nil } -func fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* brings last BG'ed process into the foreground + * returns nil + * + * Example: + * (bg vim file.txt) + * (fg) + */ +func Fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { if len(bgProcs) < 1 { return nil } @@ -173,7 +194,16 @@ func fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return nil } -func jobs(in *ast.Token, vt ast.VarTable, fg ast.FuncTable) *ast.Token { +/* Takes 0 args + * returns a string containing info about current jobs + * returns total jobs as well as their PIDs and place in the bg queue + * + * Example: + * (bg ping google.com) + * (bg .........) + * (jobs) + */ +func Jobs(in *ast.Token, vt ast.VarTable, fg ast.FuncTable) *ast.Token { ret := &ast.Token{ Tag: ast.LIST, } @@ -197,7 +227,12 @@ func jobs(in *ast.Token, vt ast.VarTable, fg ast.FuncTable) *ast.Token { return ret } -func read_cmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* calls a command (blocks until completion) + * captures stdout and returns it as a string + * + * Example: ($ echo hello world) + */ +func ReadCmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, true) if in == nil { @@ -254,19 +289,37 @@ func read_cmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { } } + output := out.String() + olen := len(output) + if olen > 0 && output[olen - 1] == '\n' { + output = output[:olen - 1] + } + ret := &ast.Token{Tag: ast.STRING} - ret.Set(out.String()) + ret.Set(output) return ret } -func get_exit(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* Takes 0 arguments + * returns the exit code of the last executed program + * + * Example: + * (sudo apt update) + * (?) <- gets exit code + */ +func GetExit(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { ret := &ast.Token{Tag: ast.NUMBER} ret.Set(fmt.Sprintf("%d", LastExitCode)) return ret } -func kill(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* takes an argument (pid of process to be killed) + * calls Process.Kill() on it + * do not use this if you already have a native implementation + * (this function not added to functable in stdlib.go) + */ +func Kill(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, true) if in.Tag == ast.LIST { diff --git a/stdlib/control_flow.go b/stdlib/control_flow.go index 5889e33..0238ff5 100644 --- a/stdlib/control_flow.go +++ b/stdlib/control_flow.go @@ -22,9 +22,29 @@ import ( "gitlab.com/whom/shs/log" ) -/* return one evaluated form or another based on the boolean statement +/* eval N forms. return the last one + * + * Example: + * (progn (print "hello") (print "world") (+ 1 2)) + * This example will print "hello world" to stdout and return 3 */ -func shs_if(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +func ShsProgn(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + var res *ast.Token + for iter := in; iter != nil; iter = iter.Next { + res = iter.Eval(ft, vt, false) + } + + return res +} + +/* return one evaluated form or another based on the boolean statement + * arg 1 is a boolean cond, arg 2 is evaluated if the cond is true, arg 3 is evaluated if cond is not true + * in total it takes 3 arguments + * + * Example: + * (if (eq (number "3") 3) (print "test passed") (print "test failed")) + */ +func ShsIf(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { cond := in t := cond.Next f := t.Next @@ -55,8 +75,14 @@ func shs_if(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { } /* continually eval n forms while element #1 evals to T + * has rather progn like behavior in that it returns the result of the last form to be evaluated + * + * Example: + * (export cond F) + * (while cond (export cond T) (print "will only be printed once") (+ 1 2)) + * loop will iter one time, print "will only be printed once" and return 3 */ -func shs_while(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +func ShsWhile(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { cond := in forms := in.Next in.Next = nil diff --git a/stdlib/filesys.go b/stdlib/filesys.go index 6504221..182cdca 100644 --- a/stdlib/filesys.go +++ b/stdlib/filesys.go @@ -46,7 +46,14 @@ func AbsPath(arg string) string { return arg } -func cd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* Takes one arg, returns nil + * changes directory to the path in the arg + * fails if arg is not stringable + * + * Example: + * (cd (concat HOME "/go")) + */ +func Cd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, true) if in == nil { @@ -68,11 +75,19 @@ func cd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return nil } -func fexists(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* Takes one arg, returns a bool + * Returns true if arg is a filepath that exists + * returns nil if arg is not a string type + * + * Example: + * (touch test) + * (fexists test) + */ +func Fexists(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, false) - if in == nil || (in.Tag != ast.NUMBER && in.Tag != ast.STRING) { + if in == nil || in.Tag != ast.STRING { log.Log(log.ERR, - "argument to fexists must be a string or number", + "argument to fexists must be a string", "fexists") return nil } @@ -92,9 +107,16 @@ func fexists(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return ret } -func fread(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* Takes one arg, returns a string + * Returns contents of file in arg + * returns nil if file doesnt exist + * + * Example: + * (fread (concat HOME ".shsrc")) + */ +func Fread(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, false) - exists := fexists(in, vt, ft) // some waste, extra use of Eval + exists := Fexists(in, vt, ft) // some waste, extra use of Eval if exists == nil || exists.Tag != ast.BOOL || exists.Value() == ast.FALSE { log.Log(log.ERR, "error calling fexists or file doesnt exist", @@ -116,7 +138,14 @@ func fread(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return ret } -func fwrite(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* Takes two arguments a filepath and a string + * CLOBBERS FILE CONTENTS + * Returns nil + * + * Example: + * (fwrite "test" "one two three") + */ +func Fwrite(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, false) if in == nil || in.Tag == ast.SYMBOL || in.Tag == ast.LIST { log.Log(log.ERR, @@ -146,7 +175,14 @@ func fwrite(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return nil } -func fappend(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* Takes two arguments a filepath and a string + * DOES NOT CLOBBER FILE CONTENTS + * Returns nil + * + * Example: + * (fwrite "test" "one two three") + */ +func Fappend(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, false) if in == nil || in.Tag == ast.SYMBOL || in.Tag == ast.LIST { log.Log(log.ERR, @@ -163,7 +199,7 @@ func fappend(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return nil } - exists := fexists(in, vt, ft) + exists := Fexists(in, vt, ft) if exists.Value() == ast.FALSE { log.Log(log.ERR, "file "+in.Value()+" does not exist", diff --git a/stdlib/funcs.go b/stdlib/funcs.go index 93de50d..63f3130 100644 --- a/stdlib/funcs.go +++ b/stdlib/funcs.go @@ -22,7 +22,17 @@ import ( "gitlab.com/whom/shs/log" ) -func decl_func(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { +/* Takes 3 arguments: name, list of arg names, and logic form + * DOES NOT EVALUATE THE LOGIC FORM + * adds an anonymous function to the FuncTable under the name specified + * anonymous function will expect the args declared in arg2 and expand them in arg3 + * Then evaluates and returns result of arg3. This constitutes a function call + * + * Example: + * (func foo (x) (print x)) + * (foo 4) -> prints 4 + */ +func DeclFunc(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { name := input if name.Tag != ast.SYMBOL { log.Log(log.ERR, @@ -60,30 +70,34 @@ func decl_func(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.To ASTSYNCSTATE := ast.SyncTablesWithOSEnviron inner := func(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - temp := in.Eval(ft, vt, false) - if temp == nil { - log.Log(log.ERR, - "error parsing arguments", - name.Value()) - return nil - } - - ast.SyncTablesWithOSEnviron = false - key_iter := args.Expand() - val_iter := temp - - for key_iter != nil { - if val_iter == nil { + var temp *ast.Token + if numArgs != 0 || in != nil { + temp = in.Eval(ft, vt, false) + if temp == nil { log.Log(log.ERR, - "Not enough arguments supplied", + "error parsing arguments", name.Value()) + return nil } - ast.SetVar(key_iter.Value(), val_iter, vt) - key_iter = key_iter.Next - val_iter = val_iter.Next + ast.SyncTablesWithOSEnviron = false + key_iter := args.Expand() + val_iter := temp + + for key_iter != nil { + if val_iter == nil { + log.Log(log.ERR, + "Not enough arguments supplied", + name.Value()) + } + + ast.SetVar(key_iter.Value(), val_iter, vt) + key_iter = key_iter.Next + val_iter = val_iter.Next + } } + // maybe we actually should put the inner scope var into the env ast.SyncTablesWithOSEnviron = ASTSYNCSTATE ret := form.Eval(ft, vt, false) ast.SyncTablesWithOSEnviron = false diff --git a/stdlib/list.go b/stdlib/list.go index ac20d28..cec7d80 100644 --- a/stdlib/list.go +++ b/stdlib/list.go @@ -27,7 +27,7 @@ import ( * retuns a sequence of elements (list contents) * in event a not-list is passed in, returns the arg. */ -func expand(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { +func Expand(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { if input.Tag != ast.LIST { log.Log(log.DEBUG, "expand called on not a list", "expand") return input @@ -41,7 +41,7 @@ func expand(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token * Arg one is a list, next args are appended * if no args are a list, a list is made from all args */ -func l_append(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { +func L_append(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { src := input if input.Tag != ast.LIST { diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 5ad08a5..83749c2 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -22,202 +22,234 @@ import ( "fmt" "gitlab.com/whom/shs/log" "gitlab.com/whom/shs/ast" + "gitlab.com/whom/shs/util" ) +/* Makes a FuncTable from all functions in the stdlib + * add your function here if you are extending the standard library + */ func GenFuncTable() ast.FuncTable { var stdlib ast.FuncTable stdlib = &map[string]*ast.Function{ "if": &ast.Function{ - Function: shs_if, + Function: ShsIf, Name: "if", TimesCalled: 0, Args: 3, }, "while": &ast.Function{ - Function: shs_while, + Function: ShsWhile, Name: "while", TimesCalled: 0, Args: -1, }, - "eval": &ast.Function{ - Function: eval, - Name: "eval", - TimesCalled: 0, - Args: -1, + "progn": &ast.Function{ + Function: ShsProgn, + Name: "shs_progn", + TimesCalled: 0, + Args: -1, }, "func": &ast.Function{ - Function: decl_func, + Function: DeclFunc, Name: "decl_func", TimesCalled: 0, Args: 3, }, "export": &ast.Function{ - Function: export, + Function: Export, Name: "export", TimesCalled: 0, Args: 2, }, - "input": &ast.Function{ - Function: input, + "input": &ast.Function{ + Function: Input, Name: "input", TimesCalled: 0, Args: 1, }, - "...": &ast.Function{ - Function: expand, + "load": &ast.Function{ + Function: Load, + Name: "load", + TimesCalled: 0, + Args: 1, + }, + + "bool": &ast.Function{ + Function: BoolCast, + Name: "bool", + TimesCalled: 0, + Args: 1, + }, + + "string": &ast.Function{ + Function: StrCast, + Name: "string", + TimesCalled: 0, + Args: 1, + }, + + "number": &ast.Function{ + Function: NumCast, + Name: "number", + TimesCalled: 0, + Args: 1, + }, + + "...": &ast.Function{ + Function: Expand, Name: "...", TimesCalled: 0, Args: 1, }, "append": &ast.Function{ - Function: l_append, + Function: L_append, Name: "append", TimesCalled: 0, Args: -1, }, "exit": &ast.Function{ - Function: exit_shell, + Function: ExitShell, Name: "exit", TimesCalled: 0, Args: 0, }, "eq": &ast.Function{ - Function: eq, + Function: Eq, Name: "==", TimesCalled: 0, Args: 2, }, "ne": &ast.Function{ - Function: ne, + Function: Ne, Name: "!=", TimesCalled: 0, Args: 2, }, "<": &ast.Function{ - Function: lt, + Function: Lt, Name: "<", TimesCalled: 0, Args: 2, }, ">": &ast.Function{ - Function: gt, + Function: Gt, Name: ">", TimesCalled: 0, Args: 2, }, "<=": &ast.Function{ - Function: lte, + Function: Lte, Name: "<=", TimesCalled: 0, Args: 2, }, ">=": &ast.Function{ - Function: gte, + Function: Gte, Name: ">=", TimesCalled: 0, Args: 2, }, "!": &ast.Function{ - Function: not, + Function: Not, Name: "!", TimesCalled: 0, Args: 1, }, "+": &ast.Function{ - Function: add, + Function: Add, Name: "add", TimesCalled: 0, Args: -1, }, "-": &ast.Function{ - Function: sub, + Function: Sub, Name: "sub", TimesCalled: 0, Args: -1, }, "*": &ast.Function{ - Function: mult, + Function: Mult, Name: "mult", TimesCalled: 0, Args: -1, }, "/": &ast.Function{ - Function: div, + Function: Div, Name: "div", TimesCalled: 0, Args: -1, }, "cd": &ast.Function{ - Function: cd, + Function: Cd, Name: "changedir", TimesCalled: 0, Args: 1, }, "concat": &ast.Function{ - Function: concat, + Function: Concat, Name:"concatenate", TimesCalled: 0, Args: -1, }, "print": &ast.Function{ - Function:print_str, + Function: PrintStr, Name: "print", TimesCalled: 0, Args: 1, }, "l": &ast.Function{ - Function: call, + Function: Call, Name: "call", TimesCalled: 0, Args: -1, }, "bg": &ast.Function{ - Function: bgcall, + Function: Bgcall, Name: "background call", TimesCalled: 0, Args: -1, }, "fg": &ast.Function{ - Function: fg, + Function: Fg, Name: "foreground", TimesCalled: 0, Args: 0, }, "$": &ast.Function{ - Function: read_cmd, + Function: ReadCmd, Name: "read cmd", TimesCalled: 0, Args: -1, }, "?": &ast.Function{ - Function: get_exit, + Function: GetExit, Name:"get exit code", TimesCalled: 0, Args: 0, @@ -234,42 +266,42 @@ func GenFuncTable() ast.FuncTable { */ "jobs": &ast.Function{ - Function: jobs, + Function: Jobs, Name: "list jobs", TimesCalled: 0, Args: 0, }, "info": &ast.Function{ - Function: sh_info, + Function: ShInfo, Name: "Shell Info", TimesCalled: 0, Args: 1, }, "fexists": &ast.Function{ - Function: fexists, + Function: Fexists, Name: "file exists", TimesCalled: 0, Args: 1, }, "fread": &ast.Function{ - Function: fread, + Function: Fread, Name: "read file", TimesCalled: 0, Args: 1, }, "fwrite": &ast.Function{ - Function: fwrite, + Function: Fwrite, Name: "write file", TimesCalled: 0, Args: 2, }, "fappend": &ast.Function{ - Function: fappend, + Function: Fappend, Name:"append to file", TimesCalled: 0, Args: 2, @@ -279,12 +311,22 @@ func GenFuncTable() ast.FuncTable { return stdlib } -func exit_shell(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* takes no args + * exits shell when called + * + * Example: (exit) + */ +func ExitShell(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { os.Exit(0) return nil // I hope execution doesnt get here } -func sh_info(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* takes one arg, doesnt evaluate it + * returns type or metadata + * + * Example: (info append) + */ +func ShInfo(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { switch in.Tag { case ast.BOOL: fmt.Printf("BOOL LITERAL\nValue: %s\n", in.Value()) @@ -314,11 +356,14 @@ func sh_info(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return nil } -func eval(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - return in.Eval(ft, vt, false) -} - -func input(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* Takes 1 arg, uses it as a prompt + * errs if prompt is not a string or number + * gets a line from stdin + * returns it as a string + * + * Example: (print (input)) + */ +func Input(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, false) if in.Tag != ast.STRING && in.Tag != ast.NUMBER { log.Log(log.ERR, @@ -337,3 +382,23 @@ func input(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { ret.Set(output) return ret } + +/* Takes 1 arg, returns nil + * if arg is a valid existing file than load will execute it as a script + * + * Example: (load "myscript.shs") + */ +func Load(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + in = in.Eval(ft, vt, true) + if in.Tag != ast.STRING { + log.Log(log.ERR, + "argument to load must be a string", + "load") + return nil + } + + bp := in.Value() + bp = AbsPath(bp) + util.LoadScript(bp, vt, ft) + return nil +} diff --git a/stdlib/string.go b/stdlib/string.go index b8321d7..3cae616 100644 --- a/stdlib/string.go +++ b/stdlib/string.go @@ -19,11 +19,16 @@ package stdlib import ( "fmt" + "strconv" "gitlab.com/whom/shs/ast" "gitlab.com/whom/shs/log" ) -func concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { +/* Concatenates N stringables + * + * Example: (concat "hello" " " "world") + */ +func Concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, false) var res string @@ -42,7 +47,38 @@ func concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return t } -func print_str(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - fmt.Println(in.Eval(ft, vt, false)) +/* Takes 1 argument, returns its value as a string + * works on lists too. + * + * Example: (string 1) -> 1.0 + */ +func StrCast(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + body := in.Eval(ft, vt, false).String() + res := &ast.Token{ Tag: ast.STRING } + res.Set(body) + return res +} + +/* Takes one arg, returns nil + * Prints a string to stdout + * Unquotes string so user can add escaped chars like \n, \t, etc + * + * Example: (print "Line: \n, Tab: \t") + */ +func PrintStr(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + body := in.Eval(ft, vt, false).String() + if body[0] != body[len(body)-1] && body[0] != '"' { + body = "`" + body + "`" + } + + text, err := strconv.Unquote(body) + if err != nil { + log.Log(log.ERR, + "error unquoting string", + "print") + return nil + } + + fmt.Printf(text + "\n") return nil } diff --git a/stdlib/vars.go b/stdlib/vars.go index 6aed9f9..52938f2 100644 --- a/stdlib/vars.go +++ b/stdlib/vars.go @@ -22,7 +22,15 @@ import ( "gitlab.com/whom/shs/log" ) -func export(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { +/* Takes 2 args, a name and a value + * Exports a varable + * both args are evaluated + * + * Example: + * (export hw (concat "hello" " " "world")) + * (print hw) + */ +func Export(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { name := input form := name.Next.Eval(funcs, vars, false) diff --git a/util/scripts.go b/util/scripts.go new file mode 100644 index 0000000..3d2115b --- /dev/null +++ b/util/scripts.go @@ -0,0 +1,52 @@ +/* SHS: Syntactically Homogeneous Shell + * Copyright (C) 2019 Aidan Hahn + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package util + +import ( + "os" + "io/ioutil" + "gitlab.com/whom/shs/log" + "gitlab.com/whom/shs/ast" +) + +/* Opens a file and lexes+evaluates the contents + */ +func LoadScript(path string, vt ast.VarTable, ft ast.FuncTable) { + scriptFile, err := os.Open(path) + if err != nil { + log.Log(log.ERR, + "unable to open script: " + err.Error(), + "util") + return + } + + var body []byte + body, err = ioutil.ReadFile(path) + scriptFile.Close() + if err != nil { + log.Log(log.ERR, + "unable to read script: " + err.Error(), + "util") + return + } + + set := ast.Lex(string(body)) + for iter := set; iter != nil; iter = iter.Next { + iter.Eval(ft, vt, false) + } +} diff --git a/util/shell_complete.go b/util/shell_complete.go new file mode 100644 index 0000000..a6e3289 --- /dev/null +++ b/util/shell_complete.go @@ -0,0 +1,102 @@ +/* SHS: Syntactically Homogeneous Shell + * Copyright (C) 2019 Aidan Hahn + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package util + +import ( + "strings" + "io/ioutil" + "gitlab.com/whom/shs/log" + "gitlab.com/whom/shs/ast" +) + +/* gathers completions for the last word in a given line + * shell wraps this in a lambda that passes in the vt and ft + * I suppose it could be more optimal. Fix if it bothers you + */ +func ShellCompleter(line string, vt ast.VarTable, ft ast.FuncTable) []string { + var head, tail string + + idx := strings.LastIndex(line, " ") + if idx > 0 { + head = line[:idx+1] + tail = line[idx+1:] + } else { + head = "" + tail = line + } + + dir, path, tok := getPathBase(tail) + compSource := []string{} + + if !path { + dir = "." + } else { + line = tok + } + + fobjs, err := ioutil.ReadDir(dir) + if err != nil { + log.Log(log.DEBUG, + "couldnt read dir " + dir + ": " + err.Error(), + "complete") + if path { + return nil + } + } else { + for _, f := range fobjs { + compSource = append(compSource, dir + "/" + f.Name()) + } + } + + if !path { + compSource = append(compSource, ast.ListVars(vt)...) + compSource = append(compSource, ast.ListFuncs(ft)...) + } + + completions := []string{} + for _, i := range compSource { + if strings.HasPrefix(i, tail) { + completions = append(completions, head + i) + } + } + + return completions +} + +// returns everything up to the last '/' +// as well as whether or not a / was found +// and finally the token after the last / +func getPathBase(in string) (string, bool, string) { + if in == "" { + return "", false, "" + } + + isPath := false + + i := len(in) - 1 + for i > 0 { + if in[i] == '/' { + isPath = true + break + } + + i -= 1 + } + + return in[:i], isPath, in[i+1:] +}