7

I'd like to write the contents of a macro to an external file. The code below

\documentclass{article}
\begin{document}
\newcommand\foo[3]{1: #1, 2: #2 and 3: #3}

\foo{foo}{bar}{baz}

\foo{See \ref{sec}}{\begin{description}
  \item[whatever] Hello
\end{description}}{Some text}

\section{A section}
\label{sec}

\end{document}

writes 1: foo, 2: bar and 3: baz and 1: See 1, 2: whatever Hello and 3: Some text into the PDF file. This should go to an external file, perhaps as XML or CSV or something else parsable. I only need the output (line breaks can be ignored) of the macro, not the source (\ref{...} for example)

I use LuaLaTeX, so I might just go ahead and parse the output nodes, but there might be a solution at the macro level?

  • 1
    what you say is probably what you should do. what for example tex4ht does to get that into an html/xml markup is write \specials into the dvi and then use a dvi driver that generates xml markup from the specials and the text from the dvi, but working at that dvi level is conceptually the same as iterating over the lua node tree so you should probably self-answer as you're probably better at that than most people:-) – David Carlisle Jan 29 '16 at 12:30
  • the first foo bar baz case would of course be easy to do just in tex but working out what text results from a complex environment isn't really something easy to do at that level. – David Carlisle Jan 29 '16 at 12:32
  • @DavidCarlisle Thanks for the answer. I have experiemented with that a bit at the macro level, but I am such a loser at macro programming so I thought I ask here.... – topskip Jan 29 '16 at 12:45
5

Judging from the comments, you know this already but you can open a file with something like

\newwrite\myoutput
\immediate\openout\myoutput=\jobname.output

I have used the tex file name \jobname for the output file with an "output" extension. You can write to this file using

\write\myputout{stuff}

With your MWE, things go horribly wrong with expansion. To get around this in the past I have used an \unexpandedwrite macro that I found somewhere in TeXbook. Using this I can get your MWE to compile and produce the output:

1: foo
2: bar
 and 3: baz
1: See 1\hbox {}
2: \begin {description} \item [whatever] Hello \end {description}
 and 3: Some text

Depending on what you expect for #1, #2, and #3 you could of course use \write for some of them and \unexpandedwrite for others. This is what I have done below.

EDIT

@jfbu points out in the comments that there is now an \unexpanded macro so \unexpandedwrite is not needed anymore, so my MWe can be shortened to:

\documentclass{article}

\newwrite\myoutput
\immediate\openout\myoutput=\jobname.output

\newcommand\foo[3]{%
  \write\myoutput{1: #1}
  \write\myoutput{\unexpanded{2: #2}}
  \write\myoutput{ and 3: #3}
}

\begin{document}
  \foo{foo}{bar}{baz}

  \foo{See \ref{sec}}{\begin{description}
    \item[whatever] Hello
  \end{description}}{Some text}

  \section{A section}
  \label{sec}
\end{document}

Here is the original code:

\documentclass{article}

%% from the TeXbook
\def\\{\let\stoken= } \\
\long\def\unexpandedwrite#1#2{\def\finwrite{\write#1}%
  {\aftergroup\finwrite\aftergroup{\sanitize#2\endsanity}%
  }}
\def\sanitize{\futurelet\next\sanswitch}
\def\sanswitch{\ifx\next\endsanity
  \else\ifcat\noexpand\next\stoken\aftergroup\space\let\next=\eat
  \else\ifcat\noexpand\next\bgroup\aftergroup{\let\next=\eat
  \else\ifcat\noexpand\next\egroup\aftergroup}\let\next=\eat
  \else\let\next=\copytoken\fi\fi\fi\fi \next
}
\def\eat{\afterassignment\sanitize \let\next= }
\long\def\copytoken#1{%
  \ifcat\noexpand#1\relax\aftergroup\noexpand
  \else\ifcat\noexpand#1\noexpand~\aftergroup\noexpand\fi\fi
  \aftergroup#1\sanitize%
}
\def\endsanity\endsanity{}

\newwrite\myoutput
\immediate\openout\myoutput=\jobname.output

\newcommand\foo[3]{%
  \write\myoutput{1: #1}
  \unexpandedwrite\myoutput{2: #2}
  \write\myoutput{ and 3: #3}
}

\begin{document}
  \foo{foo}{bar}{baz}

  \foo{See \ref{sec}}{\begin{description}
    \item[whatever] Hello
  \end{description}}{Some text}

  \section{A section}
  \label{sec}
\end{document}
  • 1
    Nowadays, one could simply do \write\myoutput{\unexpanded{1: #1}}% etc ... with eTeX's \unexpanded which is available for many years automatically to PDFTeX, LuaTeX, XeTeX engines. – user4686 Jan 29 '16 at 13:27
  • @jfbu Really!? Thanks. I'll add a comment. – Andrew Jan 29 '16 at 13:42
  • Thanks a lot! This is a nice (yet to decipher) piece of code, which comes close to what I want. A bonus would be to remove the TeX commands.., – topskip Jan 29 '16 at 13:53
4

A quick proof of concept:

\documentclass{article}
\usepackage{atbegshi}
\AtBeginShipout{\directlua{analyzebox(tex.box["AtBeginShipoutBox"])}}
\directlua{dofile("foo.lua")}

\begin{document}
\newcommand\foo[3]{1: \directlua{starttoken(1)}#1\directlua{stoptoken(1)},%
                   2: \directlua{starttoken(2)}#2\directlua{stoptoken(2)}%
               and 3: \directlua{starttoken(3)}#3\directlua{stoptoken(3)}}

\foo{foo}{bar}{baz}

\foo{See \ref{sec}}{\begin{description}
  \item[whatever] Hellö
\end{description}}{Some text}

\section{A section}
\label{sec}

\end{document}

and foo.lua is

function starttoken(n)
    local ud = node.new("whatsit","user_defined")
    ud.user_id = 41564
    ud.type = 100
    ud.value = n  * 2
    node.write(ud)
end

function stoptoken(n)
    local ud = node.new("whatsit","user_defined")
    ud.user_id = 41564
    ud.type = 100
    ud.value = n * 2 + 1
    node.write(ud)
end

local hlist = node.id("hlist")
local vlist = node.id("vlist")
local glyph = node.id("glyph")
local glue = node.id("glue")
local whatsit = node.id("whatsit")
local user_defined
for k,v in pairs(node.whatsits()) do
    if v == "user_defined" then
        user_defined = k
    end
end

local inlist = false
local charlist = {}
local charlists = {}

function analyzebox( box )
    while box do
        if box.id == hlist or box.id == vlist then
            analyzebox(box.list)
        elseif box.id == glyph and inlist then
            charlist[#charlist + 1] = unicode.utf8.char(box.char)
        elseif inlist and box.id == glue and #charlist > 0 and charlist[#charlist] ~= " " then
            charlist[#charlist + 1] = " "
        elseif box.id == whatsit and box.subtype == user_defined and box.user_id == 41564 then
            local curlist
            local val = box.value
            if val % 2 == 0 then
                inlist = true
            else
                inlist = false
                curlist = (val - 1) / 2
                if charlist[#charlist] == " " then
                    table.remove(charlist)
                end
                if curlist < 3 then
                    charlists[curlist] = charlist
                else
                    texio.write_nl(string.format("%q, %q, %q",table.concat(charlists[1],""),table.concat(charlists[2],""),table.concat(charlist,"")))
                    charlists = {}
                end
                charlist = {}
            end
        end
        box = box.next
    end
end

writes

"foo", "bar", "baz"
"See 1", "whatever Hellö", "Some text"

to the output...

  • The first problem I've seen is when a page break occurs »within the macro«. This will insert the page number in the output. And I should take care of ligatures... Otherwise it looks fine. – topskip Jan 29 '16 at 14:18
  • do it in a minipage:) (you can throw that away and re-set it including page breaks if you need it also in the output) – David Carlisle Jan 29 '16 at 14:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.