Add a prefix to every `println()` output

When using logging macros with Julia, I noticed that the output is framed, i.e., it uses certain unicode characters as prefix to create a one-sided frame, like so:

julia> warning = """Lorem ipsum dolor sit amet, consectetur adipiscing elit.
       Donec in erat ut felis molestie tincidunt vel quis sem.
       Aenean id justo cursus, luctus justo at, dignissim ligula.
       Praesent porttitor ante in egestas commodo.
       Vestibulum ac nibh convallis, gravida leo sed, blandit turpis.
       Morbi ut nisi ut ex pretium viverra at at lorem.
       Nunc porta justo vel bibendum feugiat.
       Curabitur semper tellus ut nunc porttitor rutrum.
       """
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nDonec in erat ut felis molestie tincidunt vel quis sem.\nAenean id justo cursus, luctus justo at, dignissim ligula.\nPraesent porttitor ante in egestas commodo.\nVestibulum ac nibh convallis, gravida leo sed, blandit turpis.\nMorbi ut nisi ut ex pretium viverra at at lorem.\nNunc porta justo vel bibendum feugiat.\nCurabitur semper tellus ut nunc porttitor rutrum.\n"

julia> @warn warning
┌ Warning: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
│ Donec in erat ut felis molestie tincidunt vel quis sem.
│ Aenean id justo cursus, luctus justo at, dignissim ligula.
│ Praesent porttitor ante in egestas commodo.
│ Vestibulum ac nibh convallis, gravida leo sed, blandit turpis.
│ Morbi ut nisi ut ex pretium viverra at at lorem.
│ Nunc porta justo vel bibendum feugiat.
│ Curabitur semper tellus ut nunc porttitor rutrum.
└ @ Main REPL[18]:1

julia>

Notice that the @warn output is all within a frame to mark its context. I would like to use this syntax elsewhere in my code. Is it possible to get just the framed output without calling one of the logging macros?

In other words, it is possible to add a prefix to every println() function output without creating a custom function?

The logging output code is here and is somewhat specialized for logging, but it would be pretty easy to write something similar.

For example:

function printframed(io::IO, args...; label=nothing)
    lines = split(string(args...), '\n')
    iob = IOBuffer()
    if isempty(lines)
        println(iob, '┌')
    else
        println(iob, "┌ ", lines[1])
        for i = 2:length(lines)
            println(iob, "│ ", lines[i])
        end
    end
    if !isnothing(label)
        println(iob, "└ ", label)
    end
    print(io, String(take!(iob)))
end
printframed(args...; label=nothing) = printframed(stdout, args...; label=label)

gives

julia> printframed("The quick\nbrown fox\njumped over the lazy dog.", label="@ example")
┌ The quick
│ brown fox
│ jumped over the lazy dog.
└ @ example
1 Like

Sorry, I believe I wasn’t clear with my question.
What I really meant was to achieve that framed output (or the middle lines of the framed output) but with mutiple println calls.
Basically something like set_printprefix("│ "), so that all consequent printed lines have a prefix.
I do not know how to achieve this in a language like Julia without OOP (i could use classes and methods in Python).

So, to be clear, I want a way to make it so that the default println (or a custom printprefixed) function prints with a prefix without explicitly providing it. Not a just single function which I call once.

Define a higher-order function:

julia> function prefixprinter(prefix)
           function wrappedprint(s)
               print(prefix)
               println(s)
           end
       end
prefixprinter (generic function with 1 method)

julia> const greet = prefixprinter("hello: ")
(::var"#wrappedprint#4"{String}) (generic function with 1 method)

julia> greet("world")
hello: world

julia> greet("everyone")
hello: everyone

And, I have found a solution, which is to use closures:
Here is the code:

function get_printinfo(prefix::String="", prefix_arr::Vector=[""])::Function
    if prefix != ""
        push!(prefix_arr, prefix)
    end
    function printinfo(s::String=""; prefix::String="", pop::Bool=false)::Function
        if pop
            pop!(prefix_arr)
        end
        if s != ""
            fullprefix = join(prefix_arr, "")
            print(fullprefix)
            println(s)
        end
        return get_printinfo(prefix, prefix_arr)
    end
    return printinfo
