40

Let's say I compile the following file with pdflatex -shell-escape test.tex:

\documentclass{minimal}

\begin{document}

File listing is:

\immediate\write18{ls /usr}

\end{document}

This will send the output of the command ls /usr to report/log of pdflatex (primarily to stdout).

There are then two cases I'd like to utilize:

  • The output of ls /usr being included directly in the document (LaTeX stream).
  • The output of ls /usr becoming the contents of a \newcommand (by this I mean, that I'd like the script to be executed when the \newcommand is executed first - and upon subsequent calls to the new command, the shell code should not be executed anew).

I have read through How to execute shell script from LaTeX?, but I'm not sure if this "pipe input" can be applied to \newcommand.

I have also read through tex - How can I save shell output to a variable in LaTeX? - and it seems that Tex' file I/O should be used; but I'm quite disliking the fact that I'd still have to redirect the script output (actually, in this case, the ls /usr output) to a file, and then read it in, to have it as contents of a command.

So, is there an easier way to achieve what I'd want (hopefully, illustrated through an example based on the above code)?


EDIT: Ehm, I should have asked one more question earlier :) I'll try with an edit here, although it will probably get missed .. :)

I originally asked for a \newcommand that will execute shell code only upon its definition (i.e., in a sense it is "cached"); and the answer from @egreg does exactly that. But then - would it be possible to have a different \newcommand definition, such that each time this newcommand is called, the shell command is executed anew? I.e. executing something like \@@input|"cat tempfile" (which cannot be executed as such), where tempfile changes between calls?

