The problem you are facing is described in the docstring of display:
In general, you cannot assume that display output goes to stdout (unlike print(x) or show(x)). For example, display(x) may open up a separate window with an image. display(x) means "show x in the best way you can for the current output device(s)." If you want REPL-like text output that is guaranteed to go to stdout, use show(stdout, "text/plain", x) instead.
And the problem is that if you just write dtf the output is displayed using display.
Unfortunately I cannot recommend a simple work-around of this behavior (you could e.g. modify the destination where the display sends its output but then you will get also e.g. terminal escape sequences which I assume you do not want).
For instance Documenter.jl needs to intercept the output to test the REPL results and what it does is quite complex unfortunately, as you can check here.
Maybe there are better solutions, but the simplest one is to do what display documentation recommends, i.e. use show(stdout, "text/plain", x) instead (and just replace stdout with a custom stream).
For instance here you can check how we test output in DataFrames.jl.
PS. If you want to pass a Matrix to DataFrame constructor and want to generate column names automatically add :auto argument for column names, as DataFrame docsting explains:
It is also allowed to pass a vector of vectors or a matrix as as the first argument. In this case the second argument must be a vector of Symbols or strings specifying column names, or the symbol :auto to generate column names x1, x2, ... automatically.
EDIT
If you need to run external code blocks and fetch their output then the task is simpler. The only assumption is that these code blocks do not use display explicitly (which is unlikely). In this case just use e.g. Suppresor.jl and wrap whole code block in print like this:
julia> output = @capture_out print(begin
println("Hello")
dtf = DataFrame(
[8.04 9.14 7.46 6.58;
6.95 8.14 6.77 5.76;
8.33 9.26 7.81 8.47], :auto)
dtf
end)
"Hello\n3×4 DataFrame\n Row │ x1 x2 x3 x4\n │ Float64 Float64 Float64 Float64\n─────┼────────────────────────────────────\n 1 │ 8.04 9.14 7.46 6.58\n 2 │ 6.95 8.14 6.77 5.76\n 3 │ 8.33 9.26 7.81 8.47"
julia> println(output)
Hello
3×4 DataFrame
Row │ x1 x2 x3 x4
│ Float64 Float64 Float64 Float64
─────┼────────────────────────────────────
1 │ 8.04 9.14 7.46 6.58
2 │ 6.95 8.14 6.77 5.76
3 │ 8.33 9.26 7.81 8.47
in this way you fetch both all what is printed to stdout within the block and the value of last expression gets printed (or shown - whatever you would prefer).
As commented this will work correctly unless display is called by the user within the block but it is unlikely as display is normally not used explicitly.