Skip to content

Commit 99c7a56

Browse files
jw3126JeffBezansonfredrikekreKristofferC
authored and
Amit Shirodkar
committed
more ergonomic stream redirection (JuliaLang#37978)
Co-authored-by: Jeff Bezanson <[email protected]> Co-authored-by: Fredrik Ekre <[email protected]> Co-authored-by: Kristoffer Carlsson <[email protected]>
1 parent 3164581 commit 99c7a56

File tree

5 files changed

+200
-9
lines changed

5 files changed

+200
-9
lines changed

NEWS.md

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ New library functions
5757
* New functor `Returns(value)`, which returns `value` for any arguments ([#39794])
5858
* New macro `Base.@invoke f(arg1::T1, arg2::T2; kwargs...)` provides an easier syntax to call `invoke(f, Tuple{T1,T2}, arg1, arg2; kwargs...)` ([#38438])
5959
* New macros `@something` and `@coalesce` which are short-circuiting versions of `something` and `coalesce`, respectively ([#40729])
60+
* New function `redirect_stdio` for redirecting `stdin`, `stdout` and `stderr` ([#37978]).
6061

6162
New library features
6263
--------------------

base/exports.jl

+1
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,7 @@ export
833833
readline,
834834
readlines,
835835
readuntil,
836+
redirect_stdio,
836837
redirect_stderr,
837838
redirect_stdin,
838839
redirect_stdout,

base/stream.jl

+129-9
Original file line numberDiff line numberDiff line change
@@ -1127,15 +1127,15 @@ function _fd(x::Union{LibuvStream, LibuvServer})
11271127
return fd[]
11281128
end
11291129

1130-
struct redirect_stdio <: Function
1130+
struct RedirectStdStream <: Function
11311131
unix_fd::Int
11321132
writable::Bool
11331133
end
11341134
for (f, writable, unix_fd) in
11351135
((:redirect_stdin, false, 0),
11361136
(:redirect_stdout, true, 1),
11371137
(:redirect_stderr, true, 2))
1138-
@eval const ($f) = redirect_stdio($unix_fd, $writable)
1138+
@eval const ($f) = RedirectStdStream($unix_fd, $writable)
11391139
end
11401140
function _redirect_io_libc(stream, unix_fd::Int)
11411141
posix_fd = _fd(stream)
@@ -1154,7 +1154,7 @@ function _redirect_io_global(io, unix_fd::Int)
11541154
unix_fd == 2 && (global stderr = io)
11551155
nothing
11561156
end
1157-
function (f::redirect_stdio)(handle::Union{LibuvStream, IOStream})
1157+
function (f::RedirectStdStream)(handle::Union{LibuvStream, IOStream})
11581158
_redirect_io_libc(handle, f.unix_fd)
11591159
c_sym = f.unix_fd == 0 ? cglobal(:jl_uv_stdin, Ptr{Cvoid}) :
11601160
f.unix_fd == 1 ? cglobal(:jl_uv_stdout, Ptr{Cvoid}) :
@@ -1164,31 +1164,31 @@ function (f::redirect_stdio)(handle::Union{LibuvStream, IOStream})
11641164
_redirect_io_global(handle, f.unix_fd)
11651165
return handle
11661166
end
1167-
function (f::redirect_stdio)(::DevNull)
1167+
function (f::RedirectStdStream)(::DevNull)
11681168
nulldev = @static Sys.iswindows() ? "NUL" : "/dev/null"
11691169
handle = open(nulldev, write=f.writable)
11701170
_redirect_io_libc(handle, f.unix_fd)
11711171
close(handle) # handle has been dup'ed in _redirect_io_libc
11721172
_redirect_io_global(devnull, f.unix_fd)
11731173
return devnull
11741174
end
1175-
function (f::redirect_stdio)(io::AbstractPipe)
1175+
function (f::RedirectStdStream)(io::AbstractPipe)
11761176
io2 = (f.writable ? pipe_writer : pipe_reader)(io)
11771177
f(io2)
11781178
_redirect_io_global(io, f.unix_fd)
11791179
return io
11801180
end
1181-
function (f::redirect_stdio)(p::Pipe)
1181+
function (f::RedirectStdStream)(p::Pipe)
11821182
if p.in.status == StatusInit && p.out.status == StatusInit
11831183
link_pipe!(p)
11841184
end
11851185
io2 = getfield(p, f.writable ? :in : :out)
11861186
f(io2)
11871187
return p
11881188
end
1189-
(f::redirect_stdio)() = f(Pipe())
1189+
(f::RedirectStdStream)() = f(Pipe())
11901190

1191-
# Deprecate these in v2 (redirect_stdio support)
1191+
# Deprecate these in v2 (RedirectStdStream support)
11921192
iterate(p::Pipe) = (p.out, 1)
11931193
iterate(p::Pipe, i::Int) = i == 1 ? (p.in, 2) : nothing
11941194
getindex(p::Pipe, key::Int) = key == 1 ? p.out : key == 2 ? p.in : throw(KeyError(key))
@@ -1204,6 +1204,8 @@ the pipe.
12041204
!!! note
12051205
`stream` must be a compatible objects, such as an `IOStream`, `TTY`,
12061206
`Pipe`, socket, or `devnull`.
1207+
1208+
See also [`redirect_stdio`](@ref).
12071209
"""
12081210
redirect_stdout
12091211

@@ -1215,6 +1217,8 @@ Like [`redirect_stdout`](@ref), but for [`stderr`](@ref).
12151217
!!! note
12161218
`stream` must be a compatible objects, such as an `IOStream`, `TTY`,
12171219
`Pipe`, socket, or `devnull`.
1220+
1221+
See also [`redirect_stdio`](@ref).
12181222
"""
12191223
redirect_stderr
12201224

@@ -1227,10 +1231,125 @@ Note that the direction of the stream is reversed.
12271231
!!! note
12281232
`stream` must be a compatible objects, such as an `IOStream`, `TTY`,
12291233
`Pipe`, socket, or `devnull`.
1234+
1235+
See also [`redirect_stdio`](@ref).
12301236
"""
12311237
redirect_stdin
12321238

1233-
function (f::redirect_stdio)(thunk::Function, stream)
1239+
"""
1240+
redirect_stdio(;stdin=stdin, stderr=stderr, stdout=stdout)
1241+
1242+
Redirect a subset of the streams `stdin`, `stderr`, `stdout`.
1243+
Each argument must be an `IOStream`, `TTY`, `Pipe`, socket, or `devnull`.
1244+
1245+
!!! compat "Julia 1.7"
1246+
`redirect_stdio` requires Julia 1.7 or later.
1247+
"""
1248+
function redirect_stdio(;stdin=nothing, stderr=nothing, stdout=nothing)
1249+
stdin === nothing || redirect_stdin(stdin)
1250+
stderr === nothing || redirect_stderr(stderr)
1251+
stdout === nothing || redirect_stdout(stdout)
1252+
end
1253+
1254+
"""
1255+
redirect_stdio(f; stdin=nothing, stderr=nothing, stdout=nothing)
1256+
1257+
Redirect a subset of the streams `stdin`, `stderr`, `stdout`,
1258+
call `f()` and restore each stream.
1259+
1260+
Possible values for each stream are:
1261+
* `nothing` indicating the stream should not be redirected.
1262+
* `path::AbstractString` redirecting the stream to the file at `path`.
1263+
* `io` an `IOStream`, `TTY`, `Pipe`, socket, or `devnull`.
1264+
1265+
# Examples
1266+
```julia
1267+
julia> redirect_stdio(stdout="stdout.txt", stderr="stderr.txt") do
1268+
print("hello stdout")
1269+
print(stderr, "hello stderr")
1270+
end
1271+
1272+
julia> read("stdout.txt", String)
1273+
"hello stdout"
1274+
1275+
julia> read("stderr.txt", String)
1276+
"hello stderr"
1277+
```
1278+
1279+
# Edge cases
1280+
1281+
It is possible to pass the same argument to `stdout` and `stderr`:
1282+
```julia
1283+
julia> redirect_stdio(stdout="log.txt", stderr="log.txt", stdin=devnull) do
1284+
...
1285+
end
1286+
```
1287+
1288+
However it is not supported to pass two distinct descriptors of the same file.
1289+
```julia
1290+
julia> io1 = open("same/path", "w")
1291+
1292+
julia> io2 = open("same/path", "w")
1293+
1294+
julia> redirect_stdio(f, stdout=io1, stderr=io2) # not suppored
1295+
```
1296+
Also the `stdin` argument may not be the same descriptor as `stdout` or `stderr`.
1297+
```julia
1298+
julia> io = open(...)
1299+
1300+
julia> redirect_stdio(f, stdout=io, stdin=io) # not supported
1301+
```
1302+
1303+
!!! compat "Julia 1.7"
1304+
`redirect_stdio` requires Julia 1.7 or later.
1305+
"""
1306+
function redirect_stdio(f; stdin=nothing, stderr=nothing, stdout=nothing)
1307+
1308+
function resolve(new::Nothing, oldstream, mode)
1309+
(new=nothing, close=false, old=nothing)
1310+
end
1311+
function resolve(path::AbstractString, oldstream,mode)
1312+
(new=open(path, mode), close=true, old=oldstream)
1313+
end
1314+
function resolve(new, oldstream, mode)
1315+
(new=new, close=false, old=oldstream)
1316+
end
1317+
1318+
same_path(x, y) = false
1319+
function same_path(x::AbstractString, y::AbstractString)
1320+
# if x = y = "does_not_yet_exist.txt" then samefile will return false
1321+
(abspath(x) == abspath(y)) || samefile(x,y)
1322+
end
1323+
if same_path(stderr, stdin)
1324+
throw(ArgumentError("stdin and stderr cannot be the same path"))
1325+
end
1326+
if same_path(stdout, stdin)
1327+
throw(ArgumentError("stdin and stdout cannot be the same path"))
1328+
end
1329+
1330+
new_in , close_in , old_in = resolve(stdin , Base.stdin , "r")
1331+
new_out, close_out, old_out = resolve(stdout, Base.stdout, "w")
1332+
if same_path(stderr, stdout)
1333+
# make sure that in case stderr = stdout = "same/path"
1334+
# only a single io is used instead of opening the same file twice
1335+
new_err, close_err, old_err = new_out, false, Base.stderr
1336+
else
1337+
new_err, close_err, old_err = resolve(stderr, Base.stderr, "w")
1338+
end
1339+
1340+
redirect_stdio(; stderr=new_err, stdin=new_in, stdout=new_out)
1341+
1342+
try
1343+
return f()
1344+
finally
1345+
redirect_stdio(;stderr=old_err, stdin=old_in, stdout=old_out)
1346+
close_err && close(new_err)
1347+
close_in && close(new_in )
1348+
close_out && close(new_out)
1349+
end
1350+
end
1351+
1352+
function (f::RedirectStdStream)(thunk::Function, stream)
12341353
stdold = f.unix_fd == 0 ? stdin :
12351354
f.unix_fd == 1 ? stdout :
12361355
f.unix_fd == 2 ? stderr :
@@ -1243,6 +1362,7 @@ function (f::redirect_stdio)(thunk::Function, stream)
12431362
end
12441363
end
12451364

1365+
12461366
"""
12471367
redirect_stdout(f::Function, stream)
12481368

doc/src/base/io-network.md

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Base.iswritable
3636
Base.isreadable
3737
Base.isopen
3838
Base.fd
39+
Base.redirect_stdio
3940
Base.redirect_stdout
4041
Base.redirect_stdout(::Function, ::Any)
4142
Base.redirect_stderr

test/spawn.jl

+68
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,74 @@ end
261261
end
262262
end
263263

264+
@testset "redirect_stdio" begin
265+
266+
function hello_err_out()
267+
println(stderr, "hello from stderr")
268+
println(stdout, "hello from stdout")
269+
end
270+
@testset "same path for multiple streams" begin
271+
@test_throws ArgumentError redirect_stdio(hello_err_out,
272+
stdin="samepath.txt", stdout="samepath.txt")
273+
@test_throws ArgumentError redirect_stdio(hello_err_out,
274+
stdin="samepath.txt", stderr="samepath.txt")
275+
276+
@test_throws ArgumentError redirect_stdio(hello_err_out,
277+
stdin=joinpath("tricky", "..", "samepath.txt"),
278+
stderr="samepath.txt")
279+
mktempdir() do dir
280+
path = joinpath(dir, "stdouterr.txt")
281+
redirect_stdio(hello_err_out, stdout=path, stderr=path)
282+
@test read(path, String) == """
283+
hello from stderr
284+
hello from stdout
285+
"""
286+
end
287+
end
288+
289+
mktempdir() do dir
290+
path_stdout = joinpath(dir, "stdout.txt")
291+
path_stderr = joinpath(dir, "stderr.txt")
292+
redirect_stdio(hello_err_out, stderr=devnull, stdout=path_stdout)
293+
@test read(path_stdout, String) == "hello from stdout\n"
294+
295+
open(path_stderr, "w") do ioerr
296+
redirect_stdio(hello_err_out, stderr=ioerr, stdout=devnull)
297+
end
298+
@test read(path_stderr, String) == "hello from stderr\n"
299+
end
300+
301+
mktempdir() do dir
302+
path_stderr = joinpath(dir, "stderr.txt")
303+
path_stdin = joinpath(dir, "stdin.txt")
304+
path_stdout = joinpath(dir, "stdout.txt")
305+
306+
content_stderr = randstring()
307+
content_stdout = randstring()
308+
309+
redirect_stdio(stdout=path_stdout, stderr=path_stderr) do
310+
print(content_stdout)
311+
print(stderr, content_stderr)
312+
end
313+
314+
@test read(path_stderr, String) == content_stderr
315+
@test read(path_stdout, String) == content_stdout
316+
end
317+
318+
# stdin is unavailable on the workers. Run test on master.
319+
ret = Core.eval(Main,
320+
quote
321+
remotecall_fetch(1) do
322+
mktempdir() do dir
323+
path = joinpath(dir, "stdin.txt")
324+
write(path, "hello from stdin\n")
325+
redirect_stdio(readline, stdin=path)
326+
end
327+
end
328+
end)
329+
@test ret == "hello from stdin"
330+
end
331+
264332
# issue #36136
265333
@testset "redirect to devnull" begin
266334
@test redirect_stdout(devnull) do; println("Hello") end === nothing

0 commit comments

Comments
 (0)