Merge branch 'dev' into 'master'

merge current dev to master

See merge request whom/shs!4
This commit is contained in:
Aidan Hahn 2020-07-09 04:23:13 +00:00
commit 5363896c85
22 changed files with 1570 additions and 380 deletions

3
.gitignore vendored
View file

@ -14,3 +14,6 @@ print_ast
*~
*.swp
*.swo
# notes
*.txt*

View file

@ -4,15 +4,84 @@ 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 (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)
- `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" 0)
```
## 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.

View file

@ -19,86 +19,100 @@ package ast
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
}
var reduce func(*Token) *Token
reduce = func(t_ *Token) *Token {
var unwrap bool
if t_.Next != nil {
t_.Next = reduce(t_.Next)
}
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 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")
/* 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, cnvtUndefVars bool) *Token {
if in == nil {
return nil
}
i := &t_
for (*i).Next != nil {
i = &((*i).Next)
var res *Token
switch in.Tag {
case BOOL, NUMBER, STRING:
res = in.Copy()
case SYMBOL:
res = GetVar(in.Value(), vars)
if res == nil {
res = in.Copy()
if GetFunction(in.Value(), funcs) == nil {
if cnvtUndefVars {
res.Tag = STRING
break
}
(*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",
log.Log(log.ERR,
"undefined symbol: "+in.Value(),
"eval")
return ret, false
return nil
}
}
// see the use of CallFunction below
ret = &Token{Next: ret}
case LIST:
inner := in.Expand()
if inner == nil {
res = in.Copy()
break
}
if inner.Tag != SYMBOL {
in.Direct(inner.Eval(funcs, vars, cnvtUndefVars))
res = in.Copy()
break
}
makeHead := false
funct := GetFunction(inner.Value(), funcs)
if funct == nil {
if ExecWhenFuncUndef {
funct = GetFunction(ExecFunc, funcs)
makeHead = true
}
}
if funct != nil {
if makeHead {
inner = &Token{Next: inner}
}
res = funct.CallFunction(inner.Next, vars, funcs).Eval(funcs, vars, false)
if res == nil {
// function failed. logging is its responsibility
return nil
}
res.Append(in.Next)
} else {
log.Log(log.DEBUG,
"could not find definition for symbol " + ret.Inner.(string),
log.Log(log.ERR,
"undefined function "+inner.Value()+" called",
"eval")
return ret, false
}
return nil
}
return (*f).CallFunction(ret.Next, vars, funcs), true
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, cnvtUndefVars)
}
return res
}

View file

@ -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
}

203
ast/lex.go Normal file
View file

@ -0,0 +1,203 @@
/* 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 ast
import (
"gitlab.com/whom/shs/log"
"unicode"
)
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)
ret = temp
}
return ret
}
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 if tok == "T" || tok == "F" {
(*iter).Tag = BOOL
} 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
}

View file

@ -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:
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"
}

View file

@ -17,10 +17,7 @@
package ast
import (
"gitlab.com/whom/shs/log"
"unicode"
)
import "fmt"
type Token_t int
const (
@ -28,172 +25,151 @@ const (
STRING Token_t = iota
NUMBER Token_t = iota
SYMBOL Token_t = iota
BOOL Token_t = iota
TRUE string = "T"
FALSE string = "F"
)
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 {
/* 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.
*/
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
return t.inner.(*Token)
}
*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 {
/* 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)
}
}
/* 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 {
switch tag {
case LIST:
return "LIST"
case STRING:
return "STRING"
case BOOL:
return "BOOL"
case NUMBER:
return "NUMBER"
case SYMBOL:
return "SYMBOL"
}
return "UNKNOWN"
}

View file

@ -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,12 @@ func GetVar(arg string, vt VarTable) *Token {
}
// TODO: this could be much more optimal
// 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)
@ -89,15 +90,22 @@ 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]}
t := &Token{inner: variable[1]}
if StrIsNumber(variable[1]) {
t.Tag = NUMBER
} else {
t.Tag = STRING
}
if variable[0] == "HOME" {
SetVar("~", t, table)
}
SetVar(variable[0], t, table)
}
}
@ -113,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)
}

View file

@ -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:], " ")))
}

View file

@ -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() {
ast.CallExecutablesFromUndefFuncCalls = true
var prompt string
var debug string
var hist string
debug := os.Getenv("SH_DEBUG_MODE")
hist := os.Getenv("SH_HIST_FILE")
prompt := os.Getenv("SHS_SH_PROMPT")
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")
@ -96,12 +106,8 @@ func main() {
ast.PrintSExprsIndividually(userInput)
}
result, unwrap := userInput.Eval(funcs, vars)
result := userInput.Eval(funcs, vars, false)
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() + " ")
}

66
config/config.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

View file

@ -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`.

View file

@ -32,13 +32,15 @@ import (
func add(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
var res float64
in = in.Eval(f, a, false)
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)
token := i.Value()
isFloat := false
for _, char := range token {
if char == '.' {
@ -64,20 +66,24 @@ func add(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
}
}
return &ast.Token{Tag: ast.NUMBER, Inner: fmt.Sprintf("%f", res)}
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, false)
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)
token := i.Value()
isFloat := false
var inner float64
for _, char := range token {
@ -110,19 +116,23 @@ func sub(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
}
}
return &ast.Token{Tag: ast.NUMBER, Inner: fmt.Sprintf("%f", res - sub)}
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, false)
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)
token := i.Value()
isFloat := false
for _, char := range token {
if char == '.' {
@ -148,12 +158,16 @@ func mult(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
}
}
return &ast.Token{Tag: ast.NUMBER, Inner: fmt.Sprintf("%f", res)}
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, false)
for i := in; i != nil; i = i.Next {
inner := 0.0
@ -162,7 +176,7 @@ func div(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
return nil
}
token := i.Inner.(string)
token := i.Value()
isFloat := false
for _, char := range token {
if char == '.' {
@ -194,5 +208,7 @@ func div(in *ast.Token, a ast.VarTable, f ast.FuncTable) *ast.Token {
}
}
return &ast.Token{Tag: ast.NUMBER, Inner: fmt.Sprintf("%f", res)}
t := &ast.Token{Tag: ast.NUMBER}
t.Set(fmt.Sprintf("%f", res))
return t
}

182
stdlib/bool.go Normal file
View file

@ -0,0 +1,182 @@
/* 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 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 {
in = in.Eval(ft, vt, false)
if in.Tag != ast.BOOL {
log.Log(log.ERR, "non-bool argument to 'not'", "not")
return nil
}
out := ast.TRUE
if in.Value() == ast.TRUE {
out = ast.FALSE
}
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 := ast.TRUE
in = in.Eval(ft, vt, false)
second := in.Next
if in.Tag != second.Tag {
out = ast.FALSE
} 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 = ast.FALSE
}
case ast.STRING, ast.BOOL:
if in.Value() != second.Value() {
out = ast.FALSE
}
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 = ast.FALSE
}
}
}
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 := ast.TRUE
second := in.Next
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", ">/<=")
return nil
}
l, _ := strconv.ParseInt(in.Value(), 10, 64)
r, _ := strconv.ParseInt(second.Value(), 10, 64)
if l >= r {
out = ast.FALSE
}
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 := ast.TRUE
second := in.Next
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", ">/<=")
return nil
}
l, _ := strconv.ParseInt(in.Value(), 10, 64)
r, _ := strconv.ParseInt(second.Value(), 10, 64)
if l <= r {
out = ast.FALSE
}
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)
}

View file

@ -21,6 +21,7 @@ import (
"os"
"fmt"
"bytes"
"strconv"
"os/exec"
"syscall"
"os/signal"
@ -30,15 +31,30 @@ 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 {
in = in.Eval(ft, vt, true)
if in == nil {
return nil
}
path, err := exec.LookPath(in.Inner.(string))
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.Inner.(string) + ", file not found", "call")
log.Log(log.ERR, "Couldnt exec " + in.Value() + ", file not found", "call")
return nil
}
@ -49,7 +65,7 @@ func call(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return nil
}
args = append(args, i.Inner.(string))
args = append(args, i.Value())
}
var cmd *exec.Cmd
@ -63,7 +79,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 +87,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()
@ -88,9 +104,14 @@ func bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return nil
}
path, err := exec.LookPath(in.Inner.(string))
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.Inner.(string) + ", file not found", "call")
log.Log(log.ERR, "Couldnt exec " + in.Value() + ", file not found", "call")
return nil
}
@ -101,7 +122,7 @@ func bgcall(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return nil
}
args = append(args, i.Inner.(string))
args = append(args, i.Value())
}
var cmd *exec.Cmd
@ -113,7 +134,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 +151,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()
@ -149,15 +173,46 @@ 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,
}
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.Inner.(string))
path, err := exec.LookPath(in.Value())
if err != nil {
log.Log(log.ERR, "Couldnt exec " + in.Inner.(string) + ", file not found", "call")
log.Log(log.ERR, "Couldnt exec " + in.Value() + ", file not found", "call")
return nil
}
@ -168,7 +223,7 @@ func read_cmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
return nil
}
args = append(args, i.Inner.(string))
args = append(args, i.Value())
}
var cmd *exec.Cmd
@ -182,7 +237,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 +245,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()
@ -199,10 +254,59 @@ func read_cmd(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
}
}
return &ast.Token{Tag: ast.STRING, Inner: out.String()}
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 {
return &ast.Token{Tag: ast.NUMBER, Inner: fmt.Sprintf("%d", LastExitCode)}
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
}

85
stdlib/control_flow.go Normal file
View file

@ -0,0 +1,85 @@
/* 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 stdlib
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 ast.VarTable, ft ast.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 ast.VarTable, ft ast.FuncTable) *ast.Token {
cond := in
forms := in.Next
in.Next = nil
var res *ast.Token
eval := cond.Eval(ft, vt, false)
if eval == nil || eval.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() == 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)
}
return res
}

View file

@ -19,19 +19,176 @@ 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)
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
}
err := os.Chdir(in.Inner.(string))
err := os.Chdir(in.Value())
if err != nil {
log.Log(log.ERR, err.Error(), "cd")
}
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
}

106
stdlib/funcs.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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.SYMBOL {
log.Log(log.ERR,
"argument 1 of func must be a symbol to be exported",
"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 {
temp := in.Eval(ft, vt, false)
if temp == nil {
log.Log(log.ERR,
"error parsing arguments",
name.Value())
return nil
}
ast.SyncTablesWithOSEnviron = false
key_iter := args.Expand()
val_iter := temp
for key_iter != nil {
if val_iter == nil {
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
}

View file

@ -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.Eval(funcs, vars, false).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 {

View file

@ -19,12 +19,56 @@ 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{
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,
},
"func": &ast.Function{
Function: decl_func,
Name: "decl_func",
TimesCalled: 0,
Args: 3,
},
"export": &ast.Function{
Function: export,
Name: "export",
TimesCalled: 0,
Args: 2,
},
"input": &ast.Function{
Function: input,
Name: "input",
TimesCalled: 0,
Args: 1,
},
"...": &ast.Function{
Function: expand,
Name: "...",
@ -39,6 +83,62 @@ func GenFuncTable() ast.FuncTable {
Args: -1,
},
"exit": &ast.Function{
Function: exit_shell,
Name: "exit",
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,
},
"+": &ast.Function{
Function: add,
Name: "add",
@ -67,6 +167,27 @@ func GenFuncTable() ast.FuncTable {
Args: -1,
},
"cd": &ast.Function{
Function: cd,
Name: "changedir",
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,
},
"l": &ast.Function{
Function: call,
Name: "call",
@ -88,13 +209,6 @@ func GenFuncTable() ast.FuncTable {
Args: 0,
},
"cd": &ast.Function{
Function: cd,
Name: "changedir",
TimesCalled: 0,
Args: 1,
},
"$": &ast.Function{
Function: read_cmd,
Name: "read cmd",
@ -102,33 +216,64 @@ func GenFuncTable() ast.FuncTable {
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,
},
*/
"jobs": &ast.Function{
Function: jobs,
Name: "list jobs",
TimesCalled: 0,
Args: 0,
},
"info": &ast.Function{
Function: sh_info,
Name: "Shell Info",
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
@ -138,3 +283,57 @@ 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
}
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
}

View file

@ -24,6 +24,8 @@ import (
)
func concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
in = in.Eval(ft, vt, false)
var res string
for i := in; i != nil; i = i.Next {
if i.Tag == ast.LIST {
@ -32,13 +34,15 @@ func concat(in *ast.Token, vt ast.VarTable, ft ast.FuncTable) *ast.Token {
continue
}
res += i.Inner.(string)
res += i.Value()
}
return &ast.Token{Tag: ast.STRING, Inner: res}
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.Inner.(string))
fmt.Println(in.Eval(ft, vt, false))
return nil
}

39
stdlib/vars.go Normal file
View file

@ -0,0 +1,39 @@
/* 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 stdlib
import (
"gitlab.com/whom/shs/ast"
"gitlab.com/whom/shs/log"
)
func export(input *ast.Token, vars ast.VarTable, funcs ast.FuncTable) *ast.Token {
name := input
form := name.Next.Eval(funcs, vars, false)
if name.Tag != ast.SYMBOL {
log.Log(log.ERR,
"first arg should be a symbol",
"export")
return nil
}
ast.SetVar(name.Value(), form, vars)
return nil
}