In addition to another good answer (+1!) I would like to present a clearer solution to such problems.
The basic idea is to make format/2 available in DCG, and then use DCG to describe the output.
It is very simple using the codes format/3 argument provided by several Prolog implementations. All you need is the following short helper definitions:
format_(Data, Args) --> call(format_dlist(Data, Args)). format_dlist(Data, Args, Cs0, Cs) :- format(codes(Cs0,Cs), Data, Args).
A nonterminal call//1 calls its argument with two additional arguments that allow you to access the implicit DCG arguments, and this is used to describe additional codes through format/3 .
Now we can just use nonterminal format_//2 in DCG.
For example, to describe a simple table:
table --> row([a,b,c]), row([d,e,f]). row(Ls) --> format_("~t~w~10+~t~w~10+~t~w~10+~n", Ls).
Usage example and result:
?- phrase(table, Cs), format("~s", [Cs]). abc def Cs = [32, 32, 32, 32, 32, 32, 32, 32, 32|...].
Note that the one last remaining format/2 used to actually input the output to the screen.
However, everything else does not contain side effects and declaratively describes the table.
An important advantage of this method is that you can easily write test cases to make sure your tables are (still) formatted correctly. It's easy to talk about lists of Prolog codes (described in DCG), but it's pretty hard to talk about things that only appear on the terminal.