Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions term.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,20 @@ func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
return stdStreams()
}

// GetFdInfo returns the file descriptor for an os.File and indicates whether the file represents a terminal.
func GetFdInfo(in any) (fd uintptr, isTerminal bool) {
// terminalInfo provides the file descriptor and terminal status of a stream.
// It matches the relevant methods of github.com/docker/cli/cli/streams.Out.
type terminalInfo interface {
FD() uintptr
IsTerminal() bool
}

// GetFdInfo returns the file descriptor and terminal status of the given stream.
// It supports *os.File and streams that provide their own file descriptor and
// terminal status.
func GetFdInfo(in any) (uintptr, bool) {
if t, ok := in.(terminalInfo); ok {
return t.FD(), t.IsTerminal()
}
return getFdInfo(in)
}

Expand Down
67 changes: 54 additions & 13 deletions term_test.go → term_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,22 +83,63 @@ func TestSetWinsize(t *testing.T) {
}
}

type testTerminalInfo struct {
fd uintptr
isTerminal bool
}

func (t testTerminalInfo) FD() uintptr {
return t.fd
}

func (t testTerminalInfo) IsTerminal() bool {
return t.isTerminal
}

func TestGetFdInfo(t *testing.T) {
tty := newTTYForTest(t)
inFd, isTerm := GetFdInfo(tty)
if inFd != tty.Fd() {
t.Errorf("expected: %d, got: %d", tty.Fd(), inFd)
}
if !isTerm {
t.Error("expected file-descriptor to be a terminal")
}
tmpFile := newTempFile(t)
inFd, isTerm = GetFdInfo(tmpFile)
if inFd != tmpFile.Fd() {
t.Errorf("expected: %d, got: %d", tty.Fd(), inFd)
}
if isTerm {
t.Error("expected file-descriptor to not be a terminal")

for _, tc := range []struct {
name string
stream any
wantFD uintptr
wantIsTerm bool
}{
{
name: "tty",
stream: tty,
wantFD: tty.Fd(),
wantIsTerm: true,
},
{
name: "file",
stream: tmpFile,
wantFD: tmpFile.Fd(),
wantIsTerm: false,
},
// Stream wrappers, such as docker/cli's streams.Out, provide their
// own terminal status. Use a TTY descriptor to verify that the
// reported status takes precedence over OS-level detection.
{
name: "stream terminal",
stream: testTerminalInfo{fd: tty.Fd(), isTerminal: true},
wantFD: tty.Fd(),
wantIsTerm: true,
},
{
name: "stream non-terminal",
stream: testTerminalInfo{fd: tty.Fd(), isTerminal: false},
wantFD: tty.Fd(),
wantIsTerm: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
fd, isTerm := GetFdInfo(tc.stream)
if fd != tc.wantFD || isTerm != tc.wantIsTerm {
t.Errorf("GetFdInfo() = (%d, %t), want (%d, %t)", fd, isTerm, tc.wantFD, tc.wantIsTerm)
}
})
}
}

Expand Down
46 changes: 46 additions & 0 deletions term_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package term_test

import (
"testing"

"github.com/moby/term"
)

type testTerminalInfo struct {
fd uintptr
isTerminal bool
}

func (t testTerminalInfo) FD() uintptr {
return t.fd
}

func (t testTerminalInfo) IsTerminal() bool {
return t.isTerminal
}

func TestGetFdInfoStream(t *testing.T) {
for _, tc := range []struct {
name string
isTerminal bool
}{
{name: "terminal", isTerminal: true},
{name: "non-terminal", isTerminal: false},
} {
t.Run(tc.name, func(t *testing.T) {
// Stream wrappers, such as docker/cli's streams.Out, provide
// their own terminal status, regardless of the underlying
// descriptor.
stream := testTerminalInfo{
fd: 123,
isTerminal: tc.isTerminal,
}

fd, isTerm := term.GetFdInfo(stream)
if fd != stream.FD() || isTerm != stream.IsTerminal() {
t.Errorf("GetFdInfo() = (%d, %t), want (%d, %t)",
fd, isTerm, stream.FD(), stream.IsTerminal())
}
})
}
}
Loading