7

TLDR: Problem: kpsewhich outputs \par for unset environment variables and I do not know how to strip it.

I was inspired from the question Can I access system/environment variables from LaTeX? For instance, $HOME after finding out that kpsewhich works without --shell-escape and am trying to create a switch based on whether or not a given environment variable is set. Then I can check if e.g. DARK is set and swap text and background color.

However the problem emerged that even for unset environment variables kpsewhich apparently outputs a newline which leads to the captured output not being empty. I already tried to strip it using \tl_trim_spaces:N (as is already the case in the linked answer) but to no avail.

Is it somehow possible to either (a) instruct kpsewhich not to print a newline for unset environment variables, (b) read the return code from sys_get_shell:nnN as kpsewhich fails with a non-zero exit code if the variable is not set or (c) strip the resulting \par from the variable?

Credit goes to @Phelype Oleinik, check out his anwer for an explanation and also check out @egreg's answer for a solution without changing \endlinechar
I could now implement what I wanted using the following code:

\documentclass{article}
\usepackage{fontspec}
\usepackage{xparse}
\usepackage{xcolor}

\ExplSyntaxOn
\NewDocumentCommand{\ifenvset}{m m}
{
  \sys_get_shell:nnN { kpsewhich ~ --var-value ~ #1 }
    { \int_set:Nn \tex_endlinechar:D { -1 } }
    \l_tmpa_tl
  \tl_if_empty:NTF { \l_tmpa_tl } {} { #2 }
}
\ExplSyntaxOff

\ifenvset{DARK}{
  \pagecolor{black!90}
  \color{white!90}
}

\begin{document}
\noindent
\verb|lualatex darkmode.tex|\\
\verb|env DARK=1 lualatex darkmode.tex|\\
\end{document}
3
  • 1
    kpsewhich does not generate \par (you can run it on the commandline, it is generated by tex from the the implicit end-of file as you input the result, it's probably adjustable but it would help people test answers if you provided a test document Commented Jun 10, 2020 at 21:13
  • 1
    see egreg's \endlinechar=-1 comment under his question so you ignore the eol Commented Jun 10, 2020 at 21:15
  • Yes I always try to add a MWE though this time it did not seem too useful as I just copied the LaTeX3 update from the linked answer and played around with \tl_if_blank, \tl_to_str and \tl_count
    – Septatrix
    Commented Jun 10, 2020 at 21:23

2 Answers 2

4

When you execute a shell command, TeX sees it as if you had written the output of the command to a temporary (pseudo) file, and then read that file in. While reading that file, the usual rules apply: a line end is turned into a space, and an empty line is turned into a \par token. When the output of the command is empty, it is as if the file had an empty line, and that is turned into a \par. The usual trick is to set the \endlinechar parameter to -1, so that TeX doesn't insert anything at the end of a line, neither a \par token.

The second argument in \sys_get_shell:nnN is the <setup>, in which you can change these types of parameters before the command is executed and the pseudo file is read in, so you can do:

\sys_get_shell:nnN { kpsewhich ~ --var-value ~ #2 }
  { \int_set:Nn \tex_endlinechar:D { -1 } }
  \l_septatrix_env_tl

Putting that in egreg's \getenv code:

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\tl_new:N \l_septatrix_env_tl
\NewDocumentCommand \getenv { o m }
  {
    \sys_get_shell:nnN { kpsewhich ~ --var-value ~ #2 }
      { \int_set:Nn \tex_endlinechar:D { -1 } }
      \l_septatrix_env_tl
    \IfNoValueTF {#1}
      { \tl_use:N \l_septatrix_env_tl }
      { \tl_set_eq:NN #1 \l_septatrix_env_tl }
  }
\ExplSyntaxOff

\begin{document}

\getenv[\HOME]{HOME}\show\HOME

\getenv[\HOMER]{HOMER}\show\HOMER

\end{document}
3
  • Using \int_set:Nn \tex_endlinechar definitely seems to be more latex3 like compared to \endlinechar though I have been unable to find where this is documented. Are these internal variables just undocumented (\endlinechar is actually mentioned a few times in interface3)? Nice to see you use my username as a scope name :D
    – Septatrix
    Commented Jun 10, 2020 at 21:30
  • Okay I just found the section. They are apparently undocumented on purpose as these primitives should not be used outside of kernel-level code which makes this solution a bit less clean but it work nevertheless
    – Septatrix
    Commented Jun 10, 2020 at 21:37
  • @Septatrix \endlinechar and \tex_endlinechar:D are two names for the same internal parameter: if you change one, the other will change as well. \tex_endlinechar:D is not documented because primitives (with signature :D) should not be used (the :D stands for “do not use”, even though it looks so happy :-). However we don't have an interface for \endlinechar yet, so we have to break the rules a bit. Commented Jun 10, 2020 at 22:09
4

You can define \ifenvsetTF (this is a better name, in my opinion} by

\tl_const:Nn \c_getenv_par_tl { \par }

\NewDocumentCommand{\ifenvsetTF}{mmm}
 {
  \sys_get_shell:nnN { kpsewhich ~ --var-value ~ #1 } { } \l_tmpa_tl
  \tl_if_eq:NNTF \l_tmpa_tl \c_getenv_par_tl { #3 } { #2 }
 }

Full example:

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn

\NewDocumentCommand{\getenv}{om}
 {
  \sys_get_shell:nnN { kpsewhich ~ --var-value ~ #2 } { } \l_tmpa_tl
  \tl_trim_spaces:N \l_tmpa_tl
  \IfNoValueTF { #1 }
   {
    \tl_use:N \l_tmpa_tl
   }
   {
    \tl_set_eq:NN #1 \l_tmpa_tl
   }
 }

\tl_const:Nn \c_getenv_par_tl { \par }

\NewDocumentCommand{\ifenvsetTF}{mmm}
 {
  \sys_get_shell:nnN { kpsewhich ~ --var-value ~ #1 } { } \l_tmpa_tl
  \tl_if_eq:NNTF \l_tmpa_tl \c_getenv_par_tl { #3 } { #2 }
 }

\ExplSyntaxOff

\begin{document}

\getenv{LC_CTYPE}

\ifenvsetTF{LC_CTYPE}{SET}{UNSET}

\ifenvsetTF{BOZO}{SET}{UNSET}

\end{document}

enter image description here

However, note that variables set to empty value will trigger “unset” anyway (with this method and also with the \endlinechar method).

How to define an expl3 conditional?

\prg_new_protected_conditional:Nnn \septatrix_if_setenv:n {T,F,TF}
 {
  \sys_get_shell:nnN { kpsewhich ~ --var-value ~ #1 } { } \l_tmpa_tl
  \tl_if_eq:NNTF \l_tmpa_tl \c_getenv_par_tl
   { \prg_return_false: }
   { \prg_return_true: }
 }

Full code

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn

\NewDocumentCommand{\getenv}{om}
 {
  \sys_get_shell:nnN { kpsewhich ~ --var-value ~ #2 } { } \l_tmpa_tl
  \tl_trim_spaces:N \l_tmpa_tl
  \IfNoValueTF { #1 }
   {
    \tl_use:N \l_tmpa_tl
   }
   {
    \tl_set_eq:NN #1 \l_tmpa_tl
   }
 }

\tl_const:Nn \c_getenv_par_tl { \par }
\prg_new_protected_conditional:Nnn \septatrix_if_setenv:n {T,F,TF}
 {
  \sys_get_shell:nnN { kpsewhich ~ --var-value ~ #1 } { } \l_tmpa_tl
  \tl_if_eq:NNTF \l_tmpa_tl \c_getenv_par_tl
   { \prg_return_false: }
   { \prg_return_true: }
 }

\NewDocumentCommand{\ifenvsetTF}{mmm}
 {
  \septatrix_if_setenv:nTF { #1 } { #2 } { #3 }
 }

\ExplSyntaxOff

\begin{document}

\getenv{LC_CTYPE}

\ifenvsetTF{LC_CTYPE}{SET}{UNSET}

\ifenvsetTF{BOZO}{SET}{UNSET}

\end{document}
4
  • This methods seems a bit more clean without changing the \endlinechar (catcode?). Actually I am trying to figure out how to use \prg_new_conditional right now to get the complete set automatically :D
    – Septatrix
    Commented Jun 10, 2020 at 22:23
  • @Septatrix I added the code
    – egreg
    Commented Jun 10, 2020 at 22:28
  • Wow thanks, that looks very clean - I am starting to fall in love with latex3. Is the protected necessary or just a precaution?
    – Septatrix
    Commented Jun 10, 2020 at 22:56
  • @Septatrix It’s necessary, because the definition uses assignments.
    – egreg
    Commented Jun 11, 2020 at 8:22

You must log in to answer this question.

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