sdaau
  • 17,079
  • 5
    You can say \newcommand{\foo}{\@@input|"cat tempfile"} (protect it with \makeatletter and \makeatother; each call of \foo will run the shell command. – egreg Apr 28 '11 at 17:50

4 Answers4

39
\documentclass{article}

\begingroup\makeatletter\endlinechar=\m@ne\everyeof{\noexpand}
\edef\x{\endgroup\def\noexpand\TeXpath{\@@input|"which tex" }}\x

\begin{document}
File listing is

{\catcode`_=12 \ttfamily
\input{|"ls /usr" }

}

\TeX{} is \TeXpath
\end{document}

We must use \@@input (the primitive \input command) because \input in LaTeX does assignments. The setting of \endlinechar is to avoid a spurious space in the expansion of \TeXpath.

When shell escape is active and the primitive \input finds a |, it accepts as input the standard output of the following shell command.

There should be a package by H. Oberdiek that does something of this kind.

Note An assignment is any TeX operation that gives a meaning or a value to a control sequence or register. During the \edef operation, TeX expands all commands it finds between the braces until only unexpandable tokens remain, but doesn't perform any assignment; rather, something like \catch=22 (where \catch is the name of a count register) remains completely inaltered. Since the definition of \input in LaTeX is

\@ifnextchar\bgroup\@iinput\@@input

the implicit assignments performed by \@ifnextchar would not be performed and both \@input and \@@input would be expanded, which results in a complete disaster. Conversely, the \input primitive (that LaTeX saves as \@@input) is expandable and its expansion consists in causing TeX to read the named file. One has, of course, to be careful about what this file contains, as also this will be expanded. So other precautions have to be taken when doing this kind of operations, depending on the nature of the tokens produced by the command we want to perform and this "solution" is only a skeleton for possible "real" applications.


Update 2019

After some years, things have changed and better methods are available.

For instance, with xparse and expl3 the code can be improved:

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\captureshell}{som}
 {
  \sdaau_captureshell:Ne \l__sdaau_captureshell_out_tl { #3 }
  \IfBooleanT { #1 }
   {% we may need to stringify the result
    \tl_set:Nx \l__sdaau_captureshell_out_tl
     { \tl_to_str:N \l__sdaau_captureshell_out_tl }
   }
  \IfNoValueTF { #2 }
   {
    \tl_use:N \l__sdaau_captureshell_out_tl
   }
   {
    \tl_set_eq:NN #2 \l__sdaau_captureshell_out_tl
   }
 }

\tl_new:N \l__sdaau_captureshell_out_tl

\cs_new_protected:Nn \sdaau_captureshell:Nn
 {
  \sys_get_shell:nnN { #2 } { } #1
  \tl_trim_spaces:N #1 % remove leading and trailing spaces
 }
\cs_generate_variant:Nn \sdaau_captureshell:Nn { Ne }
\ExplSyntaxOff

\begin{document}

\captureshell*[\TeXpath]{which tex} % we need to stringify it because of _

File listing is

{\ttfamily\captureshell{ls \jobname.*}\par}

\TeX{} is \texttt{\TeXpath}

\end{document}

enter image description here

We could add an error message if the user doesn't pass the -shell-escape option for the LaTeX run.

Check also texosquery (requires Java).

egreg
  • 1,121,712
  • @egreg: can we do it without a temporary file? – Display Name Jun 11 '11 at 06:37
  • 1
    @xport: this does not use a temporary file. – egreg Jun 11 '11 at 09:24
  • @egreg: How to use your code above, I got an error when compiling it. I am using Windows. – Display Name Jun 11 '11 at 09:26
  • @xport Did you enable the shell-escape on the command line with --enable-write18? – egreg Jun 11 '11 at 09:40
  • @egreg: Yes. Of course. I think "which tex" is the source of problem. – Display Name Jun 11 '11 at 09:43
  • 1
    @xport: use any command that the Windows shell accepts. That was only by way of example. – egreg Jun 11 '11 at 09:56
  • Hi egreg, could you explain "We must use \@@input (the primitive \input command) because \input in LaTeX does assignments." a bit further? I'm not sure what you mean by assignments. – Clément Jan 09 '12 at 19:04
  • @egreg When shell escape is active and the primitive \input finds a |, it accepts as input the standard output of the following shell command. I'm late to the party, but is there a way to make \input accept as inputs both stdout and stderr of the command? – jub0bs Jan 01 '15 at 09:54
  • 1
    @Jubobs Try redirecting STDERR to STDOUT. – egreg Jan 01 '15 at 09:56
  • @egreg Oh, do you mean in the shell command itself? – jub0bs Jan 01 '15 at 09:58
  • @Jubobs \input|"ls WHATEVER* 2>&1" gives me ls: WHATEVER*: No such file or directory; without 2>&1 the error message is in the log file – egreg Jan 01 '15 at 10:00
  • @egreg Yes, that works. Thank you. Do you know whether STDERR can be captured directly, though, without messing with the command itself? – jub0bs Jan 01 '15 at 10:01
  • @Jubobs No, there's only an input channel; you can redirect STDERR to a file and then examine it. – egreg Jan 01 '15 at 10:03
  • @egreg Ok. I'm trying to avoid saving anything to external files. I'll redirect STDERR in the shell command, so. Thanks again! – jub0bs Jan 01 '15 at 10:04
  • This fails for me with Undefined control sequence.\sdaau_captureshell:Nn ...->\tl_set_from_file:Nnn #1{}{|"#2"} l.26 \captureshell[\TeXpath]{which tex} – Karalga Nov 21 '17 at 16:21
  • @Karalga Are you sure you have an up-to-date TeX distribution? – egreg Nov 21 '17 at 16:36
  • I thought I had, but turns out I did not. – Karalga Nov 21 '17 at 16:50
  • I am trying to do \captureshell[\version]{git log -1 --pretty=format:"\@percentcharh\@percentcharx3b\@percentcharad" --date=short}, but it seems like .tex is being appended to the command (the error is unknown date format short.tex - this is on Windows, now latest MiKTeX. – Karalga Nov 21 '17 at 18:20
  • @Karalga Sorry, but I cannot test MiKTeX. Recall that for using \@percentchar you need \makeatletter and \makeatother; you seem to be missing spaces, though. – egreg Nov 21 '17 at 18:22
  • I should have searched harder. appending . helps, and \captureshell[\version]{git log -1 --pretty=format:"%h%x3b%ad" --date=short .} now works. – Karalga Nov 21 '17 at 18:26
  • I'm attempting to use the 2019 solution (with \sys_shell_get replaced with \sys_get_shell). This works fine as long as the shell command doesn't need to be expanded first (i.e. I've got some \edefed variables in there). Is there a simple way to expand the command fist? All my attempts so far have failed miserably. E.g. how to get this to work: \captureshell[\someVar]{cat \someOthervar}? – AVH Nov 27 '19 at 21:24
  • @Darhuuk I fixed the typo and added full expansion of the argument. – egreg Nov 27 '19 at 21:40
  • @egreg Fantastic, thank you so much! I don't have my head wrapped around the expl3-syntax yet. Seems so easy in hindsight :). – AVH Nov 27 '19 at 21:59
  • In the second solution tl;dr the key point is to use the \sys_get_shell:nnN expl3 function. Side note, \cctab_select:N \c_other_cctab in the setup to be foolproof -- at least excluding the case of XeLaTeX character char code ≥ 256 and weird catcode. – user202729 Aug 05 '22 at 11:42
  • I really like the updated solution. I kind of would have liked it as a separate answer. However, it not only solves my issue of capturing (calling git log too often is extremely slow otherwise) but also the stringification to work with underscores. Many thanks. – Gunter May 12 '23 at 06:05
  • I've tried to define \captureshell*[\NTeX]{ls *.tex | wc -l} and then added in the text Number of \TeX{} in current folder is: \num{\NTeX{}}, using the \num from siunitx package. But it gives me error. – LEo Oct 24 '23 at 19:16
  • @LEo No error for me. Did you run pdflatex with -shell-escape enabled? – egreg Oct 24 '23 at 21:04
  • @egreg yes. The error message is: ! Argument of \__siunitx_number_parse_loop_main_digit:NNNN has an extra }. – LEo Oct 24 '23 at 21:16
  • @LEo What's the console output if you add \show\NTeX? – egreg Oct 24 '23 at 21:45
  • @egreg > \NTeX=macro: ->1. the right number of .tex files in current folder – LEo Oct 24 '23 at 22:24
  • 1
    @LEo Now I see! You have {} that’s the cause of the problem. Use it after a macro when you want to preserve a space, which is out of the question here. – egreg Oct 25 '23 at 06:05
18

Here is a simple way of doing this, using my bashful package

\documentclass{article}
\usepackage[a6paper]{geometry}
\usepackage{bashful}

\begin{document}
\bash[script,stdout]
ls -F /usr
\END
\end{document}

which generates Incorporating the output of <code>ls</code> into your LaTeX document

Yossi Gil
  • 15,951
  • 1
    This doesn't seem to save the output to a variable, which I probably the main point of the question? –  Feb 14 '18 at 10:32
  • The output is stored in \bashStdout, and without the script option, the output is not expanded by the \bash command itself. – Nicola Gigante Dec 20 '19 at 17:57
2

I suppose this snippet will be helpful in this thread.

https://gist.github.com/w495/7328b76e76aee49657e0bd7a3b46c870

% !TeX encoding = UTF-8
\ProvidesPackage{bashline}[2016/10/24 v. 0.1]

\makeatletter
    \newcommand{\bashline@file@name}[1]{%
        /tmp/${USER}-${HOSTNAME}-\jobname-#1.tex%
    }
    \newread\bashline@file
    \newcommand{\bashline@command@one}[2][tmp]{%
        \immediate\write18{#2 > \bashline@file@name{#1}}
        \openin\bashline@file=\bashline@file@name{#1}
        % The group localizes the change to \endlinechar
        \bgroup
            \endlinechar=-1
            \read\bashline@file to \localline
            % Since everything in the group is local, 
            % we have to explicitly make the assignment global
            \global\let\bashline@result\localline
        \egroup
        \closein\bashline@file
        % Clean up after ourselves
        \immediate\write18{rm \bashline@file@name{#1}}
        \bashline@result
    }
    \newcommand{\bashline@command@many}[2][tmp]{%
        \immediate\write18{#2 > \bashline@file@name{#1}}
        \openin\bashline@file=\bashline@file@name{#1}
        % The group localizes the change to \endlinechar
        \newcount\linecnt
        \bgroup
            \endlinechar=-1
            \loop\unless\ifeof\bashline@file 
                \read\bashline@file to \localline%
                \localline
                \newline
            \repeat
        \egroup
        \closein\bashline@file
        % Clean up after ourselves
        \immediate\write18{rm \bashline@file@name{#1}}
    }
    \newcommand{\bashline}[2][tmp]{%
        \bashline@command@one[#1]{#2}%
    }
    \newcommand{\bashlines}[2][tmp]{%
        \bashline@command@many[#1]{#2}%
    }
\makeatother

\newcommand{\urandomstring}[1]{%
    \bashline{cat /dev/urandom | tr -dc "A-Za-z0-9" | fold -c#1 | head -1}%
}

\newcommand{\bashdate}{%
    \bashline{date --iso-8601}%
}

\newcommand{\bashdatetime}{%
    \bashline{date --iso-8601=seconds}%
}

\newcommand{\commit}{%
    \bashline{git describe --dirty }%
}

\newcommand{\commitlog}{%
    \bashline{git log -1 --oneline}%
}

\newcommand{\branch}{%
    \bashline{git describe --all}%
}

\endinput

It is based on Antal's answer in «How can I save shell output to a variable in LaTeX?». For example check \urandomstring. It generates new random string with every call. Also, see \bashlines macros. It works for me like native bash.

Here an example: https://www.sharelatex.com/project/580e8926fe7b0dfd2ef8ae52

As you can see, \bashdatetime gives a different nanoseconds each time.

0

You can use my package iexec:

\documentclass{minimal}
\usepackage{iexec}
\begin{document}
File listing is:
\iexec{ls /usr}
\end{document}
yegor256
  • 12,021