From 37c56cb322ea02e41db0e834582f329537a662ac Mon Sep 17 00:00:00 2001 From: Aidan Date: Sun, 28 Jun 2020 17:00:44 -0700 Subject: [PATCH 01/27] fix bg/fg implementations --- stdlib/call.go | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/stdlib/call.go b/stdlib/call.go index 95e2050..c158979 100644 --- a/stdlib/call.go +++ b/stdlib/call.go @@ -30,6 +30,8 @@ import ( var bgProcs = make([]*exec.Cmd, 0) var LastExitCode int +var sigs = []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGTSTP, syscall.SIGTTIN, syscall.SIGTTOU, syscall.SIGCONT} + func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { if in == nil { @@ -63,7 +65,7 @@ func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { cmd.Stdin = os.Stdin signalChan := make(chan os.Signal, 2) - signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) + signal.Notify(signalChan, sigs...) go func() { sig := <-signalChan cmd.Process.Signal(sig) @@ -71,7 +73,7 @@ func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { err = cmd.Run() close(signalChan) - signal.Reset(os.Interrupt, syscall.SIGTERM) + signal.Reset(sigs...) if err != nil { if exitError, ok := err.(*exec.ExitError); ok { LastExitCode = exitError.ExitCode() @@ -113,7 +115,12 @@ func bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { cmd.Stderr = os.Stderr bgProcs = append(bgProcs, cmd) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + cmd.Start() + cmd.Process.Signal(syscall.SIGTSTP) return nil } @@ -125,19 +132,17 @@ func fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { cmd := bgProcs[0] bgProcs = bgProcs[1:] - cmd.Stdout = os.Stdout - cmd.Stdin = os.Stdin - signalChan := make(chan os.Signal, 2) - signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) + signal.Notify(signalChan, sigs...) go func() { sig := <-signalChan cmd.Process.Signal(sig) }() + cmd.Process.Signal(syscall.SIGCONT) err := cmd.Wait() close(signalChan) - signal.Reset(os.Interrupt, syscall.SIGTERM) + signal.Reset(sigs...) if err != nil { if exitError, ok := err.(*exec.ExitError); ok { LastExitCode = exitError.ExitCode() @@ -182,7 +187,7 @@ func read_cmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { cmd.Stdin = os.Stdin signalChan := make(chan os.Signal, 2) - signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) + signal.Notify(signalChan, sigs...) go func() { sig := <-signalChan cmd.Process.Signal(sig) @@ -190,7 +195,7 @@ func read_cmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { err = cmd.Run() close(signalChan) - signal.Reset(os.Interrupt, syscall.SIGTERM) + signal.Reset(sigs...) if err != nil { if exitError, ok := err.(*exec.ExitError); ok { LastExitCode = exitError.ExitCode() From e7d25057f67e87cc0081abf9c0b12aad42ae500f Mon Sep 17 00:00:00 2001 From: Aidan Date: Sun, 28 Jun 2020 18:01:36 -0700 Subject: [PATCH 02/27] add jobs, kill commands --- stdlib/call.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ stdlib/stdlib.go | 17 ++++++++++++ stdlib/string.go | 2 +- 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/stdlib/call.go b/stdlib/call.go index c158979..0956647 100644 --- a/stdlib/call.go +++ b/stdlib/call.go @@ -21,6 +21,7 @@ import ( "os" "fmt" "bytes" + "strconv" "os/exec" "syscall" "os/signal" @@ -154,6 +155,29 @@ 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 { + ret := &ast.Token{ + Tag: ast.LIST, + Inner: &ast.Token{ + Tag: ast.STRING, + Inner: fmt.Sprintf("Total: %d", len(bgProcs)), + }, + } + + ptr := ret.Inner.(*ast.Token) + iter := &ptr + for i := 0; i < len(bgProcs); i += 1 { + (*iter).Next = &ast.Token{ + Tag: ast.STRING, + Inner: fmt.Sprintf("[%d]: %d", i, bgProcs[i].Process.Pid), + } + + iter = &(*iter).Next + } + + return ret +} + func read_cmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { if in == nil { return nil @@ -211,3 +235,46 @@ func read_cmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { func get_exit(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return &ast.Token{Tag: ast.NUMBER, Inner: fmt.Sprintf("%d", LastExitCode)} } + +func kill(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + if in.Tag == ast.LIST { + log.Log(log.ERR, "non-number argument to kill function", "kill") + return nil + } + + pid, err := strconv.ParseInt(in.Inner.(string), 10, 64) + if err != nil { + log.Log(log.ERR, "error parsing arg to kill: " + err.Error(), "kill") + return nil + } + + found := false + newBgProcs := []*exec.Cmd{} + for _, i := range bgProcs { + if i.Process.Pid != int(pid) { + newBgProcs = append(newBgProcs, i) + } else { + found = true + err = i.Process.Kill() + if err != nil { + log.Log(log.ERR, fmt.Sprintf("error killing process %d: %s", + int(pid), err.Error()), "kill") + newBgProcs = append(newBgProcs, i) + } + } + } + + bgProcs = newBgProcs + + if !found { + // docs say no error on unix systems + proc, _ := os.FindProcess(int(pid)) + err = proc.Kill() + if err != nil { + log.Log(log.ERR, fmt.Sprintf("error killing process %d: %s", + int(pid), err.Error()), "kill") + } + } + + return nil +} diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index a7971f4..e4efcde 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -129,6 +129,23 @@ func GenFuncTable() ast.FuncTable { TimesCalled: 0, Args: 0, }, + + /* + USE NATIVE KILL COMMAND. + "kill": &ast.Function{ + Function: kill, + Name: "kill job", + TimesCalled: 0, + Args: 1, + }, + */ + + "jobs": &ast.Function{ + Function: jobs, + Name: "list jobs", + TimesCalled: 0, + Args: 0, + }, } return stdlib diff --git a/stdlib/string.go b/stdlib/string.go index 278bb30..f10ede7 100644 --- a/stdlib/string.go +++ b/stdlib/string.go @@ -39,6 +39,6 @@ func concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { } func print_str(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - fmt.Println(in.Inner.(string)) + fmt.Println(in) return nil } From 89d6a1013ba71c2cdb25257ba19f51036093f742 Mon Sep 17 00:00:00 2001 From: Aidan Date: Sun, 28 Jun 2020 20:06:55 -0700 Subject: [PATCH 03/27] boolean system --- .gitignore | 3 + ast/print.go | 2 +- ast/token.go | 1 + stdlib/bool.go | 155 +++++++++++++++++++++++++++++++++++++++++++++++ stdlib/stdlib.go | 49 +++++++++++++++ 5 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 stdlib/bool.go diff --git a/.gitignore b/.gitignore index 9b9aaf0..719ef32 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ print_ast *~ *.swp *.swo + +# notes +*.txt* diff --git a/ast/print.go b/ast/print.go index 8727b74..2460603 100644 --- a/ast/print.go +++ b/ast/print.go @@ -30,7 +30,7 @@ func (t *Token) String() string { case STRING: return "\"" + t.Inner.(string) + "\"" - case NUMBER: + case NUMBER, BOOL: return t.Inner.(string) case LIST: diff --git a/ast/token.go b/ast/token.go index 9d7bbef..69978cd 100644 --- a/ast/token.go +++ b/ast/token.go @@ -28,6 +28,7 @@ const ( STRING Token_t = iota NUMBER Token_t = iota SYMBOL Token_t = iota + BOOL Token_t = iota ) type Token struct { diff --git a/stdlib/bool.go b/stdlib/bool.go new file mode 100644 index 0000000..df2c413 --- /dev/null +++ b/stdlib/bool.go @@ -0,0 +1,155 @@ +/* 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 stdlib + +import ( + "strconv" + "gitlab.com/whom/shs/log" + "gitlab.com/whom/shs/ast" +) + +// lt, gt + +func not(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + if in.Tag != ast.BOOL { + log.Log(log.ERR, "non-bool argument to 'not'", "not") + return nil + } + + out := "T" + if in.Inner.(string) == "T" { + out = "F" + } + + return &ast.Token{Tag: ast.BOOL, Inner: out} +} + +func eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + out := "T" + second := in.Next + + if in.Tag != second.Tag { + out = "F" + } else { + switch in.Tag { + case ast.LIST: + // returns true if difference found + var consume_list func(*ast.Token, *ast.Token) bool + consume_list = func(l *ast.Token, r *ast.Token) bool { + if (l == nil && r != nil) || (r == nil && l != nil) { + return true + } + + if l.Tag != r.Tag { + return true + } + + l_iter := l + r_iter := r + for l_iter != nil { + if r_iter == nil || l_iter.Tag != r_iter.Tag { + return true + } + + if l_iter.Tag == ast.LIST { + diff := consume_list(l_iter.Inner.(*ast.Token), + r_iter.Inner.(*ast.Token)) + if diff { + return true + } + + } else { + if l_iter.Inner.(string) != r_iter.Inner.(string) { + return true + } + } + + l_iter = l_iter.Next + r_iter = r_iter.Next + } + + if r_iter != nil { + return true + } else { + return false + } + } + + if consume_list(in.Inner.(*ast.Token), second.Inner.(*ast.Token)) { + out = "F" + } + + case ast.NUMBER, ast.STRING, ast.BOOL: + if in.Inner.(string) != second.Inner.(string) { + out = "F" + } + } + } + + return &ast.Token{Tag: ast.BOOL, Inner: out} +} + +func lt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + out := "T" + + second := in.Next + if in.Tag != ast.NUMBER || second.Tag != ast.NUMBER { + log.Log(log.ERR, "non-number argument to numeric boolean operator", ">/<=") + return nil + } + + l, _ := strconv.ParseInt(in.Inner.(string), 10, 64) + r, _ := strconv.ParseInt(second.Inner.(string), 10, 64) + + if l >= r { + out = "F" + } + + return &ast.Token{Tag: ast.BOOL, Inner: out} +} + +func gt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + out := "T" + + second := in.Next + if in.Tag != ast.NUMBER || second.Tag != ast.NUMBER { + log.Log(log.ERR, "non-number argument to numeric boolean operator", ">/<=") + return nil + } + + l, _ := strconv.ParseInt(in.Inner.(string), 10, 64) + r, _ := strconv.ParseInt(second.Inner.(string), 10, 64) + + if l <= r { + out = "F" + } + + return &ast.Token{Tag: ast.BOOL, Inner: out} +} + +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 lte(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + return not(gt(in, vt, ft), vt, ft) +} diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index e4efcde..806c0f0 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -140,6 +140,55 @@ func GenFuncTable() ast.FuncTable { }, */ + "eq": &ast.Function{ + Function: eq, + Name: "==", + TimesCalled: 0, + Args: 2, + }, + + "ne": &ast.Function{ + Function: ne, + Name: "!=", + TimesCalled: 0, + Args: 2, + }, + + "<": &ast.Function{ + Function: lt, + Name: "<", + TimesCalled: 0, + Args: 2, + }, + + ">": &ast.Function{ + Function: gt, + Name: ">", + TimesCalled: 0, + Args: 2, + }, + + "<=": &ast.Function{ + Function: lte, + Name: "<=", + TimesCalled: 0, + Args: 2, + }, + + ">=": &ast.Function{ + Function: gte, + Name: ">=", + TimesCalled: 0, + Args: 2, + }, + + "!": &ast.Function{ + Function: not, + Name: "!", + TimesCalled: 0, + Args: 1, + }, + "jobs": &ast.Function{ Function: jobs, Name: "list jobs", From 2a2e5b4527eb88193538e6e360fc8c866a028266 Mon Sep 17 00:00:00 2001 From: Aidan Date: Mon, 29 Jun 2020 00:06:53 -0700 Subject: [PATCH 04/27] new eval.go --- ast/eval.go | 150 +++++++++++++------------ ast/lex.go | 185 ++++++++++++++++++++++++++++++ ast/print.go | 64 +---------- ast/token.go | 255 +++++++++++++++++------------------------ ast/var_table.go | 8 +- cmd/print_ast.go | 3 +- cmd/shs_repl.go | 10 +- stdlib/Readme.md | 47 -------- stdlib/arith.go | 198 -------------------------------- stdlib/bool.go | 155 ------------------------- stdlib/call.go | 280 ---------------------------------------------- stdlib/filesys.go | 37 ------ stdlib/list.go | 11 +- stdlib/stdlib.go | 150 ------------------------- stdlib/string.go | 44 -------- 15 files changed, 382 insertions(+), 1215 deletions(-) create mode 100644 ast/lex.go delete mode 100644 stdlib/Readme.md delete mode 100644 stdlib/arith.go delete mode 100644 stdlib/bool.go delete mode 100644 stdlib/call.go delete mode 100644 stdlib/filesys.go delete mode 100644 stdlib/string.go diff --git a/ast/eval.go b/ast/eval.go index ab1a29a..729bef5 100644 --- a/ast/eval.go +++ b/ast/eval.go @@ -17,88 +17,96 @@ package ast -import "gitlab.com/whom/shs/log" +import ( + "gitlab.com/whom/shs/log" +) -var CallExecutablesFromUndefFuncCalls = false -var CallExecutableToken = "l" +/* determines whether or not to execute a system call + * 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 +var ExecFunc = "l" -func (t *Token) Eval(funcs FuncTable, vars VarTable) (*Token, bool) { - if t == nil { - return nil, false +/* Runs through an AST of tokens + * Evaluates the Tokens to determine simplest form + * Returns simplest form + * + * canFunc determines whether a symbol could be a function to call + * (true when first elem of a list) + */ +func (in *Token) Eval(funcs FuncTable, vars VarTable) *Token { + if in == nil { + return nil } - var reduce func(*Token) *Token - reduce = func(t_ *Token) *Token { - var unwrap bool + var res *Token - if t_.Next != nil { - t_.Next = reduce(t_.Next) - } + switch in.Tag { + case BOOL, NUMBER, STRING: + res = in - switch (t_.Tag) { - case SYMBOL: - maybeToken := GetVar(t_.Inner.(string), vars) - if maybeToken != nil { - tok, _ := maybeToken.Eval(funcs, vars) - tok.Next = t_.Next - return tok - } + case SYMBOL: + res = GetVar(in.Value(), vars) + if res == nil { + res = in - case LIST: - t_.Inner, unwrap = t_.Inner.(*Token).Eval(funcs, vars) - if unwrap { - next := t_.Next - t_ = t_.Inner.(*Token) - if t_ == nil { - log.Log(log.DEBUG, "nil Inner on list unwrap", "eval") - return nil - } - - i := &t_ - for (*i).Next != nil { - i = &((*i).Next) - } - - (*i).Next = next - } - } - - return t_ - } - - ret := reduce(t) - if ret == nil { - log.Log(log.INFO, "reduce returned nil", "eval") - return nil, false - } - - //if symbol in front of a list, could be a function call - if ret.Tag == SYMBOL { - f := GetFunction(ret.Inner.(string), funcs) - if f == nil { - if CallExecutablesFromUndefFuncCalls { - f = GetFunction(CallExecutableToken, funcs) - if f == nil { - log.Log(log.DEBUG, "Symbol " + ret.Inner.(string) + - " has no definition in function table. Additionally " + - "the configured LoadExecutableToken is also not defined", - "eval") - return ret, false - } - - // see the use of CallFunction below - ret = &Token{Next: ret} - - } else { - log.Log(log.DEBUG, - "could not find definition for symbol " + ret.Inner.(string), + if GetFunction(in.Value(), funcs) == nil { + log.Log(log.ERR, + "undefined symbol: "+in.Value(), "eval") - return ret, false + return nil + } + } + res.Next = in.Next + + + case LIST: + inner := in.Expand() + if inner == nil { + res = in + break + } + + if inner.Tag != SYMBOL { + in.Direct(inner.Eval(funcs, vars)) + res = in + break + } + + makeHead := false + funct := GetFunction(inner.Value(), funcs) + if funct == nil { + if ExecWhenFuncUndef { + funct = GetFunction(ExecFunc, funcs) + makeHead = true } } - return (*f).CallFunction(ret.Next, vars, funcs), true + if funct != nil { + if makeHead { + inner = &Token{inner: inner} + } + res = funct.CallFunction(inner.Next, vars, funcs).Eval(funcs, vars) + res.Append(in.Next) + + } else { + log.Log(log.ERR, + "undefined function "+inner.Value()+" called", + "eval") + return nil + } + + default: + log.Log(log.ERR, + "Eval hit unknown token type!", + "eval") + return nil } - return ret, false + if res.Next != nil { + res.Next = res.Next.Eval(funcs, vars) + } + return res } diff --git a/ast/lex.go b/ast/lex.go new file mode 100644 index 0000000..f5e2ff4 --- /dev/null +++ b/ast/lex.go @@ -0,0 +1,185 @@ +/* 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 ast + +import ( + "gitlab.com/whom/shs/log" + "unicode" +) + +const string_delims string = "\"'`" + +func Lex(input string) *Token { + if len(input) == 0 { + return nil + } + + var ret *Token + iter := &ret + is_str := false + is_list := false + + tokenBuilder := func (pos int, tok string) { + if len(tok) == 0 && !is_list && !is_str { + return + } + + *iter = new(Token) + (*iter).Position = pos + + if is_list { + (*iter).inner = Lex(tok) + (*iter).Tag = LIST + is_list = false + + } else { + (*iter).inner = tok + if is_str { + (*iter).Tag = STRING + is_str = false + + } else if StrIsNumber(tok) { + (*iter).Tag = NUMBER + + } else { + (*iter).Tag = SYMBOL + } + } + + iter = &(*iter).Next + } + + // returns -1 on unmatched string delim + matchStrEnd := func(start int, delim byte) int { + for i := start; i < len(input); i++ { + if input[i] == delim { + return i + } + } + + return -1 + } + + // returns -1 on unmatched string delim + // returns -2 on unmatched list delim + matchListEnd := func(start int) int { + depth := 0 + + for i := start; i < len(input); i++ { + switch input[i] { + case '"','\'','`': + i = matchStrEnd(i + 1, input[i]) + if i == -1 { + return -1 + } + + case '(': + depth++ + + case ')': + if depth == 0 { + return i + } else { + depth -= 1 + } + } + } + + return -2 + } + + needs_alloc := false + start_pos := 0 + for i := 0; i < len(input); i++ { + switch input[i] { + case '(': + start_pos = i + 1 + i = matchListEnd(start_pos) + is_list = true + needs_alloc = true + + case '"','\'','`': + start_pos = i + 1 + i = matchStrEnd(start_pos, input[i]) + is_str = true + needs_alloc = true + + case ' ': + if i == start_pos { + start_pos += 1 + continue + } + + needs_alloc = true + } + + if needs_alloc { + needs_alloc = false + if (i < 0) { + // TODO: Maybe not overload this. + start_pos = i + goto error + } + + tokenBuilder(start_pos, input[start_pos:i]) + start_pos = i+1 + } + } + + if start_pos < len(input) { + tokenBuilder(start_pos, input[start_pos:]) + } + + return ret + +error: + // TODO: Hook into error module + // TODO: Finalize and GC alloced tokens + if start_pos == -1 { + log.Log(log.ERR, + "Unmatched string delimiter in input. discarding.", + "lex") + } else if start_pos == -2 { + log.Log(log.ERR, + "Unmatched list delimiter in input. discarding.", + "lex") + } else { + log.Log(log.ERR, + "Unknown error in input. discarding.", + "lex") + } + + return nil +} + +func StrIsNumber(arg string) bool { + dotCount := 0 + + for _, char := range arg { + if !unicode.IsDigit(char) { + if char == '.' && dotCount == 0 { + dotCount++ + } else { + return false + } + } + } + + return true +} + diff --git a/ast/print.go b/ast/print.go index 2460603..944b698 100644 --- a/ast/print.go +++ b/ast/print.go @@ -19,39 +19,8 @@ package ast import ( "strings" - "fmt" ) -/* Print function which is better suited for repl. - * This one prints the SEXPRs as one would write them. - */ -func (t *Token) String() string { - switch t.Tag { - case STRING: - return "\"" + t.Inner.(string) + "\"" - - case NUMBER, BOOL: - return t.Inner.(string) - - case LIST: - repr := "(" - if t.Inner.(*Token) == nil { - return repr + ")" - } - - for i := t.Inner.(*Token); i != nil; i = i.Next { - repr = repr + i.String() + " " - } - // remove trailing space - return repr[:len(repr)-1] + ")" - - case SYMBOL: - return "<" + t.Inner.(string) + ">" - } - - return "[UNKNOWN CELL TYPE]" -} - /* Print function which breaks each embedded list out on individual lines. * Used in the print_ast debug tool. not too useful for repl applications. * Very useful for debugging syntax though. @@ -74,42 +43,13 @@ loop: for iter := i; iter != nil; iter = iter.Next { if iter.Tag == LIST { - lists.Push(iter.Inner.(*Token)) + lists.Push(iter.Expand()) } - constructor.WriteString(FmtToken(iter)) + constructor.WriteString(iter.FmtToken()) } println(constructor.String()) goto loop } -func FmtToken(arg *Token) string { - suffix := "->" - if arg.Next == nil { - suffix = "" - } - - switch arg.Tag { - case LIST: - return fmt.Sprintf("(%s, [List])%s", "LIST", suffix) - - default: - return fmt.Sprintf("(%s, %s)%s", GetTagAsStr(arg.Tag), arg.Inner, suffix) - } -} - -func GetTagAsStr(tag Token_t) string { - switch tag { - case LIST: - return "LIST" - case STRING: - return "STRING" - case NUMBER: - return "NUMBER" - case SYMBOL: - return "SYMBOL" - } - return "UNKNOWN" -} - diff --git a/ast/token.go b/ast/token.go index 69978cd..aee78b6 100644 --- a/ast/token.go +++ b/ast/token.go @@ -17,10 +17,7 @@ package ast -import ( - "gitlab.com/whom/shs/log" - "unicode" -) +import "fmt" type Token_t int const ( @@ -35,166 +32,116 @@ type Token struct { Next *Token Tag Token_t Position int - Inner interface{} + inner interface{} } -const string_delims string = "\"'`" +/* Appends another token to the end of this token list + */ +func (t *Token) Append(arg *Token) { + if t.Next != nil { + t.Next.Append(arg) + } else { + t.Next = arg + } +} -func Lex(input string) *Token { - if len(input) == 0 { +/* Print function which is better suited for repl. + * This one prints the SEXPRs as one would write them. + * Does not evaluate tokens. + */ +func (t *Token) String() string { + switch t.Tag { + case STRING: + return "\"" + t.inner.(string) + "\"" + + case NUMBER, BOOL: + return t.inner.(string) + + case LIST: + repr := "(" + if t.inner.(*Token) == nil { + return repr + ")" + } + + for i := t.inner.(*Token); i != nil; i = i.Next { + repr = repr + i.String() + " " + } + // remove trailing space + return repr[:len(repr)-1] + ")" + + case SYMBOL: + return "<" + t.inner.(string) + ">" + } + + return "[UNKNOWN CELL TYPE]" +} + +/* Returns a list held by a token + * returns nil if token holds no list + */ +func (t *Token) Expand() *Token { + if t.Tag != LIST { return nil } - var ret *Token - iter := &ret - is_str := false - is_list := false - - tokenBuilder := func (pos int, tok string) { - if len(tok) == 0 && !is_list && !is_str { - return - } - - *iter = new(Token) - (*iter).Position = pos - - if is_list { - (*iter).Inner = Lex(tok) - (*iter).Tag = LIST - is_list = false - - } else { - (*iter).Inner = tok - if is_str { - (*iter).Tag = STRING - is_str = false - - } else if StrIsNumber(tok) { - (*iter).Tag = NUMBER - - } else { - (*iter).Tag = SYMBOL - } - } - - iter = &(*iter).Next - } - - // returns -1 on unmatched string delim - matchStrEnd := func(start int, delim byte) int { - for i := start; i < len(input); i++ { - if input[i] == delim { - return i - } - } - - return -1 - } - - // returns -1 on unmatched string delim - // returns -2 on unmatched list delim - matchListEnd := func(start int) int { - depth := 0 - - for i := start; i < len(input); i++ { - switch input[i] { - case '"','\'','`': - i = matchStrEnd(i + 1, input[i]) - if i == -1 { - return -1 - } - - case '(': - depth++ - - case ')': - if depth == 0 { - return i - } else { - depth -= 1 - } - } - } - - return -2 - } - - needs_alloc := false - start_pos := 0 - for i := 0; i < len(input); i++ { - switch input[i] { - case '(': - start_pos = i + 1 - i = matchListEnd(start_pos) - is_list = true - needs_alloc = true - - case '"','\'','`': - start_pos = i + 1 - i = matchStrEnd(start_pos, input[i]) - is_str = true - needs_alloc = true - - case ' ': - if i == start_pos { - start_pos += 1 - continue - } - - needs_alloc = true - } - - if needs_alloc { - needs_alloc = false - if (i < 0) { - // TODO: Maybe not overload this. - start_pos = i - goto error - } - - tokenBuilder(start_pos, input[start_pos:i]) - start_pos = i+1 - } - } - - if start_pos < len(input) { - tokenBuilder(start_pos, input[start_pos:]) - } - return ret - -error: - // TODO: Hook into error module - // TODO: Finalize and GC alloced tokens - if start_pos == -1 { - log.Log(log.ERR, - "Unmatched string delimiter in input. discarding.", - "lex") - } else if start_pos == -2 { - log.Log(log.ERR, - "Unmatched list delimiter in input. discarding.", - "lex") - } else { - log.Log(log.ERR, - "Unknown error in input. discarding.", - "lex") - } - - return nil + return t.inner.(*Token) } -func StrIsNumber(arg string) bool { - dotCount := 0 - - for _, char := range arg { - if !unicode.IsDigit(char) { - if char == '.' && dotCount == 0 { - dotCount++ - } else { - return false - } - } +/* Sets inner to a Token value + * returns false if parent token is not a list + * otherwise returns true + */ +func (t *Token) Direct(head *Token) bool { + if t.Tag != LIST { + return false } + t.inner = head return true } +/* If token holds an atomic value + * (not a symbol or list) + * will return its value as a string + * else returns "" + */ +func (t *Token) Value() string { + if t.Tag == LIST { + return "" + } + + return t.inner.(string) +} + +/* returns an ascii representation of a token + */ +func (t *Token) FmtToken() string { + suffix := "->" + if t.Next == nil { + suffix = "" + } + + switch t.Tag { + case LIST: + return fmt.Sprintf("(%s, [List])%s", "LIST", suffix) + + default: + return fmt.Sprintf("(%s, %s)%s", GetTagAsStr(t.Tag), t.inner.(string), suffix) + } +} + +/* Returns a tag in text + */ +func GetTagAsStr(tag Token_t) string { + switch tag { + case LIST: + return "LIST" + case STRING: + return "STRING" + case NUMBER: + return "NUMBER" + case SYMBOL: + return "SYMBOL" + } + return "UNKNOWN" +} diff --git a/ast/var_table.go b/ast/var_table.go index 0042545..ae0519a 100644 --- a/ast/var_table.go +++ b/ast/var_table.go @@ -40,7 +40,7 @@ func GetVar(arg string, vt VarTable) *Token { e := os.Getenv(arg) if e != "" { - t := &Token{Inner: e} + t := &Token{inner: e} if StrIsNumber(e) { t.Tag = NUMBER } else { @@ -56,11 +56,13 @@ func GetVar(arg string, vt VarTable) *Token { } // TODO: this could be much more optimal +// TODO: Make sure variables are evaluated before being set +// probably a stdlib thing func SetVar(variable string, value *Token, vt VarTable) { (*vt)[variable] = value if SyncTablesWithOSEnviron && (value.Tag == NUMBER || value.Tag == STRING) { - token := value.Inner.(string) + token := value.Value() if value.Tag == NUMBER { // make sure its an int a, err := strconv.ParseFloat(token, 64) @@ -91,7 +93,7 @@ func GetVarFromTables(arg string, library []VarTable) *Token { func InitVarTable(table VarTable) { for _, val := range os.Environ() { variable := strings.Split(val, "=") - t := &Token{Inner: variable[1]} + t := &Token{inner: variable[1]} if StrIsNumber(variable[1]) { t.Tag = NUMBER } else { diff --git a/cmd/print_ast.go b/cmd/print_ast.go index fa2e029..81db310 100644 --- a/cmd/print_ast.go +++ b/cmd/print_ast.go @@ -20,10 +20,9 @@ package main import ( "strings" "os" - "gitlab.com/whom/shs/log" "gitlab.com/whom/shs/ast" ) func main() { - log.PrintSExprsIndividually(ast.Lex(strings.Join(os.Args[1:], " "))) + ast.PrintSExprsIndividually(ast.Lex(strings.Join(os.Args[1:], " "))) } diff --git a/cmd/shs_repl.go b/cmd/shs_repl.go index 9900b4e..4635c05 100644 --- a/cmd/shs_repl.go +++ b/cmd/shs_repl.go @@ -46,8 +46,6 @@ func setLogLvl() { } func main() { - ast.CallExecutablesFromUndefFuncCalls = true - debug := os.Getenv("SH_DEBUG_MODE") hist := os.Getenv("SH_HIST_FILE") prompt := os.Getenv("SHS_SH_PROMPT") @@ -57,8 +55,10 @@ func main() { funcs = stdlib.GenFuncTable() vars = &map[string]*ast.Token{} + ast.InitVarTable(vars) ast.SyncTablesWithOSEnviron = true + ast.ExecWhenFuncUndef = false var err error @@ -96,12 +96,8 @@ func main() { ast.PrintSExprsIndividually(userInput) } - result, unwrap := userInput.Eval(funcs, vars) + result := userInput.Eval(funcs, vars) if result != nil { - if result.Tag == ast.LIST && unwrap { - result = result.Inner.(*ast.Token) - } - for i := result; i != nil; i = i.Next { fmt.Printf(i.String() + " ") } diff --git a/stdlib/Readme.md b/stdlib/Readme.md deleted file mode 100644 index afec954..0000000 --- a/stdlib/Readme.md +++ /dev/null @@ -1,47 +0,0 @@ -# SHS STDLIB - -In here are definitions for builtin functions that can be used from the repl. If you are looking for a way to extend this shell to fit your use case you are in the right spot. - -## Datatypes - -Within the AST package you can see [a number of important definitions.]("https://git.callpipe.com/aidan/shs/-/blob/master/ast/func_table.go") Worth noting are FuncTable, Operation, and Function. - -```go -type Operation func(*Token, VarTable, FuncTable) *Token - -type Function struct { - function Operation - name string - timesCalled int - args int -} - -type FuncTable map[string]*Function -``` - -## The Standard Library - -The standard library is loaded during the init step of the repl (or interpreter if you have embedded one). Alternatively, it can be loaded into your application by importing `git.callpipe.com/shs/stdlib`. In order to get a table of all available functions the `GenFuncTable` function is called, returning a struct of type `ast.FuncTable`. Any functions in the standard lib must be accounted for here in order for them to be available at repl or interpreter. Each Operation operates on a list of [Tokens](https://git.callpipe.com/aidan/shs/-/blob/master/ast/token.go). Every function should adhere to the idea that it takes in a list of Tokens and returns a list of Tokens. - -## Working with Tokens -[Tokens](https://git.callpipe.com/aidan/shs/-/blob/master/ast/token.go) are a rudimentary linked list of parsed [Lexemes](https://en.wikipedia.org/wiki/Lexeme). In the ast package there are definitions for Tokens, as well as code for the combined Lex/Parse loop that creates them. Tokens are built in a way that makes operating over them with either recursive or iterative alrogithms easy. When consuming Tokens, one can expect their type by looking at the Tag field. The data stored in the Inner field will be either a string or a \*Token depending on what Tag is. You can expect a \*Token if the Tag field is ast.LIST, and a string in all other cases. If the Tag field is ast.SYMBOL you can look it up in the VarTable or the FuncTable. The VarTable will return either a \*Token (if the symbol is a Variable) or *nil* if nothing is found. The FuncTable will return either a \*Function (if there is a match) or it will return *nil*. -P.S.: Ideally a token should not be re-used. You may consider them disposable. It is up to you to make sure that any Token you edit/reuse remains consistant with the type declared in its TAG. Make sure to differentiate between NUMBER and STRING with the `ast.StrIsNumber(arg string) bool` function. -## Adding a function -1. *Write your function in the form of an `ast.Operation`.* Any function that has the defined signature can be an Operation. -2. *Create a `Function` to encapsulate your `Operation`.* Make sure to set the `args` and `name` fields. Args will be used to validate function calls and Name will be used in debug/log output. -3. *Add your `Function` to the `FuncTable`.* Make sure your `Operations`s get added to the table generated in `GenFuncTable`. - -## Tips -- You can use the Log module to add helpful output to your functions. -- Try not to clutter the output with unnessesary ERR level logging. -- Make sure you properly set the Next element of any Token you swap into a list -- Make sure you properly set the Next element of the previous Token as you swap a token into a list - -## License -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 https://www.gnu.org/licenses/ diff --git a/stdlib/arith.go b/stdlib/arith.go deleted file mode 100644 index a5b1274..0000000 --- a/stdlib/arith.go +++ /dev/null @@ -1,198 +0,0 @@ -/* 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 stdlib - -import ( - "fmt" - "strconv" - "gitlab.com/whom/shs/ast" - "gitlab.com/whom/shs/log" -) - -// PKG WIDE TODO: BIGNUM SYSTEM -// then write actually optimal routines once it is in place -// 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 { - var res float64 - - for i := in; i != nil; i = i.Next { - if i.Tag != ast.NUMBER { - log.Log(log.ERR, "Non-number given to ADD", "add") - return nil - } - - token := i.Inner.(string) - isFloat := false - for _, char := range token { - if char == '.' { - isFloat = true - n, err := strconv.ParseFloat(token, 64) - if err != nil { - log.Log(log.ERR, "Err parsing number: " + err.Error(), "add") - return nil - } - - res += n - } - } - - if !isFloat { - n, err := strconv.ParseInt(token, 10, 64) - if err != nil { - log.Log(log.ERR, "Err parsing number: " + err.Error(), "add") - return nil - } - - res += float64(n) - } - } - - return &ast.Token{Tag: ast.NUMBER, Inner: fmt.Sprintf("%f", res)} -} - -func sub(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { - var res float64 - var sub float64 - - for i := in; i != nil; i = i.Next { - if i.Tag != ast.NUMBER { - log.Log(log.ERR, "Non-number given to SUB", "sub") - return nil - } - - token := i.Inner.(string) - isFloat := false - var inner float64 - for _, char := range token { - if char == '.' { - isFloat = true - n, err := strconv.ParseFloat(token, 64) - if err != nil { - log.Log(log.ERR, "Err parsing number: " + err.Error(), "sub") - return nil - } - - inner = n - } - } - - if !isFloat { - n, err := strconv.ParseInt(token, 10, 64) - if err != nil { - log.Log(log.ERR, "Err parsing number: " + err.Error(), "sub") - return nil - } - - inner = float64(n) - } - - if i.Next != nil { - sub += inner - } else { - res = inner - } - } - - return &ast.Token{Tag: ast.NUMBER, Inner: fmt.Sprintf("%f", res - sub)} -} - -func mult(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { - res := 1.0 - - for i := in; i != nil; i = i.Next { - if i.Tag != ast.NUMBER { - log.Log(log.ERR, "Non-number given to MULT", "mult") - return nil - } - - token := i.Inner.(string) - isFloat := false - for _, char := range token { - if char == '.' { - isFloat = true - n, err := strconv.ParseFloat(token, 64) - if err != nil { - log.Log(log.ERR, "Err parsing number: " + err.Error(), "mult") - return nil - } - - res *= n - } - } - - if !isFloat { - n, err := strconv.ParseInt(token, 10, 64) - if err != nil { - log.Log(log.ERR, "Err parsing number: " + err.Error(), "mult") - return nil - } - - res *= float64(n) - } - } - - return &ast.Token{Tag: ast.NUMBER, Inner: fmt.Sprintf("%f", res)} -} - -func div(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { - var res float64 - - for i := in; i != nil; i = i.Next { - inner := 0.0 - - if i.Tag != ast.NUMBER { - log.Log(log.ERR, "Non-number given to DIV", "div") - return nil - } - - token := i.Inner.(string) - isFloat := false - for _, char := range token { - if char == '.' { - isFloat = true - n, err := strconv.ParseFloat(token, 64) - if err != nil { - log.Log(log.ERR, "Err parsing number: " + err.Error(), "div") - return nil - } - - inner = n - } - } - - if !isFloat { - n, err := strconv.ParseInt(token, 10, 64) - if err != nil { - log.Log(log.ERR, "Err parsing number: " + err.Error(), "div") - return nil - } - - inner = float64(n) - } - - if i == in { - res = inner - } else { - res = res / inner - } - } - - return &ast.Token{Tag: ast.NUMBER, Inner: fmt.Sprintf("%f", res)} -} diff --git a/stdlib/bool.go b/stdlib/bool.go deleted file mode 100644 index df2c413..0000000 --- a/stdlib/bool.go +++ /dev/null @@ -1,155 +0,0 @@ -/* 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 stdlib - -import ( - "strconv" - "gitlab.com/whom/shs/log" - "gitlab.com/whom/shs/ast" -) - -// lt, gt - -func not(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - if in.Tag != ast.BOOL { - log.Log(log.ERR, "non-bool argument to 'not'", "not") - return nil - } - - out := "T" - if in.Inner.(string) == "T" { - out = "F" - } - - return &ast.Token{Tag: ast.BOOL, Inner: out} -} - -func eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - out := "T" - second := in.Next - - if in.Tag != second.Tag { - out = "F" - } else { - switch in.Tag { - case ast.LIST: - // returns true if difference found - var consume_list func(*ast.Token, *ast.Token) bool - consume_list = func(l *ast.Token, r *ast.Token) bool { - if (l == nil && r != nil) || (r == nil && l != nil) { - return true - } - - if l.Tag != r.Tag { - return true - } - - l_iter := l - r_iter := r - for l_iter != nil { - if r_iter == nil || l_iter.Tag != r_iter.Tag { - return true - } - - if l_iter.Tag == ast.LIST { - diff := consume_list(l_iter.Inner.(*ast.Token), - r_iter.Inner.(*ast.Token)) - if diff { - return true - } - - } else { - if l_iter.Inner.(string) != r_iter.Inner.(string) { - return true - } - } - - l_iter = l_iter.Next - r_iter = r_iter.Next - } - - if r_iter != nil { - return true - } else { - return false - } - } - - if consume_list(in.Inner.(*ast.Token), second.Inner.(*ast.Token)) { - out = "F" - } - - case ast.NUMBER, ast.STRING, ast.BOOL: - if in.Inner.(string) != second.Inner.(string) { - out = "F" - } - } - } - - return &ast.Token{Tag: ast.BOOL, Inner: out} -} - -func lt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - out := "T" - - second := in.Next - if in.Tag != ast.NUMBER || second.Tag != ast.NUMBER { - log.Log(log.ERR, "non-number argument to numeric boolean operator", ">/<=") - return nil - } - - l, _ := strconv.ParseInt(in.Inner.(string), 10, 64) - r, _ := strconv.ParseInt(second.Inner.(string), 10, 64) - - if l >= r { - out = "F" - } - - return &ast.Token{Tag: ast.BOOL, Inner: out} -} - -func gt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - out := "T" - - second := in.Next - if in.Tag != ast.NUMBER || second.Tag != ast.NUMBER { - log.Log(log.ERR, "non-number argument to numeric boolean operator", ">/<=") - return nil - } - - l, _ := strconv.ParseInt(in.Inner.(string), 10, 64) - r, _ := strconv.ParseInt(second.Inner.(string), 10, 64) - - if l <= r { - out = "F" - } - - return &ast.Token{Tag: ast.BOOL, Inner: out} -} - -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 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 deleted file mode 100644 index 0956647..0000000 --- a/stdlib/call.go +++ /dev/null @@ -1,280 +0,0 @@ -/* 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 stdlib - -import ( - "os" - "fmt" - "bytes" - "strconv" - "os/exec" - "syscall" - "os/signal" - "gitlab.com/whom/shs/ast" - "gitlab.com/whom/shs/log" -) - -var bgProcs = make([]*exec.Cmd, 0) -var LastExitCode int -var sigs = []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGTSTP, syscall.SIGTTIN, syscall.SIGTTOU, syscall.SIGCONT} - - -func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - if in == nil { - return nil - } - - path, err := exec.LookPath(in.Inner.(string)) - if err != nil { - log.Log(log.ERR, "Couldnt exec " + in.Inner.(string) + ", file not found", "call") - return nil - } - - args := []string{} - for i := in.Next; i != nil; i = i.Next { - if i.Tag == ast.LIST { - log.Log(log.ERR, "Couldnt exec " + path + ", element in arguments is a list", "call") - return nil - } - - args = append(args, i.Inner.(string)) - } - - var cmd *exec.Cmd - if len(args) > 0 { - cmd = exec.Command(path, args...) - } else { - cmd = exec.Command(path) - } - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - signalChan := make(chan os.Signal, 2) - signal.Notify(signalChan, sigs...) - go func() { - sig := <-signalChan - cmd.Process.Signal(sig) - }() - - err = cmd.Run() - close(signalChan) - signal.Reset(sigs...) - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - LastExitCode = exitError.ExitCode() - } else { - log.Log(log.ERR, "Execution step returned unparsable error: " + err.Error(), "call") - } - } - - return nil -} - -func bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - if in == nil { - return nil - } - - path, err := exec.LookPath(in.Inner.(string)) - if err != nil { - log.Log(log.ERR, "Couldnt exec " + in.Inner.(string) + ", file not found", "call") - return nil - } - - args := []string{} - for i := in.Next; i != nil; i = i.Next { - if i.Tag == ast.LIST { - log.Log(log.ERR, "Couldnt exec " + path + ", element in arguments is a list", "call") - return nil - } - - args = append(args, i.Inner.(string)) - } - - var cmd *exec.Cmd - if len(args) > 0 { - cmd = exec.Command(path, args...) - } else { - cmd = exec.Command(path) - } - cmd.Stderr = os.Stderr - - bgProcs = append(bgProcs, cmd) - cmd.Stdout = os.Stdout - cmd.Stdin = os.Stdin - cmd.Stderr = os.Stderr - - cmd.Start() - cmd.Process.Signal(syscall.SIGTSTP) - return nil -} - -func fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - if len(bgProcs) < 1 { - return nil - } - - cmd := bgProcs[0] - bgProcs = bgProcs[1:] - - signalChan := make(chan os.Signal, 2) - signal.Notify(signalChan, sigs...) - go func() { - sig := <-signalChan - cmd.Process.Signal(sig) - }() - - cmd.Process.Signal(syscall.SIGCONT) - err := cmd.Wait() - close(signalChan) - signal.Reset(sigs...) - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - LastExitCode = exitError.ExitCode() - } else { - log.Log(log.ERR, "Execution step returned error: " + err.Error(), "call") - } - } - - return nil -} - -func jobs(in *ast.Token, vt ast.VarTable, fg ast.FuncTable) *ast.Token { - ret := &ast.Token{ - Tag: ast.LIST, - Inner: &ast.Token{ - Tag: ast.STRING, - Inner: fmt.Sprintf("Total: %d", len(bgProcs)), - }, - } - - ptr := ret.Inner.(*ast.Token) - iter := &ptr - for i := 0; i < len(bgProcs); i += 1 { - (*iter).Next = &ast.Token{ - Tag: ast.STRING, - Inner: fmt.Sprintf("[%d]: %d", i, bgProcs[i].Process.Pid), - } - - iter = &(*iter).Next - } - - return ret -} - -func read_cmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - if in == nil { - return nil - } - - var out bytes.Buffer - path, err := exec.LookPath(in.Inner.(string)) - if err != nil { - log.Log(log.ERR, "Couldnt exec " + in.Inner.(string) + ", file not found", "call") - return nil - } - - args := []string{} - for i := in.Next; i != nil; i = i.Next { - if i.Tag == ast.LIST { - log.Log(log.ERR, "Couldnt exec " + path + ", element in arguments is a list", "call") - return nil - } - - args = append(args, i.Inner.(string)) - } - - var cmd *exec.Cmd - if len(args) > 0 { - cmd = exec.Command(path, args...) - } else { - cmd = exec.Command(path) - } - cmd.Stdout = &out - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - signalChan := make(chan os.Signal, 2) - signal.Notify(signalChan, sigs...) - go func() { - sig := <-signalChan - cmd.Process.Signal(sig) - }() - - err = cmd.Run() - close(signalChan) - signal.Reset(sigs...) - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - LastExitCode = exitError.ExitCode() - } else { - log.Log(log.ERR, "Execution step returned error: " + err.Error(), "$") - } - } - - return &ast.Token{Tag: ast.STRING, Inner: out.String()} - -} - -func get_exit(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - return &ast.Token{Tag: ast.NUMBER, Inner: fmt.Sprintf("%d", LastExitCode)} -} - -func kill(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - if in.Tag == ast.LIST { - log.Log(log.ERR, "non-number argument to kill function", "kill") - return nil - } - - pid, err := strconv.ParseInt(in.Inner.(string), 10, 64) - if err != nil { - log.Log(log.ERR, "error parsing arg to kill: " + err.Error(), "kill") - return nil - } - - found := false - newBgProcs := []*exec.Cmd{} - for _, i := range bgProcs { - if i.Process.Pid != int(pid) { - newBgProcs = append(newBgProcs, i) - } else { - found = true - err = i.Process.Kill() - if err != nil { - log.Log(log.ERR, fmt.Sprintf("error killing process %d: %s", - int(pid), err.Error()), "kill") - newBgProcs = append(newBgProcs, i) - } - } - } - - bgProcs = newBgProcs - - if !found { - // docs say no error on unix systems - proc, _ := os.FindProcess(int(pid)) - err = proc.Kill() - if err != nil { - log.Log(log.ERR, fmt.Sprintf("error killing process %d: %s", - int(pid), err.Error()), "kill") - } - } - - return nil -} diff --git a/stdlib/filesys.go b/stdlib/filesys.go deleted file mode 100644 index dee9f0b..0000000 --- a/stdlib/filesys.go +++ /dev/null @@ -1,37 +0,0 @@ -/* 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 stdlib - -import ( - "os" - "gitlab.com/whom/shs/ast" - "gitlab.com/whom/shs/log" -) - -func cd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - if in.Tag == ast.LIST { - log.Log(log.ERR, "Couldnt change dir to a list", "cd") - return nil - } - - err := os.Chdir(in.Inner.(string)) - if err != nil { - log.Log(log.ERR, err.Error(), "cd") - } - return nil -} diff --git a/stdlib/list.go b/stdlib/list.go index 3c59a1b..e97270b 100644 --- a/stdlib/list.go +++ b/stdlib/list.go @@ -33,7 +33,7 @@ func expand(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token return input } - return input.Inner.(*ast.Token) + return input.Expand() } /* L_APPEND (append from repl) @@ -45,15 +45,16 @@ func l_append(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Tok src := input if input.Tag != ast.LIST { - // TODO: position?? - return input + r := &ast.Token{Tag: ast.LIST} + r.Direct(input) + return r } // deref inner first - i := src.Inner.(*ast.Token) + i := src.Expand() iter := &i if *iter == nil { - src.Inner = input.Next + src.Direct(input.Next) src.Next = nil } else { diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 806c0f0..97d6018 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -39,162 +39,12 @@ func GenFuncTable() ast.FuncTable { Args: -1, }, - "+": &ast.Function{ - Function: add, - Name: "add", - TimesCalled: 0, - Args: -1, - }, - - "-": &ast.Function{ - Function: sub, - Name: "sub", - TimesCalled: 0, - Args: -1, - }, - - "*": &ast.Function{ - Function: mult, - Name: "mult", - TimesCalled: 0, - Args: -1, - }, - - "/": &ast.Function{ - Function: div, - Name: "div", - TimesCalled: 0, - Args: -1, - }, - - "l": &ast.Function{ - Function: call, - Name: "call", - TimesCalled: 0, - Args: -1, - }, - - "bg": &ast.Function{ - Function: bgcall, - Name: "background call", - TimesCalled: 0, - Args: -1, - }, - - "fg": &ast.Function{ - Function: fg, - Name: "foreground", - TimesCalled: 0, - Args: 0, - }, - - "cd": &ast.Function{ - Function: cd, - Name: "changedir", - TimesCalled: 0, - Args: 1, - }, - - "$": &ast.Function{ - Function: read_cmd, - Name: "read cmd", - TimesCalled: 0, - Args: -1, - }, - - "concat": &ast.Function{ - Function: concat, - Name:"concatenate", - TimesCalled: 0, - Args: -1, - }, - - "print": &ast.Function{ - Function:print_str, - Name: "print", - TimesCalled: 0, - Args: 1, - }, - "exit": &ast.Function{ Function: exit_shell, Name: "exit", TimesCalled: 0, Args: 0, }, - - "?": &ast.Function{ - Function: get_exit, - Name:"get exit code", - TimesCalled: 0, - Args: 0, - }, - - /* - USE NATIVE KILL COMMAND. - "kill": &ast.Function{ - Function: kill, - Name: "kill job", - TimesCalled: 0, - Args: 1, - }, - */ - - "eq": &ast.Function{ - Function: eq, - Name: "==", - TimesCalled: 0, - Args: 2, - }, - - "ne": &ast.Function{ - Function: ne, - Name: "!=", - TimesCalled: 0, - Args: 2, - }, - - "<": &ast.Function{ - Function: lt, - Name: "<", - TimesCalled: 0, - Args: 2, - }, - - ">": &ast.Function{ - Function: gt, - Name: ">", - TimesCalled: 0, - Args: 2, - }, - - "<=": &ast.Function{ - Function: lte, - Name: "<=", - TimesCalled: 0, - Args: 2, - }, - - ">=": &ast.Function{ - Function: gte, - Name: ">=", - TimesCalled: 0, - Args: 2, - }, - - "!": &ast.Function{ - Function: not, - Name: "!", - TimesCalled: 0, - Args: 1, - }, - - "jobs": &ast.Function{ - Function: jobs, - Name: "list jobs", - TimesCalled: 0, - Args: 0, - }, } return stdlib diff --git a/stdlib/string.go b/stdlib/string.go deleted file mode 100644 index f10ede7..0000000 --- a/stdlib/string.go +++ /dev/null @@ -1,44 +0,0 @@ -/* 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 stdlib - -import ( - "fmt" - "gitlab.com/whom/shs/ast" - "gitlab.com/whom/shs/log" -) - -func concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - var res string - for i := in; i != nil; i = i.Next { - if i.Tag == ast.LIST { - log.Log(log.ERR, "Not concatenating list", "conc") - log.Log(log.DEBUG, "Try using the expand operator (...)", "conc") - continue - } - - res += i.Inner.(string) - } - - return &ast.Token{Tag: ast.STRING, Inner: res} -} - -func print_str(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - fmt.Println(in) - return nil -} From 2151b6c5d27bc5a33ba27f01848ec45636f6d3df Mon Sep 17 00:00:00 2001 From: Aidan Date: Mon, 29 Jun 2020 00:10:54 -0700 Subject: [PATCH 05/27] fixed expand function --- stdlib/Readme.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ stdlib/list.go | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 stdlib/Readme.md diff --git a/stdlib/Readme.md b/stdlib/Readme.md new file mode 100644 index 0000000..afec954 --- /dev/null +++ b/stdlib/Readme.md @@ -0,0 +1,47 @@ +# SHS STDLIB + +In here are definitions for builtin functions that can be used from the repl. If you are looking for a way to extend this shell to fit your use case you are in the right spot. + +## Datatypes + +Within the AST package you can see [a number of important definitions.]("https://git.callpipe.com/aidan/shs/-/blob/master/ast/func_table.go") Worth noting are FuncTable, Operation, and Function. + +```go +type Operation func(*Token, VarTable, FuncTable) *Token + +type Function struct { + function Operation + name string + timesCalled int + args int +} + +type FuncTable map[string]*Function +``` + +## The Standard Library + +The standard library is loaded during the init step of the repl (or interpreter if you have embedded one). Alternatively, it can be loaded into your application by importing `git.callpipe.com/shs/stdlib`. In order to get a table of all available functions the `GenFuncTable` function is called, returning a struct of type `ast.FuncTable`. Any functions in the standard lib must be accounted for here in order for them to be available at repl or interpreter. Each Operation operates on a list of [Tokens](https://git.callpipe.com/aidan/shs/-/blob/master/ast/token.go). Every function should adhere to the idea that it takes in a list of Tokens and returns a list of Tokens. + +## Working with Tokens +[Tokens](https://git.callpipe.com/aidan/shs/-/blob/master/ast/token.go) are a rudimentary linked list of parsed [Lexemes](https://en.wikipedia.org/wiki/Lexeme). In the ast package there are definitions for Tokens, as well as code for the combined Lex/Parse loop that creates them. Tokens are built in a way that makes operating over them with either recursive or iterative alrogithms easy. When consuming Tokens, one can expect their type by looking at the Tag field. The data stored in the Inner field will be either a string or a \*Token depending on what Tag is. You can expect a \*Token if the Tag field is ast.LIST, and a string in all other cases. If the Tag field is ast.SYMBOL you can look it up in the VarTable or the FuncTable. The VarTable will return either a \*Token (if the symbol is a Variable) or *nil* if nothing is found. The FuncTable will return either a \*Function (if there is a match) or it will return *nil*. +P.S.: Ideally a token should not be re-used. You may consider them disposable. It is up to you to make sure that any Token you edit/reuse remains consistant with the type declared in its TAG. Make sure to differentiate between NUMBER and STRING with the `ast.StrIsNumber(arg string) bool` function. +## Adding a function +1. *Write your function in the form of an `ast.Operation`.* Any function that has the defined signature can be an Operation. +2. *Create a `Function` to encapsulate your `Operation`.* Make sure to set the `args` and `name` fields. Args will be used to validate function calls and Name will be used in debug/log output. +3. *Add your `Function` to the `FuncTable`.* Make sure your `Operations`s get added to the table generated in `GenFuncTable`. + +## Tips +- You can use the Log module to add helpful output to your functions. +- Try not to clutter the output with unnessesary ERR level logging. +- Make sure you properly set the Next element of any Token you swap into a list +- Make sure you properly set the Next element of the previous Token as you swap a token into a list + +## License +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 https://www.gnu.org/licenses/ diff --git a/stdlib/list.go b/stdlib/list.go index e97270b..31e57b0 100644 --- a/stdlib/list.go +++ b/stdlib/list.go @@ -33,7 +33,7 @@ func expand(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token return input } - return input.Expand() + return input.Eval(funcs, vars).Expand() } /* L_APPEND (append from repl) From f3e39e156ce5b112450adffa3c9d725cfefec7ed Mon Sep 17 00:00:00 2001 From: Aidan Date: Mon, 29 Jun 2020 12:26:01 -0700 Subject: [PATCH 06/27] bool functions retrofitted for eval change --- ast/token.go | 11 +++ stdlib/bool.go | 169 +++++++++++++++++++++++++++++++++++++++++++++++ stdlib/stdlib.go | 50 ++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 stdlib/bool.go diff --git a/ast/token.go b/ast/token.go index aee78b6..8c15140 100644 --- a/ast/token.go +++ b/ast/token.go @@ -130,6 +130,17 @@ func (t *Token) FmtToken() string { } } +/* Sets the string value for a non-list token + */ +func (t *Token) Set(arg string) bool { + if t.Tag == LIST { + return false + } + + t.inner = arg + return true +} + /* Returns a tag in text */ func GetTagAsStr(tag Token_t) string { diff --git a/stdlib/bool.go b/stdlib/bool.go new file mode 100644 index 0000000..b59e160 --- /dev/null +++ b/stdlib/bool.go @@ -0,0 +1,169 @@ +/* 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 stdlib + +import ( + "strconv" + "gitlab.com/whom/shs/log" + "gitlab.com/whom/shs/ast" +) + +func not(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + if in.Tag != ast.BOOL { + log.Log(log.ERR, "non-bool argument to 'not'", "not") + return nil + } + + out := "T" + if in.Value() == "T" { + out = "F" + } + + t := &ast.Token{Tag: ast.BOOL} + t.Set(out) + return t +} + +func eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + out := "T" + second := in.Next + + in = in.Eval(ft, vt) + second = second.Eval(ft, vt) + + if in.Tag != second.Tag { + out = "F" + } else { + switch in.Tag { + case ast.LIST: + // returns true if difference found + var consume_list func(*ast.Token, *ast.Token) bool + consume_list = func(l *ast.Token, r *ast.Token) bool { + if (l == nil && r != nil) || (r == nil && l != nil) { + return true + } + + if l.Tag != r.Tag { + return true + } + + l_iter := l + r_iter := r + for l_iter != nil { + if r_iter == nil || l_iter.Tag != r_iter.Tag { + return true + } + + if l_iter.Tag == ast.LIST { + diff := consume_list(l_iter.Expand(), r_iter.Expand()) + if diff { + return true + } + + } else { + if l_iter.Value() != r_iter.Value() { + return true + } + } + + l_iter = l_iter.Next + r_iter = r_iter.Next + } + + if r_iter != nil { + return true + } else { + return false + } + } + + if consume_list(in.Expand(), second.Expand()) { + out = "F" + } + + case ast.NUMBER, ast.STRING, ast.BOOL: + if in.Value() != second.Value() { + out = "F" + } + } + } + + t := &ast.Token{Tag: ast.BOOL} + t.Set(out) + return t +} + +func lt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + out := "T" + second := in.Next + + in = in.Eval(ft, vt) + second = second.Eval(ft, vt) + + if in.Tag != ast.NUMBER || second.Tag != ast.NUMBER { + log.Log(log.ERR, "non-number argument to numeric boolean operator", ">/<=") + return nil + } + + l, _ := strconv.ParseInt(in.Value(), 10, 64) + r, _ := strconv.ParseInt(second.Value(), 10, 64) + + if l >= r { + out = "F" + } + + t := &ast.Token{Tag: ast.BOOL} + t.Set(out) + return t +} + +func gt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + out := "T" + second := in.Next + + in = in.Eval(ft, vt) + second = second.Eval(ft, vt) + + if in.Tag != ast.NUMBER || second.Tag != ast.NUMBER { + log.Log(log.ERR, "non-number argument to numeric boolean operator", ">/<=") + return nil + } + + l, _ := strconv.ParseInt(in.Value(), 10, 64) + r, _ := strconv.ParseInt(second.Value(), 10, 64) + + if l <= r { + out = "F" + } + + t := &ast.Token{Tag: ast.BOOL} + t.Set(out) + return t +} + +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 lte(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + return not(gt(in, vt, ft), vt, ft) +} diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 97d6018..5f64a42 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -45,6 +45,56 @@ func GenFuncTable() ast.FuncTable { TimesCalled: 0, Args: 0, }, + + "eq": &ast.Function{ + Function: eq, + Name: "==", + TimesCalled: 0, + Args: 2, + }, + + "ne": &ast.Function{ + Function: ne, + Name: "!=", + TimesCalled: 0, + Args: 2, + }, + + "<": &ast.Function{ + Function: lt, + Name: "<", + TimesCalled: 0, + Args: 2, + }, + + ">": &ast.Function{ + Function: gt, + Name: ">", + TimesCalled: 0, + Args: 2, + }, + + "<=": &ast.Function{ + Function: lte, + Name: "<=", + TimesCalled: 0, + Args: 2, + }, + + ">=": &ast.Function{ + Function: gte, + Name: ">=", + TimesCalled: 0, + Args: 2, + }, + + "!": &ast.Function{ + Function: not, + Name: "!", + TimesCalled: 0, + Args: 1, + }, + } return stdlib From 7c630d5a38009dbecb4f37c8b9cfb9f4960371c6 Mon Sep 17 00:00:00 2001 From: Aidan Date: Mon, 29 Jun 2020 12:51:41 -0700 Subject: [PATCH 07/27] retrofitted arithmetic functions --- ast/eval.go | 5 ++ stdlib/arith.go | 214 +++++++++++++++++++++++++++++++++++++++++++++++ stdlib/bool.go | 18 +++- stdlib/stdlib.go | 28 +++++++ 4 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 stdlib/arith.go diff --git a/ast/eval.go b/ast/eval.go index 729bef5..998200c 100644 --- a/ast/eval.go +++ b/ast/eval.go @@ -89,6 +89,11 @@ func (in *Token) Eval(funcs FuncTable, vars VarTable) *Token { inner = &Token{inner: inner} } res = funct.CallFunction(inner.Next, vars, funcs).Eval(funcs, vars) + if res == nil { + // function failed. logging is its responsibility + return nil + } + res.Append(in.Next) } else { diff --git a/stdlib/arith.go b/stdlib/arith.go new file mode 100644 index 0000000..1cd9da8 --- /dev/null +++ b/stdlib/arith.go @@ -0,0 +1,214 @@ +/* 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 stdlib + +import ( + "fmt" + "strconv" + "gitlab.com/whom/shs/ast" + "gitlab.com/whom/shs/log" +) + +// PKG WIDE TODO: BIGNUM SYSTEM +// then write actually optimal routines once it is in place +// 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 { + var res float64 + + in = in.Eval(f, a) + + for i := in; i != nil; i = i.Next { + if i.Tag != ast.NUMBER { + log.Log(log.ERR, "Non-number given to ADD", "add") + return nil + } + + token := i.Value() + isFloat := false + for _, char := range token { + if char == '.' { + isFloat = true + n, err := strconv.ParseFloat(token, 64) + if err != nil { + log.Log(log.ERR, "Err parsing number: " + err.Error(), "add") + return nil + } + + res += n + } + } + + if !isFloat { + n, err := strconv.ParseInt(token, 10, 64) + if err != nil { + log.Log(log.ERR, "Err parsing number: " + err.Error(), "add") + return nil + } + + res += float64(n) + } + } + + t := &ast.Token{Tag: ast.NUMBER} + t.Set(fmt.Sprintf("%f", res)) + return t +} + +func sub(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { + var res float64 + var sub float64 + + in = in.Eval(f, a) + + for i := in; i != nil; i = i.Next { + if i.Tag != ast.NUMBER { + log.Log(log.ERR, "Non-number given to SUB", "sub") + return nil + } + + token := i.Value() + isFloat := false + var inner float64 + for _, char := range token { + if char == '.' { + isFloat = true + n, err := strconv.ParseFloat(token, 64) + if err != nil { + log.Log(log.ERR, "Err parsing number: " + err.Error(), "sub") + return nil + } + + inner = n + } + } + + if !isFloat { + n, err := strconv.ParseInt(token, 10, 64) + if err != nil { + log.Log(log.ERR, "Err parsing number: " + err.Error(), "sub") + return nil + } + + inner = float64(n) + } + + if i.Next != nil { + sub += inner + } else { + res = inner + } + } + + t := &ast.Token{Tag: ast.NUMBER} + t.Set(fmt.Sprintf("%f", res - sub)) + return t +} + +func mult(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { + res := 1.0 + + in = in.Eval(f, a) + + for i := in; i != nil; i = i.Next { + if i.Tag != ast.NUMBER { + log.Log(log.ERR, "Non-number given to MULT", "mult") + return nil + } + + token := i.Value() + isFloat := false + for _, char := range token { + if char == '.' { + isFloat = true + n, err := strconv.ParseFloat(token, 64) + if err != nil { + log.Log(log.ERR, "Err parsing number: " + err.Error(), "mult") + return nil + } + + res *= n + } + } + + if !isFloat { + n, err := strconv.ParseInt(token, 10, 64) + if err != nil { + log.Log(log.ERR, "Err parsing number: " + err.Error(), "mult") + return nil + } + + res *= float64(n) + } + } + + t := &ast.Token{Tag: ast.NUMBER} + t.Set(fmt.Sprintf("%f", res)) + return t +} + +func div(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { + var res float64 + + in = in.Eval(f, a) + + for i := in; i != nil; i = i.Next { + inner := 0.0 + + if i.Tag != ast.NUMBER { + log.Log(log.ERR, "Non-number given to DIV", "div") + return nil + } + + token := i.Value() + isFloat := false + for _, char := range token { + if char == '.' { + isFloat = true + n, err := strconv.ParseFloat(token, 64) + if err != nil { + log.Log(log.ERR, "Err parsing number: " + err.Error(), "div") + return nil + } + + inner = n + } + } + + if !isFloat { + n, err := strconv.ParseInt(token, 10, 64) + if err != nil { + log.Log(log.ERR, "Err parsing number: " + err.Error(), "div") + return nil + } + + inner = float64(n) + } + + if i == in { + res = inner + } else { + res = res / inner + } + } + + t := &ast.Token{Tag: ast.NUMBER} + t.Set(fmt.Sprintf("%f", res)) + return t +} diff --git a/stdlib/bool.go b/stdlib/bool.go index b59e160..3d0dd0c 100644 --- a/stdlib/bool.go +++ b/stdlib/bool.go @@ -24,6 +24,8 @@ import ( ) func not(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + in = in.Eval(ft, vt) + if in.Tag != ast.BOOL { log.Log(log.ERR, "non-bool argument to 'not'", "not") return nil @@ -96,10 +98,24 @@ func eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { out = "F" } - case ast.NUMBER, ast.STRING, ast.BOOL: + case ast.STRING, ast.BOOL: if in.Value() != second.Value() { out = "F" } + + case ast.NUMBER: + l_val, parse_err := strconv.ParseFloat(in.Value(), 64) + r_val, parse_err := strconv.ParseFloat(second.Value(), 64) + if parse_err != nil { + log.Log(log.ERR, + "error parsing number: "+parse_err.Error(), + "eq") + return nil + } + + if l_val != r_val { + out = "F" + } } } diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 5f64a42..4e53d01 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -95,6 +95,34 @@ func GenFuncTable() ast.FuncTable { Args: 1, }, + "+": &ast.Function{ + Function: add, + Name: "add", + TimesCalled: 0, + Args: -1, + }, + + "-": &ast.Function{ + Function: sub, + Name: "sub", + TimesCalled: 0, + Args: -1, + }, + + "*": &ast.Function{ + Function: mult, + Name: "mult", + TimesCalled: 0, + Args: -1, + }, + + "/": &ast.Function{ + Function: div, + Name: "div", + TimesCalled: 0, + Args: -1, + }, + } return stdlib From d70a5ec77c8db8d152f109d27e01b9dee66f4a43 Mon Sep 17 00:00:00 2001 From: Aidan Date: Mon, 29 Jun 2020 16:58:43 -0700 Subject: [PATCH 08/27] retrofitted filesys functions --- stdlib/filesys.go | 38 ++++++++++++++++++++++++++++++++++++++ stdlib/stdlib.go | 6 ++++++ 2 files changed, 44 insertions(+) create mode 100644 stdlib/filesys.go diff --git a/stdlib/filesys.go b/stdlib/filesys.go new file mode 100644 index 0000000..67da663 --- /dev/null +++ b/stdlib/filesys.go @@ -0,0 +1,38 @@ +/* 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 stdlib + +import ( + "os" + "gitlab.com/whom/shs/ast" + "gitlab.com/whom/shs/log" +) + +func cd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + in = in.Eval(ft, vt) + if in.Tag == ast.LIST { + log.Log(log.ERR, "Couldnt change dir to a list", "cd") + return nil + } + + err := os.Chdir(in.Value()) + if err != nil { + log.Log(log.ERR, err.Error(), "cd") + } + return nil +} diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 4e53d01..6c71e90 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -123,6 +123,12 @@ func GenFuncTable() ast.FuncTable { Args: -1, }, + "cd": &ast.Function{ + Function: cd, + Name: "changedir", + TimesCalled: 0, + Args: 1, + }, } return stdlib From ea99142b3aa0111492c8a3e47a52918b08bc5af0 Mon Sep 17 00:00:00 2001 From: Aidan Date: Mon, 29 Jun 2020 17:06:27 -0700 Subject: [PATCH 09/27] retrofitted string functions --- stdlib/stdlib.go | 14 ++++++++++++++ stdlib/string.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 stdlib/string.go diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 6c71e90..c09520f 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -129,6 +129,20 @@ func GenFuncTable() ast.FuncTable { TimesCalled: 0, Args: 1, }, + + "concat": &ast.Function{ + Function: concat, + Name:"concatenate", + TimesCalled: 0, + Args: -1, + }, + + "print": &ast.Function{ + Function:print_str, + Name: "print", + TimesCalled: 0, + Args: 1, + }, } return stdlib diff --git a/stdlib/string.go b/stdlib/string.go new file mode 100644 index 0000000..d6f7bc9 --- /dev/null +++ b/stdlib/string.go @@ -0,0 +1,48 @@ +/* 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 stdlib + +import ( + "fmt" + "gitlab.com/whom/shs/ast" + "gitlab.com/whom/shs/log" +) + +func concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + in = in.Eval(ft, vt) + + var res string + for i := in; i != nil; i = i.Next { + if i.Tag == ast.LIST { + log.Log(log.ERR, "Not concatenating list", "conc") + log.Log(log.DEBUG, "Try using the expand operator (...)", "conc") + continue + } + + res += i.Value() + } + + t := &ast.Token{Tag: ast.STRING} + t.Set(res) + return t +} + +func print_str(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + fmt.Println(in.Eval(ft, vt)) + return nil +} From 2ae1145a5073b6e84d6a62a98f62245722aaac52 Mon Sep 17 00:00:00 2001 From: Aidan Date: Mon, 29 Jun 2020 19:15:00 -0700 Subject: [PATCH 10/27] retrofit call functions for new eval alg --- ast/eval.go | 20 +-- cmd/shs_repl.go | 4 +- stdlib/arith.go | 8 +- stdlib/bool.go | 11 +- stdlib/call.go | 305 ++++++++++++++++++++++++++++++++++++++++++++++ stdlib/filesys.go | 2 +- stdlib/list.go | 2 +- stdlib/stdlib.go | 53 ++++++++ stdlib/string.go | 4 +- 9 files changed, 384 insertions(+), 25 deletions(-) create mode 100644 stdlib/call.go diff --git a/ast/eval.go b/ast/eval.go index 998200c..0d1c86a 100644 --- a/ast/eval.go +++ b/ast/eval.go @@ -17,9 +17,7 @@ package ast -import ( - "gitlab.com/whom/shs/log" -) +import "gitlab.com/whom/shs/log" /* determines whether or not to execute a system call * when a function cannot be found in the functable @@ -36,7 +34,7 @@ var ExecFunc = "l" * canFunc determines whether a symbol could be a function to call * (true when first elem of a list) */ -func (in *Token) Eval(funcs FuncTable, vars VarTable) *Token { +func (in *Token) Eval(funcs FuncTable, vars VarTable, cnvtUndefVars bool) *Token { if in == nil { return nil } @@ -53,6 +51,12 @@ func (in *Token) Eval(funcs FuncTable, vars VarTable) *Token { res = in if GetFunction(in.Value(), funcs) == nil { + if cnvtUndefVars { + in.Tag = STRING + res = in + break + } + log.Log(log.ERR, "undefined symbol: "+in.Value(), "eval") @@ -70,7 +74,7 @@ func (in *Token) Eval(funcs FuncTable, vars VarTable) *Token { } if inner.Tag != SYMBOL { - in.Direct(inner.Eval(funcs, vars)) + in.Direct(inner.Eval(funcs, vars, cnvtUndefVars)) res = in break } @@ -86,9 +90,9 @@ func (in *Token) Eval(funcs FuncTable, vars VarTable) *Token { if funct != nil { if makeHead { - inner = &Token{inner: inner} + inner = &Token{Next: inner} } - res = funct.CallFunction(inner.Next, vars, funcs).Eval(funcs, vars) + res = funct.CallFunction(inner.Next, vars, funcs).Eval(funcs, vars, false) if res == nil { // function failed. logging is its responsibility return nil @@ -111,7 +115,7 @@ func (in *Token) Eval(funcs FuncTable, vars VarTable) *Token { } if res.Next != nil { - res.Next = res.Next.Eval(funcs, vars) + res.Next = res.Next.Eval(funcs, vars, cnvtUndefVars) } return res } diff --git a/cmd/shs_repl.go b/cmd/shs_repl.go index 4635c05..2f32bca 100644 --- a/cmd/shs_repl.go +++ b/cmd/shs_repl.go @@ -58,7 +58,7 @@ func main() { ast.InitVarTable(vars) ast.SyncTablesWithOSEnviron = true - ast.ExecWhenFuncUndef = false + ast.ExecWhenFuncUndef = true var err error @@ -96,7 +96,7 @@ func main() { ast.PrintSExprsIndividually(userInput) } - result := userInput.Eval(funcs, vars) + result := userInput.Eval(funcs, vars, false) if result != nil { for i := result; i != nil; i = i.Next { fmt.Printf(i.String() + " ") diff --git a/stdlib/arith.go b/stdlib/arith.go index 1cd9da8..7a61754 100644 --- a/stdlib/arith.go +++ b/stdlib/arith.go @@ -32,7 +32,7 @@ import ( func add(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { var res float64 - in = in.Eval(f, a) + in = in.Eval(f, a, false) for i := in; i != nil; i = i.Next { if i.Tag != ast.NUMBER { @@ -75,7 +75,7 @@ func sub(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { var res float64 var sub float64 - in = in.Eval(f, a) + in = in.Eval(f, a, false) for i := in; i != nil; i = i.Next { if i.Tag != ast.NUMBER { @@ -124,7 +124,7 @@ func sub(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { func mult(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { res := 1.0 - in = in.Eval(f, a) + in = in.Eval(f, a, false) for i := in; i != nil; i = i.Next { if i.Tag != ast.NUMBER { @@ -166,7 +166,7 @@ func mult(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { func div(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { var res float64 - in = in.Eval(f, a) + in = in.Eval(f, a, false) for i := in; i != nil; i = i.Next { inner := 0.0 diff --git a/stdlib/bool.go b/stdlib/bool.go index 3d0dd0c..d0851bd 100644 --- a/stdlib/bool.go +++ b/stdlib/bool.go @@ -24,7 +24,7 @@ import ( ) func not(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - in = in.Eval(ft, vt) + in = in.Eval(ft, vt, false) if in.Tag != ast.BOOL { log.Log(log.ERR, "non-bool argument to 'not'", "not") @@ -45,8 +45,7 @@ func eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { out := "T" second := in.Next - in = in.Eval(ft, vt) - second = second.Eval(ft, vt) + in = in.Eval(ft, vt, false) if in.Tag != second.Tag { out = "F" @@ -128,8 +127,7 @@ func lt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { out := "T" second := in.Next - in = in.Eval(ft, vt) - second = second.Eval(ft, vt) + in = in.Eval(ft, vt, false) if in.Tag != ast.NUMBER || second.Tag != ast.NUMBER { log.Log(log.ERR, "non-number argument to numeric boolean operator", ">/<=") @@ -152,8 +150,7 @@ func gt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { out := "T" second := in.Next - in = in.Eval(ft, vt) - second = second.Eval(ft, vt) + in = in.Eval(ft, vt, false) if in.Tag != ast.NUMBER || second.Tag != ast.NUMBER { log.Log(log.ERR, "non-number argument to numeric boolean operator", ">/<=") diff --git a/stdlib/call.go b/stdlib/call.go new file mode 100644 index 0000000..a07b057 --- /dev/null +++ b/stdlib/call.go @@ -0,0 +1,305 @@ +/* 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 stdlib + +import ( + "os" + "fmt" + "bytes" + "strconv" + "os/exec" + "syscall" + "os/signal" + "gitlab.com/whom/shs/ast" + "gitlab.com/whom/shs/log" +) + +var bgProcs = make([]*exec.Cmd, 0) +var LastExitCode int +var sigs = []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGTSTP, syscall.SIGTTIN, syscall.SIGTTOU, syscall.SIGCONT} + + +func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + in = in.Eval(ft, vt, true) + if in == nil { + return nil + } + + if in.Tag == ast.LIST { + log.Log(log.ERR, "couldnt exec, target bin is a list", "call") + return nil + } + + path, err := exec.LookPath(in.Value()) + if err != nil { + log.Log(log.ERR, "Couldnt exec " + in.Value() + ", file not found", "call") + return nil + } + + args := []string{} + for i := in.Next; i != nil; i = i.Next { + if i.Tag == ast.LIST { + log.Log(log.ERR, "Couldnt exec " + path + ", element in arguments is a list", "call") + return nil + } + + args = append(args, i.Value()) + } + + var cmd *exec.Cmd + if len(args) > 0 { + cmd = exec.Command(path, args...) + } else { + cmd = exec.Command(path) + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + signalChan := make(chan os.Signal, 2) + signal.Notify(signalChan, sigs...) + go func() { + sig := <-signalChan + cmd.Process.Signal(sig) + }() + + err = cmd.Run() + close(signalChan) + signal.Reset(sigs...) + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + LastExitCode = exitError.ExitCode() + } else { + log.Log(log.ERR, "Execution step returned unparsable error: " + err.Error(), "call") + } + } + + return nil +} + +func bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + if in == nil { + return nil + } + + if in.Tag == ast.LIST { + log.Log(log.ERR, "couldnt exec, target bin is a list", "call") + return nil + } + + path, err := exec.LookPath(in.Value()) + if err != nil { + log.Log(log.ERR, "Couldnt exec " + in.Value() + ", file not found", "call") + return nil + } + + args := []string{} + for i := in.Next; i != nil; i = i.Next { + if i.Tag == ast.LIST { + log.Log(log.ERR, "Couldnt exec " + path + ", element in arguments is a list", "call") + return nil + } + + args = append(args, i.Value()) + } + + var cmd *exec.Cmd + if len(args) > 0 { + cmd = exec.Command(path, args...) + } else { + cmd = exec.Command(path) + } + cmd.Stderr = os.Stderr + + bgProcs = append(bgProcs, cmd) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + + cmd.Start() + cmd.Process.Signal(syscall.SIGTSTP) + return nil +} + +func fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + if len(bgProcs) < 1 { + return nil + } + + cmd := bgProcs[0] + bgProcs = bgProcs[1:] + + signalChan := make(chan os.Signal, 2) + signal.Notify(signalChan, sigs...) + go func() { + sig := <-signalChan + cmd.Process.Signal(sig) + }() + + cmd.Process.Signal(syscall.SIGCONT) + err := cmd.Wait() + close(signalChan) + signal.Reset(sigs...) + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + LastExitCode = exitError.ExitCode() + } else { + log.Log(log.ERR, "Execution step returned error: " + err.Error(), "call") + } + } + + return nil +} + +func jobs(in *ast.Token, vt ast.VarTable, fg ast.FuncTable) *ast.Token { + ret := &ast.Token{ + Tag: ast.LIST, + } + + _inner := &ast.Token{ + Tag: ast.STRING, + } + + ret.Direct(_inner) + _inner.Set(fmt.Sprintf("Total: %d", len(bgProcs))) + + iter := &_inner + for i := 0; i < len(bgProcs); i += 1 { + (*iter).Next = &ast.Token{ + Tag: ast.STRING, + } + (*iter).Next.Set(fmt.Sprintf("[%d]: %d", i, bgProcs[i].Process.Pid)) + iter = &(*iter).Next + } + + return ret +} + +func read_cmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + in = in.Eval(ft, vt, true) + + if in == nil { + return nil + } + + if in.Tag == ast.LIST { + log.Log(log.ERR, "couldnt exec, target bin is a list", "call") + return nil + } + + var out bytes.Buffer + path, err := exec.LookPath(in.Value()) + if err != nil { + log.Log(log.ERR, "Couldnt exec " + in.Value() + ", file not found", "call") + return nil + } + + args := []string{} + for i := in.Next; i != nil; i = i.Next { + if i.Tag == ast.LIST { + log.Log(log.ERR, "Couldnt exec " + path + ", element in arguments is a list", "call") + return nil + } + + args = append(args, i.Value()) + } + + var cmd *exec.Cmd + if len(args) > 0 { + cmd = exec.Command(path, args...) + } else { + cmd = exec.Command(path) + } + cmd.Stdout = &out + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + signalChan := make(chan os.Signal, 2) + signal.Notify(signalChan, sigs...) + go func() { + sig := <-signalChan + cmd.Process.Signal(sig) + }() + + err = cmd.Run() + close(signalChan) + signal.Reset(sigs...) + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + LastExitCode = exitError.ExitCode() + } else { + log.Log(log.ERR, "Execution step returned error: " + err.Error(), "$") + } + } + + ret := &ast.Token{Tag: ast.STRING} + ret.Set(out.String()) + return ret + +} + +func get_exit(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 { + in = in.Eval(ft, vt, true) + + if in.Tag == ast.LIST { + log.Log(log.ERR, "non-number argument to kill function", "kill") + return nil + } + + pid, err := strconv.ParseInt(in.Value(), 10, 64) + if err != nil { + log.Log(log.ERR, "error parsing arg to kill: " + err.Error(), "kill") + return nil + } + + found := false + newBgProcs := []*exec.Cmd{} + for _, i := range bgProcs { + if i.Process.Pid != int(pid) { + newBgProcs = append(newBgProcs, i) + } else { + found = true + err = i.Process.Kill() + if err != nil { + log.Log(log.ERR, fmt.Sprintf("error killing process %d: %s", + int(pid), err.Error()), "kill") + newBgProcs = append(newBgProcs, i) + } + } + } + + bgProcs = newBgProcs + + if !found { + // docs say no error on unix systems + proc, _ := os.FindProcess(int(pid)) + err = proc.Kill() + if err != nil { + log.Log(log.ERR, fmt.Sprintf("error killing process %d: %s", + int(pid), err.Error()), "kill") + } + } + + return nil +} diff --git a/stdlib/filesys.go b/stdlib/filesys.go index 67da663..09e3c6b 100644 --- a/stdlib/filesys.go +++ b/stdlib/filesys.go @@ -24,7 +24,7 @@ import ( ) func cd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - in = in.Eval(ft, vt) + in = in.Eval(ft, vt, false) if in.Tag == ast.LIST { log.Log(log.ERR, "Couldnt change dir to a list", "cd") return nil diff --git a/stdlib/list.go b/stdlib/list.go index 31e57b0..ac20d28 100644 --- a/stdlib/list.go +++ b/stdlib/list.go @@ -33,7 +33,7 @@ func expand(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token return input } - return input.Eval(funcs, vars).Expand() + return input.Eval(funcs, vars, false).Expand() } /* L_APPEND (append from repl) diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index c09520f..31b3db8 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -143,6 +143,59 @@ func GenFuncTable() ast.FuncTable { TimesCalled: 0, Args: 1, }, + + "l": &ast.Function{ + Function: call, + Name: "call", + TimesCalled: 0, + Args: -1, + }, + + "bg": &ast.Function{ + Function: bgcall, + Name: "background call", + TimesCalled: 0, + Args: -1, + }, + + "fg": &ast.Function{ + Function: fg, + Name: "foreground", + TimesCalled: 0, + Args: 0, + }, + + "$": &ast.Function{ + Function: read_cmd, + Name: "read cmd", + TimesCalled: 0, + Args: -1, + }, + + "?": &ast.Function{ + Function: get_exit, + Name:"get exit code", + TimesCalled: 0, + Args: 0, + }, + + /* + USE NATIVE KILL COMMAND. + "kill": &ast.Function{ + Function: kill, + Name: "kill job", + TimesCalled: 0, + Args: 1, + }, + */ + + "jobs": &ast.Function{ + Function: jobs, + Name: "list jobs", + TimesCalled: 0, + Args: 0, + }, + } return stdlib diff --git a/stdlib/string.go b/stdlib/string.go index d6f7bc9..b8321d7 100644 --- a/stdlib/string.go +++ b/stdlib/string.go @@ -24,7 +24,7 @@ import ( ) func concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - in = in.Eval(ft, vt) + in = in.Eval(ft, vt, false) var res string for i := in; i != nil; i = i.Next { @@ -43,6 +43,6 @@ func concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { } func print_str(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - fmt.Println(in.Eval(ft, vt)) + fmt.Println(in.Eval(ft, vt, false)) return nil } From de5566b3ecf7d8ff30e13a1ca760b4c62db22110 Mon Sep 17 00:00:00 2001 From: Aidan Date: Mon, 29 Jun 2020 21:21:30 -0700 Subject: [PATCH 11/27] add info func --- stdlib/call.go | 9 ++++++++- stdlib/stdlib.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/stdlib/call.go b/stdlib/call.go index a07b057..4359503 100644 --- a/stdlib/call.go +++ b/stdlib/call.go @@ -31,7 +31,14 @@ import ( var bgProcs = make([]*exec.Cmd, 0) var LastExitCode int -var sigs = []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGTSTP, syscall.SIGTTIN, syscall.SIGTTOU, syscall.SIGCONT} +var sigs = []os.Signal{ + os.Interrupt, + syscall.SIGTERM, + syscall.SIGTSTP, + syscall.SIGTTIN, + syscall.SIGTTOU, + syscall.SIGCONT, +} func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 31b3db8..0bd2b40 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -19,6 +19,7 @@ package stdlib import ( "os" + "fmt" "gitlab.com/whom/shs/ast" ) @@ -196,6 +197,12 @@ func GenFuncTable() ast.FuncTable { Args: 0, }, + "info": &ast.Function{ + Function: sh_info, + Name: "Shell Info", + TimesCalled: 0, + Args: 1, + }, } return stdlib @@ -205,3 +212,33 @@ func exit_shell(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 { + switch in.Tag { + case ast.BOOL: + fmt.Printf("BOOL LITERAL\nValue: %s\n", in.Value()) + case ast.STRING: + fmt.Printf("STRING LITERAL \nValue: %s\n", in.Value()) + case ast.NUMBER: + fmt.Printf("NUMBER LITERAL \nValue: %s\n", in.Value()) + case ast.LIST: + fmt.Printf("LIST \nString Value: %s, AST:\n", in.String()) + ast.PrintSExprsIndividually(in) + case ast.SYMBOL: + repr := ast.GetVar(in.Value(), vt) + if repr != nil { + fmt.Printf("VARIABLE\nTYPE: %s\nVALUE: %s\n", ast.GetTagAsStr(repr.Tag), repr.Value()) + break + } + + funct := ast.GetFunction(in.Value(), ft) + if funct != nil { + fmt.Printf("FUNCTION\nNAME: %s\nTIMES CALLED: %s\nNUM ARGS: %d\n", funct.Name, funct.TimesCalled, funct.Args) + break + } + + fmt.Printf("UNKNOWN SYMBOL\n") + } + + return nil +} From a216d9af410cfabb3a30f94d8c0ffa059654cf0d Mon Sep 17 00:00:00 2001 From: Aidan Date: Tue, 30 Jun 2020 20:27:12 -0700 Subject: [PATCH 12/27] add rudimentary control flow --- ast/var_table.go | 7 ++++ stdlib/control_flow.go | 82 ++++++++++++++++++++++++++++++++++++++++++ stdlib/stdlib.go | 27 +++++++++++++- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 stdlib/control_flow.go diff --git a/ast/var_table.go b/ast/var_table.go index ae0519a..4b91a18 100644 --- a/ast/var_table.go +++ b/ast/var_table.go @@ -91,6 +91,10 @@ func GetVarFromTables(arg string, library []VarTable) *Token { } func InitVarTable(table VarTable) { + if !SyncTablesWithOSEnviron { + return + } + for _, val := range os.Environ() { variable := strings.Split(val, "=") t := &Token{inner: variable[1]} @@ -100,6 +104,9 @@ func InitVarTable(table VarTable) { t.Tag = STRING } + if variable[0] == "HOME" { + SetVar('~', t, table) + } SetVar(variable[0], t, table) } } diff --git a/stdlib/control_flow.go b/stdlib/control_flow.go new file mode 100644 index 0000000..e937a17 --- /dev/null +++ b/stdlib/control_flow.go @@ -0,0 +1,82 @@ +/* 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 stdlib + +import "gitlab.com/whom/shs/ast" + +/* return one evaluated form or another based on the boolean statement + */ +func shs_if(in *ast.Token, vt VarTable, ft FuncTable) *ast.Token { + cond := in + t := cond.Next + f := t.Next + cond.Next = nil + t.Next = nil + + cond = cond.Eval(ft, vt, false) + if cond == nil || cond.Tag != ast.BOOL { + log.Log(log.ERR, + "first argument to if must be a bool statement", + "if") + return nil + } + + switch cond.Value() { + case ast.TRUE: + return t + + case ast.FALSE: + return f + + default: + log.Log(log.ERR, + "improper bool!", + "if") + return nil + } +} + +/* continually eval n forms while element #1 evals to T + */ +func shs_while(in *ast.Token, vt VarTable, ft FuncTable) *ast.Token { + cond := in + forms := in.Next + in.Next = nil + var res *ast.Token + + eval := cond.Eval(ft, vt, false) + if cond == nil || cond.Tag != ast.BOOL { + log.Log(log.ERR, + "first argument to while must be a bool statement", + "while") + return nil + } + + // slight downside here: doesnt log when the wrong tag is set + for eval.Tag == ast.BOOL && eval.Value() == 'T' { + // eval all forms + for i := forms; i != nil; i = i.Next { + res = i.Eval(ft, vt, false) + } + + // retest eval + eval := cond.Eval(ft, vt, false) + } + + return res +} diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 0bd2b40..f5cdd7d 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -26,7 +26,28 @@ import ( func GenFuncTable() ast.FuncTable { var stdlib ast.FuncTable stdlib = &map[string]*ast.Function{ - "...": &ast.Function{ + "if": &ast.Function{ + Function: shs_if, + Name: "if", + TimesCalled: 0, + Args: 3, + }, + + "while": &ast.Function{ + Function: shs_while, + Name: "while", + TimesCalled: 0, + Args: -1, + }, + + "eval": &ast.Function{ + Function: eval, + Name: "eval", + TimesCalled: 0, + Args: -1, + }, + + "...": &ast.Function{ Function: expand, Name: "...", TimesCalled: 0, @@ -242,3 +263,7 @@ 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.VarTable) *ast.Token { + return in.Eval(ft, vt, false) +} From 53adeacc6d29642b99016a0a69ab600b70519d76 Mon Sep 17 00:00:00 2001 From: Aidan Date: Tue, 30 Jun 2020 20:36:45 -0700 Subject: [PATCH 13/27] use constants for True and False instead of tokens --- ast/token.go | 3 +++ ast/var_table.go | 2 +- stdlib/bool.go | 24 ++++++++++++------------ stdlib/control_flow.go | 13 ++++++++----- stdlib/stdlib.go | 2 +- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/ast/token.go b/ast/token.go index 8c15140..721143a 100644 --- a/ast/token.go +++ b/ast/token.go @@ -26,6 +26,9 @@ const ( NUMBER Token_t = iota SYMBOL Token_t = iota BOOL Token_t = iota + + TRUE string = "T" + FALSE string = "F" ) type Token struct { diff --git a/ast/var_table.go b/ast/var_table.go index 4b91a18..cf14127 100644 --- a/ast/var_table.go +++ b/ast/var_table.go @@ -105,7 +105,7 @@ func InitVarTable(table VarTable) { } if variable[0] == "HOME" { - SetVar('~', t, table) + SetVar("~", t, table) } SetVar(variable[0], t, table) } diff --git a/stdlib/bool.go b/stdlib/bool.go index d0851bd..5e142bc 100644 --- a/stdlib/bool.go +++ b/stdlib/bool.go @@ -31,9 +31,9 @@ func not(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return nil } - out := "T" - if in.Value() == "T" { - out = "F" + out := ast.TRUE + if in.Value() == ast.TRUE { + out = ast.FALSE } t := &ast.Token{Tag: ast.BOOL} @@ -42,13 +42,13 @@ func not(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 := "T" + out := ast.TRUE second := in.Next in = in.Eval(ft, vt, false) if in.Tag != second.Tag { - out = "F" + out = ast.FALSE } else { switch in.Tag { case ast.LIST: @@ -94,12 +94,12 @@ func eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { } if consume_list(in.Expand(), second.Expand()) { - out = "F" + out = ast.FALSE } case ast.STRING, ast.BOOL: if in.Value() != second.Value() { - out = "F" + out = ast.FALSE } case ast.NUMBER: @@ -113,7 +113,7 @@ func eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { } if l_val != r_val { - out = "F" + out = ast.FALSE } } } @@ -124,7 +124,7 @@ func eq(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 := "T" + out := ast.TRUE second := in.Next in = in.Eval(ft, vt, false) @@ -138,7 +138,7 @@ func lt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { r, _ := strconv.ParseInt(second.Value(), 10, 64) if l >= r { - out = "F" + out = ast.FALSE } t := &ast.Token{Tag: ast.BOOL} @@ -147,7 +147,7 @@ func lt(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 := "T" + out := ast.TRUE second := in.Next in = in.Eval(ft, vt, false) @@ -161,7 +161,7 @@ func gt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { r, _ := strconv.ParseInt(second.Value(), 10, 64) if l <= r { - out = "F" + out = ast.FALSE } t := &ast.Token{Tag: ast.BOOL} diff --git a/stdlib/control_flow.go b/stdlib/control_flow.go index e937a17..c14cb31 100644 --- a/stdlib/control_flow.go +++ b/stdlib/control_flow.go @@ -17,11 +17,14 @@ package stdlib -import "gitlab.com/whom/shs/ast" +import ( + "gitlab.com/whom/shs/ast" + "gitlab.com/whom/shs/log" +) /* return one evaluated form or another based on the boolean statement */ -func shs_if(in *ast.Token, vt VarTable, ft FuncTable) *ast.Token { +func shs_if(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { cond := in t := cond.Next f := t.Next @@ -53,7 +56,7 @@ func shs_if(in *ast.Token, vt VarTable, ft FuncTable) *ast.Token { /* continually eval n forms while element #1 evals to T */ -func shs_while(in *ast.Token, vt VarTable, ft FuncTable) *ast.Token { +func shs_while(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { cond := in forms := in.Next in.Next = nil @@ -68,14 +71,14 @@ func shs_while(in *ast.Token, vt VarTable, ft FuncTable) *ast.Token { } // slight downside here: doesnt log when the wrong tag is set - for eval.Tag == ast.BOOL && eval.Value() == 'T' { + for eval.Tag == ast.BOOL && eval.Value() == ast.TRUE { // eval all forms for i := forms; i != nil; i = i.Next { res = i.Eval(ft, vt, false) } // retest eval - eval := cond.Eval(ft, vt, false) + eval = cond.Eval(ft, vt, false) } return res diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index f5cdd7d..eed2e2b 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -264,6 +264,6 @@ 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.VarTable) *ast.Token { +func eval(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { return in.Eval(ft, vt, false) } From c253dc6375e8f3d396103ee6ea1639ebbc42ac6d Mon Sep 17 00:00:00 2001 From: Aidan Date: Tue, 30 Jun 2020 20:53:40 -0700 Subject: [PATCH 14/27] fix typo, update tag to string function --- ast/token.go | 2 ++ stdlib/control_flow.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ast/token.go b/ast/token.go index 721143a..645e203 100644 --- a/ast/token.go +++ b/ast/token.go @@ -152,6 +152,8 @@ func GetTagAsStr(tag Token_t) string { return "LIST" case STRING: return "STRING" + case BOOL: + return "BOOL" case NUMBER: return "NUMBER" case SYMBOL: diff --git a/stdlib/control_flow.go b/stdlib/control_flow.go index c14cb31..5889e33 100644 --- a/stdlib/control_flow.go +++ b/stdlib/control_flow.go @@ -63,7 +63,7 @@ func shs_while(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { var res *ast.Token eval := cond.Eval(ft, vt, false) - if cond == nil || cond.Tag != ast.BOOL { + if eval == nil || eval.Tag != ast.BOOL { log.Log(log.ERR, "first argument to while must be a bool statement", "while") From a5f157dbd70d437f76be34f128110ae54b3b0097 Mon Sep 17 00:00:00 2001 From: Aidan Date: Thu, 2 Jul 2020 16:24:34 -0700 Subject: [PATCH 15/27] add input function --- stdlib/stdlib.go | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index eed2e2b..acb74ee 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -20,34 +20,42 @@ package stdlib import ( "os" "fmt" + "gitlab.com/whom/shs/log" "gitlab.com/whom/shs/ast" ) func GenFuncTable() ast.FuncTable { var stdlib ast.FuncTable stdlib = &map[string]*ast.Function{ - "if": &ast.Function{ + "if": &ast.Function{ Function: shs_if, Name: "if", TimesCalled: 0, Args: 3, }, - "while": &ast.Function{ + "while": &ast.Function{ Function: shs_while, Name: "while", TimesCalled: 0, Args: -1, }, - "eval": &ast.Function{ + "eval": &ast.Function{ Function: eval, Name: "eval", TimesCalled: 0, Args: -1, }, - "...": &ast.Function{ + "input": &ast.Function{ + Function: input, + Name: "input", + TimesCalled: 0, + Args: 1, + }, + + "...": &ast.Function{ Function: expand, Name: "...", TimesCalled: 0, @@ -267,3 +275,23 @@ func sh_info(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { 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 { + in = in.Eval(ft, vt, false) + if in.Tag != ast.STRING && in.Tag != ast.NUMBER { + log.Log(log.ERR, + "argument to input must be a string or number", + "input") + return nil + } + + prompt := in.Value() + var output string + + fmt.Printf(prompt) + fmt.Scanln(&output) + + ret := &ast.Token{Tag: ast.STRING} + ret.Set(output) + return ret +} From 1802bce6043018c58b451c7cbab326b9ecc49be7 Mon Sep 17 00:00:00 2001 From: Aidan Date: Thu, 2 Jul 2020 16:44:14 -0700 Subject: [PATCH 16/27] fix cd bug --- stdlib/filesys.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/stdlib/filesys.go b/stdlib/filesys.go index 09e3c6b..83a45ef 100644 --- a/stdlib/filesys.go +++ b/stdlib/filesys.go @@ -24,7 +24,15 @@ import ( ) func cd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { - in = in.Eval(ft, vt, false) + in = in.Eval(ft, vt, true) + + if in == nil { + log.Log(log.ERR, + "arguments to cd evaluated to nil!", + "cd") + return nil + } + if in.Tag == ast.LIST { log.Log(log.ERR, "Couldnt change dir to a list", "cd") return nil From de3e3e5d4e1e5e182710965f118b8be4e8746eb7 Mon Sep 17 00:00:00 2001 From: Aidan Date: Thu, 2 Jul 2020 19:35:22 -0700 Subject: [PATCH 17/27] file operations in stdlib --- stdlib/bool.go | 2 +- stdlib/filesys.go | 148 ++++++++++++++++++++++++++++++++++++++++++++++ stdlib/stdlib.go | 28 +++++++++ 3 files changed, 177 insertions(+), 1 deletion(-) diff --git a/stdlib/bool.go b/stdlib/bool.go index 5e142bc..328c4e7 100644 --- a/stdlib/bool.go +++ b/stdlib/bool.go @@ -43,9 +43,9 @@ func not(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 - second := in.Next in = in.Eval(ft, vt, false) + second := in.Next if in.Tag != second.Tag { out = ast.FALSE diff --git a/stdlib/filesys.go b/stdlib/filesys.go index 83a45ef..6504221 100644 --- a/stdlib/filesys.go +++ b/stdlib/filesys.go @@ -19,10 +19,33 @@ package stdlib import ( "os" + "io/ioutil" "gitlab.com/whom/shs/ast" "gitlab.com/whom/shs/log" ) +/* Take a path, return the absolute path + * does not verify that the absolute path is correct + * currently only supports paths using forward slashes + * + * TODO: handle ~ + */ +func AbsPath(arg string) string { + if arg[0] != '/' { + dir, err := os.Getwd() + if err != nil { + log.Log(log.ERR, + "Couldnt get working directory: " + err.Error(), + "path") + return arg + } + + return dir + "/" + arg + } + + return arg +} + func cd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { in = in.Eval(ft, vt, true) @@ -44,3 +67,128 @@ 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 { + in = in.Eval(ft, vt, false) + if in == nil || (in.Tag != ast.NUMBER && in.Tag != ast.STRING) { + log.Log(log.ERR, + "argument to fexists must be a string or number", + "fexists") + return nil + } + + filename := in.Value() + out := ast.TRUE + + if _, err := os.Stat(AbsPath(filename)); err != nil { + log.Log(log.DEBUG, + "couldnt stat file: " + err.Error(), + "fexists") + out = ast.FALSE + } + + ret := &ast.Token{Tag: ast.BOOL} + ret.Set(out) + return ret +} + +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 + if exists == nil || exists.Tag != ast.BOOL || exists.Value() == ast.FALSE { + log.Log(log.ERR, + "error calling fexists or file doesnt exist", + "fread") + return nil + } + + fname := in.Value() + text, err := ioutil.ReadFile(fname) + if err != nil { + log.Log(log.ERR, + "error reading file" + err.Error(), + "fread") + return nil + } + + ret := &ast.Token{Tag: ast.STRING} + ret.Set(string(text)) + return ret +} + +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, + "first argument must be a filename", + "fwrite") + return nil + } + + text := in.Next + if text == nil || text.Tag == ast.SYMBOL || text.Tag == ast.LIST { + log.Log(log.ERR, + "second argument must be stringable", + "fwrite") + return nil + } + + err := ioutil.WriteFile( + AbsPath(in.Value()), + []byte(text.Value()), + 0644) + if err != nil { + log.Log(log.ERR, + "error writing file: " + err.Error(), + "fwrite") + } + + return nil +} + +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, + "first argument must be a filename", + "fappend") + return nil + } + + text := in.Next + if text == nil || text.Tag == ast.SYMBOL || text.Tag == ast.LIST { + log.Log(log.ERR, + "second argument must be stringable", + "fappend") + return nil + } + + exists := fexists(in, vt, ft) + if exists.Value() == ast.FALSE { + log.Log(log.ERR, + "file "+in.Value()+" does not exist", + "fappend") + return nil + } + + + f, err := os.OpenFile( + AbsPath(in.Value()), + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + 0644) + if err != nil { + log.Log(log.ERR, + "couldnt open file for append: " + err.Error(), + "fappend") + return nil + } + defer f.Close() + + if _, err := f.WriteString(text.Value()); err != nil { + log.Log(log.ERR, + "error appending to file: " + err.Error(), + "fappend") + } + + return nil +} diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index acb74ee..c0b9c0f 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -232,6 +232,34 @@ func GenFuncTable() ast.FuncTable { TimesCalled: 0, Args: 1, }, + + "fexists": &ast.Function{ + Function: fexists, + Name: "file exists", + TimesCalled: 0, + Args: 1, + }, + + "fread": &ast.Function{ + Function: fread, + Name: "read file", + TimesCalled: 0, + Args: 1, + }, + + "fwrite": &ast.Function{ + Function: fwrite, + Name: "write file", + TimesCalled: 0, + Args: 2, + }, + + "fappend": &ast.Function{ + Function: fappend, + Name:"append to file", + TimesCalled: 0, + Args: 2, + }, } return stdlib From 0d7eb4139b55b6b6b12c05179986a194fbd85856 Mon Sep 17 00:00:00 2001 From: Aidan Date: Fri, 3 Jul 2020 01:05:25 -0700 Subject: [PATCH 18/27] dont require top level parens --- ast/eval.go | 2 +- ast/lex.go | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ast/eval.go b/ast/eval.go index 0d1c86a..bc0c17b 100644 --- a/ast/eval.go +++ b/ast/eval.go @@ -57,7 +57,7 @@ func (in *Token) Eval(funcs FuncTable, vars VarTable, cnvtUndefVars bool) *Token break } - log.Log(log.ERR, + log.Log(log.ERR, "undefined symbol: "+in.Value(), "eval") return nil diff --git a/ast/lex.go b/ast/lex.go index f5e2ff4..3c3bf4c 100644 --- a/ast/lex.go +++ b/ast/lex.go @@ -25,6 +25,17 @@ import ( const string_delims string = "\"'`" func Lex(input string) *Token { + ret := lex(input) + if ret.Tag != LIST { + temp := &Token{Tag: LIST} + temp.Direct(ret) + ret = temp + } + + return ret +} + +func lex(input string) *Token { if len(input) == 0 { return nil } @@ -43,7 +54,7 @@ func Lex(input string) *Token { (*iter).Position = pos if is_list { - (*iter).inner = Lex(tok) + (*iter).inner = lex(tok) (*iter).Tag = LIST is_list = false From f98cd751605195c27dee865d5e7a08076955ada4 Mon Sep 17 00:00:00 2001 From: Aidan Date: Fri, 3 Jul 2020 02:02:12 -0700 Subject: [PATCH 19/27] load from config file --- cmd/shs_repl.go | 44 +++++++++++++++++++------------- config/config.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 config/config.go diff --git a/cmd/shs_repl.go b/cmd/shs_repl.go index 2f32bca..ea6007c 100644 --- a/cmd/shs_repl.go +++ b/cmd/shs_repl.go @@ -18,21 +18,26 @@ package main import ( - "os" "fmt" "strconv" "github.com/chzyer/readline" "gitlab.com/whom/shs/ast" "gitlab.com/whom/shs/log" - "gitlab.com/whom/shs/stdlib" + "gitlab.com/whom/shs/config" ) const ( def_prompt string = "λ " ) -func setLogLvl() { - loglvl := os.Getenv("SH_LOGGING") +func setLogLvl(vars ast.VarTable) { + var loglvl string + + loglvl_t := ast.GetVar("SH_LOGGING", vars) + if loglvl_t != nil { + loglvl = loglvl_t.Value() + } + if loglvl != "" { llvl, err := strconv.ParseInt(loglvl, 10, 8) if err != nil { @@ -46,23 +51,28 @@ func setLogLvl() { } func main() { - debug := os.Getenv("SH_DEBUG_MODE") - hist := os.Getenv("SH_HIST_FILE") - prompt := os.Getenv("SHS_SH_PROMPT") + var prompt string + var debug string + var hist string - var vars ast.VarTable - var funcs ast.FuncTable - - funcs = stdlib.GenFuncTable() - vars = &map[string]*ast.Token{} - - ast.InitVarTable(vars) ast.SyncTablesWithOSEnviron = true ast.ExecWhenFuncUndef = true - var err error + vars, funcs := config.InitFromConfig(".shsrc") + debug_t := ast.GetVar("SH_DEBUG_MODE", vars) + if debug_t != nil { + debug = debug_t.Value() + } - if prompt == "" { + hist_t := ast.GetVar("SH_HIST_FILE", vars) + if hist_t != nil { + hist = hist_t.Value() + } + + prompt_t := ast.GetVar("SHS_SH_PROMPT", vars) + if prompt_t != nil { + prompt = prompt_t.Value() + } else { prompt = def_prompt } @@ -79,7 +89,7 @@ func main() { } for { - setLogLvl() + setLogLvl(vars) text, err := rl.Readline() if err != nil { log.Log(log.ERR, "couldnt read user input: " + err.Error(), "repl") diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..6ce7d51 --- /dev/null +++ b/config/config.go @@ -0,0 +1,66 @@ +/* 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 config + +import ( + "os" + "io" + "bufio" + "gitlab.com/whom/shs/log" + "gitlab.com/whom/shs/ast" + "gitlab.com/whom/shs/stdlib" +) + +func InitFromConfig(configFile string) (ast.VarTable, ast.FuncTable) { + funcs := stdlib.GenFuncTable() + vars := &map[string]*ast.Token{} + + ast.InitVarTable(vars) + + 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') + } + + log.Log(log.DEBUG, + "config file fully evaluated", + "config") + cfile.Close() + return vars, funcs +} From 346e24179dfb23f0078c5e22b6bb5ab13d290d31 Mon Sep 17 00:00:00 2001 From: Aidan Date: Fri, 3 Jul 2020 10:58:25 -0700 Subject: [PATCH 20/27] update top level readme --- Readme.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/Readme.md b/Readme.md index 993274c..ced6331 100644 --- a/Readme.md +++ b/Readme.md @@ -4,6 +4,51 @@ Syntactically Homogeneous Shell ## Overview This shell was created to have extremely simple syntax. S-Expressions were chosen to represent statements and the scope of language features were constrained to what could be considered practical for daily shell use. This program is meant to be practical for administrators and daily power users. +## Basic Syntax +When in doubt the `print_ast` utility can be used to examine the output of the Lex+Parse process. Here you can spot any bugs regarding syntax. + +### Lists +Any sequence of items within a set of parenthesis is a list +`(1 "two" three 4)` + +Lists can be infinitely nested +`("one" (2 3 4 (5)))` + +### Data types +We use the following data types +* Number: 1, 2.0, etc +* String: "this is a string" (string delimiters: ' " \`) +* Bool: T or F +* Symbol: a string with no delimiters +* List: a sequence of elements within parenthesis + +### Function calls +Any list beginning in a symbol will be considered a function call. +From within the `shs_repl` utility, unknown symbols will be assumed to be system binaries. + +`(append () 1 2 3)` +`(vim Readme.md)` +`(if (eq "example" (fread 'test_file')) (print "test worked) (rm -rf /))` + +### Variable declaration +There are a few ways to export variables +* export: `(export NAME (value))` +* let: `(let ((var1 val1) (var2 val2)) (form_to_be_evaluated))` + +Currently, let has yet to be implemented + +### Function declaration +Use the `func` function from the stdlib: +`(func name (var1, var2, var3) (form_to_be_evaluated))` +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: +`(if (cond) (then) (else))` +`(when (cond) (form1)....... (formN))` + +We also have functioning implementations of map and reduce in the stdlib + ## 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 @@ -13,6 +58,14 @@ This shell was created to have extremely simple syntax. S-Expressions were chose - Make sure the GPLv3 is adhered to - *OVERRIDE THE STDLIB GenFuncTable FUNCTION.* You very likely do NOT want an available function to call system binaries in your embedded shell. Make sure the stdlib Call function is not included. +## Configuration +* 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 + - `SH_HIST_FILE` Sets the history file + - `SH_DEBUG_MODE` Adds additional debug output for the lexer + ## Contributing - Any contribution to this software is welcome as long as it adheres to the conduct guidelines specified in the `Contributing.md` file in this repository. - Consider reading the [STDLIB Readme](https://git.callpipe.com/aidan/shs/-/blob/master/stdlib/Readme.md) for more information on how to extend this project. From a08677b4f486d8a2be91eb9b8d5a3a1d83e43f55 Mon Sep 17 00:00:00 2001 From: Aidan Date: Fri, 3 Jul 2020 11:01:29 -0700 Subject: [PATCH 21/27] test --- stdlib/vars.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 stdlib/vars.go diff --git a/stdlib/vars.go b/stdlib/vars.go new file mode 100644 index 0000000..1213cb8 --- /dev/null +++ b/stdlib/vars.go @@ -0,0 +1,25 @@ +/* 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 stdlib + +import ( + "gitlab.com/whom/shs/ast" + "gitlab.com/whom/shs/log" +) + + From 4865c7ce927b076cdc53935a97a7929f836635d0 Mon Sep 17 00:00:00 2001 From: Aidan Date: Fri, 3 Jul 2020 16:27:02 -0700 Subject: [PATCH 22/27] export var functions --- Readme.md | 9 +++++++++ ast/func_table.go | 4 ++-- ast/var_table.go | 1 - stdlib/stdlib.go | 7 +++++++ stdlib/vars.go | 15 +++++++++++++++ 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index ced6331..36ec924 100644 --- a/Readme.md +++ b/Readme.md @@ -65,6 +65,15 @@ We also have functioning implementations of map and reduce in the stdlib - `SHS_SH_PROMPT` Sets the prompt - `SH_HIST_FILE` Sets the history file - `SH_DEBUG_MODE` Adds additional debug output for the lexer +Here is an example of a shs configuration file: +```lisp +(export "GOPATH" (concat HOME "/go")) +(export "GOBIN" (concat GOPATH "/bin")) +(export "PATH" (concat PATH ":" GOBIN)) +(export "GIT_TERMINAL_PROMPT" 1) +(export "SH_HIST_FILE" (concat HOME "/.shs_hist")) +(export "SH_LOGGING" 3) +``` ## Contributing - Any contribution to this software is welcome as long as it adheres to the conduct guidelines specified in the `Contributing.md` file in this repository. diff --git a/ast/func_table.go b/ast/func_table.go index f248c05..b70f758 100644 --- a/ast/func_table.go +++ b/ast/func_table.go @@ -43,7 +43,7 @@ func (f Function) ParseFunction(args *Token) bool { } if i != 0 { - log.Log(log.ERR, + log.Log(log.ERR, "Incorrect number of arguments", "eval") return false @@ -69,7 +69,7 @@ func GetFunction(arg string, table FuncTable) *Function { if !ok { log.Log(log.DEBUG, "function " + arg + " not found", - "eval") + "ftable") return nil } diff --git a/ast/var_table.go b/ast/var_table.go index cf14127..7f463dd 100644 --- a/ast/var_table.go +++ b/ast/var_table.go @@ -56,7 +56,6 @@ func GetVar(arg string, vt VarTable) *Token { } // TODO: this could be much more optimal -// TODO: Make sure variables are evaluated before being set // probably a stdlib thing func SetVar(variable string, value *Token, vt VarTable) { (*vt)[variable] = value diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index c0b9c0f..046a229 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -48,6 +48,13 @@ func GenFuncTable() ast.FuncTable { Args: -1, }, + "export": &ast.Function{ + Function: export, + Name: "export", + TimesCalled: 0, + Args: 2, + }, + "input": &ast.Function{ Function: input, Name: "input", diff --git a/stdlib/vars.go b/stdlib/vars.go index 1213cb8..ced45f8 100644 --- a/stdlib/vars.go +++ b/stdlib/vars.go @@ -22,4 +22,19 @@ import ( "gitlab.com/whom/shs/log" ) +func export(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { + input = input.Eval(funcs, vars, false) + + name := input + form := name.Next + if name.Tag != ast.STRING { + log.Log(log.ERR, + "non string handed to name arg", + "export") + return nil + } + + ast.SetVar(name.Value(), form, vars) + return nil +} From c90d445d7d2a5e85b8629abdec70159ca6795a2f Mon Sep 17 00:00:00 2001 From: Aidan Date: Fri, 3 Jul 2020 19:51:03 -0700 Subject: [PATCH 23/27] added ability for user to enter boolean literals --- ast/lex.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ast/lex.go b/ast/lex.go index 3c3bf4c..532f32a 100644 --- a/ast/lex.go +++ b/ast/lex.go @@ -26,6 +26,10 @@ const string_delims string = "\"'`" func Lex(input string) *Token { ret := lex(input) + if ret == nil { + return nil + } + if ret.Tag != LIST { temp := &Token{Tag: LIST} temp.Direct(ret) @@ -67,6 +71,9 @@ func lex(input string) *Token { } else if StrIsNumber(tok) { (*iter).Tag = NUMBER + } else if tok == "T" || tok == "F" { + (*iter).Tag = BOOL + } else { (*iter).Tag = SYMBOL } From 19a16d8de0351a6f0f062cdced9cb89b92656b46 Mon Sep 17 00:00:00 2001 From: Aidan Date: Sat, 4 Jul 2020 20:41:10 -0700 Subject: [PATCH 24/27] Updated readme's, add prototype function declaration operation --- Readme.md | 13 ++++-- ast/var_table.go | 12 ++++++ stdlib/Readme.md | 1 + stdlib/funcs.go | 106 +++++++++++++++++++++++++++++++++++++++++++++++ stdlib/stdlib.go | 9 +++- 5 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 stdlib/funcs.go diff --git a/Readme.md b/Readme.md index 36ec924..9d46e87 100644 --- a/Readme.md +++ b/Readme.md @@ -47,18 +47,25 @@ See `stdlib/control_flow.go`. We have if and while forms: `(if (cond) (then) (else))` `(when (cond) (form1)....... (formN))` -We also have functioning implementations of map and reduce in the stdlib +We also have functioning implementations of map and reduce in the stdlib (incomplete) ## 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 ### Adding SHS to your application -- TODO: write a how to here +* Make sure to set ast.SyncTablesWithOSEnviron, ast.ExecWhenFuncUndef. All of which control integrations with the underlying system. + - If you do not want the user to be able to set environment variables set ast.SyncTablesWithOSEnviron to false. + - If you do not want the user to be able to call binaries from the host system, set ast.ExecWhenFuncUndef to false. +- Get text you are interested in parsing +- Create a new VarTable and FuncTable (see ast/var_table.go and ast/func_table.go) +- Call `Lex(text)` on the `text` you want to evaluate to recieve a tree of parsed lexemes. +- Call `tree.Eval(FuncTable, VarTable, false)` where tree is the returned data from Lex, and the final boolean argument is whether or not to convert unknown symbols to strings. (this is a helpful option if you are writing functions such as those in stdlib/call.go, or any funciton in which you may want to be able to edit and transform the final ast based on your own varaiable table) - Make sure the GPLv3 is adhered to - *OVERRIDE THE STDLIB GenFuncTable FUNCTION.* You very likely do NOT want an available function to call system binaries in your embedded shell. Make sure the stdlib Call function is not included. ## Configuration +* variables exported in the repl, if of types string or number, will result in a corresponding variable added to the Environment. * 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) @@ -72,7 +79,7 @@ Here is an example of a shs configuration file: (export "PATH" (concat PATH ":" GOBIN)) (export "GIT_TERMINAL_PROMPT" 1) (export "SH_HIST_FILE" (concat HOME "/.shs_hist")) -(export "SH_LOGGING" 3) +(export "SH_LOGGING" 0) ``` ## Contributing diff --git a/ast/var_table.go b/ast/var_table.go index 7f463dd..7037e4a 100644 --- a/ast/var_table.go +++ b/ast/var_table.go @@ -121,3 +121,15 @@ func DeleteVarTable(table VarTable) { } } } + +func RemoveVar(arg string, table VarTable) { + if SyncTablesWithOSEnviron { + err := os.Unsetenv(arg) + if err != nil { + log.Log(log.DEBUG, + "Failed to remove "+arg+" from env: "+err.Error(), + "vartable") + } + } + delete(*table, arg) +} diff --git a/stdlib/Readme.md b/stdlib/Readme.md index afec954..a766c68 100644 --- a/stdlib/Readme.md +++ b/stdlib/Readme.md @@ -27,6 +27,7 @@ The standard library is loaded during the init step of the repl (or interpreter [Tokens](https://git.callpipe.com/aidan/shs/-/blob/master/ast/token.go) are a rudimentary linked list of parsed [Lexemes](https://en.wikipedia.org/wiki/Lexeme). In the ast package there are definitions for Tokens, as well as code for the combined Lex/Parse loop that creates them. Tokens are built in a way that makes operating over them with either recursive or iterative alrogithms easy. When consuming Tokens, one can expect their type by looking at the Tag field. The data stored in the Inner field will be either a string or a \*Token depending on what Tag is. You can expect a \*Token if the Tag field is ast.LIST, and a string in all other cases. If the Tag field is ast.SYMBOL you can look it up in the VarTable or the FuncTable. The VarTable will return either a \*Token (if the symbol is a Variable) or *nil* if nothing is found. The FuncTable will return either a \*Function (if there is a match) or it will return *nil*. P.S.: Ideally a token should not be re-used. You may consider them disposable. It is up to you to make sure that any Token you edit/reuse remains consistant with the type declared in its TAG. Make sure to differentiate between NUMBER and STRING with the `ast.StrIsNumber(arg string) bool` function. ## Adding a function +There are two ways to define functions: Either by writing it in shs code (using the 'func' function) or by extending the standard library. The steps below assume you are extending the standard library. 1. *Write your function in the form of an `ast.Operation`.* Any function that has the defined signature can be an Operation. 2. *Create a `Function` to encapsulate your `Operation`.* Make sure to set the `args` and `name` fields. Args will be used to validate function calls and Name will be used in debug/log output. 3. *Add your `Function` to the `FuncTable`.* Make sure your `Operations`s get added to the table generated in `GenFuncTable`. diff --git a/stdlib/funcs.go b/stdlib/funcs.go new file mode 100644 index 0000000..f370e30 --- /dev/null +++ b/stdlib/funcs.go @@ -0,0 +1,106 @@ +/* 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 stdlib + +import ( + "gitlab.com/whom/shs/ast" + "gitlab.com/whom/shs/log" +) + +func decl_func(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { + name := input + if name.Tag != ast.STRING { + log.Log(log.ERR, + "non string handed to name arg", + "func") + return nil + } + + var numArgs int + args := name.Next + if args.Tag != ast.LIST { + log.Log(log.ERR, + "argument 2 of func must be a flat list of argument symbols", + "func") + return nil + } + + form := args.Next + if form.Tag != ast.LIST { + log.Log(log.ERR, + "argument 3 of func must be a form to be evaluated", + "func") + return nil + } + + for i := args.Expand(); i != nil; i = i.Next { + if i.Tag != ast.SYMBOL { + log.Log(log.ERR, + "all args in user defined functions must be declared in the form of symbols", + "func") + return nil + } + numArgs += 1 + } + + ASTSYNCSTATE := ast.SyncTablesWithOSEnviron + inner := func(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { + in = in.Eval(ft, vt, false) + if in == nil { + log.Log(log.ERR, + "error parsing arguments", + name.Value()) + return nil + } + + ast.SyncTablesWithOSEnviron = false + key_iter := args.Expand() + val_iter := in + + 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 + } + + ast.SyncTablesWithOSEnviron = ASTSYNCSTATE + ret := form.Eval(ft, vt, false) + ast.SyncTablesWithOSEnviron = false + for i := args.Expand(); i != nil; i = i.Next { + ast.RemoveVar(i.Value(), vt) + } + + ast.SyncTablesWithOSEnviron = ASTSYNCSTATE + return ret + } + + (*funcs)[name.Value()] = &ast.Function{ + Function: inner, + Name: name.Value(), + TimesCalled: 0, + Args: numArgs, + } + return nil +} + diff --git a/stdlib/stdlib.go b/stdlib/stdlib.go index 046a229..5ad08a5 100644 --- a/stdlib/stdlib.go +++ b/stdlib/stdlib.go @@ -48,6 +48,13 @@ func GenFuncTable() ast.FuncTable { Args: -1, }, + "func": &ast.Function{ + Function: decl_func, + Name: "decl_func", + TimesCalled: 0, + Args: 3, + }, + "export": &ast.Function{ Function: export, Name: "export", @@ -55,7 +62,7 @@ func GenFuncTable() ast.FuncTable { Args: 2, }, - "input": &ast.Function{ + "input": &ast.Function{ Function: input, Name: "input", TimesCalled: 0, From 5a0a7e21f58b39cbe0b8cdfe45a18b90ad5aaf96 Mon Sep 17 00:00:00 2001 From: Aidan Date: Wed, 8 Jul 2020 19:33:25 -0700 Subject: [PATCH 25/27] fix variable declaration syntax --- stdlib/vars.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/stdlib/vars.go b/stdlib/vars.go index ced45f8..6aed9f9 100644 --- a/stdlib/vars.go +++ b/stdlib/vars.go @@ -23,13 +23,12 @@ import ( ) func export(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { - input = input.Eval(funcs, vars, false) - name := input - form := name.Next - if name.Tag != ast.STRING { + + form := name.Next.Eval(funcs, vars, false) + if name.Tag != ast.SYMBOL { log.Log(log.ERR, - "non string handed to name arg", + "first arg should be a symbol", "export") return nil } From 93cd5c5a48e0693b4b80b8a90cd337e3567a9b21 Mon Sep 17 00:00:00 2001 From: Aidan Date: Wed, 8 Jul 2020 19:39:09 -0700 Subject: [PATCH 26/27] fix func declaration syntax --- stdlib/funcs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stdlib/funcs.go b/stdlib/funcs.go index f370e30..88aafbb 100644 --- a/stdlib/funcs.go +++ b/stdlib/funcs.go @@ -24,9 +24,9 @@ import ( func decl_func(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { name := input - if name.Tag != ast.STRING { + if name.Tag != ast.SYMBOL { log.Log(log.ERR, - "non string handed to name arg", + "argument 1 of func must be a symbol to be exported", "func") return nil } From 81d299aa5ec702ae96450bdd1787f969dc3f8761 Mon Sep 17 00:00:00 2001 From: Aidan Date: Wed, 8 Jul 2020 21:09:24 -0700 Subject: [PATCH 27/27] fix bug in repeated function calls --- ast/eval.go | 13 +++++-------- ast/token.go | 12 ++++++++++++ stdlib/funcs.go | 6 +++--- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/ast/eval.go b/ast/eval.go index bc0c17b..0f6cc98 100644 --- a/ast/eval.go +++ b/ast/eval.go @@ -43,17 +43,16 @@ func (in *Token) Eval(funcs FuncTable, vars VarTable, cnvtUndefVars bool) *Token switch in.Tag { case BOOL, NUMBER, STRING: - res = in + res = in.Copy() case SYMBOL: res = GetVar(in.Value(), vars) if res == nil { - res = in + res = in.Copy() if GetFunction(in.Value(), funcs) == nil { if cnvtUndefVars { - in.Tag = STRING - res = in + res.Tag = STRING break } @@ -63,19 +62,17 @@ func (in *Token) Eval(funcs FuncTable, vars VarTable, cnvtUndefVars bool) *Token return nil } } - res.Next = in.Next - case LIST: inner := in.Expand() if inner == nil { - res = in + res = in.Copy() break } if inner.Tag != SYMBOL { in.Direct(inner.Eval(funcs, vars, cnvtUndefVars)) - res = in + res = in.Copy() break } diff --git a/ast/token.go b/ast/token.go index 645e203..367cd42 100644 --- a/ast/token.go +++ b/ast/token.go @@ -48,6 +48,18 @@ func (t *Token) Append(arg *Token) { } } +/* Shallow Copy + * in case of a LIST, + * inner will point to the same list. + */ +func (t *Token) Copy() *Token { + return &Token{ + Tag: t.Tag, + inner: t.inner, + Next: t.Next, + } +} + /* Print function which is better suited for repl. * This one prints the SEXPRs as one would write them. * Does not evaluate tokens. diff --git a/stdlib/funcs.go b/stdlib/funcs.go index 88aafbb..93de50d 100644 --- a/stdlib/funcs.go +++ b/stdlib/funcs.go @@ -60,8 +60,8 @@ 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 { - in = in.Eval(ft, vt, false) - if in == nil { + temp := in.Eval(ft, vt, false) + if temp == nil { log.Log(log.ERR, "error parsing arguments", name.Value()) @@ -70,7 +70,7 @@ func decl_func(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.To ast.SyncTablesWithOSEnviron = false key_iter := args.Expand() - val_iter := in + val_iter := temp for key_iter != nil { if val_iter == nil {