diff --git a/interp/actime/os_darwin.go b/interp/actime/os_darwin.go new file mode 100644 index 00000000..02f7e5a6 --- /dev/null +++ b/interp/actime/os_darwin.go @@ -0,0 +1,15 @@ +package actime + +import ( + "io/fs" + "syscall" + "time" +) + +func GetAtime(info fs.FileInfo) time.Time { + stat, ok := info.Sys().(*syscall.Stat_t) + if !ok { + return info.ModTime() + } + return time.Unix(stat.Atimespec.Sec, stat.Atimespec.Nsec) +} diff --git a/interp/actime/os_linux.go b/interp/actime/os_linux.go new file mode 100644 index 00000000..f2c61556 --- /dev/null +++ b/interp/actime/os_linux.go @@ -0,0 +1,16 @@ +package actime + +import ( + "io/fs" + "syscall" + "time" +) + +func GetAtime(info fs.FileInfo) time.Time { + stat, ok := info.Sys().(*syscall.Stat_t) + + if !ok { + return info.ModTime() + } + return time.Unix(stat.Atim.Sec, stat.Atim.Nsec) +} diff --git a/interp/actime/os_other.go b/interp/actime/os_other.go new file mode 100644 index 00000000..83ac7063 --- /dev/null +++ b/interp/actime/os_other.go @@ -0,0 +1,10 @@ +package actime + +import ( + "io/fs" + "time" +) + +func GetAtime(info fs.FileInfo) time.Time { + return info.ModTime() +} diff --git a/interp/actime/os_windows.go b/interp/actime/os_windows.go new file mode 100644 index 00000000..7afb76b8 --- /dev/null +++ b/interp/actime/os_windows.go @@ -0,0 +1,15 @@ +package actime + +import ( + "io/fs" + "syscall" + "time" +) + +func GetAtime(info fs.FileInfo) time.Time { + stat, ok := info.Sys().(*syscall.Win32FileAttributeData) + if !ok { + return info.ModTime() + } + return time.Unix(0, stat.LastAccessTime.Nanoseconds()) +} diff --git a/interp/interp_test.go b/interp/interp_test.go index 8b406231..689e2e3d 100644 --- a/interp/interp_test.go +++ b/interp/interp_test.go @@ -1960,9 +1960,20 @@ var runTests = []runTest{ "mkdir d; [ -r d ] && echo r; [ -w d ] && echo w; [ -x d ] && echo x", "r\nw\nx\n", }, + // -N: true if file exists and was modified since last read { - "test -N a", - "unsupported unary test op: -N\nexit status 1 #IGNORE", + ">a; cat a; sleep 0.01; echo 'Hello' >> a; test -N a && echo yes", + "yes\n", + }, + // -N: false for non-existent file + { + "test -N nonexistent", + "exit status 1", + }, + // -N: false if file was read after last modification + { + ">a; sleep 0.01; cat a; test -N a; echo $?", + "1\n", }, { "test -? a", diff --git a/interp/test.go b/interp/test.go index 23101cb9..d1af1ec4 100644 --- a/interp/test.go +++ b/interp/test.go @@ -12,6 +12,7 @@ import ( "golang.org/x/term" "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/interp/actime" "mvdan.cc/sh/v3/syntax" ) @@ -166,8 +167,11 @@ func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) case syntax.TsGIDSet: return r.statMode(ctx, x, os.ModeSetgid) case syntax.TsModif: - r.errf("unsupported unary test op: %v\n", op) - return false + info, err := r.stat(ctx, x) + if err != nil { + return false + } + return info.ModTime().After(actime.GetAtime(info)) case syntax.TsRead: return r.access(ctx, r.absPath(x), access_R_OK) == nil case syntax.TsWrite: