Skip to content

Commit

Permalink
Add IO#ttyname that returns the tty name or nil
Browse files Browse the repository at this point in the history
  • Loading branch information
nobu committed Dec 2, 2024
1 parent 82a1efc commit fdad351
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 3 deletions.
61 changes: 61 additions & 0 deletions ext/io/console/console.c
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ getattr(int fd, conmode *t)
static ID id_getc, id_close;
static ID id_gets, id_flush, id_chomp_bang;

#ifndef HAVE_RB_INTERNED_STR_CSTR
# define rb_str_to_interned_str(str) rb_str_freeze(str)
# define rb_interned_str_cstr(str) rb_str_freeze(rb_usascii_str_new_cstr(str))
#endif

#if defined HAVE_RUBY_FIBER_SCHEDULER_H
# include "ruby/fiber/scheduler.h"
#elif defined HAVE_RB_SCHEDULER_TIMEOUT
Expand Down Expand Up @@ -1818,6 +1823,61 @@ io_getpass(int argc, VALUE *argv, VALUE io)
return str_chomp(str);
}

#if defined(_WIN32) || defined(HAVE_TTYNAME_R) || defined(HAVE_TTYNAME)
/*
* call-seq:
* io.ttyname -> string or nil
*
* Returns name of associated terminal (tty) if +io+ is not a tty.
* Returns +nil+ otherwise.
*/
static VALUE
console_ttyname(VALUE io)
{
int fd = rb_io_descriptor(io);
if (!isatty(fd)) return Qnil;
# if defined _WIN32
return rb_usascii_str_new_lit("con");
# elif defined HAVE_TTYNAME_R
{
char termname[1024], *tn = termname;
size_t size = sizeof(termname);
int e;
if (ttyname_r(fd, tn, size) == 0)
return rb_interned_str_cstr(tn);
if ((e = errno) == ERANGE) {
VALUE s = rb_str_new(0, size);
while (1) {
tn = RSTRING_PTR(s);
size = rb_str_capacity(s);
if (ttyname_r(fd, tn, size) == 0) {
return rb_str_to_interned_str(rb_str_resize(s, strlen(tn)));
}
if ((e = errno) != ERANGE) break;
if ((size *= 2) >= INT_MAX/2) break;
rb_str_resize(s, size);
}
}
rb_syserr_fail_str(e, rb_sprintf("ttyname_r(%d)", fd));
UNREACHABLE_RETURN(Qnil);
}
# elif defined HAVE_TTYNAME
{
const char *tn = ttyname(fd);
if (!tn) {
int e = errno;
rb_syserr_fail_str(e, rb_sprintf("ttyname(%d)", fd));
}
return rb_interned_str_cstr(tn);
}
# else
# error No ttyname function
# endif
}
#else
# define console_ttyname rb_f_notimplement
#endif

/*
* IO console methods
*/
Expand Down Expand Up @@ -1885,6 +1945,7 @@ InitVM_console(void)
rb_define_method(rb_cIO, "pressed?", console_key_pressed_p, 1);
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);
rb_define_singleton_method(rb_cIO, "console", console_dev, -1);
{
/* :stopdoc: */
Expand Down
2 changes: 2 additions & 0 deletions ext/io/console/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
have_func("rb_syserr_new_str(0, Qnil)") or
abort

have_func("rb_interned_str_cstr")
have_func("rb_io_path")
have_func("rb_io_descriptor")
have_func("rb_io_get_write_io")
Expand Down Expand Up @@ -51,6 +52,7 @@
elsif have_func("rb_scheduler_timeout") # 3.0
have_func("rb_io_wait")
end
have_func("ttyname_r") or have_func("ttyname")
create_makefile("io/console") {|conf|
conf << "\n""VK_HEADER = #{vk_header}\n"
}
Expand Down
25 changes: 22 additions & 3 deletions test/io/console/test_io_console.rb
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,11 @@ def test_console_kw
def test_sync
assert_equal(["true"], run_pty("p IO.console.sync"))
end

def test_ttyname
return unless IO.method_defined?(:ttyname)
assert_equal(["true"], run_pty("p STDIN.ttyname == STDOUT.ttyname"))
end
end

private
Expand Down Expand Up @@ -531,6 +536,13 @@ def test_sync
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
end
end

Expand All @@ -546,7 +558,7 @@ def test_getch_timeout
if noctty
require 'tempfile'
NOCTTY = noctty
def test_noctty
def run_noctty(src)
t = Tempfile.new("noctty_out")
t.close
t2 = Tempfile.new("noctty_run")
Expand All @@ -557,7 +569,7 @@ def test_noctty
'-e', 'STDOUT.reopen(f)',
'-e', 'STDERR.reopen(f)',
'-e', 'require "io/console"',
'-e', 'f.puts IO.console.inspect',
'-e', "f.puts (#{src}).inspect",
'-e', 'f.flush',
'-e', 'File.unlink(ARGV[1])',
'-e', '}',
Expand All @@ -568,11 +580,18 @@ def test_noctty
sleep 0.1
end
t.open
assert_equal("nil", t.gets(nil).chomp)
t.gets.lines(chomp: true)
ensure
t.close! if t and !t.closed?
t2.close!
end

def test_noctty
assert_equal(["nil"], run_noctty("IO.console"))
if IO.method_defined?(:ttyname)
assert_equal(["nil"], run_noctty("STDIN.ttyname rescue $!"))
end
end
end
end

Expand Down

0 comments on commit fdad351

Please sign in to comment.