diff --git a/builtins/builtins.go b/builtins/builtins.go index cbffc03..265bda3 100644 --- a/builtins/builtins.go +++ b/builtins/builtins.go @@ -6,6 +6,7 @@ package builtins import ( + "bufio" "bytes" "flag" "fmt" @@ -19,6 +20,7 @@ import ( "sort" "strconv" "strings" + "syscall" "time" "github.com/skx/yal/env" @@ -111,6 +113,9 @@ func PopulateEnvironment(env *env.Environment) { env.Set("error", &primitive.Procedure{F: errorFn, Help: helpMap["error"]}) env.Set("exists?", &primitive.Procedure{F: existsFn, Help: helpMap["exists?"]}) env.Set("file?", &primitive.Procedure{F: fileFn, Help: helpMap["file?"]}) + env.Set("file:lines", &primitive.Procedure{F: fileLinesFn, Help: helpMap["file:lines"]}) + env.Set("file:read", &primitive.Procedure{F: fileReadFn, Help: helpMap["file:read"]}) + env.Set("file:stat", &primitive.Procedure{F: fileStatFn, Help: helpMap["file:stat"]}) env.Set("gensym", &primitive.Procedure{F: gensymFn, Help: helpMap["gensym"]}) env.Set("get", &primitive.Procedure{F: getFn, Help: helpMap["get"]}) env.Set("getenv", &primitive.Procedure{F: getenvFn, Help: helpMap["getenv"]}) @@ -128,7 +133,6 @@ func PopulateEnvironment(env *env.Environment) { env.Set("print", &primitive.Procedure{F: printFn, Help: helpMap["print"]}) env.Set("set", &primitive.Procedure{F: setFn, Help: helpMap["set"]}) env.Set("shell", &primitive.Procedure{F: shellFn, Help: helpMap["shell"]}) - env.Set("slurp", &primitive.Procedure{F: slurpFn, Help: helpMap["slurp"]}) env.Set("sort", &primitive.Procedure{F: sortFn, Help: helpMap["sort"]}) env.Set("split", &primitive.Procedure{F: splitFn, Help: helpMap["split"]}) env.Set("sprintf", &primitive.Procedure{F: sprintfFn, Help: helpMap["sprintf"]}) @@ -485,6 +489,104 @@ func fileFn(env *env.Environment, args []primitive.Primitive) primitive.Primitiv return primitive.Bool(false) } +// fileLinesFn implements (file:lines) +func fileLinesFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive { + // We only need a single argument + if len(args) != 1 { + return primitive.Error("invalid argument count") + } + + // Which is a string + fName, ok := args[0].(primitive.String) + if !ok { + return primitive.Error("argument not a string") + } + + // Return value, + var res primitive.List + + // Open the file + file, err := os.Open(fName.ToString()) + if err != nil { + return primitive.Error(fmt.Sprintf("failed to open %s:%s", fName.ToString(), err)) + } + defer file.Close() + + // Read each line, and append to our list. + scanner := bufio.NewScanner(file) + for scanner.Scan() { + res = append(res, primitive.String(scanner.Text())) + } + + // All done. + return res +} + +// fileReadFn implements (file:read) +func fileReadFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive { + // We only need a single argument + if len(args) != 1 { + return primitive.Error("invalid argument count") + } + + // Which is a string + fName, ok := args[0].(primitive.String) + if !ok { + return primitive.Error("argument not a string") + } + + data, err := os.ReadFile(fName.ToString()) + if err != nil { + return primitive.Error(fmt.Sprintf("error reading %s %s", fName.ToString(), err)) + } + return primitive.String(string(data)) +} + +// fileStatFn implements (file:lines) +// +// Return value is (NAME SIZE UID GID MODE) +func fileStatFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive { + // We only need a single argument + if len(args) != 1 { + return primitive.Error("invalid argument count") + } + + // Which is a string + fName, ok := args[0].(primitive.String) + if !ok { + return primitive.Error("argument not a string") + } + + // Stat the entry + info, err := os.Stat(fName.ToString()) + + if err != nil { + return primitive.Nil{} + } + + var UID int + var GID int + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + UID = int(stat.Uid) + GID = int(stat.Gid) + } else { + // we are not in linux, this won't work anyway in windows, + // but maybe you want to log warnings + UID = os.Getuid() + GID = os.Getgid() + } + + var res primitive.List + + res = append(res, primitive.String(info.Name())) + res = append(res, primitive.Number(info.Size())) + res = append(res, primitive.Number(UID)) + res = append(res, primitive.Number(GID)) + res = append(res, primitive.String(info.Mode().String())) + + return res +} + // gensymFn is the implementation of (gensym ..) func gensymFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive { // symbol characters @@ -944,7 +1046,6 @@ func setFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive } // shellFn runs a command via the shell -// slurpFn returns the contents of the specified file func shellFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive { // We need one argument @@ -987,20 +1088,6 @@ func shellFn(env *env.Environment, args []primitive.Primitive) primitive.Primiti return ret } -// slurpFn returns the contents of the specified file -func slurpFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive { - if len(args) != 1 { - return primitive.Error("wrong number of arguments") - } - - fName := args[0].ToString() - data, err := os.ReadFile(fName) - if err != nil { - return primitive.Error(fmt.Sprintf("error reading %s %s", fName, err)) - } - return primitive.String(string(data)) -} - // sortFn implements (sort) func sortFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive { // If we have only a single argument diff --git a/builtins/builtins_test.go b/builtins/builtins_test.go index bcdb125..6896af2 100644 --- a/builtins/builtins_test.go +++ b/builtins/builtins_test.go @@ -969,6 +969,49 @@ func TestFile(t *testing.T) { } +// TestFileRead tests file:read +func TestFileRead(t *testing.T) { + + // calling with no argument + out := fileReadFn(ENV, []primitive.Primitive{}) + + // Will lead to an error + _, ok := out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + + // Call with a file that doesn't exist + out = fileReadFn(ENV, []primitive.Primitive{ + primitive.String("path/not/found")}) + + _, ok = out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + + // Create a temporary file, and read the contents + tmp, _ := os.CreateTemp("", "yal") + err := os.WriteFile(tmp.Name(), []byte("I like cake"), 0777) + if err != nil { + t.Fatalf("failed to write to file") + } + defer os.Remove(tmp.Name()) + + str := fileReadFn(ENV, []primitive.Primitive{ + primitive.String(tmp.Name())}) + + // Will lead to an error + txt, ok2 := str.(primitive.String) + if !ok2 { + t.Fatalf("expected string, got %v", out) + } + + if txt.ToString() != "I like cake" { + t.Fatalf("re-reading the temporary file gave bogus contents") + } +} + // TestGenSym tests gensym func TestGenSym(t *testing.T) { @@ -2087,50 +2130,6 @@ func TestShell(t *testing.T) { } } -// TestSlurp tests slurp -func TestSlurp(t *testing.T) { - - // calling with no argument - out := slurpFn(ENV, []primitive.Primitive{}) - - // Will lead to an error - _, ok := out.(primitive.Error) - if !ok { - t.Fatalf("expected error, got %v", out) - } - - // Call with a file that doesn't exist - out = slurpFn(ENV, []primitive.Primitive{ - primitive.String("path/not/found")}) - - _, ok = out.(primitive.Error) - if !ok { - t.Fatalf("expected error, got %v", out) - } - - // Create a temporary file, and read the contents - tmp, _ := os.CreateTemp("", "yal") - err := os.WriteFile(tmp.Name(), []byte("I like cake"), 0777) - if err != nil { - t.Fatalf("failed to write to file") - } - defer os.Remove(tmp.Name()) - - str := slurpFn(ENV, []primitive.Primitive{ - primitive.String(tmp.Name())}) - - // Will lead to an error - txt, ok2 := str.(primitive.String) - if !ok2 { - t.Fatalf("expected string, got %v", out) - } - - if txt.ToString() != "I like cake" { - t.Fatalf("re-reading the temporary file gave bogus contents") - } - -} - func TestSort(t *testing.T) { // No arguments diff --git a/builtins/help.txt b/builtins/help.txt index 27eed60..caa3c92 100644 --- a/builtins/help.txt +++ b/builtins/help.txt @@ -76,6 +76,30 @@ More specifically something is regarded as a file if it is NOT a directory. See also: directory? exists? Example: (print (file? "/dev/null")) %% +file:lines + +file:lines returns the contents of the given file, as a list of lines + +See also: file:read +%% +file:read + +file:read returns the contents of the given file, as a string. + +See also: file:lines +%% + + +file:stat + +file:stat returns a list containing details of the given file/directory, +or an error if it couldn't be found. + +The return value is (NAME SIZE UID GID MODE). + +See also: file:stat:gid file:stat:mode file:stat:size file:stat:uid +Example: (print (file:stat "/etc/passwd")) +%% gensym gensym returns a symbol which is guaranteed to be unique. It is primarily diff --git a/stdlib/stdlib.lisp b/stdlib/stdlib.lisp index 261d513..7f89acc 100644 --- a/stdlib/stdlib.lisp +++ b/stdlib/stdlib.lisp @@ -415,3 +415,36 @@ "Return everything but the last element from the specified list." (take (dec (length l)) l))) + +;; Wrappers for our file functions +(set! file:stat:size (fn* (path) + "Return the size of the given file, return -1 on error." + (let* (info (file:stat path)) + (cond + (nil? info) -1 + true (nth info 1))))) + +(set! file:stat:uid (fn* (path) + "Return the UID of the given file owner, return '' on error." + (let* (info (file:stat path)) + (cond + (nil? info) "" + true (nth info 2))))) + + +(set! file:stat:gid (fn* (path) + "Return the GID of the given file owner, return '' on error." + (let* (info (file:stat path)) + (cond + (nil? info) "" + true (nth info 3))))) + +(set! file:stat:mode (fn* (path) + "Return the mode of the given file, return '' on error." + (let* (info (file:stat path)) + (cond + (nil? info) "" + true (nth info 4))))) + +; Slurp used to be a primitive for reading file contents +(set! slurp file:read)