end

This function returns a closure function that, when called, prints the string with the given prefix (implicitly passed).
The new function can also be used to recursively add more prefixes on top or pop the last one.
Note that when the prefix and pop arguments are used, they actually modify the existing function itself, and also return itself. prefix argument takes effect in next call, while pop argument takes effect in current one.

An example on how to print repl-like output with this (repl> lines are actually printed output not actual an repl prompt):

julia> replprint=get_printinfo("repl> ")
(::var"#printinfo#13"{var"#printinfo#10#14"{Vector{String}}}) (generic function with 2 methods)

julia> replprint("@time somefunc(arg1, arg2)")
repl> @time somefunc(arg1, arg2)
(::var"#printinfo#13"{var"#printinfo#10#14"{Vector{String}}}) (generic function with 2 methods)

julia> timereplprint=replprint(prefix="@time ")  # `replprint` itself is also modified
(::var"#printinfo#13"{var"#printinfo#10#14"{Vector{String}}}) (generic function with 2 methods)

julia> timereplprint("somefunc(arg1, arg2)")
repl> @time somefunc(arg1, arg2)
(::var"#printinfo#13"{var"#printinfo#10#14"{Vector{String}}}) (generic function with 2 methods)

julia> # new prefix takes effect in next call, but pop takes effect in current one
julia> whichreplprint=timereplprint("somefunc(arg1, arg2)", prefix="@which ", pop=true)
repl> somefunc(arg1, arg2)
(::var"#printinfo#13"{var"#printinfo#10#14"{Vector{String}}}) (generic function with 2 methods)

julia> timereplprint("somefunc(arg1, arg2)")  # `timereplprint` itself was also modified
repl> @which somefunc(arg1, arg2)
(::var"#printinfo#13"{var"#printinfo#10#14"{Vector{String}}}) (generic function with 2 methods)

julia> whichreplprint("somefunc(arg1, arg2)")
repl> @which somefunc(arg1, arg2)
(::var"#printinfo#13"{var"#printinfo#10#14"{Vector{String}}}) (generic function with 2 methods)

Thanks for your help! I wrote a very similar function but with the ability to recursively add more prefixes on top, and pop the last one.

Actually, I intend to use this while writing tests, so I want to use a single convenient printinfo function at every point in the tests, so that I can dynamically change its prefix. Also the context of @testset and the pop functionality helps to revert back to a previous prefix.

All I have to do is manually add “┌” and "└ " characters to the beginning of the first and last print statements and define printinfo, the middle lines are prefixed appropriately.

Example usage:

using Test

# `get_printinfo()` definition here...

# define `printinfo` once
printinfo = get_printinfo()

@testset "Set 1" begin
    printinfo("┌ Testing `Set 1`...")  # not updated
    @test true
    printinfo("└ Done.")
end
@testset "Set 2" begin
    printinfo("┌ Testing `Set 2`..."; prefix="│ ")  # updated `prefix`
    @testset "Set 2 A" begin
        printinfo("┌ Testing subset A..."; prefix="│ ")  # updated `prefix` again
        printinfo("check type")
        @test isa(true, Bool)
        printinfo("└ Done."; pop=true)
    end
    @testset "Set 2 B" begin
        printinfo("┌ Testing subset B..."; prefix="│ ")  # updated `prefix` again
        @test true
        printinfo("└ Done."; pop=true)
    end
    printinfo("└ Done.", pop=true)
end

Output:

┌ Testing `Set 1`...
└ Done.
┌ Testing `Set 2`...
│ ┌ Testing subset A...
│ │ check type
│ └ Done.
│ ┌ Testing subset B...
│ │ check inequality
│ └ Done.
└ Done.
1 Like

(You could also define a callable object, which is useful if you want to do more than just call it. Or an alternative IO stream type or similar to pass to println so that you can call println(prefixobject, ...) and it will call your own println method.)

I thought about using a callable object but then realized that my function provides pretty much the same functionality.
However, I think defining an alternative IO stream type could be useful, but I do not know how to do that or where to learn more about it. I did not find anything in the IO and Networking section of the Julia docs.