slope/lib.go

2451 lines
68 KiB
Go

package main
import (
"bufio"
"crypto/md5"
"crypto/sha256"
"crypto/tls"
"fmt"
"io"
"io/ioutil"
"math"
"math/rand"
"net"
"net/url"
"os"
"os/exec"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"unicode/utf8"
"git.rawtext.club/sloum/slope/termios"
)
var stdLibrary = vars{
"stdin": &IOHandle{os.Stdin, true, "file (read-only)"},
"stdout": &IOHandle{os.Stdout, true, "file (write-only)"},
"stderr": &IOHandle{os.Stderr, true, "file (write-only)"},
"devnull": func() *IOHandle {
devnull, _ := os.OpenFile(os.DevNull, os.O_WRONLY, 0755)
dn := &IOHandle{devnull, true, "file (write-only)"}
openFiles = append(openFiles, dn)
return dn
}(),
"mod-path": func(a ...expression) expression {
return getModBaseDir()
},
"__SIGINT": func(a ...expression) expression {
SafeExit(1)
return false
},
"signal-catch-sigint": func(a ...expression) expression {
if len(a) < 1 {
return exception("'signal-catch-sigint' expected a procedure but no value was given")
}
switch p := a[0].(type) {
case proc, func(...expression) expression:
globalenv.vars["__SIGINT"] = p
return true
default:
return exception("'signal-catch-sigint' expected a procedure, but a non-procedure value was given")
}
},
"PHI": number(math.Phi),
"PI": number(math.Pi),
"E": number(math.E),
"sys-args": func(a ...expression) expression {
args := os.Args
if len(args) > 0 && args[0] == "slope" || args[0] == "./slope" || args[0] == "../slope" {
args = args[1:]
}
return StringSliceToExpressionSlice(args)
}(),
"exception-mode-panic": func(a ...expression) expression {
panicOnException = true
return make([]expression, 0)
},
"exception-mode-pass": func(a ...expression) expression {
panicOnException = false
return make([]expression, 0)
},
"exception-mode-pass?": func(a ...expression) expression {
return panicOnException == false
},
"exception-mode-panic?": func(a ...expression) expression {
return panicOnException == true
},
"!": func(a ...expression) expression {
if len(a) < 0 {
return exception("'!' expects a value, no value was given")
}
if panicOnException {
panic(a[0])
}
return exception(String(a[0], false))
},
"positive?": func(a ...expression) expression {
if len(a) > 0 {
if n, ok := a[0].(number); ok {
return n > 0
}
}
return false
},
"negative?": func(a ...expression) expression {
if len(a) > 0 {
if n, ok := a[0].(number); ok {
return n < 0
}
}
return false
},
"zero?": func(a ...expression) expression {
if len(a) > 0 {
if n, ok := a[0].(number); ok {
return n == 0
}
}
return false
},
"exception?": func(a ...expression) expression {
if len(a) > 0 {
if _, ok := a[0].(exception); ok {
return true
}
}
return false
},
"abs": func(a ...expression) expression {
if len(a) < 1 {
return exception("'abs' expected a value, but no value was given")
}
i, ok := a[0].(number)
if !ok {
return exception("'abs' expected a number, but a non-number value was received")
}
if i < 0 {
i = i * -1
}
return i
},
"floor": func(a ...expression) expression {
if len(a) < 1 {
return exception("'floor' expected a value, but no value was given")
}
i, ok := a[0].(number)
if !ok {
return exception("'floor' expected a number, but a non-number value was received")
}
return number(math.Floor(float64(i)))
},
"ceil": func(a ...expression) expression {
if len(a) < 1 {
return exception("'ceil' expected a value, but no value was given")
}
i, ok := a[0].(number)
if !ok {
return exception("'ceil' expected a number, but a non-number value was received")
}
return number(math.Ceil(float64(i)))
},
"type": func(a ...expression) expression {
if len(a) == 0 {
return exception("'type' expected a value, but no value was given")
}
t := reflect.TypeOf(a[0]).String()
switch t {
case "main.number":
return "number"
case "[]main.expression":
return "list"
case "*main.IOHandle":
return "IOHandle"
case "main.symbol":
return "symbol"
case "func(...main.expression) main.expression":
return "compiled-procedure"
case "main.proc":
return "procedure"
case "main.exception":
return "exception"
case "main.GUI":
return "GUI"
case "main.guiWidget":
return "Widget"
case "fyne.Widget":
return "Widget"
case "fyne.CanvasObject":
return "Widget"
case "fyne.Container":
return "Container"
case "main.guiContainer":
return "Container"
default:
return t
}
},
"round": func(a ...expression) expression {
if len(a) < 1 {
return exception("'round' expected a value, but no value was given")
}
decimals := number(0.0)
if len(a) >= 2 {
if dec, ok := a[1].(number); ok {
decimals = dec
}
}
num, ok := a[0].(number)
if !ok {
return exception("'round' expected a number, but a non-number value was received")
}
return number(math.Round(float64(num)*math.Pow(10, float64(decimals))) / math.Pow(10, float64(decimals)))
},
"+": func(a ...expression) expression {
if len(a) == 0 {
return number(0)
}
v, ok := a[0].(number)
if !ok {
return exception("'+' expected a number, but a non-number value was received")
}
for _, i := range a[1:] {
x, ok := i.(number)
if !ok {
return exception("'+' expected a number, but a non-number value was received")
}
v += x
}
return v
},
"-": func(a ...expression) expression {
if len(a) == 0 {
return exception("'-' expected a value, but no value was given")
}
v, ok := a[0].(number)
if !ok {
return exception("'-' expected a number, but a non-number value was received")
}
if len(a) == 1 {
return v * -1
}
for _, i := range a[1:] {
x, ok := i.(number)
if !ok {
return exception("'-' expected a number, but a non-number value was received")
}
v -= x
}
return v
},
"*": func(a ...expression) expression {
if len(a) == 0 {
return number(1)
}
v, ok := a[0].(number)
if !ok {
return exception("'*' expected a number, but a non-number value was received")
}
if len(a) == 1 {
return 1 / v
}
for _, i := range a[1:] {
x, ok := i.(number)
if !ok {
return exception("'*' expected a number, but a non-number value was received")
}
v *= x
}
return v
},
"/": func(a ...expression) expression {
if len(a) == 0 {
return exception("'/' expected a value, but no value was given")
}
v, ok := a[0].(number)
if !ok {
return exception("'/' expected a number, but a non-number value was received")
}
for _, i := range a[1:] {
x, ok := i.(number)
if !ok {
return exception("'/' expected a number, but a non-number value was received")
}
v /= x
}
return v
},
"min": func(a ...expression) expression {
if len(a) == 0 {
return exception("'min' expected a value, but no value was given")
}
val := number(math.MaxFloat64)
for _, v := range a {
value, ok := v.(number)
if !ok {
return exception("'min' expected a number, but a non-number value was received")
}
if value < val {
val = value
}
}
return val
},
"max": func(a ...expression) expression {
if len(a) == 0 {
return exception("'max' expected a value, but no value was given")
}
val := number(math.SmallestNonzeroFloat64)
for _, v := range a {
value, ok := v.(number)
if !ok {
return exception("'max' expected a number, but a non-number value was received")
}
if value > val {
val = value
}
}
return val
},
"<=": func(a ...expression) expression {
if len(a) < 2 {
return exception("'<=' expected, but did not receive, two values")
}
n1, ok1 := a[0].(number)
n2, ok2 := a[1].(number)
if !ok1 || !ok2 {
return exception("'<=' expected numbers, but a non-number value was received")
}
return n1 <= n2
},
"<": func(a ...expression) expression {
if len(a) < 2 {
return exception("'<' expected, but did not receive, two values")
}
n1, ok1 := a[0].(number)
n2, ok2 := a[1].(number)
if !ok1 || !ok2 {
return exception("'<' expected numbers, but a non-number value was received")
}
return n1 < n2
},
">=": func(a ...expression) expression {
if len(a) < 2 {
return exception("'>=' expected, but did not receive, two values")
}
n1, ok1 := a[0].(number)
n2, ok2 := a[1].(number)
if !ok1 || !ok2 {
return exception("'>=' expected numbers, but a non-number value was received")
}
return n1 >= n2
},
">": func(a ...expression) expression {
if len(a) < 2 {
return exception("'>' expected, but did not receive, two values")
}
n1, ok1 := a[0].(number)
n2, ok2 := a[1].(number)
if !ok1 || !ok2 {
return exception("'>' expected numbers, but a non-number value was received")
}
return n1 > n2
},
"equal?": func(a ...expression) expression {
if len(a) < 1 {
return exception("'equal?' expected, but did not receive, at least one value")
} else if len(a) == 1 {
return true
}
eq := false
for i := 0; i < len(a)-1; i++ {
eq = reflect.DeepEqual(a[i], a[i+1])
if !eq {
return false
}
}
return true
},
"atom?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'atom?' expected a value, but no value was given")
}
switch a[0].(type) {
case []expression:
return false
default:
return true
}
},
"assoc?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'assoc?' expected a value, but no value was given")
}
list, ok := a[0].([]expression)
if !ok {
return false
}
for x := range list {
l, ok := list[x].([]expression)
if !ok || len(l) != 2 {
return false
}
}
return true
},
"assoc": func(a ...expression) expression {
if len(a) < 2 {
return exception("'assoc' expected at least two values, a list and a value; insufficient arguments were given")
}
list, ok := a[0].([]expression)
if !ok {
return exception("'assoc' expected argument 1 to be an association list, a non-list value was given")
}
for x := range list {
l, ok := list[x].([]expression)
if !ok || len(l) != 2 {
return exception("'assoc' expected argument 1 to be an association list, a list was given, but it is not an association list")
}
if reflect.DeepEqual(l[0], a[1]) {
if len(a) >= 3 {
l[1] = a[2]
list[x] = l
return list
}
return l[1]
}
}
if len(a) >= 3 {
list = append(list, []expression{a[1], a[2]})
return list
}
return exception("'assoc' could not find the key value " + String(a[1], true))
},
"number?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'number?' expected a value, but no value was given")
}
switch a[0].(type) {
case float64, number:
return true
default:
return false
}
},
"string?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'string?' expected a value, but no value was given")
}
switch a[0].(type) {
case string:
return true
default:
return false
}
},
"io-handle?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'io-handle?' expected a value, but no value was given")
}
switch a[0].(type) {
case *IOHandle:
return true
default:
return false
}
},
"string-buf?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'string-buf?' expected a value, but no value was given")
}
switch v := a[0].(type) {
case *IOHandle:
_, ok := v.Obj.(*strings.Builder)
return ok
default:
return false
}
},
"bool?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'bool?' expected a value, but no value was given")
}
switch a[0].(type) {
case bool:
return true
default:
return false
}
},
"symbol?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'symbol?' expected a value, but no value was given")
}
switch a[0].(type) {
case symbol:
return true
default:
return false
}
},
"null?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'null?' expected a list, but no value was given")
}
switch i := a[0].(type) {
case []expression:
return len(i) == 0
default:
return false
}
},
"pair?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'pair?' expected a value, but no value was given")
}
switch i := a[0].(type) {
case []expression:
return len(i) > 0
default:
return false
}
},
"list?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'list?' expected a value, but no value was given")
}
switch a[0].(type) {
case []expression:
return true
default:
return false
}
},
"procedure?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'proc?' expected a value, but no value was given")
}
switch a[0].(type) {
case proc, func(a ...expression) expression:
return true
default:
return false
}
},
"length": func(a ...expression) expression {
switch i := a[0].(type) {
case []expression:
return number(len(i))
case string:
return number(len([]rune(String(i, false))))
case exception:
return number(len([]rune(String(string(i), false))))
case *IOHandle:
switch h := i.Obj.(type) {
case *strings.Builder:
return number(len([]rune(String(h.String(), false))))
default:
return exception("'length' is not defined for a non-string-buf IOHandle")
}
default:
return exception("'length' expected a list or a string, but the given value was not a list or a string")
}
},
"string-make-buf": func(a ...expression) expression {
var b strings.Builder
if len(a) > 0 {
b.WriteString(String(a[0], false))
}
obj := &IOHandle{&b, true, "string-buf"}
openFiles = append(openFiles, obj)
return obj
},
"string-buf-clear": func(a ...expression) expression {
if len(a) == 0 {
return exception("'string-buf-clear' expects an IOHandle for a string-buf as an argument, no argument was given")
}
h, ok := a[0].(*IOHandle)
if !ok {
return exception("'string-buf-clear' expects an IOHandle as its argument, a non-IOHandle argument was given")
}
if !h.Open {
return exception("a closed IOHandle was given to 'string-buf-clear'")
}
switch buf := h.Obj.(type) {
case *strings.Builder:
buf.Reset()
default:
return exception("'string-buf-clear' expects an IOHandle representing a string-buf as its argument, a non-string-buf-IOHandle argument was given")
}
return make([]expression, 0)
},
"string-upper": func(a ...expression) expression {
if len(a) == 0 {
return exception("no value was given to 'string-upper'")
}
s, ok := a[0].(string)
if !ok {
return exception("'string-upper' expected a string but received a non-string value")
}
return strings.ToUpper(s)
},
"string-lower": func(a ...expression) expression {
if len(a) == 0 {
return exception("no value was given to 'string-lower'")
}
s, ok := a[0].(string)
if !ok {
return exception("'string-lower' expected a string but received a non-string value")
}
return strings.ToLower(s)
},
"string-format": func(a ...expression) expression {
if len(a) == 0 {
return exception("no value was given to 'string-format'")
}
format, ok := a[0].(string)
if !ok {
return exception("'string-format' expected a format string and was given a non-string value as its first argument")
}
if len(a) == 1 {
return format
}
var outputBuf, percentBuf strings.Builder
parsePercent := false
varNum := 1
for _, c := range format {
if parsePercent {
if c == 'v' {
parsePercent = false
if varNum < len(a) {
s, err := formatValue(percentBuf.String(), a[varNum])
if err != nil {
return exception("'string-format' was given an invalid formatting specifier")
}
outputBuf.WriteString(s)
percentBuf.Reset()
varNum++
} else {
return exception("'string-format' was not given enough values to format the string")
}
} else {
percentBuf.WriteRune(c)
}
} else if c == '%' {
parsePercent = true
} else if c == '%' {
outputBuf.WriteRune(c)
} else {
outputBuf.WriteRune(c)
}
}
return unescapeString(outputBuf.String())
},
"string-index-of": func(a ...expression) expression {
// (string-index-of [haystack] [needle])
if len(a) < 2 {
return exception("'string-index-of' expects two strings, a haystack and a needle, insufficient arguments were given")
}
haystack, ok := a[0].(string)
if !ok {
return exception("'string-index-of' expects a string as its first argument; a non-string value was given")
}
needle, ok := a[1].(string)
if !ok {
return exception("'string-index-of' expects a string as its second argument; a non-string value was given")
}
return number(strings.Index(haystack, needle))
},
"string-ref": func(a ...expression) expression {
if len(a) < 2 {
return exception("'string-index-of' expects a string and a number; insufficient arguments were given")
}
haystack, ok := a[0].(string)
if !ok {
return exception("'string-ref' expects a string as its first argument; a non-string value was given")
}
index, ok := a[1].(number)
if !ok {
return exception("'string-ref' expects a number as its first argument; a non-number value was given")
}
i := int(index)
if i >= len(haystack) || i < 0 {
return exception("'string-ref' was given a reference index that is out of bounds for the given string")
}
return string(haystack[int(index)])
},
"cons": func(a ...expression) expression {
if len(a) < 2 {
return exception(fmt.Sprintf("'cons' expected two arguments, %d given", len(a)))
}
switch car := a[0]; cdr := a[1].(type) {
case []expression:
return append([]expression{car}, cdr...)
default:
return []expression{car, cdr}
}
},
"car": func(a ...expression) expression {
if len(a) < 1 {
return exception(fmt.Sprintf("%q expected a list with at least one item in it, but a nil value was given", "car"))
}
switch i := a[0].(type) {
case []expression:
return a[0].([]expression)[0]
default:
return exception(fmt.Sprintf("%q expected a list, but a %T was given", "car", i))
}
},
"cdr": func(a ...expression) expression {
if len(a) < 1 {
return exception("\"cdr\" expected a list with at least one item in it, but a nil value was given")
}
switch i := a[0].(type) {
case []expression:
return a[0].([]expression)[1:]
default:
return exception(fmt.Sprintf("%q expected a list, but a %T was given", "car", i))
}
},
"list->string": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'list->string', expected a list and a string")
}
listIn, ok := a[0].([]expression)
if !ok {
return exception("'list->string' expected a list, but was given a non-list value")
}
joinOn := ""
if len(a) >= 2 {
switch sp := a[1].(type) {
case string:
joinOn = sp
case exception:
joinOn = string(sp)
case symbol:
joinOn = string(sp)
case number:
joinOn = strconv.Itoa(int(sp))
case []expression:
joinOn = String(sp, false)
default:
return exception("'list->string' could not convert the given join point to a string upon which to join")
}
}
sSlice := make([]string, len(listIn))
for i := range listIn {
sSlice[i] = String(listIn[i], false)
}
return strings.Join(sSlice, joinOn)
},
"list-seed": func(a ...expression) expression {
if len(a) < 2 {
return exception("insufficient number of arguments given to 'list-seed', expected number and value")
}
count, ok := a[0].(number)
if !ok {
return exception("'list-seed' expected a number as its first argument, but was given a non-number value")
}
if count < 1 {
return exception("'list-seed' expects a positive non-zero number as its first argument, a number less than 1 was given")
}
l := make([]expression, int(count))
switch t := a[1].(type) {
case []expression:
for i := range l {
s := make([]expression, len(t))
copy(s, t)
l[i] = s
}
default:
for i := range l {
l[i] = t
}
}
return l
},
"list-sort": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'list-sort', expected a list")
}
listIn, ok := a[0].([]expression)
if !ok {
return exception("'list-sort' expected a list, but was given a non-list value")
}
if len(a) > 1 {
n, ok := a[1].(number)
if !ok {
return exception("'list-sort' expected a number as its optional second argument, but a non-number was given")
}
return MergeSort(listIn, int(n))
}
return MergeSort(listIn, -1)
},
"append": func(a ...expression) expression {
if len(a) == 0 {
return make([]expression, 0)
}
mainList, ok := a[0].([]expression)
if !ok {
return exception("'append' expects argument 1 to be a list, a non-list was given")
}
for _, v := range a[1:] {
switch i := v.(type) {
case []expression:
mainList = append(mainList, i...)
default:
mainList = append(mainList, i)
}
}
return mainList
},
"list": eval(Parse("(lambda z z)").([]expression)[0], &globalenv),
"list-ref": func(a ...expression) expression {
if len(a) < 2 {
return exception("'list-ref' expects 2 arguments, a list and a number, insufficient arguments were given")
}
list, ok := a[0].([]expression)
if !ok {
return exception("'list-ref' expects argument one to be a list, a non-list argument was given")
}
index, ok := a[1].(number)
if !ok {
return exception("'list-ref' expects argument two to be a whole number, a non-number argument was given")
}
if int(index) >= len(list) {
return exception("the list reference value is larger than the number of items in the list given to 'list-ref'")
}
if len(a) >= 3 {
list[int(index)] = a[2]
return list
}
return list[int(index)]
},
"member?": func(a ...expression) expression {
if len(a) < 2 {
return exception("'member?' expects a list and a value to search for, insufficient arguments were given")
}
list, ok := a[0].([]expression)
if !ok {
return exception("'member?' expects argument one to be a list, a non-list argument was given")
}
for i := range list {
if reflect.DeepEqual(list[i], a[1]) {
return true
}
}
return false
},
"reverse": func(a ...expression) expression {
if len(a) == 0 {
return make([]expression, 0)
}
switch l := a[0].(type) {
case []expression:
out := make([]expression, len(l))
for i, n := range l {
j := len(l) - i - 1
out[j] = n
}
return out
case string:
var b strings.Builder
r := []rune(l)
for i := len(r) - 1; i >= 0; i-- {
b.WriteRune(r[i])
}
return b.String()
default:
return exception("'reverse' expected a list but was given a non-list value")
}
},
"not": func(a ...expression) expression {
if len(a) == 0 {
return exception("'not' expects a value but no value was given")
}
if i, ok := a[0].(bool); ok && !i {
return true
}
return false
},
"~bool": func(a ...expression) expression {
if len(a) == 0 {
return exception("`~bool` expects a value, but n value was given")
}
switch v := a[0].(type) {
case string:
if v == "" {
return false
}
return true
case number:
if v == 0 {
return false
}
return true
case *IOHandle:
return v.Open
case []expression:
if len(v) > 0 {
return true
}
return false
case bool:
return v
default:
return true
}
},
"map": func(a ...expression) expression {
if len(a) < 2 {
return exception("'map' expects a procedure followed by at least one list")
}
ml, err := MergeLists(a[1:])
if err != nil {
return exception("map error: " + err.Error())
}
mergedLists := ml.([]expression)
switch i := a[0].(type) {
case proc, func(...expression) expression:
out := make([]expression, 0, len(mergedLists))
for merge := range mergedLists {
out = append(out, apply(i, mergedLists[merge].([]expression)))
}
return out
default:
return exception("map expects a procedure followed by at least one list")
}
},
"for-each": func(a ...expression) expression {
if len(a) < 2 {
return exception("for-each expects a procedure followed by at least one list")
}
ml, err := MergeLists(a[1:])
if err != nil {
return exception("for-each error: " + err.Error())
}
mergedLists := ml.([]expression)
switch i := a[0].(type) {
case proc, func(...expression) expression:
for merge := range mergedLists {
apply(i, mergedLists[merge].([]expression))
}
return make([]expression, 0)
default:
return exception("for-each expects a procedure followed by at least one list")
}
},
"display": func(a ...expression) expression {
var out strings.Builder
for i := range a {
out.WriteString(String(a[i], false))
}
fmt.Print(out.String())
return make([]expression, 0)
},
"display-lines": func(a ...expression) expression {
var out strings.Builder
for i := range a {
switch val := a[i].(type) {
case *IOHandle:
buf, ok := val.Obj.(*strings.Builder)
if ok {
out.WriteString(buf.String())
} else {
out.WriteString(String(val, true))
}
out.WriteRune('\n')
default:
out.WriteString(String(val, false))
out.WriteRune('\n')
}
}
fmt.Print(out.String())
return make([]expression, 0)
},
"write": func(a ...expression) expression {
if len(a) == 1 {
fmt.Print(String(a[0], false))
return make([]expression, 0)
} else if len(a) > 1 {
stringOut := String(a[0], false)
obj, ok := a[1].(*IOHandle)
if !ok {
return exception("'write' expected an IO handle as its second argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'write' was given an IO handle that is already closed")
}
switch ft := obj.Obj.(type) {
case *os.File:
ft.WriteString(stringOut)
case *net.Conn:
(*ft).Write([]byte(stringOut))
case *tls.Conn:
ft.Write([]byte(stringOut))
case *strings.Builder:
ft.WriteString(stringOut)
default:
return exception("'write' was given an IO handle that is not supported")
}
return a[1].(*IOHandle)
}
return exception("'write' received insufficient arguments")
},
"write-raw": func(a ...expression) expression {
if len(a) == 1 {
fmt.Print(String(a[0], true))
} else if len(a) >= 2 {
obj, ok := a[1].(*IOHandle)
if !ok {
return exception("'write-raw' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'write-raw' was given an IO handle that is already closed")
}
switch ft := obj.Obj.(type) {
case *os.File:
ft.WriteString(String(a[0], true))
case *net.Conn:
(*ft).Write([]byte(String(a[0], true)))
case *tls.Conn:
ft.Write([]byte([]byte(String(a[0], true))))
case *strings.Builder:
ft.WriteString(String(a[0], true))
default:
return false
}
return a[1].(*IOHandle)
}
return make([]expression, 0)
},
"read-all": func(a ...expression) expression {
if len(a) == 0 {
reader := bufio.NewReader(os.Stdin)
text, err := reader.ReadString('\n')
if err != nil {
return false
}
return text[:len(text)-1]
} else {
obj, ok := a[0].(*IOHandle)
if !ok {
return exception("'read-all' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'read-all' was given an IO handle that is already closed")
}
switch f := obj.Obj.(type) {
case *os.File:
b, err := ioutil.ReadAll(f)
if err != nil {
return exception("'read-all' could not read from the given IOHandle")
}
return string(b)
case *net.Conn:
b, err := ioutil.ReadAll(*f)
if err != nil {
return exception("'read-all' could not read from the given IOHandle")
}
return string(b)
case *tls.Conn:
b, err := ioutil.ReadAll(f)
if err != nil {
return exception("'read-all' could not read from the given IOHandle")
}
return string(b)
case *strings.Builder:
return f.String()
default:
return exception("'read-all' has not been implemented for this object type")
}
}
},
"read-all-lines": func(a ...expression) expression {
if len(a) == 0 {
reader := bufio.NewReader(os.Stdin)
text, err := reader.ReadString('\n')
if err != nil {
return exception("'read-all-lines' could not read from the given IOHandle (Stdin)")
}
return strings.SplitN(text[:len(text)-1], "\n", -1)
} else {
obj, ok := a[0].(*IOHandle)
if !ok {
return exception("'read-all-lines' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'read-all-lines' was given an IO handle that is already closed")
}
switch f := obj.Obj.(type) {
case *os.File:
b, err := ioutil.ReadAll(f)
if err != nil {
return exception("'read-all-lines' could not read from the given IOHandle")
}
o := strings.SplitN(string(b), "\n", -1)
return StringSliceToExpressionSlice(o).([]expression)
case *net.Conn:
b, err := ioutil.ReadAll(*f)
if err != nil {
return exception("'read-all-lines' could not read from the given IOHandle")
}
o := strings.SplitN(string(b), "\n", -1)
return StringSliceToExpressionSlice(o).([]expression)
case *tls.Conn:
b, err := ioutil.ReadAll(f)
if err != nil {
return exception("'read-all-lines' could not read from the given IOHandle")
}
o := strings.SplitN(string(b), "\n", -1)
return StringSliceToExpressionSlice(o).([]expression)
case *strings.Builder:
s := f.String()
o := strings.SplitN(s, "\n", -1)
return StringSliceToExpressionSlice(o).([]expression)
default:
return exception("'read-all' has not been implemented for this object type")
}
}
},
"read-char": func(a ...expression) expression {
if len(a) == 0 {
reader := bufio.NewReader(os.Stdin)
char, _, err := reader.ReadRune()
if err != nil {
return exception("'read-char' could not read from stdin")
}
return fmt.Sprintf("%c", char)
} else {
obj, ok := a[0].(*IOHandle)
if !ok {
return exception("'read-char' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'read-char' was given an IO handle that is already closed")
}
// TODO add a strings.Builder to build output
// take a second arg for how many chars to read
// loop over readrune to read that many chars and
// build the buffer, then return the output. If an
// error is encountered while reading return the
// output if the len(buffer) > 0, otherwise an
// exception
byteBuf := make([]byte, 0, 3)
b := make([]byte, 1)
switch f := obj.Obj.(type) {
case *os.File:
for {
n, err := f.Read(b)
if err != nil && err != io.EOF {
return exception("'read-char' could not read from the given IOHandle")
} else if err != nil || n == 0 {
return symbol("EOF")
}
byteBuf = append(byteBuf, b[0])
if utf8.FullRune(byteBuf) {
break
}
}
return string(byteBuf)
case *net.Conn:
for {
n, err := (*f).Read(b)
if err != nil && err != io.EOF {
return exception("'read-char' could not read from the given IOHandle")
} else if err != nil || n == 0 {
return symbol("EOF")
}
byteBuf = append(byteBuf, b[0])
if utf8.FullRune(byteBuf) {
break
}
}
return string(byteBuf)
case *tls.Conn:
for {
n, err := f.Read(b)
if err != nil && err != io.EOF {
return exception("'read-char' could not read from the given IOHandle")
} else if err != nil || n == 0 {
return symbol("EOF")
}
byteBuf = append(byteBuf, b[0])
if utf8.FullRune(byteBuf) {
break
}
}
return string(byteBuf)
default:
return exception("'read-char' has not been implemented for this object type")
}
}
},
"read-line": func(a ...expression) expression {
if len(a) == 0 {
reader := bufio.NewReader(os.Stdin)
text, err := reader.ReadString('\n')
if err != nil {
return false
}
return text[:len(text)-1]
} else {
obj, ok := a[0].(*IOHandle)
if !ok {
return exception("'read-line' expected an IO handle as its argument, but was not given an IO handle")
}
if !obj.Open {
return exception("'read-line' was given an IO handle that is already closed")
}
split := byte('\n')
if len(a) > 1 {
s := String(a[1], false)
if len(s) > 0 {
split = s[0]
}
}
switch f := obj.Obj.(type) {
case *os.File:
var o strings.Builder
for {
b := make([]byte, 1)
n, err := f.Read(b)
if n > 0 {
if b[0] == split {
break
}
o.Write(b)
}
if err != nil && err != io.EOF && o.Len() == 0 {
return exception("'read-line' could not read from the given IOHandle")
} else if err != nil && err == io.EOF {
return symbol("EOF")
} else if err != nil {
break
}
}
return o.String()
case *net.Conn:
var o strings.Builder
for {
b := make([]byte, 1)
n, err := (*f).Read(b)
if n > 0 {
if b[0] == '\n' {
break
}
o.Write(b)
}
if err != nil && err != io.EOF && o.Len() == 0 {
return exception("'read-line' could not read from the given IOHandle")
} else if err != nil && err == io.EOF {
return symbol("EOF")
} else if err != nil {
break
}
}
return o.String()
case *tls.Conn:
var o strings.Builder
for {
b := make([]byte, 1)
n, err := (*f).Read(b)
if n > 0 {
if b[0] == '\n' {
break
}
o.Write(b)
}
if err != nil && err != io.EOF && o.Len() == 0 {
return exception("'read-line' could not read from the given IOHandle")
} else if err != nil && err == io.EOF {
return symbol("EOF")
} else if err != nil {
break
}
}
return o.String()
default:
return exception("'read-line' has not been implemented for this object type")
}
}
},
"newline": func(a ...expression) expression {
fmt.Print("\r\n")
return make([]expression, 0)
},
"exit": func(a ...expression) expression {
code := 0
if len(a) > 0 {
if v, ok := a[0].(number); ok {
code = int(v)
}
}
SafeExit(code)
return 0
},
"substring": func(a ...expression) expression {
if len(a) < 3 {
return exception("insufficient number of arguments given to 'substring'")
}
var s string
var n1, n2 number
var ok bool
if s, ok = a[0].(string); !ok {
return exception("'substring' expects a string as its first argument")
}
if n1, ok = a[1].(number); !ok {
return exception("'substring' expects a number as its second argument")
}
if n2, ok = a[2].(number); !ok {
return exception("'substring' expects a number as its second argument")
}
if n1 < 0 || n1 >= n2 {
return exception("start position of 'substring' is out of range")
}
if int(n2) > len(s) {
return exception("end position of 'substring' is out of range")
}
return s[int(n1):int(n2)]
},
"string-trim-space": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'string-trim-space'")
}
if s, ok := a[0].(string); ok {
return strings.TrimSpace(s)
}
return exception("'string-trim-space' expected a string but was given a non-string value")
},
"string-append": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'string-append'")
}
var out strings.Builder
for i := range a {
out.WriteString(String(a[i], false))
}
return out.String()
},
"string->number": func(a ...expression) expression {
// (string->number [string] [[base: number]])
if len(a) == 0 {
return exception("insufficient number of arguments given to 'string->number'")
}
s, ok := a[0].(string)
if !ok {
return exception("'string->number' expects a string as its first argument, a non-string value was given")
}
base := 10
if len(a) > 1 {
b, ok2 := a[1].(number)
if !ok2 {
return exception("'string->number' expects a number representing the base being used for number parsing as its second argument, a non-number value was given")
}
base = int(b)
}
if len(a) == 1 || base == 10 {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return number(f)
}
}
i, err := strconv.ParseInt(s, base, 64)
if err != nil {
return false
}
return number(i)
},
"string-fields": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'string-fields'")
}
stringIn, ok := a[0].(string)
if !ok {
return exception("'string-fields' expected a string, but was given a non-string value")
}
sf := strings.Fields(stringIn)
return StringSliceToExpressionSlice(sf).([]expression)
},
"string->list": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'string->list'")
}
stringIn, ok := a[0].(string)
if !ok {
return exception("'string->list' expected a string, but was given a non-string value")
}
splitPoint := ""
if len(a) >= 2 {
switch sp := a[1].(type) {
case string:
splitPoint = sp
case symbol:
splitPoint = string(sp)
case number:
splitPoint = strconv.Itoa(int(sp))
case []expression:
splitPoint = String(sp, true)
default:
return exception("'string->list' could not convert the given split point to a string upon which to split")
}
}
count := -1
if len(a) > 2 {
num, ok := a[2].(number)
if !ok {
return exception("'string->list' expected a number as its third argument, but a non-number value was given")
}
if num < 1 {
count = -1
} else {
count = int(num)
}
}
sSlice := strings.SplitN(stringIn, splitPoint, count)
return StringSliceToExpressionSlice(sSlice).([]expression)
},
"regex-match?": func(a ...expression) expression {
if len(a) < 2 {
return exception("'regex-match?' expects two arguments, a pattern string and an input string, insufficient arguments were given")
}
pattern, ok := a[0].(string)
if !ok {
return exception("'regex-match?' expects a string representing a regular expression pattern as its first argument, but was given a non-string value")
}
str, ok := a[1].(string)
if !ok {
return exception("'regex-match?' expects a string as its second argument, but was given a non-string value")
}
match, err := regexp.MatchString(pattern, str)
if err != nil {
return exception("'regex-match?' encountered a regular expression error while matching")
}
return match
},
"regex-find": func(a ...expression) expression {
if len(a) < 2 {
return exception("'regex-find' expects two arguments, a pattern string and an input string, insufficient arguments were given")
}
pattern, ok := a[0].(string)
if !ok {
return exception("'regex-find' expects a string representing a regular expression pattern as its first argument, but was given a non-string value")
}
str, ok := a[1].(string)
if !ok {
return exception("'regex-find' expects a string as its second argument, but was given a non-string value")
}
re := regexp.MustCompile(pattern)
found := re.FindAllString(str, -1)
if found == nil {
return make([]expression, 0)
}
return StringSliceToExpressionSlice(found).([]expression)
},
"regex-replace": func(a ...expression) expression {
if len(a) < 3 {
return exception("'regex-replace' expects three arguments: a pattern string, an input string, and a replacement string, insufficient arguments were given")
}
pattern, ok := a[0].(string)
if !ok {
return exception("'regex-replace' expects a string representing a regular expression pattern as its first argument, but was given a non-string value")
}
str, ok := a[1].(string)
if !ok {
return exception("'regex-find' expects a string as its second argument, but was given a non-string value")
}
replace, ok := a[2].(string)
if !ok {
return exception("'regex-find' expects a replacement string as its third argument, but was given a non-string value")
}
re := regexp.MustCompile(pattern)
return re.ReplaceAllString(str, replace)
},
"number->string": func(a ...expression) expression {
if len(a) == 0 {
return exception("insufficient number of arguments given to 'string->number'")
}
n, ok := a[0].(number)
if !ok {
return exception("'number->string' expected a number as its first argument, a non-number value was given")
}
if len(a) == 2 {
base, ok2 := a[1].(number)
if !ok2 {
return exception("'number->string' expected a number as its second argument, a non-number value was given")
}
if base != 10 {
return strconv.FormatInt(int64(n), int(base))
}
}
return strconv.FormatFloat(float64(n), 'f', -1, 64)
},
"file-create-temp": func(a ...expression) expression {
if len(a) == 0 {
return exception("'file-create-temp' expects a filename pattern as a string, no value was given")
}
fp, ok := a[0].(string)
if !ok {
return exception("'file-create-temp' expects a string representing a filename pattern, a non-string value was given")
}
f, err := os.CreateTemp("", fp)
if err != nil {
return exception("'file-create-temp' was unable to create the requested temporary file: " + err.Error())
}
obj := &IOHandle{f, true, "file (write-only)"}
openFiles = append(openFiles, obj)
return obj
},
"file-name": func(a ...expression) expression {
if len(a) == 0 {
return exception("'file-path' expects an IOHandle, no value was given")
}
ioh, ok := a[0].(*IOHandle)
if !ok {
return exception("'file-path' expects an IOHandle, a non-IOHandle value was given")
}
switch f := ioh.Obj.(type) {
case *os.File:
return f.Name()
default:
return exception("'file-path' encountered an IOHandle type that it cannot process")
}
},
"file-stat": func(a ...expression) expression {
if len(a) == 0 {
return exception("'file-stat' expects a filepath, no filepath was given")
}
fp, ok := a[0].(string)
if !ok {
return exception("'file-stat' expects a filepath as a string, a non-string value was given")
}
info, err := os.Stat(ExpandedAbsFilepath(fp))
if err != nil {
if os.IsNotExist(err) {
return false
}
return exception("'file-stat' could not stat file: " + err.Error())
}
out := make([]expression, 7)
out[0] = []expression{"name", info.Name()}
out[1] = []expression{"size", number(info.Size())}
out[2] = []expression{"mode", number(info.Mode())}
out[3] = []expression{"mod-time", number(info.ModTime().Unix())}
out[4] = []expression{"is-dir?", info.IsDir()}
out[5] = []expression{"is-symlink?", false}
out[6] = []expression{"path", fp}
ln, err := os.Readlink(fp)
if err == nil {
out[5] = []expression{"is-symlink?", true}
if !filepath.IsAbs(ln) {
root := fp
if !info.IsDir() {
root = filepath.Dir(fp)
}
ln = ExpandedAbsFilepath(filepath.Join(root, ln))
}
out[6] = []expression{"path", ln}
}
return out
},
"file-create": func(a ...expression) expression {
if len(a) < 1 {
return exception("'file-create' expects a filepath, no filepath was given")
}
fp, ok := a[0].(string)
if !ok {
return exception("'file-create' expects a filepath as a string, a non-string value was given")
}
f, err := os.Create(fp)
if err != nil {
return exception("'file-create' encountered an error: " + err.Error())
}
obj := &IOHandle{f, true, "file (write-only)"}
openFiles = append(openFiles, obj)
return obj
},
"file-open-read": func(a ...expression) expression {
if len(a) == 0 {
return exception("'file-open-read' expects a filepath, no filepath was given")
}
if fp, ok := a[0].(string); ok {
f, err := os.Open(fp)
if err != nil {
return false
}
obj := &IOHandle{f, true, "file (read-only)"}
openFiles = append(openFiles, obj)
return obj
}
return exception("'file-open-read' expects a filepath as a string, a non-string value was given")
},
"file-open-write": func(a ...expression) expression {
// (file-open-write [filepath: string] [[truncate: bool]])
if len(a) == 0 {
return exception("'file-open-write' expects a filepath, no filepath was given")
}
if fp, ok := a[0].(string); ok {
flags := os.O_CREATE | os.O_WRONLY
if len(a) >= 2 {
if b, ok := a[1].(bool); ok && b {
flags = flags | os.O_TRUNC
}
}
f, err := os.OpenFile(fp, flags, 0664)
if err != nil {
return false
}
obj := &IOHandle{f, true, "file (write-only)"}
openFiles = append(openFiles, obj)
return obj
}
return exception("'file-write' expects a filepath as a string, a non-string value was given")
},
"file-append-to": func(a ...expression) expression {
// (file-append-to [filepath: string] [[string...]])
if len(a) < 2 {
return exception("'file-append-to' requires a filepath and at least one string")
}
if fp, ok := a[0].(string); ok {
f, err := os.OpenFile(fp, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return false
}
defer f.Close()
for i := range a[1:] {
f.WriteString(String(a[i+1], false))
}
return make([]expression, 0)
}
return exception("'file-append-to' expects a filepath, as a string, as its first argument")
},
"open?": func(a ...expression) expression {
if len(a) < 1 {
return exception("'open?' expects an IO handle, no argument was given")
}
if h, ok := a[0].(*IOHandle); ok {
return h.Open
}
return exception("'open?' expects an IO handle")
},
"close": func(a ...expression) expression {
if len(a) < 1 {
return exception("'close' expects an IO handle, no argument was given")
}
if f, ok := a[0].(*IOHandle); ok {
f.Open = false
switch o := f.Obj.(type) {
case *os.File:
o.Close()
case *net.Conn:
(*o).Close()
case *tls.Conn:
o.Close()
case *strings.Builder:
f.Open = false // duplicate here to not have a blank line
default:
return exception("'close' encountered an unsupported IO handle type")
}
} else {
return exception("'close' expects an IO handle")
}
return make([]expression, 0)
},
"path-exists?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-exists' expects a filepath as a string, no value was supplied")
}
switch p := a[0].(type) {
case string:
_, err := os.Stat(p)
if err != nil {
return false
}
return true
case symbol:
_, err := os.Stat(string(p))
if err != nil {
return false
}
return true
default:
return exception("'path-exists' expected a string, but a non-string value was given")
}
},
"path-abs": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-abs' expects a filepath as a string, no value was given")
}
p, ok := a[0].(string)
if !ok {
return exception("'path-abs' expected a filepath as a string, a non-string value was given")
}
return ExpandedAbsFilepath(p)
},
"path-is-dir?": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-is-dir?' expects a filepath as a string, no value was given")
}
switch p := a[0].(type) {
case string:
f, err := os.Stat(ExpandedAbsFilepath(p))
if err != nil {
return exception(strings.Replace(err.Error(), "stat ", "", -1))
}
return f.IsDir()
case symbol:
f, err := os.Stat(ExpandedAbsFilepath(string(p)))
if err != nil {
return exception(strings.Replace(err.Error(), "stat ", "", -1))
}
return f.IsDir()
default:
return exception("'path-is-dir?' expected a string, but a non-string value was given")
}
},
"path-join": func(a ...expression) expression {
if len(a) == 0 {
return "/"
}
strs := make([]string, len(a))
for i := range a {
v, ok := a[i].(string)
if !ok {
return exception(fmt.Sprintf("Argument %d to 'path-join' is not a string", i+1))
}
strs[i] = v
}
return filepath.Join(strs...)
},
"path-extension": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-extension' expects a string and, optionally, a second string; no value was given")
}
p, ok := a[0].(string)
if !ok {
return exception("'path-extension' expects a path string as its first argument, a non-string value was given")
}
if len(a) > 1 {
e, ok := a[1].(string)
if !ok {
return exception("'path-extension' expects an extension as a string as its second argument, a non-string value was given")
}
ex := filepath.Ext(p)
if ex == "" {
return p + e
}
li := strings.LastIndex(p, ex)
if li < 0 {
return p
}
return p[:li] + e
}
return filepath.Ext(p)
},
"path-base": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-base' expects a string, no value was given")
}
p, ok := a[0].(string)
if !ok {
return exception("'path-base' expects a path string as its first argument, a non-string value was given")
}
return filepath.Base(p)
},
"path-dir": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-dir' expects a string, no value was given")
}
p, ok := a[0].(string)
if !ok {
return exception("'path-dir' expects a path string as its first argument, a non-string value was given")
}
return filepath.Dir(p)
},
"path-glob": func(a ...expression) expression {
if len(a) == 0 {
return exception("'path-glob' expects a filepath as a string, too few values given")
}
p, ok := a[0].(string)
if !ok {
return exception("'path-glob' expects a filepath as a string, a non-string value was given")
}
g, err := filepath.Glob(p)
if err != nil {
return exception("'path-glob' received an invalid filepath glob pattern as a string")
}
return StringSliceToExpressionSlice(g)
},
"hostname": func(a ...expression) expression {
hostname, err := os.Hostname()
if err != nil {
return exception("'hostname' could not retrieve the hostname: " + err.Error())
}
return hostname
},
"net-conn": func(a ...expression) expression {
// (net-conn host port use-tls timeout-seconds)
// (net-conn string string bool number)
if len(a) < 2 {
return exception("'net-conn' expects a host and a port as a string, too few values given")
}
host, ok := a[0].(string)
if !ok {
return exception("'net-conn' expects a host as a string, a non-string value was given")
}
var port string
switch p := a[1].(type) {
case string:
port = p
case number:
port = strconv.Itoa(int(p))
default:
return exception("'net-conn' expects a port as a string, a non-string value was given")
}
usetls := false
if len(a) >= 3 {
t, ok := a[2].(bool)
if !ok {
return exception("'net-conn' expects a boolean value as the third argument (use-tls), a non-bool value was given")
}
usetls = t
}
timeout := -1
if len(a) >= 4 {
switch to := a[3].(type) {
case number:
timeout = int(to)
case string:
t, err := strconv.Atoi(to)
if err != nil {
return exception("'net-conn' was given a timeout string that does not cast to an integer")
}
timeout = t
default:
return exception("'net-conn' expects a string or number value representing a timeout, in seconds; a non-string non-number value was given ")
}
}
var conn net.Conn
var tlsconn *tls.Conn
var err error
addr := fmt.Sprintf("%s:%s", host, port)
if usetls {
conf := &tls.Config{InsecureSkipVerify: true}
if timeout > 0 {
tlsconn, err = tls.DialWithDialer(&net.Dialer{Timeout: time.Duration(timeout) * time.Second}, "tcp", addr, conf)
if err != nil {
return false
}
} else {
tlsconn, err = tls.Dial("tcp", addr, conf)
if err != nil {
return false
}
}
} else {
if timeout > 0 {
conn, err = net.DialTimeout("tcp", addr, time.Duration(timeout)*time.Second)
if err != nil {
return false
}
} else {
conn, err = net.Dial("tcp", addr)
if err != nil {
return false
}
}
}
// based on the tls value use tls or not, same with the timeout value for adding a timeout
var handle *IOHandle
if usetls {
handle = &IOHandle{tlsconn, true, "net-conn (tls)"}
} else {
handle = &IOHandle{&conn, true, "net-conn"}
}
openFiles = append(openFiles, handle)
return handle
},
"url-host": func(a ...expression) expression {
if len(a) == 0 {
return exception("'url-host' expects a url as a string, no value was given")
}
rawURL, ok := a[0].(string)
if !ok {
return exception("'url-host' expects a url as a string, a non-string value was given")
}
u, err := url.Parse(rawURL)
if err != nil {
return false
}
if len(a) >= 2 {
p := ":" + u.Port()
if p == ":" {
p = ""
}
switch s := a[1].(type) {
case string:
u.Host = s + p
case symbol:
u.Host = string(s) + p
default:
return exception("'url-host' expects either a string or a symbol as its second argument, a non-string non-symbol value was given")
}
return u.String()
}
return u.Hostname()
},
"url-port": func(a ...expression) expression {
if len(a) == 0 {
return exception("'url-port' expects a url as a string, no value was given")
}
rawURL, ok := a[0].(string)
if !ok {
return exception("'url-port' expects a url as a string, a non-string value was given")
}
u, err := url.Parse(rawURL)
if err != nil {
return false
}
if len(a) >= 2 {
switch s := a[1].(type) {
case string:
u.Host = u.Hostname() + ":" + s
case symbol:
u.Host = u.Hostname() + ":" + string(s)
case number:
u.Host = u.Hostname() + ":" + strconv.Itoa(int(s))
default:
return exception("'url-port' expects either a string or a symbol as its second argument, a non-string non-symbol value was given")
}
return u.String()
}
return u.Port()
},
"url-query": func(a ...expression) expression {
if len(a) == 0 {
return exception("'url-query' expects a url as a string, no value was given")
}
rawURL, ok := a[0].(string)
if !ok {
return exception("'url-query' expects a url as a string, a non-string value was given")
}
u, err := url.Parse(rawURL)
if err != nil {
return false
}
if len(a) >= 2 {
switch s := a[1].(type) {
case string:
u.RawQuery = s
case symbol:
u.RawQuery = string(s)
default:
return exception("'url-query' expects either a string or a symbol as its second argument, a non-string non-symbol value was given")
}
return u.String()
}
return u.RawQuery
},
"url-path": func(a ...expression) expression {
if len(a) == 0 {
return exception("'url-path' expects a url as a string, no value was given")
}
rawURL, ok := a[0].(string)
if !ok {
return exception("'url-path' expects a url as a string, a non-string value was given")
}
u, err := url.Parse(rawURL)
if err != nil {
return false
}
if len(a) >= 2 {
switch s := a[1].(type) {
case string:
u.Path = s
case symbol:
u.Path = string(s)
default:
return exception("'url-path' expects either a string or a symbol as its second argument, a non-string non-symbol value was given")
}
return u.String()
}
return u.EscapedPath()
},
"url-scheme": func(a ...expression) expression {
if len(a) == 0 {
return exception("'url-scheme' expects a url as a string, no value was given")
}
rawURL, ok := a[0].(string)
if !ok {
return exception("'url-scheme' expects a url as a string, a non-string value was given")
}
u, err := url.Parse(rawURL)
if err != nil {
return false
}
if len(a) >= 2 {
switch s := a[1].(type) {
case string:
u.Scheme = s
case symbol:
u.Scheme = string(s)
default:
return exception("'url-scheme' expects either a string or a symbol as its second argument, a non-string non-symbol value was given")
}
return u.String()
}
return u.Scheme
},
"license": func(a ...expression) expression {
fmt.Println(licenseText)
return make([]expression, 0)
},
"chdir": func(a ...expression) expression {
if len(a) == 0 {
return exception("'chdir' expects a path as a string, no value was given")
}
p, ok := a[0].(string)
if !ok {
return exception("'chdir' expects a string; a non-string value was given")
}
err := os.Chdir(ExpandedAbsFilepath(p))
if err != nil {
return exception(err.Error())
}
return make([]exception, 0)
},
"chmod": func(a ...expression) expression {
if len(a) < 2 {
return exception("'chmod' expects a path as a string and a file mode as a number, no value was given")
}
p, ok := a[0].(string)
if !ok {
return exception("'chdir' expects a string; a non-string value was given")
}
n, ok := a[1].(number)
err := os.Chmod(ExpandedAbsFilepath(p), os.FileMode(n))
if err != nil {
return exception(err.Error())
}
return make([]exception, 0)
},
"env": func(a ...expression) expression {
if len(a) == 0 {
e := make([]expression, 0, 20)
env := os.Environ()
for _, v := range env {
sp := strings.Split(v, "=")
if len(sp) < 2 {
sp = append(sp, "")
}
e = append(e, []expression{sp[0], sp[1]})
}
return e
}
if len(a) == 1 {
switch i := a[0].(type) {
case string:
return os.Getenv(i)
default:
return exception("Invalid argument to single argument 'env' call, was not given a string")
}
}
variable, ok := a[0].(string)
if !ok {
return exception("Invalid argument to dual argument 'env' call, was not given a string as the first argument")
}
err := os.Setenv(variable, String(a[1], false))
if err != nil {
return exception(err.Error())
}
return make([]expression, 0)
},
"mkdir": func(a ...expression) expression {
if len(a) == 0 {
return exception("'mkdir' expects a path string and a number representing file permissions and optionally a bool; no value was given")
}
path, ok1 := a[0].(string)
if !ok1 {
return exception("'mkdir' expects a path string as its first argument, a non-string value was given")
}
perms, ok2 := a[1].(number)
if !ok2 {
return exception("'mkdir' expects a number representing a file permissions setting as its second argument, a non-number value was given")
}
prmInt := os.FileMode(perms)
var ok3, mkdirAll bool
if len(a) > 2 {
mkdirAll, ok3 = a[2].(bool)
if !ok3 {
return exception("'mkdir' expects a bool as its third argument, a non-bool value was given")
}
}
var err error
if mkdirAll {
err = os.MkdirAll(ExpandedAbsFilepath(path), prmInt)
} else {
err = os.Mkdir(ExpandedAbsFilepath(path), prmInt)
}
if err != nil {
return exception("'mkdir' could not create directories for the given path with the given permissions: " + err.Error())
}
return make([]expression, 0)
},
"rm": func(a ...expression) expression {
if len(a) == 0 {
return exception("'rm' expects a path string and, optionally, a bool; no value was given")
}
path, ok1 := a[0].(string)
if !ok1 {
return exception("'rm' expects a path string as its first argument, a non-string value was given")
}
var ok2, mkdirAll bool
if len(a) > 1 {
mkdirAll, ok2 = a[1].(bool)
if !ok2 {
return exception("'rm' expects a bool as its second argument, a non-bool value was given")
}
}
var err error
if mkdirAll {
err = os.RemoveAll(ExpandedAbsFilepath(path))
} else {
err = os.Remove(ExpandedAbsFilepath(path))
}
if err != nil {
return exception("'mkdir' could not create directories for the given path with the given permissions: " + err.Error())
}
return make([]expression, 0)
},
"pwd": func(a ...expression) expression {
wd, err := os.Getwd()
if err != nil {
return exception("'pwd' could not retrieve the current working directory: " + err.Error())
}
return wd
},
"mv": func(a ...expression) expression {
if len(a) < 2 {
return exception("'mv' expected two path strings (from and to), but was given an insufficient number of arguments")
}
from, ok := a[0].(string)
if !ok {
return exception("'mv' expected a path string as its first argument, a non-string value was given")
}
to, ok2 := a[1].(string)
if !ok2 {
return exception("'mv' expected a path string as its second argument, a non-string value was given")
}
err := os.Rename(from, to)
if err != nil {
return exception("'mv' could not perform the requested action: " + err.Error())
}
return make([]expression, 0)
},
"subprocess": func(a ...expression) expression {
if len(a) == 0 {
return exception("'subprocess' expects a list containing the process to start and any arguments, as strings. It can also, optionally, take 1 or 2 file handles to write Stdout and Stderr to. No arguments were given.")
}
processList, ok := a[0].([]expression)
if !ok {
return exception("'subprocess' expects a list containing the process to start and any arguments as separate strings in the list. A non-list was given.")
}
process := make([]string, len(processList))
for i := range processList {
if s, ok := processList[i].(string); ok {
process[i] = s
} else {
return exception("a list passed to 'subprocess' contained the non-string value: " + String(processList[i], true))
}
}
var o, e *IOHandle
if len(a) > 2 {
e1, e1ok := a[2].(*IOHandle)
if !e1ok {
if b, ok := a[2].(bool); !ok || (ok && b) {
return exception("'subprocess' was given an error redirection that is not an IOHandle or the boolean '#f'")
}
} else if !e1.Open {
return exception("'subprocess' was given an error redirection IOHandle that is already closed")
}
e = e1
}
if len(a) > 1 {
o1, o1ok := a[1].(*IOHandle)
if !o1ok {
if b, ok := a[1].(bool); !ok || (ok && b) {
return exception("'subprocess' was given an output redirection that is not an IOHandle or the boolean '#f'")
}
} else if !o1.Open {
return exception("'subprocess' was given an output redirection IOHandle that is already closed")
}
o = o1
}
cmd := exec.Command(process[0], process[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if o != nil {
switch io := o.Obj.(type) {
case *os.File:
cmd.Stdout = io
case *net.Conn:
cmd.Stdout = (*io)
case *tls.Conn:
cmd.Stdout = io
case *strings.Builder:
cmd.Stdout = io
default:
return exception("'subprocess' was given an output redirection IOHandle of an unknown type")
}
}
if e != nil {
switch io := e.Obj.(type) {
case *os.File:
cmd.Stderr = io
case *net.Conn:
cmd.Stderr = (*io)
case *tls.Conn:
cmd.Stderr = io
case *strings.Builder:
cmd.Stderr = io
default:
return exception("'subprocess' was given an error redirection IOHandle of an unknown type")
}
}
err := cmd.Run()
if err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
return number(status.ExitStatus())
}
} else {
return exception("'subprocess' encountered an error running the given process and the process could not be run")
}
}
return number(0)
},
"term-size": func(a ...expression) expression {
cols, rows := termios.GetWindowSize()
return []expression{number(cols), number(rows)}
},
"term-restore": func(a ...expression) expression {
termios.Restore()
return make([]expression, 0)
},
"term-char-mode": func(a ...expression) expression {
termios.SetCharMode()
return make([]expression, 0)
},
"term-raw-mode": func(a ...expression) expression {
termios.SetRawMode()
return make([]expression, 0)
},
"term-sane-mode": func(a ...expression) expression {
termios.SetSaneMode()
return make([]expression, 0)
},
"term-cooked-mode": func(a ...expression) expression {
termios.SetCookedMode()
return make([]expression, 0)
},
"timestamp": func(a ...expression) expression {
return number(time.Now().Unix())
},
"date": func(a ...expression) expression {
// (date [[format: string]])
if len(a) == 0 {
return time.Now().Format("Mon Jan 2 15:04:05 -0700 MST 2006")
}
layout, ok := a[0].(string)
if !ok {
return exception("'date' was given a non-string layout value")
}
return time.Now().Format(createTimeFormatString(layout))
},
"timestamp->date": func(a ...expression) expression {
// (timestamp->date [timestamp: string] [[layout: string]])
if len(a) >= 1 {
layout := "Mon Jan 2 15:04:05 -0700 MST 2006"
var dateint int64
switch d := a[0].(type) {
case string:
di, err := strconv.ParseInt(d, 10, 64)
if err != nil {
return exception("'timestamp->date' was given an invalid timestamp string, it could not be converted to a number")
}
dateint = di
case number:
dateint = int64(d)
default:
return exception("'timestamp->date' was given a non-string non-number value as a timestamp")
}
if len(a) >= 2 {
var ok1 bool
layout, ok1 = a[1].(string)
if !ok1 {
return exception("'timestamp->date' was given a non-string layout value")
}
layout = createTimeFormatString(layout)
}
t := time.Unix(dateint, 0)
return t.Local().Format(layout)
}
return exception("'timestamp->date' received an insufficient argument count, expected 1 or 2 arguments")
},
"date-format": func(a ...expression) expression {
// (date-format [input-format: string] [input-date: string] [output-format: string])
if len(a) == 3 {
// datestring->datestring
layout1, ok1 := a[0].(string)
datestring, ok2 := a[1].(string)
layout2, ok3 := a[2].(string)
if !ok1 || !ok2 || !ok3 {
fmt.Println(a)
return exception("'date-format' was given a non-string argument, it expects strings")
}
t, err := time.ParseInLocation(createTimeFormatString(layout1), datestring, time.Now().Location())
if err != nil {
return exception("'date-format' could not parse the input date to a time value based on the given layout string")
}
return t.Format(createTimeFormatString(layout2))
}
return exception("'date-format' received an unexpected number of arguments")
},
"date->timestamp": func(a ...expression) expression {
// (date->timestamp [date: string] [format: string])
if len(a) == 2 {
layout1, ok1 := a[1].(string)
datestring, ok2 := a[0].(string)
if !ok1 || !ok2 {
return exception("'date->timestamp' was given a non-string argument, it expects strings")
}
fmt.Println(layout1)
t, err := time.ParseInLocation(createTimeFormatString(layout1), datestring, time.Now().Location())
if err != nil {
return exception("'date->timestamp' could not parse the input date to a time value based on the given layout string")
}
return number(t.Unix())
}
return exception("'date' received an unexpected number of arguments")
},
"date-default-format": func(a ...expression) expression {
return "%w %f %d %g:%I:%S %O %Z %Y"
},
"sleep": func(a ...expression) expression {
// (sleep [ms: number])
if len(a) > 0 {
ms, ok := a[0].(number)
if !ok {
return exception("'sleep' was given a non-number value")
}
time.Sleep(time.Duration(ms) * time.Millisecond)
}
return make([]expression, 0)
},
"rand": func(a ...expression) expression {
rand.Seed(time.Now().UnixNano())
if len(a) == 0 {
// equal to: (rand 1 0)
return number(rand.Float64())
}
var max, min number
var ok bool
if len(a) >= 1 {
// equal to: (rand [max] 0)
max, ok = a[0].(number)
if !ok {
return exception("'rand' expected a number as its first argument, a non-number value was given")
}
}
if len(a) == 1 {
return number(rand.Float64() * float64(max))
}
// equal to: (rand [max] [min])
min, ok = a[1].(number)
if !ok {
return exception("'rand' expected a number as its second argument, a non-number value was given")
}
return number(rand.Float64()*(float64(max)-float64(min)) + float64(min))
},
"string->md5": func(a ...expression) expression {
if len(a) == 0 {
return exception("'string->md5' expects a string, no value was given")
}
s, ok := a[0].(string)
if !ok {
return exception("'string->md5' expected a string as its first argument, a non-string argument was given")
}
return fmt.Sprintf("%x", md5.Sum([]byte(s)))
},
"string->sha256": func(a ...expression) expression {
if len(a) == 0 {
return exception("'string->sha256' expects a string, no value was given")
}
s, ok := a[0].(string)
if !ok {
return exception("'string->sha256' expected a string as its first argument, a non-string argument was given")
}
return fmt.Sprintf("%x", sha256.Sum256([]byte(s)))
},
}
/*
TODO:
Number based:
[ ] Adjust >, >=, <, <= to accept any number args and require all to be sequenced correctly for the given operator
[ ] modulo
[ ] remainder
[ ] quotient
[ ] expt
[ ] sin
[ ] cos
[ ] tan
[ ] atan
[ ] sqrt
IO Based:
[ ] file-seek
Net Based:
[ ] http-get
[ ] http-post
System Based:
[ ] eval
*/