diff --git a/base/util.jl b/base/util.jl index c489503d40133..991a93ec4d3a2 100644 --- a/base/util.jl +++ b/base/util.jl @@ -278,18 +278,62 @@ end getpass(prompt::AbstractString) = getpass(stdin, stdout, prompt) """ - prompt(message; default="") -> Union{String, Nothing} + prompt(message; default="", timeout=nothing) -> Union{String, Nothing} Displays the `message` then waits for user input. Input is terminated when a newline (\\n) is encountered or EOF (^D) character is entered on a blank line. If a `default` is provided -then the user can enter just a newline character to select the `default`. +then the user can enter just a newline character to select the `default`. A `timeout` in seconds +greater than 0 can be provided, after which the default will be returned. + +For instance, the user enters a value and hits `return`: +```julia +julia> Base.prompt("Proceed? y/n"; default="n", timeout=5) +Proceed? y/n [n] timeout 5 seconds: y +"y" +``` + +The user hits `return` alone to select the default: +```julia +julia> Base.prompt("Proceed? y/n"; default="n", timeout=5) +Proceed? y/n [n] timeout 5 seconds: +"n" +``` + +The user doesn't input and hit `return` before the timeout, so default returns: +```julia +julia> Base.prompt("Proceed? y/n"; default="n", timeout=5) +Proceed? y/n [n] timeout 5 seconds: timed out +"n" +``` See also `Base.getpass` and `Base.winprompt` for secure entry of passwords. """ -function prompt(input::IO, output::IO, message::AbstractString; default::AbstractString="") - msg = !isempty(default) ? "$message [$default]: " : "$message: " +function prompt(input::LibuvStream, output::IO, message::AbstractString; default::AbstractString="", timeout::Union{Nothing, Real} = nothing) + in_stat_before = input.status + timeout_timer = if !isnothing(timeout) && timeout > 0 + plural = timeout == 1 ? "" : "s" + msg = !isempty(default) ? "$message [$default] timeout $timeout second$(plural): " : "$message: " + Timer(timeout) do t + lock(input.cond) + input.status = StatusEOF + notify(input.cond) + unlock(input.cond) + end + else + msg = !isempty(default) ? "$message [$default]: " : "$message: " + nothing + end print(output, msg) uinput = readline(input, keep=true) + if !isnothing(timeout_timer) + if isopen(timeout_timer) + close(timeout_timer) + else + println(output, "timed out") + input.status = in_stat_before + return default + end + end isempty(uinput) && return nothing # Encountered an EOF uinput = chomp(uinput) isempty(uinput) ? default : uinput @@ -297,7 +341,7 @@ end # allow new prompt methods to be defined if stdin has been # redirected to some custom stream, e.g. in IJulia. -prompt(message::AbstractString; default::AbstractString="") = prompt(stdin, stdout, message, default=default) +prompt(message::AbstractString; default::AbstractString="",timeout::Union{Nothing, Real} = nothing) = prompt(stdin, stdout, message, default=default, timeout=timeout) # Windows authentication prompt if Sys.iswindows() diff --git a/test/misc.jl b/test/misc.jl index 2305b78250ef2..59acfab1245d5 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -510,12 +510,37 @@ end # PR #28038 (prompt/getpass stream args) @test_throws MethodError Base.getpass(IOBuffer(), stdout, "pass") -let buf = IOBuffer() - @test Base.prompt(IOBuffer("foo\nbar\n"), buf, "baz") == "foo" - @test String(take!(buf)) == "baz: " - @test Base.prompt(IOBuffer("\n"), buf, "baz", default="foobar") == "foobar" - @test String(take!(buf)) == "baz [foobar]: " - @test Base.prompt(IOBuffer("blah\n"), buf, "baz", default="foobar") == "blah" +original_stdin = stdin +try + let buf = IOBuffer() + (rd, wr) = redirect_stdin() + print(wr, "foo\nbar\n") + @test Base.prompt(rd, buf, "baz") == "foo" + @test String(take!(buf)) == "baz: " + readavailable(rd) + print(wr, "\n") + @test Base.prompt(rd, buf, "baz", default="foobar") == "foobar" + @test String(take!(buf)) == "baz [foobar]: " + print(wr, "blah\n") + @test Base.prompt(rd, buf, "baz", default="foobar") == "blah" + take!(buf) + # prompt timeout + + @test Base.prompt(rd, buf, "baz", default="foobar", timeout = 1) == "foobar" + @test String(take!(buf)) == "baz [foobar] timeout 1 second: timed out\n" + @test Base.prompt(rd, buf, "baz", default="foobar", timeout = 2) == "foobar" + @test String(take!(buf)) == "baz [foobar] timeout 2 seconds: timed out\n" + print(wr, "foo\n") + @test Base.prompt(rd, buf, "baz", default="foobar", timeout = 1) == "foo" + @test String(take!(buf)) == "baz [foobar] timeout 1 second: " + print(wr, "\n") + @test Base.prompt(rd, buf, "baz", default="foobar", timeout = 1) == "foobar" + @test String(take!(buf)) == "baz [foobar] timeout 1 second: " + end +catch + rethrow() +finally + redirect_stdin(original_stdin) end # these tests are not in a test block so that they will compile separately