Skip to content

Commit

Permalink
add timeout to prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
IanButterworth committed Apr 15, 2021
1 parent aaf2b9f commit e829f45
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 11 deletions.
54 changes: 49 additions & 5 deletions base/util.jl
Original file line number Diff line number Diff line change
Expand Up @@ -278,26 +278,70 @@ 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
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()
Expand Down
37 changes: 31 additions & 6 deletions test/misc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit e829f45

Please sign in to comment.