From 989ee71140902b89f18f4f361e38806c401c0a53 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 5 Feb 2020 19:45:52 +0900 Subject: [PATCH 1/9] Extend `IO#tty?` for cygwin/msys terminals --- ext/io/console/console.c | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/ext/io/console/console.c b/ext/io/console/console.c index bedfb27..654adcd 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -1914,6 +1914,55 @@ console_ttyname(VALUE io) # define console_ttyname rb_f_notimplement #endif +#if defined _WIN32 || defined __CYGWIN__ +static VALUE +console_wintty_p(int argc, VALUE *argv, VALUE io) +{ + HANDLE h; + union { + FILE_NAME_INFO info; + WCHAR rest[MAX_PATH]; + } buffer; + WCHAR *const name = buffer.info.FileName; + const WCHAR *ptr; + DWORD len; + static const WCHAR msys_prefix[] = L"\\msys-"; + static const WCHAR cygwin_prefix[] = L"\\cygwin-"; + VALUE ret; + enum {mode_normal, mode_cygwin, mode_msys} mode = mode_normal; + + if (rb_check_arity(argc, 0, 1)) { + VALUE m = argv[0]; + if (!NIL_P(m)) { + Check_Type(m, T_SYMBOL); + if (m == ID2SYM(rb_intern("cygwin"))) { + mode = mode_cygwin; + } + else if (m == ID2SYM(rb_intern("msys"))) { + mode = mode_msys; + } + } + } + ret = rb_call_super(0, 0); + if (mode == mode_normal || RTEST(ret)) return ret; + h = (HANDLE)rb_w32_get_osfhandle(GetReadFD(io)); + if (GetFileType(h) != FILE_TYPE_PIPE) return Qfalse; + if (!GetFileInformationByHandleEx(h, FileNameInfo, &buffer, sizeof(buffer))) return Qfalse; + len = buffer.info.FileNameLength / sizeof(WCHAR); + name[len] = L'\0'; + if (memcmp(name, cygwin_prefix, sizeof(cygwin_prefix)-sizeof(WCHAR)) == 0) { + ptr = name + sizeof(cygwin_prefix)/sizeof(WCHAR) - 1; + } + else if (mode == mode_msys && memcmp(name, msys_prefix, sizeof(msys_prefix)-sizeof(WCHAR)) == 0) { + ptr = name + sizeof(msys_prefix)/sizeof(WCHAR) - 1; + } + else { + return Qfalse; + } + return wcsstr(ptr, L"-pty") ? Qtrue : Qfalse; +} +#endif + /* * IO console methods */ @@ -1982,6 +2031,14 @@ InitVM_console(void) rb_define_method(rb_cIO, "check_winsize_changed", console_check_winsize_changed, 0); rb_define_method(rb_cIO, "getpass", console_getpass, -1); rb_define_method(rb_cIO, "ttyname", console_ttyname, 0); +#if defined _WIN32 || defined __CYGWIN__ + { + VALUE wintty = rb_module_new(); + rb_define_method(wintty, "tty?", console_wintty_p, -1); + rb_define_method(wintty, "isatty", console_wintty_p, -1); + rb_prepend_module(rb_cIO, wintty); + } +#endif rb_define_singleton_method(rb_cIO, "console", console_dev, -1); { /* :nodoc: */ From 4cfaca6ad6c35fc37fb747bebd3c5779efe3ff3c Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 7 Dec 2024 09:18:49 +0900 Subject: [PATCH 2/9] Rename the internal term "wintty" as "platform" --- ext/io/console/console.c | 84 +++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/ext/io/console/console.c b/ext/io/console/console.c index 654adcd..7b4241c 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -1914,54 +1914,68 @@ console_ttyname(VALUE io) # define console_ttyname rb_f_notimplement #endif +typedef enum { + platform_none, #if defined _WIN32 || defined __CYGWIN__ + platform_cygwin, + platform_msys, +#endif + platform_max +} console_platform_t; + static VALUE -console_wintty_p(int argc, VALUE *argv, VALUE io) +console_platform_tty_p(int argc, VALUE *argv, VALUE io) { - HANDLE h; - union { - FILE_NAME_INFO info; - WCHAR rest[MAX_PATH]; - } buffer; - WCHAR *const name = buffer.info.FileName; - const WCHAR *ptr; - DWORD len; - static const WCHAR msys_prefix[] = L"\\msys-"; - static const WCHAR cygwin_prefix[] = L"\\cygwin-"; VALUE ret; - enum {mode_normal, mode_cygwin, mode_msys} mode = mode_normal; + console_platform_t mode = platform_none; if (rb_check_arity(argc, 0, 1)) { VALUE m = argv[0]; if (!NIL_P(m)) { Check_Type(m, T_SYMBOL); +#if defined _WIN32 || defined __CYGWIN__ if (m == ID2SYM(rb_intern("cygwin"))) { - mode = mode_cygwin; + mode = platform_cygwin; } else if (m == ID2SYM(rb_intern("msys"))) { - mode = mode_msys; + mode = platform_msys; } +#endif } } ret = rb_call_super(0, 0); - if (mode == mode_normal || RTEST(ret)) return ret; - h = (HANDLE)rb_w32_get_osfhandle(GetReadFD(io)); - if (GetFileType(h) != FILE_TYPE_PIPE) return Qfalse; - if (!GetFileInformationByHandleEx(h, FileNameInfo, &buffer, sizeof(buffer))) return Qfalse; - len = buffer.info.FileNameLength / sizeof(WCHAR); - name[len] = L'\0'; - if (memcmp(name, cygwin_prefix, sizeof(cygwin_prefix)-sizeof(WCHAR)) == 0) { - ptr = name + sizeof(cygwin_prefix)/sizeof(WCHAR) - 1; - } - else if (mode == mode_msys && memcmp(name, msys_prefix, sizeof(msys_prefix)-sizeof(WCHAR)) == 0) { - ptr = name + sizeof(msys_prefix)/sizeof(WCHAR) - 1; - } - else { - return Qfalse; + if (mode != platform_none && !RTEST(ret)) { +#if defined _WIN32 || defined __CYGWIN__ + HANDLE h; + union { + FILE_NAME_INFO info; + WCHAR rest[MAX_PATH]; + } buffer; + WCHAR *const name = buffer.info.FileName; + const WCHAR *ptr; + DWORD len; + static const WCHAR msys_prefix[] = L"\\msys-"; + static const WCHAR cygwin_prefix[] = L"\\cygwin-"; + + h = (HANDLE)rb_w32_get_osfhandle(GetReadFD(io)); + if (GetFileType(h) != FILE_TYPE_PIPE) return Qfalse; + if (!GetFileInformationByHandleEx(h, FileNameInfo, &buffer, sizeof(buffer))) return Qfalse; + len = buffer.info.FileNameLength / sizeof(WCHAR); + name[len] = L'\0'; + if (memcmp(name, cygwin_prefix, sizeof(cygwin_prefix)-sizeof(WCHAR)) == 0) { + ptr = name + sizeof(cygwin_prefix)/sizeof(WCHAR) - 1; + } + else if (mode == platform_msys && memcmp(name, msys_prefix, sizeof(msys_prefix)-sizeof(WCHAR)) == 0) { + ptr = name + sizeof(msys_prefix)/sizeof(WCHAR) - 1; + } + else { + return Qfalse; + } + if (wcsstr(ptr, L"-pty")) ret = Qtrue; +#endif } - return wcsstr(ptr, L"-pty") ? Qtrue : Qfalse; + return ret; } -#endif /* * IO console methods @@ -2031,14 +2045,12 @@ InitVM_console(void) rb_define_method(rb_cIO, "check_winsize_changed", console_check_winsize_changed, 0); rb_define_method(rb_cIO, "getpass", console_getpass, -1); rb_define_method(rb_cIO, "ttyname", console_ttyname, 0); -#if defined _WIN32 || defined __CYGWIN__ { - VALUE wintty = rb_module_new(); - rb_define_method(wintty, "tty?", console_wintty_p, -1); - rb_define_method(wintty, "isatty", console_wintty_p, -1); - rb_prepend_module(rb_cIO, wintty); + VALUE platform = rb_define_module_under(rb_cIO, "platform_tty"); + rb_define_method(platform, "tty?", console_platform_tty_p, -1); + rb_define_method(platform, "isatty", console_platform_tty_p, -1); + rb_prepend_module(rb_cIO, platform); } -#endif rb_define_singleton_method(rb_cIO, "console", console_dev, -1); { /* :nodoc: */ From 28cdf86e815999e1fbd9e53dc1ea6794f70a59fc Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 7 Dec 2024 16:23:27 +0900 Subject: [PATCH 3/9] Win32: ttyname will not be used --- ext/io/console/extconf.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/io/console/extconf.rb b/ext/io/console/extconf.rb index 6f11793..1a0f193 100644 --- a/ext/io/console/extconf.rb +++ b/ext/io/console/extconf.rb @@ -23,6 +23,7 @@ hdr = nil case when macro_defined?("_WIN32", "") + win32 = true # rb_w32_map_errno: 1.8.7 vk_header = File.exist?("#$srcdir/win32_vk.list") ? "chksum" : "inc" vk_header = "#{'{$(srcdir)}' if $nmake == ?m}win32_vk.#{vk_header}" @@ -47,7 +48,7 @@ elsif have_func("rb_scheduler_timeout") # Ruby 3.0 (internal) have_func("rb_io_wait") # Ruby 3.0 end - have_func("ttyname_r") or have_func("ttyname") + win32 or have_func("ttyname_r") or have_func("ttyname") create_makefile("io/console") {|conf| conf << "\n""VK_HEADER = #{vk_header}\n" } From 27754105c7f410ccbb23b3698f772c1faa566b4e Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 4 Feb 2025 21:10:32 +0900 Subject: [PATCH 4/9] Do nothing on old TruffleRuby --- ext/io/console/console.c | 8 ++++++++ ext/io/console/extconf.rb | 1 + 2 files changed, 9 insertions(+) diff --git a/ext/io/console/console.c b/ext/io/console/console.c index 7b4241c..f6530a0 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -1916,9 +1916,11 @@ console_ttyname(VALUE io) typedef enum { platform_none, +#ifdef HAVE_RB_PREPEND_MODULE #if defined _WIN32 || defined __CYGWIN__ platform_cygwin, platform_msys, +#endif #endif platform_max } console_platform_t; @@ -1933,6 +1935,7 @@ console_platform_tty_p(int argc, VALUE *argv, VALUE io) VALUE m = argv[0]; if (!NIL_P(m)) { Check_Type(m, T_SYMBOL); +#ifdef HAVE_RB_PREPEND_MODULE #if defined _WIN32 || defined __CYGWIN__ if (m == ID2SYM(rb_intern("cygwin"))) { mode = platform_cygwin; @@ -1940,11 +1943,13 @@ console_platform_tty_p(int argc, VALUE *argv, VALUE io) else if (m == ID2SYM(rb_intern("msys"))) { mode = platform_msys; } +#endif #endif } } ret = rb_call_super(0, 0); if (mode != platform_none && !RTEST(ret)) { +#ifdef HAVE_RB_PREPEND_MODULE #if defined _WIN32 || defined __CYGWIN__ HANDLE h; union { @@ -1972,6 +1977,7 @@ console_platform_tty_p(int argc, VALUE *argv, VALUE io) return Qfalse; } if (wcsstr(ptr, L"-pty")) ret = Qtrue; +#endif #endif } return ret; @@ -2045,12 +2051,14 @@ InitVM_console(void) rb_define_method(rb_cIO, "check_winsize_changed", console_check_winsize_changed, 0); rb_define_method(rb_cIO, "getpass", console_getpass, -1); rb_define_method(rb_cIO, "ttyname", console_ttyname, 0); +#ifdef HAVE_RB_PREPEND_MODULE { VALUE platform = rb_define_module_under(rb_cIO, "platform_tty"); rb_define_method(platform, "tty?", console_platform_tty_p, -1); rb_define_method(platform, "isatty", console_platform_tty_p, -1); rb_prepend_module(rb_cIO, platform); } +#endif rb_define_singleton_method(rb_cIO, "console", console_dev, -1); { /* :nodoc: */ diff --git a/ext/io/console/extconf.rb b/ext/io/console/extconf.rb index 1a0f193..fdeede5 100644 --- a/ext/io/console/extconf.rb +++ b/ext/io/console/extconf.rb @@ -49,6 +49,7 @@ have_func("rb_io_wait") # Ruby 3.0 end win32 or have_func("ttyname_r") or have_func("ttyname") + have_func("rb_prepend_module") # exclude TruffleRuby create_makefile("io/console") {|conf| conf << "\n""VK_HEADER = #{vk_header}\n" } From 47dcadc644ba8bcecfb9152f53f8e82dcf7cf84d Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 7 Feb 2025 10:52:51 +0900 Subject: [PATCH 5/9] Support `tty?(:any)` on any platform --- ext/io/console/console.c | 29 +++++--- test/io/console/test_io_console.rb | 112 +++++++++++++++-------------- 2 files changed, 78 insertions(+), 63 deletions(-) diff --git a/ext/io/console/console.c b/ext/io/console/console.c index f6530a0..a229af2 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -1917,6 +1917,7 @@ console_ttyname(VALUE io) typedef enum { platform_none, #ifdef HAVE_RB_PREPEND_MODULE + platform_any, #if defined _WIN32 || defined __CYGWIN__ platform_cygwin, platform_msys, @@ -1925,6 +1926,7 @@ typedef enum { platform_max } console_platform_t; +#ifdef HAVE_RB_PREPEND_MODULE static VALUE console_platform_tty_p(int argc, VALUE *argv, VALUE io) { @@ -1935,21 +1937,24 @@ console_platform_tty_p(int argc, VALUE *argv, VALUE io) VALUE m = argv[0]; if (!NIL_P(m)) { Check_Type(m, T_SYMBOL); -#ifdef HAVE_RB_PREPEND_MODULE + if (m == ID2SYM(rb_intern("any"))) { + mode = platform_any; + } #if defined _WIN32 || defined __CYGWIN__ - if (m == ID2SYM(rb_intern("cygwin"))) { + else if (m == ID2SYM(rb_intern("cygwin"))) { mode = platform_cygwin; } else if (m == ID2SYM(rb_intern("msys"))) { mode = platform_msys; } #endif -#endif + else { + rb_raise(rb_eArgError, "unknown tty type: %+" PRIsVALUE, m); + } } } ret = rb_call_super(0, 0); if (mode != platform_none && !RTEST(ret)) { -#ifdef HAVE_RB_PREPEND_MODULE #if defined _WIN32 || defined __CYGWIN__ HANDLE h; union { @@ -1959,29 +1964,31 @@ console_platform_tty_p(int argc, VALUE *argv, VALUE io) WCHAR *const name = buffer.info.FileName; const WCHAR *ptr; DWORD len; - static const WCHAR msys_prefix[] = L"\\msys-"; - static const WCHAR cygwin_prefix[] = L"\\cygwin-"; h = (HANDLE)rb_w32_get_osfhandle(GetReadFD(io)); if (GetFileType(h) != FILE_TYPE_PIPE) return Qfalse; if (!GetFileInformationByHandleEx(h, FileNameInfo, &buffer, sizeof(buffer))) return Qfalse; len = buffer.info.FileNameLength / sizeof(WCHAR); name[len] = L'\0'; - if (memcmp(name, cygwin_prefix, sizeof(cygwin_prefix)-sizeof(WCHAR)) == 0) { - ptr = name + sizeof(cygwin_prefix)/sizeof(WCHAR) - 1; +# define skip_platform_tty_prefix(type) \ + (memcmp(name, L"\\" #type "-", sizeof(L"\\" #type)) == 0 ? \ + &name[rb_strlen_lit(L"\\" #type "-")] : 0) + if (mode == platform_cygwin || mode == platform_any) { + ptr = skip_platform_tty_prefix(cygwin); } - else if (mode == platform_msys && memcmp(name, msys_prefix, sizeof(msys_prefix)-sizeof(WCHAR)) == 0) { - ptr = name + sizeof(msys_prefix)/sizeof(WCHAR) - 1; + else if (mode == platform_msys || mode == platform_any) { + ptr = skip_platform_tty_prefix(msys); } else { return Qfalse; } + if (!ptr) return Qfalse; if (wcsstr(ptr, L"-pty")) ret = Qtrue; -#endif #endif } return ret; } +#endif /* * IO console methods diff --git a/test/io/console/test_io_console.rb b/test/io/console/test_io_console.rb index c3f9c91..55187da 100644 --- a/test/io/console/test_io_console.rb +++ b/test/io/console/test_io_console.rb @@ -60,6 +60,14 @@ def test_bad_keyword end end end + + def test_tty? + omit "not supported" if IO.instance_method(:tty?).arity == 0 + assert_include([true, false], STDIN.tty?(:any)) + File.open(IO::NULL) do |f| + assert_not_operator(f, :tty?, :any) + end + end end defined?(PTY) and defined?(IO.console) and TestIO_Console.class_eval do @@ -495,66 +503,64 @@ def run_pty(src, n = 1) end end -defined?(IO.console) and TestIO_Console.class_eval do - if IO.console - def test_get_winsize_console - s = IO.console.winsize - assert_kind_of(Array, s) - assert_equal(2, s.size) - assert_kind_of(Integer, s[0]) - assert_kind_of(Integer, s[1]) - end +defined?(IO.console) and IO.console and TestIO_Console.class_eval do + def test_get_winsize_console + s = IO.console.winsize + assert_kind_of(Array, s) + assert_equal(2, s.size) + assert_kind_of(Integer, s[0]) + assert_kind_of(Integer, s[1]) + end - def test_set_winsize_console - set_winsize_setup - s = IO.console.winsize - assert_nothing_raised(TypeError) {IO.console.winsize = s} - bug = '[ruby-core:82741] [Bug #13888]' - begin - IO.console.winsize = [s[0], s[1]+1] - assert_equal([s[0], s[1]+1], IO.console.winsize, bug) - rescue Errno::EINVAL # Error if run on an actual console. - else - IO.console.winsize = s - assert_equal(s, IO.console.winsize, bug) - end - ensure - set_winsize_teardown + def test_set_winsize_console + set_winsize_setup + s = IO.console.winsize + assert_nothing_raised(TypeError) {IO.console.winsize = s} + bug = '[ruby-core:82741] [Bug #13888]' + begin + IO.console.winsize = [s[0], s[1]+1] + assert_equal([s[0], s[1]+1], IO.console.winsize, bug) + rescue Errno::EINVAL # Error if run on an actual console. + else + IO.console.winsize = s + assert_equal(s, IO.console.winsize, bug) end + ensure + set_winsize_teardown + end - def test_close - IO.console.close - assert_kind_of(IO, IO.console) - assert_nothing_raised(IOError) {IO.console.fileno} + def test_close + IO.console.close + assert_kind_of(IO, IO.console) + assert_nothing_raised(IOError) {IO.console.fileno} - IO.console(:close) - assert(IO.console(:tty?)) - ensure - IO.console(:close) - end + IO.console(:close) + assert(IO.console(:tty?)) + ensure + IO.console(:close) + end - def test_console_kw - io = IO.console(:clone, freeze: true) - io.close - assert_kind_of(IO, io) - end + def test_console_kw + io = IO.console(:clone, freeze: true) + io.close + assert_kind_of(IO, io) + end - def test_sync - assert(IO.console.sync, "console should be unbuffered") - ensure - IO.console(:close) - end + def test_sync + assert(IO.console.sync, "console should be unbuffered") + ensure + IO.console(:close) + end - def test_getch_timeout - assert_nil(IO.console.getch(intr: true, time: 0.1, min: 0)) - end + def test_getch_timeout + assert_nil(IO.console.getch(intr: true, time: 0.1, min: 0)) + end - def test_ttyname - return unless IO.method_defined?(:ttyname) - ttyname = IO.console.ttyname - assert_not_nil(ttyname) - File.open(ttyname) {|f| assert_predicate(f, :tty?)} - end + def test_ttyname + return unless IO.method_defined?(:ttyname) + ttyname = IO.console.ttyname + assert_not_nil(ttyname) + File.open(ttyname) {|f| assert_predicate(f, :tty?)} end case @@ -563,6 +569,8 @@ def test_ttyname when !(rubyw = RbConfig::CONFIG["RUBYW_INSTALL_NAME"]).empty? dir, base = File.split(EnvUtil.rubybin) noctty = [File.join(dir, base.sub(RUBY_ENGINE, rubyw))] + else + rubyw = nil end if noctty From f06f2d9784537cbc91622ac06d8926bd685378a8 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 7 Feb 2025 10:53:14 +0900 Subject: [PATCH 6/9] [DOC] IO.tty? optional argument --- ext/io/console/console.c | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/ext/io/console/console.c b/ext/io/console/console.c index a229af2..9c1277e 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -1927,6 +1927,20 @@ typedef enum { } console_platform_t; #ifdef HAVE_RB_PREPEND_MODULE +/* + * call-seq: + * io.tty?([mode]) -> true or false + * + * Returns +true+ if the stream is associated with a terminal device (tty), + * +false+ otherwise. + * + * If non-nil +mode+ is given, platform dependent tty is also checked + * in addition to the default tty. + * + * - +:any+ : Returns +true+ for any known kind of tty. + * - +:cygwin+ : Returns +true+ for cygwin tty, on Windows. + * - +:msys+ : Returns +true+ for msys2 tty, on Windows. + */ static VALUE console_platform_tty_p(int argc, VALUE *argv, VALUE io) { @@ -2060,9 +2074,14 @@ InitVM_console(void) rb_define_method(rb_cIO, "ttyname", console_ttyname, 0); #ifdef HAVE_RB_PREPEND_MODULE { + /* :stopdoc: */ VALUE platform = rb_define_module_under(rb_cIO, "platform_tty"); - rb_define_method(platform, "tty?", console_platform_tty_p, -1); - rb_define_method(platform, "isatty", console_platform_tty_p, -1); + /* :startdoc: */ + { + VALUE rb_cIO = platform; + rb_define_method(rb_cIO, "tty?", console_platform_tty_p, -1); + rb_define_method(rb_cIO, "isatty", console_platform_tty_p, -1); + } rb_prepend_module(rb_cIO, platform); } #endif From d140425f9fa532331ef415ca9ef8c8c5b9e7c3da Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 31 Jul 2026 18:49:10 +0900 Subject: [PATCH 7/9] Accept multiple mode arguments --- ext/io/console/console.c | 96 ++++++++++++++++-------------- test/io/console/test_io_console.rb | 44 +++++++++++--- 2 files changed, 87 insertions(+), 53 deletions(-) diff --git a/ext/io/console/console.c b/ext/io/console/console.c index 9c1277e..edc882e 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -1914,52 +1914,59 @@ console_ttyname(VALUE io) # define console_ttyname rb_f_notimplement #endif -typedef enum { - platform_none, #ifdef HAVE_RB_PREPEND_MODULE - platform_any, +typedef enum { + platform_default, #if defined _WIN32 || defined __CYGWIN__ platform_cygwin, platform_msys, #endif + platform_any, + + platform_default_bit = 1U << platform_default, +#if defined _WIN32 || defined __CYGWIN__ + platform_cygwin_bit = 1U << platform_cygwin, + platform_msys_bit = 1U << platform_msys, #endif - platform_max + platform_any_bit = (1U << platform_any) - 1 /* all bits */ } console_platform_t; -#ifdef HAVE_RB_PREPEND_MODULE /* * call-seq: - * io.tty?([mode]) -> true or false + * io.tty?([mode, ...]) -> true or false * * Returns +true+ if the stream is associated with a terminal device (tty), * +false+ otherwise. * - * If non-nil +mode+ is given, platform dependent tty is also checked - * in addition to the default tty. + * If one or more +type+s are given, returns +true+ if the stream is + * associated with any of the specified tty types. * - * - +:any+ : Returns +true+ for any known kind of tty. + * - +:any+ : Returns +true+ for any known kind of tty, including the + * default tty. * - +:cygwin+ : Returns +true+ for cygwin tty, on Windows. * - +:msys+ : Returns +true+ for msys2 tty, on Windows. */ static VALUE console_platform_tty_p(int argc, VALUE *argv, VALUE io) { - VALUE ret; - console_platform_t mode = platform_none; + VALUE ret = Qfalse; + int mode = 0; - if (rb_check_arity(argc, 0, 1)) { - VALUE m = argv[0]; - if (!NIL_P(m)) { + if (argc > 0) { + int i; + for (i = 0; i < argc; ++i) { + VALUE m = argv[i]; + if (NIL_P(m)) continue; Check_Type(m, T_SYMBOL); if (m == ID2SYM(rb_intern("any"))) { - mode = platform_any; + mode |= platform_any_bit; } #if defined _WIN32 || defined __CYGWIN__ else if (m == ID2SYM(rb_intern("cygwin"))) { - mode = platform_cygwin; + mode |= platform_cygwin_bit; } else if (m == ID2SYM(rb_intern("msys"))) { - mode = platform_msys; + mode |= platform_msys_bit; } #endif else { @@ -1967,37 +1974,34 @@ console_platform_tty_p(int argc, VALUE *argv, VALUE io) } } } - ret = rb_call_super(0, 0); - if (mode != platform_none && !RTEST(ret)) { + if ((mode & platform_default_bit) || (mode == 0)) { + ret = rb_call_super(0, 0); + } + if ((mode & ~platform_default_bit) && !RTEST(ret)) { #if defined _WIN32 || defined __CYGWIN__ - HANDLE h; - union { - FILE_NAME_INFO info; - WCHAR rest[MAX_PATH]; - } buffer; - WCHAR *const name = buffer.info.FileName; - const WCHAR *ptr; - DWORD len; - - h = (HANDLE)rb_w32_get_osfhandle(GetReadFD(io)); - if (GetFileType(h) != FILE_TYPE_PIPE) return Qfalse; - if (!GetFileInformationByHandleEx(h, FileNameInfo, &buffer, sizeof(buffer))) return Qfalse; - len = buffer.info.FileNameLength / sizeof(WCHAR); - name[len] = L'\0'; -# define skip_platform_tty_prefix(type) \ - (memcmp(name, L"\\" #type "-", sizeof(L"\\" #type)) == 0 ? \ - &name[rb_strlen_lit(L"\\" #type "-")] : 0) - if (mode == platform_cygwin || mode == platform_any) { - ptr = skip_platform_tty_prefix(cygwin); - } - else if (mode == platform_msys || mode == platform_any) { - ptr = skip_platform_tty_prefix(msys); - } - else { - return Qfalse; + if (mode & (platform_cygwin_bit | platform_msys_bit)) { + struct { + FILE_NAME_INFO info; + WCHAR rest[MAX_PATH]; + } buffer; + + HANDLE h = (HANDLE)rb_w32_get_osfhandle(GetReadFD(io)); + if ((GetFileType(h) == FILE_TYPE_PIPE) && + GetFileInformationByHandleEx(h, FileNameInfo, &buffer, sizeof(buffer))) { + WCHAR *const name = buffer.info.FileName; + DWORD len = buffer.info.FileNameLength / sizeof(WCHAR); + name[len] = L'\0'; +# define tty_pipe_p(type) \ + (memcmp(name, L"\\" #type "-", sizeof(L"\\" #type)) == 0 && \ + wcsstr(&name[rb_strlen_lit("\\" #type "-")], L"-pty") != NULL) + if (!ret && (mode & platform_cygwin_bit)) { + ret = tty_pipe_p(cygwin); + } + if (!ret && (mode & platform_msys_bit)) { + ret = tty_pipe_p(msys); + } + } } - if (!ptr) return Qfalse; - if (wcsstr(ptr, L"-pty")) ret = Qtrue; #endif } return ret; diff --git a/test/io/console/test_io_console.rb b/test/io/console/test_io_console.rb index 55187da..e40de3e 100644 --- a/test/io/console/test_io_console.rb +++ b/test/io/console/test_io_console.rb @@ -61,16 +61,32 @@ def test_bad_keyword end end + TTY_ENHANCED = IO.instance_method(:tty?).arity != 0 + def test_tty? - omit "not supported" if IO.instance_method(:tty?).arity == 0 - assert_include([true, false], STDIN.tty?(:any)) + pend "not supported" unless TTY_ENHANCED + + tty = STDIN.tty?(:any) + assert_include([true, false], tty) + assert_equal(tty, STDIN.tty?(:any, :any)) + end + + def test_tty_non_tty + pend "not supported" unless TTY_ENHANCED + File.open(IO::NULL) do |f| + assert_not_predicate(f, :tty?) assert_not_operator(f, :tty?, :any) + assert_not_send([f, :tty?, :any, :any]) + + assert_raise(TypeError) {f.tty?("any")} + assert_raise(ArgumentError) {f.tty?(:unknown)} end end end -defined?(PTY) and defined?(IO.console) and TestIO_Console.class_eval do +defined?(PTY) and defined?(IO.console) and \ +class TestIO_Console Bug6116 = '[ruby-dev:45309]' def test_raw @@ -250,6 +266,19 @@ def test_setecho2 } end + def test_tty_on_pty + pend "not supported" unless TTY_ENHANCED + + helper {|_, s| + assert_predicate(s, :tty?) + assert_operator(s, :tty?, :any) + assert_send([s, :tty?, :any, :any]) + + assert_raise(TypeError) {s.tty?("any")} + assert_raise(ArgumentError) {s.tty?(:unknown)} + } + end + def test_getpass run_pty("p IO.console.getpass('> ')") do |r, w| assert_equal("> ", r.readpartial(10)) @@ -503,7 +532,8 @@ def run_pty(src, n = 1) end end -defined?(IO.console) and IO.console and TestIO_Console.class_eval do +defined?(IO.console) and IO.console and \ +class TestIO_Console def test_get_winsize_console s = IO.console.winsize assert_kind_of(Array, s) @@ -613,8 +643,8 @@ def test_noctty end end -defined?(IO.console) and IO.console and IO.console.respond_to?(:pressed?) and - TestIO_Console.class_eval do +defined?(IO.console) and IO.console and IO.console.respond_to?(:pressed?) and \ +class TestIO_Console def test_pressed_valid assert_include([true, false], IO.console.pressed?("HOME")) assert_include([true, false], IO.console.pressed?(:"HOME")) @@ -628,7 +658,7 @@ def test_pressed_invalid end end -TestIO_Console.class_eval do +class TestIO_Console def test_stringio_getch assert_ruby_status %w"--disable=gems -rstringio -rio/console", %q{ abort unless StringIO.method_defined?(:getch) From e307d61226fab217a84b37cd2199108673f9420a Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 2 Aug 2026 07:25:10 +0900 Subject: [PATCH 8/9] Treat nil as the default tty type Allow the default tty check to be combined with platform-specific tty types. --- ext/io/console/console.c | 7 ++++++- test/io/console/test_io_console.rb | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ext/io/console/console.c b/ext/io/console/console.c index edc882e..9dae0ab 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -1941,6 +1941,8 @@ typedef enum { * If one or more +type+s are given, returns +true+ if the stream is * associated with any of the specified tty types. * + * - +nil+ : Returns the result of the default tty check, as if no type were + * given. It can be combined with other types. * - +:any+ : Returns +true+ for any known kind of tty, including the * default tty. * - +:cygwin+ : Returns +true+ for cygwin tty, on Windows. @@ -1956,7 +1958,10 @@ console_platform_tty_p(int argc, VALUE *argv, VALUE io) int i; for (i = 0; i < argc; ++i) { VALUE m = argv[i]; - if (NIL_P(m)) continue; + if (NIL_P(m)) { + mode |= platform_default_bit; + continue; + } Check_Type(m, T_SYMBOL); if (m == ID2SYM(rb_intern("any"))) { mode |= platform_any_bit; diff --git a/test/io/console/test_io_console.rb b/test/io/console/test_io_console.rb index e40de3e..1ce1de0 100644 --- a/test/io/console/test_io_console.rb +++ b/test/io/console/test_io_console.rb @@ -69,6 +69,8 @@ def test_tty? tty = STDIN.tty?(:any) assert_include([true, false], tty) assert_equal(tty, STDIN.tty?(:any, :any)) + assert_equal(tty, STDIN.tty?(nil, :any)) + assert_equal(STDIN.tty?, STDIN.tty?(nil)) end def test_tty_non_tty @@ -77,7 +79,9 @@ def test_tty_non_tty File.open(IO::NULL) do |f| assert_not_predicate(f, :tty?) assert_not_operator(f, :tty?, :any) + assert_not_operator(f, :tty?, nil) assert_not_send([f, :tty?, :any, :any]) + assert_not_send([f, :tty?, nil, :any]) assert_raise(TypeError) {f.tty?("any")} assert_raise(ArgumentError) {f.tty?(:unknown)} @@ -272,7 +276,9 @@ def test_tty_on_pty helper {|_, s| assert_predicate(s, :tty?) assert_operator(s, :tty?, :any) + assert_operator(s, :tty?, nil) assert_send([s, :tty?, :any, :any]) + assert_send([s, :tty?, nil, :any]) assert_raise(TypeError) {s.tty?("any")} assert_raise(ArgumentError) {s.tty?(:unknown)} From 3708c0ce8a0dc76bf7760f0e337bfc9eb07990c6 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 2 Aug 2026 17:27:31 +0900 Subject: [PATCH 9/9] Enable tty type checks on TruffleRuby Use Ruby-level `Module#prepend` because TruffleRuby declares but does not export `rb_prepend_module`. --- ext/io/console/console.c | 8 ++++---- ext/io/console/extconf.rb | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/io/console/console.c b/ext/io/console/console.c index 9dae0ab..ebd4c39 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -1914,7 +1914,6 @@ console_ttyname(VALUE io) # define console_ttyname rb_f_notimplement #endif -#ifdef HAVE_RB_PREPEND_MODULE typedef enum { platform_default, #if defined _WIN32 || defined __CYGWIN__ @@ -2011,7 +2010,6 @@ console_platform_tty_p(int argc, VALUE *argv, VALUE io) } return ret; } -#endif /* * IO console methods @@ -2081,7 +2079,6 @@ InitVM_console(void) rb_define_method(rb_cIO, "check_winsize_changed", console_check_winsize_changed, 0); rb_define_method(rb_cIO, "getpass", console_getpass, -1); rb_define_method(rb_cIO, "ttyname", console_ttyname, 0); -#ifdef HAVE_RB_PREPEND_MODULE { /* :stopdoc: */ VALUE platform = rb_define_module_under(rb_cIO, "platform_tty"); @@ -2091,9 +2088,12 @@ InitVM_console(void) rb_define_method(rb_cIO, "tty?", console_platform_tty_p, -1); rb_define_method(rb_cIO, "isatty", console_platform_tty_p, -1); } +#ifdef HAVE_RB_PREPEND_MODULE rb_prepend_module(rb_cIO, platform); - } +#else + rb_funcall(rb_cIO, rb_intern_const("prepend"), 1, platform); #endif + } rb_define_singleton_method(rb_cIO, "console", console_dev, -1); { /* :nodoc: */ diff --git a/ext/io/console/extconf.rb b/ext/io/console/extconf.rb index fdeede5..95680dc 100644 --- a/ext/io/console/extconf.rb +++ b/ext/io/console/extconf.rb @@ -49,7 +49,7 @@ have_func("rb_io_wait") # Ruby 3.0 end win32 or have_func("ttyname_r") or have_func("ttyname") - have_func("rb_prepend_module") # exclude TruffleRuby + have_func("rb_prepend_module") # not exported by TruffleRuby create_makefile("io/console") {|conf| conf << "\n""VK_HEADER = #{vk_header}\n" }