Merge branch 'documentation' into 'master'

Documentation

See merge request whom/shs!6
This commit is contained in:
Aidan Hahn 2020-07-20 04:47:24 +00:00
commit 63de839d98
25 changed files with 785 additions and 174 deletions

View file

@ -43,12 +43,17 @@ Use the `func` function from the stdlib:
In this case, `(form_to_be_evaluated)` will not be evaluated until the function is called. In this case, `(form_to_be_evaluated)` will not be evaluated until the function is called.
### Control flow ### Control flow
See `stdlib/control_flow.go`. We have if and while forms: See `stdlib/control_flow.go`. We have if, while, and progn forms:
`(if (cond) (then) (else))` `(if (cond) (then) (else))`
`(when (cond) (form1)....... (formN))` `(when (cond) (form1)....... (formN))`
`(progn (form1)..... (formN))`
If and While should be self explanatory. For those new to LISP, the rough idea of progn is to evaluate a sequence of N forms and return the result of the final one.
We also have functioning implementations of map and reduce in the stdlib (incomplete) We also have functioning implementations of map and reduce in the stdlib (incomplete)
## Comments
The standard delimiter for comments is ;
any characters after a semicolon will be ignored until end of line
## How to build ## How to build
### Compiling/Installation ### 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 - For now simply run `go install cmd/...` for each utility you wish to use. If you have GOPATH and GOBIN set it should be usable from PATH
@ -69,9 +74,14 @@ We also have functioning implementations of map and reduce in the stdlib (incomp
* one can write arbitrary shs script into `.shsrc` including function and variable declarations * one can write arbitrary shs script into `.shsrc` including function and variable declarations
* of note are the following variables * of note are the following variables
- `SH_LOGGING` Sets the log level (from 0 to 3) - `SH_LOGGING` Sets the log level (from 0 to 3)
- `SHS_SH_PROMPT` Sets the prompt - `SHS_STATIC_PROMPT` Sets the prompt
- `SH_HIST_FILE` Sets the history file - `SH_HIST_FILE` Sets the history file
- `SH_DEBUG_MODE` Adds additional debug output for the lexer - `SH_DEBUG_MODE` Adds additional debug output for the lexer (high clutter)
* additionally, the repl will evaluate any function you define as `_SH_PROMPT` before the shell prompt
- if defined, the function will be evaluated before printing the prompt
- the function will be given 0 arguments
- if the function does not return a string, its output will be discarded
- afterwards, the repl will print the values in `SHS_STATIC_PROMPT`
Here is an example of a shs configuration file: Here is an example of a shs configuration file:
```lisp ```lisp
(export GOPATH (concat HOME "/go")) (export GOPATH (concat HOME "/go"))
@ -80,6 +90,8 @@ Here is an example of a shs configuration file:
(export GIT_TERMINAL_PROMPT 1) (export GIT_TERMINAL_PROMPT 1)
(export SH_HIST_FILE (concat HOME "/.shs_hist")) (export SH_HIST_FILE (concat HOME "/.shs_hist"))
(export SH_LOGGING 0) (export SH_LOGGING 0)
(export SHS_STATIC_PROMPT ">")
(func _SH_PROMPT () (concat (?) ($ basename ($ pwd)) "\n"))
``` ```
## Contributing ## Contributing

View file

@ -19,12 +19,14 @@ package ast
import "gitlab.com/whom/shs/log" import "gitlab.com/whom/shs/log"
/* determines whether or not to execute a system call /* determines whether or not to execute a system binary
* when a function cannot be found in the functable * when a function cannot be found in the functable
* (use case: shell) * (use case: shell)
* ExecFunc determines the name of the system call function to fetch
*/ */
var ExecWhenFuncUndef = false var ExecWhenFuncUndef = false
/* name of the command used to execute a system binary
*/
var ExecFunc = "l" var ExecFunc = "l"
/* Runs through an AST of tokens /* Runs through an AST of tokens
@ -61,6 +63,8 @@ func (in *Token) Eval(funcs FuncTable, vars VarTable, cnvtUndefVars bool) *Token
"eval") "eval")
return nil return nil
} }
} else {
res.Next = in.Next
} }
case LIST: case LIST:

View file

@ -19,18 +19,35 @@ package ast
import "gitlab.com/whom/shs/log" import "gitlab.com/whom/shs/log"
/* expected function header for any stdlib function
*/
type Operation func(*Token, VarTable, FuncTable) *Token type Operation func(*Token, VarTable, FuncTable) *Token
/* holds a stdlib function along with relevant metadata
*/
type Function struct { type Function struct {
// go function that list of args are passed to
Function Operation Function Operation
// name of function
Name string Name string
// number of times user has called this function
TimesCalled int TimesCalled int
Args int // TODO: Make this a list of expected types (TAGs)
// number of args required
Args int
} }
/* holds a mapping of key to function
* passed to eval and into function calls
* initialized by repl at startup
*/
type FuncTable *map[string]*Function type FuncTable *map[string]*Function
// TODO: Currently only checks arg list length /* validates an individual call of a function
* makes sure correct arguments are passed in
*/
func (f Function) ParseFunction(args *Token) bool { func (f Function) ParseFunction(args *Token) bool {
// handle infinite args // handle infinite args
if f.Args < 0 { if f.Args < 0 {
@ -52,6 +69,9 @@ func (f Function) ParseFunction(args *Token) bool {
return true return true
} }
/* handles a call to a function
* calls ParseFunction and increments TimesCalled
*/
func (f Function) CallFunction(args *Token, vt VarTable, ft FuncTable) *Token { func (f Function) CallFunction(args *Token, vt VarTable, ft FuncTable) *Token {
if !f.ParseFunction(args) { if !f.ParseFunction(args) {
log.Log(log.ERR, log.Log(log.ERR,
@ -64,6 +84,8 @@ func (f Function) CallFunction(args *Token, vt VarTable, ft FuncTable) *Token {
return f.Function(args, vt, ft) return f.Function(args, vt, ft)
} }
/* searches for function mapped to argument in FuncTable
*/
func GetFunction(arg string, table FuncTable) *Function { func GetFunction(arg string, table FuncTable) *Function {
target, ok := (*table)[arg] target, ok := (*table)[arg]
if !ok { if !ok {
@ -75,3 +97,18 @@ func GetFunction(arg string, table FuncTable) *Function {
return target return target
} }
/* returns list of all functions in table
*/
func ListFuncs(ft FuncTable) []string {
keys := make([]string, len(*ft))
i := 0
for k := range *ft {
keys[i] = k
i++
}
return keys
}

View file

@ -22,8 +22,12 @@ import (
"unicode" "unicode"
) )
// all delimiters that work on strings
const string_delims string = "\"'`" const string_delims string = "\"'`"
/* takes a line of user input
* returns an unsimplified tree of tokens
*/
func Lex(input string) *Token { func Lex(input string) *Token {
ret := lex(input) ret := lex(input)
if ret == nil { if ret == nil {
@ -121,6 +125,17 @@ func lex(input string) *Token {
return -2 return -2
} }
// returns the end of the string OR the end of the line
matchLineEnd := func(start int) int {
for i := start; i < len(input); i++ {
if input[i] == '\n' {
return i
}
}
return len(input)
}
needs_alloc := false needs_alloc := false
start_pos := 0 start_pos := 0
for i := 0; i < len(input); i++ { for i := 0; i < len(input); i++ {
@ -137,13 +152,18 @@ func lex(input string) *Token {
is_str = true is_str = true
needs_alloc = true needs_alloc = true
case ' ': case ' ', '\n', '\t', '\v', '\f', '\r':
if i == start_pos { if i == start_pos {
start_pos += 1 start_pos += 1
continue continue
} }
needs_alloc = true needs_alloc = true
// comment case
case ';':
start_pos = i + 1
i = matchLineEnd(start_pos)
} }
if needs_alloc { if needs_alloc {
@ -185,6 +205,7 @@ error:
return nil return nil
} }
// returns true if a string could contain an int or float
func StrIsNumber(arg string) bool { func StrIsNumber(arg string) bool {
dotCount := 0 dotCount := 0

View file

@ -18,6 +18,7 @@
package ast package ast
import ( import (
"fmt"
"strings" "strings"
) )
@ -49,7 +50,7 @@ loop:
constructor.WriteString(iter.FmtToken()) constructor.WriteString(iter.FmtToken())
} }
println(constructor.String()) fmt.Printf(constructor.String() + "\n")
goto loop goto loop
} }

View file

@ -19,7 +19,11 @@ package ast
import "fmt" import "fmt"
/* token_t is a tag that declares the type of the
* datum contained in a token
*/
type Token_t int type Token_t int
const ( const (
LIST Token_t = iota LIST Token_t = iota
STRING Token_t = iota STRING Token_t = iota
@ -31,6 +35,9 @@ const (
FALSE string = "F" FALSE string = "F"
) )
/* Contains a parsed lexeme
* and a pointer to the next parsed lexeme in the same scope
*/
type Token struct { type Token struct {
Next *Token Next *Token
Tag Token_t Tag Token_t

View file

@ -17,16 +17,23 @@
package ast package ast
/* primitive stack type for tokens
* useful for iterative algorithms on tokens
*/
type TokenStack struct { type TokenStack struct {
buffer []*Token buffer []*Token
capacity int capacity int
} }
/* push token onto stack
*/
func (s *TokenStack) Push(v *Token) { func (s *TokenStack) Push(v *Token) {
s.capacity++ s.capacity++
s.buffer = append(s.buffer, v) s.buffer = append(s.buffer, v)
} }
/* pop token off stack
*/
func (s *TokenStack) Pop() *Token { func (s *TokenStack) Pop() *Token {
if s.capacity <= 0 { if s.capacity <= 0 {
return nil return nil

View file

@ -26,11 +26,19 @@ import (
"gitlab.com/whom/shs/log" "gitlab.com/whom/shs/log"
) )
// Trigger this if you are using this for a shell /* defines whether or not to synchronize tokens wiht os environment vars
* will not sync non stringable tokens
*/
var SyncTablesWithOSEnviron = false var SyncTablesWithOSEnviron = false
/* mapping of key to token.
*/
type VarTable *map[string]*Token type VarTable *map[string]*Token
/* retrieve the token cooresponding to a given key
* if SyncTablesWithOSEnviron is true and no token exists for a key
* os Environment variables will be searched for the key
*/
func GetVar(arg string, vt VarTable) *Token { func GetVar(arg string, vt VarTable) *Token {
val, ok := (*vt)[arg] val, ok := (*vt)[arg]
if !ok { if !ok {
@ -55,8 +63,10 @@ func GetVar(arg string, vt VarTable) *Token {
return val return val
} }
// TODO: this could be much more optimal /* adds a key->token mapping to the table
// probably a stdlib thing * if SyncTablesWithOSEnviron is true, will also add value to os environment
* will not do so for non stringable tokens
*/
func SetVar(variable string, value *Token, vt VarTable) { func SetVar(variable string, value *Token, vt VarTable) {
(*vt)[variable] = value (*vt)[variable] = value
if SyncTablesWithOSEnviron && if SyncTablesWithOSEnviron &&
@ -73,22 +83,21 @@ func SetVar(variable string, value *Token, vt VarTable) {
} }
} }
// Library represents variables defined in inner scope /* lists all vars in tables
// It is assumed library is ordered from innermost scope to outermost scope */
func GetVarFromTables(arg string, library []VarTable) *Token { func ListVars(vt VarTable) []string {
var res *Token keys := make([]string, len(*vt))
res = nil i := 0
for i := 0; i < len(library); i += 1 { for k := range *vt {
res = GetVar(arg, library[i]) keys[i] = k
if res != nil {
// TODO: Log scope res was found in?
break
}
} }
return res return keys
} }
/* if SyncTablesWithOSEnviron is true
* function will put ever environment variable into VarTable
*/
func InitVarTable(table VarTable) { func InitVarTable(table VarTable) {
if !SyncTablesWithOSEnviron { if !SyncTablesWithOSEnviron {
return return
@ -122,6 +131,9 @@ func DeleteVarTable(table VarTable) {
} }
} }
/* removes var from vartable
* if SyncTablesWithOSENviron is true, also unsets environment variable
*/
func RemoveVar(arg string, table VarTable) { func RemoveVar(arg string, table VarTable) {
if SyncTablesWithOSEnviron { if SyncTablesWithOSEnviron {
err := os.Unsetenv(arg) err := os.Unsetenv(arg)

View file

@ -18,11 +18,13 @@
package main package main
import ( import (
"os"
"fmt" "fmt"
"strconv" "strconv"
"github.com/chzyer/readline" "github.com/peterh/liner"
"gitlab.com/whom/shs/ast" "gitlab.com/whom/shs/ast"
"gitlab.com/whom/shs/log" "gitlab.com/whom/shs/log"
"gitlab.com/whom/shs/util"
"gitlab.com/whom/shs/config" "gitlab.com/whom/shs/config"
) )
@ -30,6 +32,22 @@ const (
def_prompt string = "λ " def_prompt string = "λ "
) )
// useful for when input contains escape sequences
// not checking delims cause thats up to the user who defines their prompts
func parseString(in string) string {
in = "\"" + in + "\""
out, err := strconv.Unquote(in)
if err != nil {
log.Log(log.ERR,
"Couldnt parse (pre?)prompt",
"init")
return ""
}
return out
}
func setLogLvl(vars ast.VarTable) { func setLogLvl(vars ast.VarTable) {
var loglvl string var loglvl string
@ -54,6 +72,7 @@ func main() {
var prompt string var prompt string
var debug string var debug string
var hist string var hist string
no_hist := false
ast.SyncTablesWithOSEnviron = true ast.SyncTablesWithOSEnviron = true
ast.ExecWhenFuncUndef = true ast.ExecWhenFuncUndef = true
@ -67,34 +86,62 @@ func main() {
hist_t := ast.GetVar("SH_HIST_FILE", vars) hist_t := ast.GetVar("SH_HIST_FILE", vars)
if hist_t != nil { if hist_t != nil {
hist = hist_t.Value() hist = hist_t.Value()
} else {
no_hist = true
} }
prompt_t := ast.GetVar("SHS_SH_PROMPT", vars) dyn_prompt := ast.GetFunction("_SH_PROMPT", funcs)
if dyn_prompt == nil || dyn_prompt.Args != 0 {
dyn_prompt = nil
}
prompt_t := ast.GetVar("SHS_STATIC_PROMPT", vars)
if prompt_t != nil { if prompt_t != nil {
prompt = prompt_t.Value() prompt = parseString(prompt_t.Value())
} else { } else {
prompt = def_prompt prompt = def_prompt
} }
rl, err := readline.NewEx(&readline.Config{ line := liner.NewLiner()
Prompt: prompt, defer line.Close()
HistoryFile: hist,
InterruptPrompt: "^C", line.SetCtrlCAborts(true)
line.SetCompleter(func(line string) (c []string) {
return util.ShellCompleter(line, vars, funcs)
}) })
defer rl.Close() var histFile *os.File
if err != nil { var err error
log.Log(log.ERR, "Couldnt initialize readline: " + err.Error(), "repl") if !no_hist {
return histFile, err = os.Open(hist)
if err == nil {
line.ReadHistory(histFile)
} else {
log.Log(log.ERR,
"couldnt read history: " + err.Error(),
"repl")
}
defer histFile.Close()
} }
for { for {
setLogLvl(vars) setLogLvl(vars)
text, err := rl.Readline() var prePrompt string
if err != nil { if dyn_prompt != nil {
log.Log(log.ERR, "couldnt read user input: " + err.Error(), "repl") p_tok := dyn_prompt.CallFunction(nil, vars, funcs)
if p_tok != nil && p_tok.Tag == ast.STRING {
prePrompt = parseString(p_tok.Value())
}
} }
fmt.Printf(prePrompt)
text, err := line.Prompt(prompt)
if err != nil && err != liner.ErrPromptAborted{
log.Log(log.ERR, "couldnt read user input: " + err.Error(), "repl")
continue
}
line.AppendHistory(text)
userInput := ast.Lex(text) userInput := ast.Lex(text)
if userInput == nil { if userInput == nil {
// errors handled in Lex // errors handled in Lex

View file

@ -18,14 +18,16 @@
package config package config
import ( import (
"os"
"io"
"bufio"
"gitlab.com/whom/shs/log" "gitlab.com/whom/shs/log"
"gitlab.com/whom/shs/ast" "gitlab.com/whom/shs/ast"
"gitlab.com/whom/shs/util"
"gitlab.com/whom/shs/stdlib" "gitlab.com/whom/shs/stdlib"
) )
/* creates new VarTable and FuncTable
* reads a configuration file
* executes contents and returns tables
*/
func InitFromConfig(configFile string) (ast.VarTable, ast.FuncTable) { func InitFromConfig(configFile string) (ast.VarTable, ast.FuncTable) {
funcs := stdlib.GenFuncTable() funcs := stdlib.GenFuncTable()
vars := &map[string]*ast.Token{} vars := &map[string]*ast.Token{}
@ -35,32 +37,10 @@ func InitFromConfig(configFile string) (ast.VarTable, ast.FuncTable) {
p := ast.GetVar("HOME", vars) p := ast.GetVar("HOME", vars)
configFile = p.Value() + "/" + configFile configFile = p.Value() + "/" + configFile
cfile, err := os.Open(configFile) util.LoadScript(configFile, vars, funcs)
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, log.Log(log.DEBUG,
"config file fully evaluated", "config file fully evaluated",
"config") "config")
cfile.Close()
return vars, funcs return vars, funcs
} }

5
go.mod
View file

@ -2,4 +2,7 @@ module gitlab.com/whom/shs
go 1.14 go 1.14
require github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/peterh/liner v1.2.0 // indirect
)

4
go.sum
View file

@ -1,2 +1,6 @@
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/peterh/liner v1.2.0 h1:w/UPXyl5GfahFxcTOz2j9wCIHNI+pUPr2laqpojKNCg=
github.com/peterh/liner v1.2.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=

View file

@ -29,16 +29,23 @@ const (
var logLevel int64 var logLevel int64
/* Set the log level from 0 to 4
* currently only 0, 1, and 2 are used.
*/
func SetLogLvl(lvl int64) { func SetLogLvl(lvl int64) {
if lvl < 4 && lvl > 0 { if lvl < 4 && lvl > 0 {
logLevel = lvl logLevel = lvl
} }
} }
/* get current log level as int
*/
func GetLogLvl() int64 { func GetLogLvl() int64 {
return logLevel return logLevel
} }
/* writes a message to the log
*/
func Log(lvl int, msg, context string) { func Log(lvl int, msg, context string) {
if int64(lvl) > logLevel { if int64(lvl) > logLevel {
return return

View file

@ -29,7 +29,40 @@ import (
// perhaps we simply write out arithmetic routines that operate on the strings // perhaps we simply write out arithmetic routines that operate on the strings
// then we need not worry about storage length. // then we need not worry about storage length.
func add(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { /* Takes 1 argument (must be a string)
* will attempt to cast it to a number.
* will return nil if cast fails
*
* Example: (number "3.4")
*/
func NumCast(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
in = in.Eval(f, a, false)
if in.Tag != ast.STRING {
log.Log(log.ERR,
"only a string can successfully be cast to a number",
"number_cast")
return nil
}
if !ast.StrIsNumber(in.Value()) {
log.Log(log.ERR,
"string failed number cast",
"number_cast")
return nil
}
out := in.Copy()
out.Tag = ast.NUMBER
return out
}
/* adds N number arguments
* takes N arguments
* returns the sum, or nil if improper arguments were given
*
* Example: (+ 1 2)
*/
func Add(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
var res float64 var res float64
in = in.Eval(f, a, false) in = in.Eval(f, a, false)
@ -71,7 +104,12 @@ func add(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
return t return t
} }
func sub(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { /* subtract N args from the final arg
* takes N args, returns nil if improper args given
*
* Example: (- 2 1)
*/
func Sub(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
var res float64 var res float64
var sub float64 var sub float64
@ -121,7 +159,12 @@ func sub(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
return t return t
} }
func mult(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { /* multiplies N arguments
* returns nil if an improper argument is given
*
* Example: (* 1 2)
*/
func Mult(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
res := 1.0 res := 1.0
in = in.Eval(f, a, false) in = in.Eval(f, a, false)
@ -163,7 +206,13 @@ func mult(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
return t return t
} }
func div(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token { /* divide N arguments
* the first argument is divided by each subsequent argument in order
* returns nil if an improper argument is given
*
* Example (/ 25 5)
*/
func Div(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
var res float64 var res float64
in = in.Eval(f, a, false) in = in.Eval(f, a, false)

View file

@ -23,7 +23,35 @@ import (
"gitlab.com/whom/shs/ast" "gitlab.com/whom/shs/ast"
) )
func not(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* Takes one argument, must be a string
* attempts to cast to bool (T or F are valid values)
* returns nil on failure
*
* Example: (bool "F")
*/
func BoolCast(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, false)
if in.Tag == ast.LIST || in.Tag == ast.NUMBER {
log.Log(log.ERR,
"only strings successfully cast to bool",
"bool cast")
return nil
}
body := in.Value()
if body != ast.TRUE && body != ast.FALSE {
log.Log(log.ERR,
"cast to bool failed",
"bool cast")
return nil
}
res := &ast.Token{ Tag: ast.BOOL }
res.Set(body)
return res
}
func Not(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, false) in = in.Eval(ft, vt, false)
if in.Tag != ast.BOOL { if in.Tag != ast.BOOL {
@ -41,7 +69,7 @@ func not(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return t return t
} }
func eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { func Eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
out := ast.TRUE out := ast.TRUE
in = in.Eval(ft, vt, false) in = in.Eval(ft, vt, false)
@ -123,7 +151,7 @@ func eq(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return t return t
} }
func lt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { func Lt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
out := ast.TRUE out := ast.TRUE
second := in.Next second := in.Next
@ -146,7 +174,7 @@ func lt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return t return t
} }
func gt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { func Gt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
out := ast.TRUE out := ast.TRUE
second := in.Next second := in.Next
@ -169,14 +197,14 @@ func gt(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return t return t
} }
func ne(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { func Ne(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return not(eq(in, vt, ft), vt, ft) return Not(Eq(in, vt, ft), vt, ft)
} }
func gte(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { func Gte(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return not(lt(in, vt, ft), vt, ft) return Not(Lt(in, vt, ft), vt, ft)
} }
func lte(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { func Lte(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return not(gt(in, vt, ft), vt, ft) return Not(Gt(in, vt, ft), vt, ft)
} }

View file

@ -30,7 +30,6 @@ import (
) )
var bgProcs = make([]*exec.Cmd, 0) var bgProcs = make([]*exec.Cmd, 0)
var LastExitCode int
var sigs = []os.Signal{ var sigs = []os.Signal{
os.Interrupt, os.Interrupt,
syscall.SIGTERM, syscall.SIGTERM,
@ -40,8 +39,17 @@ var sigs = []os.Signal{
syscall.SIGCONT, syscall.SIGCONT,
} }
/* Exit code of last run process
*/
var LastExitCode int
func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* Takes n arguments (list of tokens generated by lexing a shell command)
* Evaluates arguments, but does not err on undefined symbols (note the last arg to Eval(...))
* Executes shell command and returns nil
*
* Example (l vim file.txt)
*/
func Call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, true) in = in.Eval(ft, vt, true)
if in == nil { if in == nil {
return nil return nil
@ -74,9 +82,10 @@ func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
} else { } else {
cmd = exec.Command(path) cmd = exec.Command(path)
} }
cmd.Env = os.Environ()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
signalChan := make(chan os.Signal, 2) signalChan := make(chan os.Signal, 2)
signal.Notify(signalChan, sigs...) signal.Notify(signalChan, sigs...)
@ -99,7 +108,12 @@ func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return nil return nil
} }
func bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* Starts a call in the background
* Takes n args (a shell command not delimited by string delimiters)
*
* Example: (bg vim file.txt)
*/
func Bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
if in == nil { if in == nil {
return nil return nil
} }
@ -143,7 +157,14 @@ func bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return nil return nil
} }
func fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* brings last BG'ed process into the foreground
* returns nil
*
* Example:
* (bg vim file.txt)
* (fg)
*/
func Fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
if len(bgProcs) < 1 { if len(bgProcs) < 1 {
return nil return nil
} }
@ -173,7 +194,16 @@ func fg(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return nil return nil
} }
func jobs(in *ast.Token, vt ast.VarTable, fg ast.FuncTable) *ast.Token { /* Takes 0 args
* returns a string containing info about current jobs
* returns total jobs as well as their PIDs and place in the bg queue
*
* Example:
* (bg ping google.com)
* (bg .........)
* (jobs)
*/
func Jobs(in *ast.Token, vt ast.VarTable, fg ast.FuncTable) *ast.Token {
ret := &ast.Token{ ret := &ast.Token{
Tag: ast.LIST, Tag: ast.LIST,
} }
@ -197,7 +227,12 @@ func jobs(in *ast.Token, vt ast.VarTable, fg ast.FuncTable) *ast.Token {
return ret return ret
} }
func read_cmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* calls a command (blocks until completion)
* captures stdout and returns it as a string
*
* Example: ($ echo hello world)
*/
func ReadCmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, true) in = in.Eval(ft, vt, true)
if in == nil { if in == nil {
@ -254,19 +289,37 @@ func read_cmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
} }
} }
output := out.String()
olen := len(output)
if olen > 0 && output[olen - 1] == '\n' {
output = output[:olen - 1]
}
ret := &ast.Token{Tag: ast.STRING} ret := &ast.Token{Tag: ast.STRING}
ret.Set(out.String()) ret.Set(output)
return ret return ret
} }
func get_exit(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* Takes 0 arguments
* returns the exit code of the last executed program
*
* Example:
* (sudo apt update)
* (?) <- gets exit code
*/
func GetExit(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
ret := &ast.Token{Tag: ast.NUMBER} ret := &ast.Token{Tag: ast.NUMBER}
ret.Set(fmt.Sprintf("%d", LastExitCode)) ret.Set(fmt.Sprintf("%d", LastExitCode))
return ret return ret
} }
func kill(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* takes an argument (pid of process to be killed)
* calls Process.Kill() on it
* do not use this if you already have a native implementation
* (this function not added to functable in stdlib.go)
*/
func Kill(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, true) in = in.Eval(ft, vt, true)
if in.Tag == ast.LIST { if in.Tag == ast.LIST {

View file

@ -22,9 +22,29 @@ import (
"gitlab.com/whom/shs/log" "gitlab.com/whom/shs/log"
) )
/* return one evaluated form or another based on the boolean statement /* eval N forms. return the last one
*
* Example:
* (progn (print "hello") (print "world") (+ 1 2))
* This example will print "hello world" to stdout and return 3
*/ */
func shs_if(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { func ShsProgn(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
var res *ast.Token
for iter := in; iter != nil; iter = iter.Next {
res = iter.Eval(ft, vt, false)
}
return res
}
/* return one evaluated form or another based on the boolean statement
* arg 1 is a boolean cond, arg 2 is evaluated if the cond is true, arg 3 is evaluated if cond is not true
* in total it takes 3 arguments
*
* Example:
* (if (eq (number "3") 3) (print "test passed") (print "test failed"))
*/
func ShsIf(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
cond := in cond := in
t := cond.Next t := cond.Next
f := t.Next f := t.Next
@ -55,8 +75,14 @@ func shs_if(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
} }
/* continually eval n forms while element #1 evals to T /* continually eval n forms while element #1 evals to T
* has rather progn like behavior in that it returns the result of the last form to be evaluated
*
* Example:
* (export cond F)
* (while cond (export cond T) (print "will only be printed once") (+ 1 2))
* loop will iter one time, print "will only be printed once" and return 3
*/ */
func shs_while(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { func ShsWhile(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
cond := in cond := in
forms := in.Next forms := in.Next
in.Next = nil in.Next = nil

View file

@ -46,7 +46,14 @@ func AbsPath(arg string) string {
return arg return arg
} }
func cd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* Takes one arg, returns nil
* changes directory to the path in the arg
* fails if arg is not stringable
*
* Example:
* (cd (concat HOME "/go"))
*/
func Cd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, true) in = in.Eval(ft, vt, true)
if in == nil { if in == nil {
@ -68,11 +75,19 @@ func cd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return nil return nil
} }
func fexists(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* Takes one arg, returns a bool
* Returns true if arg is a filepath that exists
* returns nil if arg is not a string type
*
* Example:
* (touch test)
* (fexists test)
*/
func Fexists(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, false) in = in.Eval(ft, vt, false)
if in == nil || (in.Tag != ast.NUMBER && in.Tag != ast.STRING) { if in == nil || in.Tag != ast.STRING {
log.Log(log.ERR, log.Log(log.ERR,
"argument to fexists must be a string or number", "argument to fexists must be a string",
"fexists") "fexists")
return nil return nil
} }
@ -92,9 +107,16 @@ func fexists(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return ret return ret
} }
func fread(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* Takes one arg, returns a string
* Returns contents of file in arg
* returns nil if file doesnt exist
*
* Example:
* (fread (concat HOME ".shsrc"))
*/
func Fread(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, false) in = in.Eval(ft, vt, false)
exists := fexists(in, vt, ft) // some waste, extra use of Eval exists := Fexists(in, vt, ft) // some waste, extra use of Eval
if exists == nil || exists.Tag != ast.BOOL || exists.Value() == ast.FALSE { if exists == nil || exists.Tag != ast.BOOL || exists.Value() == ast.FALSE {
log.Log(log.ERR, log.Log(log.ERR,
"error calling fexists or file doesnt exist", "error calling fexists or file doesnt exist",
@ -116,7 +138,14 @@ func fread(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return ret return ret
} }
func fwrite(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* Takes two arguments a filepath and a string
* CLOBBERS FILE CONTENTS
* Returns nil
*
* Example:
* (fwrite "test" "one two three")
*/
func Fwrite(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, false) in = in.Eval(ft, vt, false)
if in == nil || in.Tag == ast.SYMBOL || in.Tag == ast.LIST { if in == nil || in.Tag == ast.SYMBOL || in.Tag == ast.LIST {
log.Log(log.ERR, log.Log(log.ERR,
@ -146,7 +175,14 @@ func fwrite(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return nil return nil
} }
func fappend(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* Takes two arguments a filepath and a string
* DOES NOT CLOBBER FILE CONTENTS
* Returns nil
*
* Example:
* (fwrite "test" "one two three")
*/
func Fappend(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, false) in = in.Eval(ft, vt, false)
if in == nil || in.Tag == ast.SYMBOL || in.Tag == ast.LIST { if in == nil || in.Tag == ast.SYMBOL || in.Tag == ast.LIST {
log.Log(log.ERR, log.Log(log.ERR,
@ -163,7 +199,7 @@ func fappend(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return nil return nil
} }
exists := fexists(in, vt, ft) exists := Fexists(in, vt, ft)
if exists.Value() == ast.FALSE { if exists.Value() == ast.FALSE {
log.Log(log.ERR, log.Log(log.ERR,
"file "+in.Value()+" does not exist", "file "+in.Value()+" does not exist",

View file

@ -22,7 +22,17 @@ import (
"gitlab.com/whom/shs/log" "gitlab.com/whom/shs/log"
) )
func decl_func(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { /* Takes 3 arguments: name, list of arg names, and logic form
* DOES NOT EVALUATE THE LOGIC FORM
* adds an anonymous function to the FuncTable under the name specified
* anonymous function will expect the args declared in arg2 and expand them in arg3
* Then evaluates and returns result of arg3. This constitutes a function call
*
* Example:
* (func foo (x) (print x))
* (foo 4) -> prints 4
*/
func DeclFunc(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token {
name := input name := input
if name.Tag != ast.SYMBOL { if name.Tag != ast.SYMBOL {
log.Log(log.ERR, log.Log(log.ERR,
@ -60,30 +70,34 @@ func decl_func(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.To
ASTSYNCSTATE := ast.SyncTablesWithOSEnviron ASTSYNCSTATE := ast.SyncTablesWithOSEnviron
inner := func(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { inner := func(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
temp := in.Eval(ft, vt, false) var temp *ast.Token
if temp == nil { if numArgs != 0 || in != nil {
log.Log(log.ERR, temp = in.Eval(ft, vt, false)
"error parsing arguments", if temp == nil {
name.Value())
return nil
}
ast.SyncTablesWithOSEnviron = false
key_iter := args.Expand()
val_iter := temp
for key_iter != nil {
if val_iter == nil {
log.Log(log.ERR, log.Log(log.ERR,
"Not enough arguments supplied", "error parsing arguments",
name.Value()) name.Value())
return nil
} }
ast.SetVar(key_iter.Value(), val_iter, vt) ast.SyncTablesWithOSEnviron = false
key_iter = key_iter.Next key_iter := args.Expand()
val_iter = val_iter.Next val_iter := temp
for key_iter != nil {
if val_iter == nil {
log.Log(log.ERR,
"Not enough arguments supplied",
name.Value())
}
ast.SetVar(key_iter.Value(), val_iter, vt)
key_iter = key_iter.Next
val_iter = val_iter.Next
}
} }
// maybe we actually should put the inner scope var into the env
ast.SyncTablesWithOSEnviron = ASTSYNCSTATE ast.SyncTablesWithOSEnviron = ASTSYNCSTATE
ret := form.Eval(ft, vt, false) ret := form.Eval(ft, vt, false)
ast.SyncTablesWithOSEnviron = false ast.SyncTablesWithOSEnviron = false

View file

@ -27,7 +27,7 @@ import (
* retuns a sequence of elements (list contents) * retuns a sequence of elements (list contents)
* in event a not-list is passed in, returns the arg. * in event a not-list is passed in, returns the arg.
*/ */
func expand(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { func Expand(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token {
if input.Tag != ast.LIST { if input.Tag != ast.LIST {
log.Log(log.DEBUG, "expand called on not a list", "expand") log.Log(log.DEBUG, "expand called on not a list", "expand")
return input return input
@ -41,7 +41,7 @@ func expand(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token
* Arg one is a list, next args are appended * Arg one is a list, next args are appended
* if no args are a list, a list is made from all args * if no args are a list, a list is made from all args
*/ */
func l_append(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { func L_append(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token {
src := input src := input
if input.Tag != ast.LIST { if input.Tag != ast.LIST {

View file

@ -22,202 +22,234 @@ import (
"fmt" "fmt"
"gitlab.com/whom/shs/log" "gitlab.com/whom/shs/log"
"gitlab.com/whom/shs/ast" "gitlab.com/whom/shs/ast"
"gitlab.com/whom/shs/util"
) )
/* Makes a FuncTable from all functions in the stdlib
* add your function here if you are extending the standard library
*/
func GenFuncTable() ast.FuncTable { func GenFuncTable() ast.FuncTable {
var stdlib ast.FuncTable var stdlib ast.FuncTable
stdlib = &map[string]*ast.Function{ stdlib = &map[string]*ast.Function{
"if": &ast.Function{ "if": &ast.Function{
Function: shs_if, Function: ShsIf,
Name: "if", Name: "if",
TimesCalled: 0, TimesCalled: 0,
Args: 3, Args: 3,
}, },
"while": &ast.Function{ "while": &ast.Function{
Function: shs_while, Function: ShsWhile,
Name: "while", Name: "while",
TimesCalled: 0, TimesCalled: 0,
Args: -1, Args: -1,
}, },
"eval": &ast.Function{ "progn": &ast.Function{
Function: eval, Function: ShsProgn,
Name: "eval", Name: "shs_progn",
TimesCalled: 0, TimesCalled: 0,
Args: -1, Args: -1,
}, },
"func": &ast.Function{ "func": &ast.Function{
Function: decl_func, Function: DeclFunc,
Name: "decl_func", Name: "decl_func",
TimesCalled: 0, TimesCalled: 0,
Args: 3, Args: 3,
}, },
"export": &ast.Function{ "export": &ast.Function{
Function: export, Function: Export,
Name: "export", Name: "export",
TimesCalled: 0, TimesCalled: 0,
Args: 2, Args: 2,
}, },
"input": &ast.Function{ "input": &ast.Function{
Function: input, Function: Input,
Name: "input", Name: "input",
TimesCalled: 0, TimesCalled: 0,
Args: 1, Args: 1,
}, },
"...": &ast.Function{ "load": &ast.Function{
Function: expand, Function: Load,
Name: "load",
TimesCalled: 0,
Args: 1,
},
"bool": &ast.Function{
Function: BoolCast,
Name: "bool",
TimesCalled: 0,
Args: 1,
},
"string": &ast.Function{
Function: StrCast,
Name: "string",
TimesCalled: 0,
Args: 1,
},
"number": &ast.Function{
Function: NumCast,
Name: "number",
TimesCalled: 0,
Args: 1,
},
"...": &ast.Function{
Function: Expand,
Name: "...", Name: "...",
TimesCalled: 0, TimesCalled: 0,
Args: 1, Args: 1,
}, },
"append": &ast.Function{ "append": &ast.Function{
Function: l_append, Function: L_append,
Name: "append", Name: "append",
TimesCalled: 0, TimesCalled: 0,
Args: -1, Args: -1,
}, },
"exit": &ast.Function{ "exit": &ast.Function{
Function: exit_shell, Function: ExitShell,
Name: "exit", Name: "exit",
TimesCalled: 0, TimesCalled: 0,
Args: 0, Args: 0,
}, },
"eq": &ast.Function{ "eq": &ast.Function{
Function: eq, Function: Eq,
Name: "==", Name: "==",
TimesCalled: 0, TimesCalled: 0,
Args: 2, Args: 2,
}, },
"ne": &ast.Function{ "ne": &ast.Function{
Function: ne, Function: Ne,
Name: "!=", Name: "!=",
TimesCalled: 0, TimesCalled: 0,
Args: 2, Args: 2,
}, },
"<": &ast.Function{ "<": &ast.Function{
Function: lt, Function: Lt,
Name: "<", Name: "<",
TimesCalled: 0, TimesCalled: 0,
Args: 2, Args: 2,
}, },
">": &ast.Function{ ">": &ast.Function{
Function: gt, Function: Gt,
Name: ">", Name: ">",
TimesCalled: 0, TimesCalled: 0,
Args: 2, Args: 2,
}, },
"<=": &ast.Function{ "<=": &ast.Function{
Function: lte, Function: Lte,
Name: "<=", Name: "<=",
TimesCalled: 0, TimesCalled: 0,
Args: 2, Args: 2,
}, },
">=": &ast.Function{ ">=": &ast.Function{
Function: gte, Function: Gte,
Name: ">=", Name: ">=",
TimesCalled: 0, TimesCalled: 0,
Args: 2, Args: 2,
}, },
"!": &ast.Function{ "!": &ast.Function{
Function: not, Function: Not,
Name: "!", Name: "!",
TimesCalled: 0, TimesCalled: 0,
Args: 1, Args: 1,
}, },
"+": &ast.Function{ "+": &ast.Function{
Function: add, Function: Add,
Name: "add", Name: "add",
TimesCalled: 0, TimesCalled: 0,
Args: -1, Args: -1,
}, },
"-": &ast.Function{ "-": &ast.Function{
Function: sub, Function: Sub,
Name: "sub", Name: "sub",
TimesCalled: 0, TimesCalled: 0,
Args: -1, Args: -1,
}, },
"*": &ast.Function{ "*": &ast.Function{
Function: mult, Function: Mult,
Name: "mult", Name: "mult",
TimesCalled: 0, TimesCalled: 0,
Args: -1, Args: -1,
}, },
"/": &ast.Function{ "/": &ast.Function{
Function: div, Function: Div,
Name: "div", Name: "div",
TimesCalled: 0, TimesCalled: 0,
Args: -1, Args: -1,
}, },
"cd": &ast.Function{ "cd": &ast.Function{
Function: cd, Function: Cd,
Name: "changedir", Name: "changedir",
TimesCalled: 0, TimesCalled: 0,
Args: 1, Args: 1,
}, },
"concat": &ast.Function{ "concat": &ast.Function{
Function: concat, Function: Concat,
Name:"concatenate", Name:"concatenate",
TimesCalled: 0, TimesCalled: 0,
Args: -1, Args: -1,
}, },
"print": &ast.Function{ "print": &ast.Function{
Function:print_str, Function: PrintStr,
Name: "print", Name: "print",
TimesCalled: 0, TimesCalled: 0,
Args: 1, Args: 1,
}, },
"l": &ast.Function{ "l": &ast.Function{
Function: call, Function: Call,
Name: "call", Name: "call",
TimesCalled: 0, TimesCalled: 0,
Args: -1, Args: -1,
}, },
"bg": &ast.Function{ "bg": &ast.Function{
Function: bgcall, Function: Bgcall,
Name: "background call", Name: "background call",
TimesCalled: 0, TimesCalled: 0,
Args: -1, Args: -1,
}, },
"fg": &ast.Function{ "fg": &ast.Function{
Function: fg, Function: Fg,
Name: "foreground", Name: "foreground",
TimesCalled: 0, TimesCalled: 0,
Args: 0, Args: 0,
}, },
"$": &ast.Function{ "$": &ast.Function{
Function: read_cmd, Function: ReadCmd,
Name: "read cmd", Name: "read cmd",
TimesCalled: 0, TimesCalled: 0,
Args: -1, Args: -1,
}, },
"?": &ast.Function{ "?": &ast.Function{
Function: get_exit, Function: GetExit,
Name:"get exit code", Name:"get exit code",
TimesCalled: 0, TimesCalled: 0,
Args: 0, Args: 0,
@ -234,42 +266,42 @@ func GenFuncTable() ast.FuncTable {
*/ */
"jobs": &ast.Function{ "jobs": &ast.Function{
Function: jobs, Function: Jobs,
Name: "list jobs", Name: "list jobs",
TimesCalled: 0, TimesCalled: 0,
Args: 0, Args: 0,
}, },
"info": &ast.Function{ "info": &ast.Function{
Function: sh_info, Function: ShInfo,
Name: "Shell Info", Name: "Shell Info",
TimesCalled: 0, TimesCalled: 0,
Args: 1, Args: 1,
}, },
"fexists": &ast.Function{ "fexists": &ast.Function{
Function: fexists, Function: Fexists,
Name: "file exists", Name: "file exists",
TimesCalled: 0, TimesCalled: 0,
Args: 1, Args: 1,
}, },
"fread": &ast.Function{ "fread": &ast.Function{
Function: fread, Function: Fread,
Name: "read file", Name: "read file",
TimesCalled: 0, TimesCalled: 0,
Args: 1, Args: 1,
}, },
"fwrite": &ast.Function{ "fwrite": &ast.Function{
Function: fwrite, Function: Fwrite,
Name: "write file", Name: "write file",
TimesCalled: 0, TimesCalled: 0,
Args: 2, Args: 2,
}, },
"fappend": &ast.Function{ "fappend": &ast.Function{
Function: fappend, Function: Fappend,
Name:"append to file", Name:"append to file",
TimesCalled: 0, TimesCalled: 0,
Args: 2, Args: 2,
@ -279,12 +311,22 @@ func GenFuncTable() ast.FuncTable {
return stdlib return stdlib
} }
func exit_shell(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* takes no args
* exits shell when called
*
* Example: (exit)
*/
func ExitShell(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
os.Exit(0) os.Exit(0)
return nil // I hope execution doesnt get here return nil // I hope execution doesnt get here
} }
func sh_info(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* takes one arg, doesnt evaluate it
* returns type or metadata
*
* Example: (info append)
*/
func ShInfo(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
switch in.Tag { switch in.Tag {
case ast.BOOL: case ast.BOOL:
fmt.Printf("BOOL LITERAL\nValue: %s\n", in.Value()) fmt.Printf("BOOL LITERAL\nValue: %s\n", in.Value())
@ -314,11 +356,14 @@ func sh_info(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return nil return nil
} }
func eval(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* Takes 1 arg, uses it as a prompt
return in.Eval(ft, vt, false) * errs if prompt is not a string or number
} * gets a line from stdin
* returns it as a string
func input(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { *
* Example: (print (input))
*/
func Input(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, false) in = in.Eval(ft, vt, false)
if in.Tag != ast.STRING && in.Tag != ast.NUMBER { if in.Tag != ast.STRING && in.Tag != ast.NUMBER {
log.Log(log.ERR, log.Log(log.ERR,
@ -337,3 +382,23 @@ func input(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
ret.Set(output) ret.Set(output)
return ret return ret
} }
/* Takes 1 arg, returns nil
* if arg is a valid existing file than load will execute it as a script
*
* Example: (load "myscript.shs")
*/
func Load(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, true)
if in.Tag != ast.STRING {
log.Log(log.ERR,
"argument to load must be a string",
"load")
return nil
}
bp := in.Value()
bp = AbsPath(bp)
util.LoadScript(bp, vt, ft)
return nil
}

View file

@ -19,11 +19,16 @@ package stdlib
import ( import (
"fmt" "fmt"
"strconv"
"gitlab.com/whom/shs/ast" "gitlab.com/whom/shs/ast"
"gitlab.com/whom/shs/log" "gitlab.com/whom/shs/log"
) )
func concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* Concatenates N stringables
*
* Example: (concat "hello" " " "world")
*/
func Concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, false) in = in.Eval(ft, vt, false)
var res string var res string
@ -42,7 +47,38 @@ func concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return t return t
} }
func print_str(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token { /* Takes 1 argument, returns its value as a string
fmt.Println(in.Eval(ft, vt, false)) * works on lists too.
*
* Example: (string 1) -> 1.0
*/
func StrCast(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
body := in.Eval(ft, vt, false).String()
res := &ast.Token{ Tag: ast.STRING }
res.Set(body)
return res
}
/* Takes one arg, returns nil
* Prints a string to stdout
* Unquotes string so user can add escaped chars like \n, \t, etc
*
* Example: (print "Line: \n, Tab: \t")
*/
func PrintStr(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
body := in.Eval(ft, vt, false).String()
if body[0] != body[len(body)-1] && body[0] != '"' {
body = "`" + body + "`"
}
text, err := strconv.Unquote(body)
if err != nil {
log.Log(log.ERR,
"error unquoting string",
"print")
return nil
}
fmt.Printf(text + "\n")
return nil return nil
} }

View file

@ -22,7 +22,15 @@ import (
"gitlab.com/whom/shs/log" "gitlab.com/whom/shs/log"
) )
func export(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token { /* Takes 2 args, a name and a value
* Exports a varable
* both args are evaluated
*
* Example:
* (export hw (concat "hello" " " "world"))
* (print hw)
*/
func Export(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token {
name := input name := input
form := name.Next.Eval(funcs, vars, false) form := name.Next.Eval(funcs, vars, false)

52
util/scripts.go Normal file
View file

@ -0,0 +1,52 @@
/* SHS: Syntactically Homogeneous Shell
* Copyright (C) 2019 Aidan Hahn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package util
import (
"os"
"io/ioutil"
"gitlab.com/whom/shs/log"
"gitlab.com/whom/shs/ast"
)
/* Opens a file and lexes+evaluates the contents
*/
func LoadScript(path string, vt ast.VarTable, ft ast.FuncTable) {
scriptFile, err := os.Open(path)
if err != nil {
log.Log(log.ERR,
"unable to open script: " + err.Error(),
"util")
return
}
var body []byte
body, err = ioutil.ReadFile(path)
scriptFile.Close()
if err != nil {
log.Log(log.ERR,
"unable to read script: " + err.Error(),
"util")
return
}
set := ast.Lex(string(body))
for iter := set; iter != nil; iter = iter.Next {
iter.Eval(ft, vt, false)
}
}

102
util/shell_complete.go Normal file
View file

@ -0,0 +1,102 @@
/* SHS: Syntactically Homogeneous Shell
* Copyright (C) 2019 Aidan Hahn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package util
import (
"strings"
"io/ioutil"
"gitlab.com/whom/shs/log"
"gitlab.com/whom/shs/ast"
)
/* gathers completions for the last word in a given line
* shell wraps this in a lambda that passes in the vt and ft
* I suppose it could be more optimal. Fix if it bothers you
*/
func ShellCompleter(line string, vt ast.VarTable, ft ast.FuncTable) []string {
var head, tail string
idx := strings.LastIndex(line, " ")
if idx > 0 {
head = line[:idx+1]
tail = line[idx+1:]
} else {
head = ""
tail = line
}
dir, path, tok := getPathBase(tail)
compSource := []string{}
if !path {
dir = "."
} else {
line = tok
}
fobjs, err := ioutil.ReadDir(dir)
if err != nil {
log.Log(log.DEBUG,
"couldnt read dir " + dir + ": " + err.Error(),
"complete")
if path {
return nil
}
} else {
for _, f := range fobjs {
compSource = append(compSource, dir + "/" + f.Name())
}
}
if !path {
compSource = append(compSource, ast.ListVars(vt)...)
compSource = append(compSource, ast.ListFuncs(ft)...)
}
completions := []string{}
for _, i := range compSource {
if strings.HasPrefix(i, tail) {
completions = append(completions, head + i)
}
}
return completions
}
// returns everything up to the last '/'
// as well as whether or not a / was found
// and finally the token after the last /
func getPathBase(in string) (string, bool, string) {
if in == "" {
return "", false, ""
}
isPath := false
i := len(in) - 1
for i > 0 {
if in[i] == '/' {
isPath = true
break
}
i -= 1
}
return in[:i], isPath, in[i+1:]
}