3

I am trying to insert content from another file whilst optionally skipping the first n lines. I have control over the inserted content, and my current solution is simply to wrap the first n lines in sequentially named macros which I can redefine to nothing, thereby suppressing the input. This strikes me as rather ugly.

It seems input and include are not capable of skipping lines. There are packages like standalone, but I'm not trying to remove content before a begin{document}, I'm just trying to skip lines in a verse environment.

Specifically my imported files look like this:

% lines.tex
line 1\\
line 2\\
line 3\\

Note that 'source lines' and 'output lines' are identical in this case, as exactly one output line ending in \\ is found on one input line. I don't really mind which I end up working on.

And I'm looking to do something like this:

\begin{verse}
\includelines[2]{lines} % skips first 2 lines
\end{verse}

I thought it would be relatively straightforward to iterate over the included content linewise, but having not found anything it seems this isn't really done.

If I am simply thinking in the wrong paradigm and the 'correct' way is to do as I am currently doing (wrap lines in macros which can be redefined later and thus not output anything) I don't mind sticking with that.

I am using LuaLaTeX anyhow, so I'm open to solutions using Lua.

Current Solution

Currently I do this, but it clutters up both the document and the imported document. I could avoid cluttering the document by wrapping the \lets in an environment, and using a number-to-word routine together with \csname. But I was hoping for a solution which didn't requite cluttering up the included document.

\documentclass{article}
\begin{document}

% I define a long list of these, more than I will ever need
\def\printLine#1{#1}
\let\lineOne\printLine
\let\lineTwo\printLine
\let\lineThree\printLine
\let\lineFour\printLine
\def\dummy#1{}

\begin{verse}
  \let\lineOne\dummy
  \let\lineTwo\dummy
  % In reality we have an \input statement here
  \lineOne{line1\\}
  \lineTwo{line2\\}
  \lineThree{line3\\}
  \lineFour{line4\\}
  \let\LineOne\printLine
  \let\LineTwo\printLine
\end{verse}
\end{document}
0

2 Answers 2

4

Here I use readarray (which loads the forloop package). The optional argument is the line to begin on rather than skip. If you really prefer it the other way, let me know.

Two items to note (hat tip Marcel): 1) make sure you have the [2021-09-17] version of the readarray package, which will, with the setting \ignoreblankreadarrayrecordstrue, properly skip over fully commented lines when tabulating records from the input; 2) if a braced group spans more than a single line of the input file, the record that contains the group will contain the whole group, meaning that such a record will be seen to span several lines of input.

\begin{filecontents*}[overwrite]{lines.tex}
%
Line 1
Line 2
Line 3
% A COMMENT IN THE FILE
Line 4
\end{filecontents*}
\documentclass{article}
\usepackage{readarray}[2021-09-17]
\ignoreblankreadarrayrecordstrue
\newcounter{lino}
\newcommand\includelines[2][1]{%
  \readrecordarray{#2}\linearray
  \forloop{lino}{#1}{\thelino < \numexpr1+\linearrayROWS}
   {\ifnum\value{lino}>#1\\\fi
    \linearray[\thelino]}
}

\begin{document}
\includelines{lines.tex}

\includelines[2]{lines.tex}

\includelines[3]{lines.tex}

\includelines[4]{lines.tex}

\end{document}

enter image description here

3
  • Thank you! readarray was exactly the kind of interface I was looking for. I'll stick with this so I'm not tied to lua, although I'm much more at home in lua in general than TeX.
    – 2e0byo
    Oct 4, 2021 at 16:49
  • 1
    @MarcelKrüger Thank you for your comments. The first issue was already resolved with the 2021-09-17 release of readarray, which I will add to the MWE. The second issue is what it is. Groups are preserved during the read. I will note it in the answer. Oct 4, 2021 at 18:43
  • 1
    @MarcelKrüger I needed to activate \ignoreblankreadarrayrecordstrue to ignore the blank records. As a result, I had to increase the loop terminus by 1, since the final blank record in the file is no longer counted as a record. Oct 4, 2021 at 18:59
2

This could be done with pure TeX code, but here is a Lua solution (explanations added as comments):

\documentclass{article}
\newluafunction\skiplines
\directlua{
  % Some boilerplate
  local func = \the\allocationnumber
  token.set_lua('skiplines', func, 'protected')
  local input = token.create'input'
  lua.get_functions_table()[func] = function()
    % Read the number of lines to be skipped
    local count = token.scan_int()
    % Add a callback to change how TeX files are opened
    luatexbase.add_to_callback('open_read_file', function(name)
      % We only want to affect a single file, so we should remove the callback again.
      %% luatexbase.remove_from_callback('open_read_file', 'skip_input_lines')
      % But we delay this until TeX actually tries to read from the file since LaTeX might open the file multiple times.
      % Now open the file for reading
      local f = io.open(name, 'r')
      % If the file can't be opened we return nothing.
      if not f then return end
      % Remember if we are reading the first line
      local first = true
      % And return the reader table
      return {
        reader = function()
          % If TeX is trying to read the first line, unregister the callback and skip the first lines
          if first then
            luatexbase.remove_from_callback('open_read_file', 'skip_input_lines')
            for i = 1, count do f:read'l' end
            first = false
          end
          % Then just read a line and report it to TeX
          return f:read'l'
        end,
        % Remember to close the file when we are done with it
        close = function() return f:close() end,
      }
    end, 'skip_input_lines')
    % Finally add a \input to read the filename and initiate the reading.
    token.put_next(input)
  end
  % This code contains a small bug: If the file does not exists,
  % luatexbase.remove_from_callback will not be called and the
  % next time LaTeX tries to read a file the first lines get lost.
  % Avoiding this is complicated since LaTeX sometimes tries to
  % open non-existing files before opening the real one, so we can't
  % run remove_from_callback if the file doesn't exists.
  % It's not a big issue though since \input with a non existing filename is an error anyway,
  % So the issue only occurs if the compilation fails anyway. If a user chooses to ignore the
  % error message, they shouldn't expect sensible behavior afterwards.
}
\begin{document}
\begin{verse}
  Skip 0 lines:\\
  \skiplines 0{lines}
  after\\
\end{verse}
\begin{verse}
  Skip 1 lines:\\
  \skiplines 1{lines}
  after\\
\end{verse}
\begin{verse}
  Skip 2 lines:\\
  \skiplines 2{lines}
  after\\
\end{verse}
\begin{verse}
  Skip 3 lines:\\
  \skiplines 3{lines}
  after\\
\end{verse}
\begin{verse}
  Skip 4 lines:\\
  \skiplines 4{lines}
  after\\
\end{verse}
\end{document}

The output

Maybe you are wondering why it looks like it skips one line less than expected: My version of lines.tex contains a comment in the first line (as in the question), so the first skipped line is the comment line which doesn't show in the output.

2
  • Thank you, that's extremely thorough and very easy to follow. I'm going to leave it open a bit longer in case there's a plain TeX solution, but this is a whole lot less hacky than what I'm doing, and thus a whole lot better. The 'bug' is indeed not a problem at all.
    – 2e0byo
    Oct 4, 2021 at 15:17
  • As an aside I had no idea how easy it was to override commands with Lua, I'll defintely be using that over plain TeX next time I want even remotely complicated behaviour
    – 2e0byo
    Oct 4, 2021 at 15:17

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .