From 69961de7def78f5b23ea2376237d05c2511e29f4 Mon Sep 17 00:00:00 2001 From: "R.I.Pienaar" Date: Tue, 5 Feb 2019 18:45:27 +0100 Subject: [PATCH] add os.stat() and file.stat() --- compiler/stdlib/os.go | 34 ++++++++++++++++++ compiler/stdlib/os_file.go | 10 ++++++ compiler/stdlib/os_test.go | 72 ++++++++++++++++++++++++++++++++++++++ docs/stdlib-os.md | 10 ++++++ 4 files changed, 126 insertions(+) create mode 100644 compiler/stdlib/os_test.go diff --git a/compiler/stdlib/os.go b/compiler/stdlib/os.go index d032b506..4157ac68 100644 --- a/compiler/stdlib/os.go +++ b/compiler/stdlib/os.go @@ -77,6 +77,40 @@ var osModule = map[string]objects.Object{ "start_process": &objects.UserFunction{Value: osStartProcess}, // start_process(name string, argv array(string), dir string, env array(string)) => imap(process)/error "exec_look_path": FuncASRSE(exec.LookPath), // exec_look_path(file) => string/error "exec": &objects.UserFunction{Value: osExec}, // exec(name, args...) => command + "stat": &objects.UserFunction{Value: osStat}, // stat(name) => imap(fileinfo)/error +} + +func osStat(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + return nil, objects.ErrWrongNumArguments + } + + fname, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + stat, err := os.Stat(fname) + if err != nil { + return wrapError(err), nil + } + + fstat := &objects.ImmutableMap{ + Value: map[string]objects.Object{ + "name": &objects.String{Value: stat.Name()}, + "mtime": &objects.Time{Value: stat.ModTime()}, + "size": &objects.Int{Value: stat.Size()}, + "mode": &objects.Int{Value: int64(stat.Mode())}, + }, + } + + if stat.IsDir() { + fstat.Value["directory"] = objects.TrueValue + } else { + fstat.Value["directory"] = objects.FalseValue + } + + return fstat, nil } func osCreate(args ...objects.Object) (objects.Object, error) { diff --git a/compiler/stdlib/os_file.go b/compiler/stdlib/os_file.go index 3b9c4b66..de91d7a0 100644 --- a/compiler/stdlib/os_file.go +++ b/compiler/stdlib/os_file.go @@ -66,6 +66,16 @@ func makeOSFile(file *os.File) *objects.ImmutableMap { return &objects.Int{Value: res}, nil }, }, + // stat() => imap(fileinfo)/error + "stat": &objects.UserFunction{ + Value: func(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 0 { + return nil, objects.ErrWrongNumArguments + } + + return osStat(&objects.String{Value: file.Name()}) + }, + }, }, } } diff --git a/compiler/stdlib/os_test.go b/compiler/stdlib/os_test.go new file mode 100644 index 00000000..9d7c73e9 --- /dev/null +++ b/compiler/stdlib/os_test.go @@ -0,0 +1,72 @@ +package stdlib_test + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/d5/tengo/objects" +) + +func TestFileStatArgs(t *testing.T) { + module(t, "os").call("stat").expectError() +} + +func TestFileStatFile(t *testing.T) { + content := []byte("the quick brown fox jumps over the lazy dog") + tf, err := ioutil.TempFile("", "test") + if err != nil { + t.Logf("could not open tempfile: %s", err) + return + } + defer os.Remove(tf.Name()) + + _, err = tf.Write(content) + if err != nil { + t.Logf("could not write temp content: %s", err) + return + } + + tf.Close() + + stat, err := os.Stat(tf.Name()) + if err != nil { + t.Logf("could not get tmp file stat: %s", err) + return + } + + module(t, "os").call("stat", tf.Name()).expect(&objects.ImmutableMap{ + Value: map[string]objects.Object{ + "name": &objects.String{Value: stat.Name()}, + "mtime": &objects.Time{Value: stat.ModTime()}, + "size": &objects.Int{Value: stat.Size()}, + "mode": &objects.Int{Value: int64(stat.Mode())}, + "directory": objects.FalseValue, + }, + }) +} + +func TestFileStatDir(t *testing.T) { + td, err := ioutil.TempDir("", "test") + if err != nil { + t.Logf("could not open tempdir: %s", err) + return + } + defer os.RemoveAll(td) + + stat, err := os.Stat(td) + if err != nil { + t.Logf("could not get tmp dir stat: %s", err) + return + } + + module(t, "os").call("stat", td).expect(&objects.ImmutableMap{ + Value: map[string]objects.Object{ + "name": &objects.String{Value: stat.Name()}, + "mtime": &objects.Time{Value: stat.ModTime()}, + "size": &objects.Int{Value: stat.Size()}, + "mode": &objects.Int{Value: int64(stat.Mode())}, + "directory": objects.TrueValue, + }, + }) +} diff --git a/docs/stdlib-os.md b/docs/stdlib-os.md index b39fb26b..d4751c18 100644 --- a/docs/stdlib-os.md +++ b/docs/stdlib-os.md @@ -68,6 +68,7 @@ os := import("os") - `remove_all(name string) => error `: removes path and any children it contains. - `rename(oldpath string, newpath string) => error `: renames (moves) oldpath to newpath. - `setenv(key string, value string) => error `: sets the value of the environment variable named by the key. +- `stat(filename string) => FileInfo/error`: returns a file info structure describing the file - `symlink(oldname string newname string) => error `: creates newname as a symbolic link to oldname. - `temp_dir() => string `: returns the default directory to use for temporary files. - `truncate(name string, size int) => error `: changes the size of the named file. @@ -98,6 +99,7 @@ file.close() - `write(bytes) => int/error`: writes len(b) bytes to the File. - `write_string(string) => int/error`: is like 'write', but writes the contents of string s rather than a slice of bytes. - `read(bytes) => int/error`: reads up to len(b) bytes from the File. +- `stat() => FileInfo/error`: returns a file info structure describing the file - `chmod(mode int) => error`: changes the mode of the file to mode. - `seek(offset int, whence int) => int/error`: sets the offset for the next Read or Write on file to offset, interpreted according to whence: 0 means relative to the origin of the file, 1 means relative to the current offset, and 2 means relative to the end. @@ -131,6 +133,14 @@ cmd := exec.command("echo", ["foo", "bar"]) output := cmd.output() ``` +## FileInfo + +- `name`: name of the file the info describes +- `mtime`: time the file was last modified +- `size`: file size in bytes +- `mode`: file permissions as in int, comparable to octal permissions +- `directory`: boolean indicating if the file is a directory + ## Command - `combined_output() => bytes/error`: runs the command and returns its combined standard output and standard error.