Explorar o código

pkg/mods/file: file:is-tty interprets numbers as port numbers instead of FDs.

Qi Xiao hai 1 ano
pai
achega
eed2f7dabc

+ 11 - 0
pkg/eval/frame.go

@@ -112,6 +112,17 @@ func (fm *Frame) ErrorFile() *os.File {
 	return fm.ports[2].File
 }
 
+// Port returns port i. If the port doesn't exist, it returns nil
+//
+// This is a low-level construct that shouldn't be used for writing output; for
+// that purpose, use [(*Frame).ValueOutput] and [(*Frame).ByteOutput] instead.
+func (fm *Frame) Port(i int) *Port {
+	if i >= len(fm.ports) {
+		return nil
+	}
+	return fm.ports[i]
+}
+
 // IterateInputs calls the passed function for each input element.
 func (fm *Frame) IterateInputs(f func(any)) {
 	var wg sync.WaitGroup

+ 5 - 1
pkg/mods/file/file.d.elv

@@ -7,7 +7,7 @@
 # Outputs whether `$file` is a terminal device.
 #
 # The `$file` can be a file object or a number. If it's a number, it's
-# interpreted as a numerical file descriptor.
+# interpreted as the number of an [IO port](language.html#io-ports).
 #
 # ```elvish-transcript
 # ~> var f = (file:open /dev/tty)
@@ -31,6 +31,10 @@
 # ▶ $true
 # ~> file:is-tty 2
 # ▶ $true
+# ~> file:is-tty 0 < /dev/null
+# ▶ $false
+# ~> file:is-tty 0 < /dev/tty
+# ▶ $true
 # ```
 
 #elvdoc:fn open

+ 7 - 2
pkg/mods/file/file.go

@@ -26,20 +26,25 @@ func isTTY(fm *eval.Frame, file any) (bool, error) {
 	case *os.File:
 		return sys.IsATTY(file.Fd()), nil
 	case int:
-		return sys.IsATTY(uintptr(file)), nil
+		return isTTYPort(fm, file), nil
 	case string:
 		var fd int
 		if err := vals.ScanToGo(file, &fd); err != nil {
 			return false, errs.BadValue{What: "argument to file:is-tty",
 				Valid: "file value or numerical FD", Actual: parse.Quote(file)}
 		}
-		return sys.IsATTY(uintptr(fd)), nil
+		return isTTYPort(fm, fd), nil
 	default:
 		return false, errs.BadValue{What: "argument to file:is-tty",
 			Valid: "file value or numerical FD", Actual: vals.ToString(file)}
 	}
 }
 
+func isTTYPort(fm *eval.Frame, portNum int) bool {
+	p := fm.Port(portNum)
+	return p != nil && sys.IsATTY(p.File.Fd())
+}
+
 func open(name string) (vals.File, error) {
 	return os.Open(name)
 }

+ 4 - 4
pkg/mods/file/file_test.go

@@ -6,7 +6,7 @@ import (
 
 	"src.elv.sh/pkg/eval"
 	"src.elv.sh/pkg/eval/errs"
-	. "src.elv.sh/pkg/eval/evaltest"
+	"src.elv.sh/pkg/eval/evaltest"
 	"src.elv.sh/pkg/testutil"
 )
 
@@ -19,7 +19,7 @@ func TestFile(t *testing.T) {
 	}
 	testutil.InTempDir(t)
 
-	TestWithSetup(t, setup,
+	evaltest.TestWithSetup(t, setup,
 		That(`
 			echo haha > out3
 			var f = (file:open out3)
@@ -40,13 +40,13 @@ func TestFile(t *testing.T) {
 			echo Legolas > $p
 			file:close $p[r]
 			slurp < $p
-		`).Throws(ErrorWithType(&os.PathError{})),
+		`).Throws(evaltest.ErrorWithType(&os.PathError{})),
 
 		// Verify that input redirection from a closed pipe throws an exception. That exception is a
 		// Go stdlib error whose stringified form looks something like "read |0: file already
 		// closed".
 		That(`var p = (file:pipe)`, `echo Legolas > $p`, `file:close $p[r]`,
-			`slurp < $p`).Throws(ErrorWithType(&os.PathError{})),
+			`slurp < $p`).Throws(evaltest.ErrorWithType(&os.PathError{})),
 
 		// Side effect checked below
 		That("echo > file100", "file:truncate file100 100").DoesNothing(),

+ 23 - 0
pkg/mods/file/file_unix_test.go

@@ -0,0 +1,23 @@
+//go:build !windows && !plan9 && !js
+
+package file
+
+import (
+	"testing"
+
+	"src.elv.sh/pkg/eval"
+	"src.elv.sh/pkg/eval/evaltest"
+)
+
+func TestIsTTY(t *testing.T) {
+	setup := func(ev *eval.Evaler) {
+		ev.ExtendGlobal(eval.BuildNs().AddNs("file", Ns))
+	}
+
+	evaltest.TestWithSetup(t, setup,
+		That("file:is-tty 0 < /dev/null").Puts(false),
+		That("file:is-tty (num 0) < /dev/null").Puts(false),
+		That("file:is-tty 0 < /dev/tty").Puts(true),
+		That("file:is-tty (num 0) < /dev/tty").Puts(true),
+	)
+}

+ 5 - 0
pkg/mods/file/testutil_test.go

@@ -0,0 +1,5 @@
+package file
+
+import "src.elv.sh/pkg/eval/evaltest"
+
+var That = evaltest.That