Redirect output and error to a file

I think it’s just something that nobody missed before :smiley: It’s actually pretty easy to do, take this proof of concept: (and if somebody did already implement that before me: I’m sorry, my search-foo is pretty weak. It’s also still pretty brittle.)
copytorepl.jl

File content
struct CopyToREPL <: AbstractDisplay
	io::IO
	copyto::AbstractDisplay
	insertnewlines::Bool
end

import Base: flush, close, display, displayable 

flush(d::CopyToREPL) = flush(d.io)
close(d::CopyToREPL) = close(d.io) # not the copyto display, we don't want to tear it down too

display(d::CopyToREPL, @nospecialize x) = display(d, MIME"text/plain"(), x)
display(d::CopyToREPL, M::MIME"text/plain", @nospecialize x) = _display(d, M, x)
function _display(d::CopyToREPL, M, @nospecialize x)
	show(d.io, M, x)
	d.insertnewlines && println(d.io)
	# you may insert a flush(d.io) here if you want to be able to kill the window without loosing content
	show(d.copyto, M, x)
end

displayable(d::CopyToREPL, M::MIME) = istextmime(M)
# it's probably better not to catch every MIME, idk
#function display(d::CopyToREPL, M::MIME, @nospecialize x)
#    displayable(d, M) || throw(MethodError(display, (d, M, x)))
#    _display(d.io, M, x)
#end

function gethighesttextdisplay()
	displays = Base.Multimedia.displays
	message = "Probing displays!" 
	# because we need to find the last display that actually outputs text 
	# or we may end up backing up to a plotting pane or so. There may be a better way to do this.
	for i = length(displays):-1:1
        if Base.Multimedia.xdisplayable(displays[i], message)
            try
                display(displays[i], message)
				return displays[i]
            catch e
                isa(e, MethodError) && (e.f === display || e.f === show) ||
                    rethrow()
            end
        end
    end
    error("No text display found, that's weird.")
end

function pophighestcopyto()
	displays = Base.Multimedia.displays
	for i = length(displays):-1:1
        if displays[i] isa CopyToREPL
            return splice!(displays, i)
        end
    end
    throw(KeyError(CopyToREPL))
end

function startREPLcopy(noIO; append = false)
	io = open(noIO, write = true, append = append)
	startREPLcopy(io)
end

stdoutbackup = []

function startREPLcopy(io::IO; insertnewlines = true)
	# insertnewlines because show(...) usually doesn't insert newlines after display,
	# so the output is kinda mashed together. There may be cleaner way to do this.
	backupdisplay = gethighesttextdisplay()
	cpdisplay = CopyToREPL(io, backupdisplay, insertnewlines)
	@info "Started copying REPL output to the specified location!"
	pushdisplay(cpdisplay)
	# TODO: It would be better to check here if the stdout hadn't been redirected before
	# So technically, this may snatch a stdout and display more than the REPL
	push!(stdoutbackup, Base.stdout)
	Base.redirect_stdout(io) 
	# TODO I think this does only work for IOStreams, so maybe unwrap contexts and such
	# TODO: may or may not be desirable to handle stderr
	nothing
end

function endREPLcopy()
	cpdisplay = try
		pophighestcopyto()
	catch e
		isa(e, KeyError) || rethrow()
		@warn "Couldn't stop copying the REPL output because it wasn't even started."
	end
	flush(cpdisplay)
	oldstdout = pop!(stdoutbackup)
	Base.redirect_stdout(oldstdout)
	close(cpdisplay)
	@info "Copying of REPL output stopped!"
	nothing
end

and a demonstration:

julia> include("copytorepl.jl")
endREPLcopy (generic function with 1 method)

julia> startREPLcopy("testlog.log")
"Probing displays!"
[ Info: Started copying REPL output to the specified location!

julia> test = 2
2

julia> test
2

julia> println("Bla-Keks")

julia> show("αβ↑")

julia> notdefined
ERROR: UndefVarError: notdefined not defined

julia> # note that the error does not print

julia> rand(8)
8-element Vector{Float64}:
 0.48184594197528385
 0.4667812261893638
 0.43096133172880347
 0.791219818003811
 0.12263780916474376
 0.15859996424283396
 0.28204060573062506
 0.3664465404400239

julia> "Bye"
"Bye"

julia> endREPLcopy()
[ Info: Copying of REPL output stopped!

gives:
testlog.log

2
2
Bla-Keks
"αβ↑"8-element Vector{Float64}:
 0.48184594197528385
 0.4667812261893638
 0.43096133172880347
 0.791219818003811
 0.12263780916474376
 0.15859996424283396
 0.28204060573062506
 0.3664465404400239
"Bye"

May need some more formatting and cleanup but it is doable in principle.

6 Likes