Skip to content

Instantly share code, notes, and snippets.

@mortenpi
Last active November 22, 2020 10:59
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mortenpi/554d074a582748cf3af48dd25eccfcc7 to your computer and use it in GitHub Desktop.
Save mortenpi/554d074a582748cf3af48dd25eccfcc7 to your computer and use it in GitHub Desktop.
Example code to capture STDOUT and STDERR in Julia.
# A wrapper function to capture STDOUT and STDERR into strings.
#
# Heavily inspired by
# https://github.com/JuliaStats/RCall.jl/blob/c1ff136864964cf2ac04b679f0c1b3b243df7e37/src/iface.jl#L35-L46
# referred to by the issue comment
# https://github.com/JuliaLang/julia/issues/12711#issuecomment-133045787
#
# The Base.start_reading(stream, cb) requires fixes in Base.
#
function capture_streams(f)
_stdout, _stderr = STDOUT, STDERR
stdout_rd, stdout_wr = redirect_stdout()
stderr_rd, stderr_wr = redirect_stderr()
# buf combines the stdout and stderr
buf, buf_stdout, buf_stderr = IOBuffer(), IOBuffer(), IOBuffer()
# the signature of the callback is cb(stream, n)
function cb_stdout(s,n)
bytes = read(s,n)
write(buf_stdout, bytes)
write(buf, bytes)
false
end
Base.start_reading(stdout_rd, cb_stdout)
function cb_stderr(s,n)
bytes = read(s,n)
write(buf_stderr, bytes)
write(buf, bytes)
false
end
Base.start_reading(stderr_rd, cb_stderr)
ret = try
f()
catch e
println("ERROR in capture_streams(): $e")
finally
# read and restore
redirect_stdout(_stdout)
redirect_stderr(_stderr)
close(stdout_wr)
close(stderr_wr)
#stdout_buf = readstring(stdout_rd)
#stderr_buf = readstring(stderr_rd)
close(stdout_rd)
close(stderr_rd)
end
ret, takebuf_string(buf), takebuf_string(buf_stdout), takebuf_string(buf_stderr)
end
# Test / examples
function foo(x)
for n = 1:x
info("$n/$x \t[to STDERR]")
println("$n/$x \t[to STDOUT]")
end
x
end
function test_capture(f)
ret, all, out, err = capture_streams(f)
println("Return value: $(ret)")
println("------ STDOUT+STDERR -------")
println(all)
println("---------- STDOUT ----------")
println(out)
println("---------- STDERR ----------")
println(err)
println("----------------------------")
end
info("Capturing normally:")
test_capture() do
info("Calling foo()")
foo(5)
end
println(); println()
info("Capturing when error is thrown:")
try
test_capture() do
f(200) # error -- function does not exist
end
catch e
println("ERROR: $e")
end
info("Streams restored.")
Return value: 5
------ STDOUT+STDERR -------
INFO: Calling foo()
INFO: 1/5 [to STDERR]
1/5 [to STDOUT]
INFO: 2/5 [to STDERR]
2/5 [to STDOUT]
INFO: 3/5 [to STDERR]
3/5 [to STDOUT]
INFO: 4/5 [to STDERR]
4/5 [to STDOUT]
INFO: 5/5 [to STDERR]
5/5 [to STDOUT]
---------- STDOUT ----------
1/5 [to STDOUT]
2/5 [to STDOUT]
3/5 [to STDOUT]
4/5 [to STDOUT]
5/5 [to STDOUT]
---------- STDERR ----------
INFO: Calling foo()
INFO: 1/5 [to STDERR]
INFO: 2/5 [to STDERR]
INFO: 3/5 [to STDERR]
INFO: 4/5 [to STDERR]
INFO: 5/5 [to STDERR]
----------------------------
Return value: nothing
------ STDOUT+STDERR -------
ERROR in capture_streams(): UndefVarError(:f)
---------- STDOUT ----------
ERROR in capture_streams(): UndefVarError(:f)
---------- STDERR ----------
----------------------------
INFO: Capturing normally:
INFO: Capturing when error is thrown:
INFO: Streams restored.
diff --git a/base/stream.jl b/base/stream.jl
index d649b88..963b785 100644
--- a/base/stream.jl
+++ b/base/stream.jl
@@ -445,7 +445,7 @@ function notify_filled(stream::LibuvStream, nread::Int)
more = true
while more
if isa(stream.readcb,Function)
- nreadable = (stream.line_buffered ? Int(search(stream.buffer, '\n')) : nb_available(stream.buffer))
+ nreadable = (stream.line_buffered ? Int(search(stream.buffer, UInt8('\n'))) : nb_available(stream.buffer))
if nreadable > 0
more = stream.readcb(stream, nreadable)
else
@@ -674,7 +674,7 @@ function start_reading(stream::LibuvStream)
end
function start_reading(stream::LibuvStream, cb::Function)
- failure = start_reading(stream)
+ failure_code = start_reading(stream)
stream.readcb = cb
nread = nb_available(stream.buffer)
if nread > 0
@rikhuijzer
Copy link

rikhuijzer commented Nov 20, 2020

If I understand the goal correctly, the code can be simplified by using pipeline. For example,

out = IOBuffer()
err = IOBuffer()

process = run(pipeline(`echo foo`, stdout=out, stderr=err))

exitcode = process.exitcode
out_s = String(take!(out))
err_s = String(take!(err))
close(out)
close(err)
println((exitcode, out_s, err_s))

shows

(0, "foo\n", "")

@mortenpi
Copy link
Author

@rikhuijzer Not quite. The goal of this snippet is to capture stdout/err from Julia code execution, not from subprocesses. But if you are interested in capturing subprocess output, the yes, pipeline would be the way to go.

Just for future reference for anyone stumbling upon this and planning on using it: there is a package that provides the capture_streams functionality (https://github.com/JuliaDocs/IOCapture.jl).

@rikhuijzer
Copy link

Aha, now I get it. Thanks for the clarification 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment