% \iffalse meta-comment
%
%% File: l3sys-shell.dtx
%
% Copyright (C) 2018-2024 The LaTeX Project
%
% It may be distributed and/or modified under the conditions of the
% LaTeX Project Public License (LPPL), either version 1.3c of this
% license or (at your option) any later version.  The latest version
% of this license is in the file
%
%    http://www.latex-project.org/lppl.txt
%
% This file is part of the "l3experimental bundle" (The Work in LPPL)
% and all files in that bundle must be distributed together.
%
% -----------------------------------------------------------------------
%
% The development version of the bundle can be found at
%
%    https://github.com/latex3/latex3
%
% for those people who are interested.
%
%<*driver|package>
\RequirePackage{expl3}
%</driver|package>
%<*driver>
\documentclass[full]{l3doc}
\begin{document}
  \DocInput{\jobname.dtx}
\end{document}
%</driver>
% \fi
%
% \title{^^A
%   The \pkg{l3sys-shell} package\\ System shell functions^^A
% }
%
% \author{^^A
%  The \LaTeX{} Project\thanks
%    {^^A
%      E-mail:
%        \href{mailto:latex-team@latex-project.org}
%          {latex-team@latex-project.org}^^A
%    }^^A
% }
%
% \date{Released 2024-02-20}
%
% \maketitle
%
% \begin{documentation}
%
% This module provides platform-neutral interfaces to system shell commands.
% These functions can only access the shell if a document is proceeded using
% \texttt{--shell-escape}, which allows \emph{unrestricted} access to command
% line functions.
%
% In general, arbitrary documents should \emph{not} be processed in this way,
% as shell escape could either accidentally or deliberately lead to unexpected
% side-effects. In that sense, they should be treated like any other arbitrary
% script: with caution. No attempt is made by the functions here to avoid
% injection of additional commands into the arguments.
%
% With the above security caveats in mind, these functions are provided for
% authors where a workflow scripting \emph{within} a \LaTeX{} document is
% preferable to using an external script. The latter would of course also have
% the ability to run arbitrary code.
%
% \begin{function}[added = 2018-07-28]{\sys_shell_cp:nn}
%   \begin{syntax}
%     \cs{sys_shell_cp:nn} \Arg{source} \Arg{dest}
%   \end{syntax}
%   Copies the files specified in the \meta{source} (which may include
%   wildcards) to the \meta{dest}. The file paths should be specified using
%   |/| as a path separator. Copying is \emph{not} recursive: only files at
%   the path level given are copied.  If unrestricted shell escape is not
%   enabled, no action is attempted.
% \end{function}
%
% \begin{function}[added = 2018-07-27]{\sys_shell_mkdir:n}
%   \begin{syntax}
%     \cs{sys_shell_mkdir:n} \Arg{directory}
%   \end{syntax}
%   Creates the \meta{directory}, which should be specified using |/| as
%   a path separator.  If unrestricted shell escape is not enabled, no action is
%   attempted.
% \end{function}
%
% \begin{function}[added = 2018-07-28]{\sys_shell_mv:nn}
%   \begin{syntax}
%     \cs{sys_shell_mv:nn} \Arg{old} \Arg{new}
%   \end{syntax}
%   Moves the files from the \meta{old} to \meta{new} names/locations: the
%   \meta{old} names may include wildcards. In both arguments, |/| should be
%   used as the path separator.  If unrestricted shell escape is not enabled, no
%   action is attempted.
% \end{function}
%
% \begin{function}[added = 2018-07-27]{\sys_shell_rm:n}
%   \begin{syntax}
%     \cs{sys_shell_rm:n} \Arg{files}
%   \end{syntax}
%   Removes the \meta{files} (which may include wildcards). The file path should
%   be specified using |/| as a path separator.  If unrestricted shell escape is
%   not enabled, no action is attempted.
% \end{function}
%
% \begin{function}[added = 2018-07-27]{\sys_shell_rmdir:n}
%   \begin{syntax}
%     \cs{sys_shell_rmdir:n} \Arg{directory}
%   \end{syntax}
%   Removes the \meta{directory}, which should be specified using |/| as
%   a path separator.  If unrestricted shell escape is not enabled, no action is
%   attempted.
% \end{function}
%
% \begin{function}[added = 2018-07-28]{\sys_get_shell_pwd:N}
%   \begin{syntax}
%     \cs{sys_get_shell_pwd:N} \meta{str var}
%   \end{syntax}
%   Sets the \meta{str var} to the present working directory (the output of
%   |pwd| on Unix or |cd| on Windows). Note that on Windows this will contain
%   string backslash chars (|\|).  If unrestricted shell escape is not
%   enabled, no action is taken.
% \end{function}
%
% \begin{function}[added = 2018-07-28, updated = 2024-02-20]
%   {\sys_shell_split_ls:nN}
%   \begin{syntax}
%     \cs{sys_shell_split_ls:N} \Arg{glob} \meta{seq}
%   \end{syntax}
%   Sets the \meta{seq var} to contain one entry per directory listing
%   (equivalent to |ls -1| or |dir /b /s| with relative paths) as specified by
%   the \meta{glob}: the entries are strings. If unrestricted shell is not
%   enabled, no action is taken.
% \end{function}
%
% \end{documentation}
%
% \begin{implementation}
%
% \section{\pkg{l3sys-shell} implementation}
%
%    \begin{macrocode}
%<*package>
%    \end{macrocode}
%
%    \begin{macrocode}
%<@@=sys>
%    \end{macrocode}
%
%    \begin{macrocode}
\ProvidesExplPackage{l3sys-shell}{2024-02-20}{}
  {L3 Experimental system shell functions}
%    \end{macrocode}
%
% \begin{variable}{\s_@@_stop}
%   Internal scan marks.
%    \begin{macrocode}
\scan_new:N \s_@@_stop
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}{\q_@@_nil}
%   Internal quarks.
%    \begin{macrocode}
\quark_new:N \q_@@_nil
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}[pTF]{\@@_quark_if_nil:n}
%   Branching quark conditional.
%    \begin{macrocode}
\__kernel_quark_new_conditional:Nn \@@_quark_if_nil:N { F }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_path_to_win:n}
% \begin{macro}[EXP]{\@@_path_to_win:w}
% \begin{macro}[EXP]{\@@_path_to_win:N}
%   A simple expandable search-and-replace for providing Windows-style paths.
%    \begin{macrocode}
\cs_new:Npn \@@_path_to_win:n #1
  {
    \exp_after:wN \@@_path_to_win:w \tl_to_str:n {#1} ~ \s_@@_stop
  }
\cs_new:Npn \@@_path_to_win:w #1 ~ #2 \s_@@_stop
  {
    \@@_path_to_win:N #1 \q_@@_nil
    \tl_if_empty:nF {#2}
      {
        \c_space_tl
        \@@_path_to_win:w #2 \s_@@_stop
      }
  }
\cs_new:Npn \@@_path_to_win:N #1
  {
    \@@_quark_if_nil:NF #1
      {
        \token_if_eq_meaning:NNTF #1 /
          { \c_backslash_str }
          {#1}
        \@@_path_to_win:N
      }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\sys_shell_cp:nn}
%   Simple Unix-like file copying: at some stage we may need a directory-only
%   version as Windows and Unix have different requirements here.
%    \begin{macrocode}
\cs_new_protected:Npe \sys_shell_cp:nn #1#2
  {
    \sys_if_shell_unrestricted:T
      {
        \sys_shell_now:e
          {
            \sys_if_platform_unix:T
              { 
                cp~-f~ \exp_not:N \tl_to_str:n {#1} ~
                  \exp_not:N \tl_to_str:n {#2}
              }
            \sys_if_platform_windows:T
              {
                copy~/y~ \exp_not:N \@@_path_to_win:n {#1} ~
                  \exp_not:N \@@_path_to_win:n {#2}
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\sys_shell_mkdir:n}
%   Windows (with the extensions) will automatically make directory trees but
%   issues a warning if the directory already exists: avoid by including a
%   test.
%    \begin{macrocode}
\cs_new_protected:Npe \sys_shell_mkdir:n #1
  {
    \sys_if_shell_unrestricted:T
      {
        \sys_shell_now:e
          {
            \sys_if_platform_unix:T
              { mkdir~-p~ \exp_not:N \tl_to_str:n {#1} }
            \sys_if_platform_windows:T
              {
                if~not~exist~
                  \exp_not:N \@@_path_to_win:n { #1 / nul } ~
                  mkdir~ \exp_not:N \@@_path_to_win:n {#1}
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\sys_shell_mv:nn}
%   On Windows we do not have a single |mv| operation, so copy-and-delete
%   instead.
%    \begin{macrocode}
\cs_new_protected:Npe \sys_shell_mv:nn #1#2
  {
    \sys_if_shell_unrestricted:T
      {
        \sys_shell_now:e
          {
            \sys_if_platform_unix:T
              {
                mv~ \exp_not:N \tl_to_str:n {#1} ~
                  \exp_not:N \tl_to_str:n {#2}
              }
            \sys_if_platform_windows:T
              {
                copy~/y~ \exp_not:N \@@_path_to_win:n {#1} ~
                  \exp_not:N \@@_path_to_win:n {#2}
                  \token_to_str:N & \token_to_str:N &
                  del~/f~/q~\exp_not:N \@@_path_to_win:n {#1}
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\sys_shell_rm:n}
%   Deletion: obviously a big health warning here!
%    \begin{macrocode}
\cs_new_protected:Npe \sys_shell_rm:n #1
  {
    \sys_if_shell_unrestricted:T
      {
        \sys_shell_now:e
          {
            \sys_if_platform_unix:T
              { rm~-f~ \exp_not:N \tl_to_str:n {#1}  }
            \sys_if_platform_windows:T
              { del~/f~/q~ \exp_not:N \@@_path_to_win:n {#1} }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\sys_shell_rmdir:n}
%   When removing a directory, we create it first as that avoids errors in
%   the Windows case.
%    \begin{macrocode}
\cs_new_protected:Npe \sys_shell_rmdir:n #1
  {
    \sys_if_shell_unrestricted:T
      {
        \sys_shell_mkdir:n {#1}
        \sys_shell_now:e
          {
            \sys_if_platform_unix:T
              { rm~-rf~ \exp_not:N \tl_to_str:n {#1} }
            \sys_if_platform_windows:T
              { rmdir~/s~/q~ \exp_not:N \@@_path_to_win:n {#1} }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{variable}{\l_@@_tmp_seq, \l_@@_tmp_tl}
%   Scratch space.
%    \begin{macrocode}
\seq_new:N \l_@@_tmp_seq
\tl_new:N \l_@@_tmp_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}{\sys_get_shell_pwd:N}
%   Getting the path is easy: the main work is avoiding loosing any
%   information. (This information can be obtained using the recorder file:
%   that does not require shell escape. However, it's hard to see how it might
%   be useful without the other file functions.)
%    \begin{macrocode}
\cs_new_protected:Npe \sys_get_shell_pwd:N #1
  {
    \sys_if_shell_unrestricted:T
      {
        \exp_not:N \sys_get_shell:nnN
          {
            \sys_if_platform_unix:T { pwd }
            \sys_if_platform_windows:T { cd }
          }
          {
            \char_set_catcode_other:N \exp_not:N \\
            \char_set_catcode_other:N \exp_not:N \#
            \char_set_catcode_other:N \exp_not:N \~
            \char_set_catcode_other:N \exp_not:N \%
            \char_set_catcode_space:N \exp_not:N \ %
            \tex_endlinechar:D -1 \scan_stop:
          }
        \exp_not:N \l_@@_tmp_tl
        \str_set:NV #1 \exp_not:N \l_@@_tmp_tl
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\sys_shell_split_ls:nN}
% \begin{macro}{\@@_shell_split_ls:nN}
% \begin{macro}{\@@_shell_split_ls:w}
%   Getting a one-per-line listing is easy enough. We need to set
%   \cs{ExplSyntaxOff} as that deals with the end-of-line character. After that,
%   just a case of tidying up. The listing always ends in |^^M| so there is an
%   extra entry to trim.
%    \begin{macrocode}
\cs_new_protected:Npe \sys_shell_split_ls:nN #1#2
  {
    \sys_if_shell_unrestricted:T
      {
        \exp_not:N \exp_args:Ne \exp_not:N \sys_get_shell:nnN
          {
            \sys_if_platform_unix:T { ls~-1~ #1 }
            \sys_if_platform_windows:T
              { dir~/b~/s~ \exp_not:N \@@_path_to_win:n {#1} }
          }
          {
            \ExplSyntaxOff
            \char_set_catcode_other:N \exp_not:N \\
            \char_set_catcode_other:N \exp_not:N \#
            \char_set_catcode_other:N \exp_not:N \~
            \char_set_catcode_other:N \exp_not:N \%
            \char_set_catcode_other:n { 13 }
          }
          \exp_not:N \l_@@_tmp_tl
        \str_set:NV \exp_not:N \l_@@_tmp_tl \exp_not:N \l_@@_tmp_tl
        \seq_set_split:NnV #2 { \char_generate:nn { `\^^M } { 12 } }
          \exp_not:N \l_@@_tmp_tl
        \seq_pop_right:NN #2 \exp_not:N \l_@@_tmp_tl
        \exp_not:N \tl_if_blank:eTF { \exp_not:N \seq_item:Nn #2 { 1 } }
          { \seq_clear:N #2 }
        \sys_if_platform_windows:T
          {
            \sys_get_shell_pwd:N \exp_not:N \l_@@_tmp_tl
            \exp_not:N \exp_args:NV \exp_not:N \@@_shell_split_ls:nN
              \exp_not:N \l_@@_tmp_tl #2
          }
      }
  }
%    \end{macrocode}
%   On Windows, we can either get just file names (|dir /b|) or file names with
%   complete paths (|dir /s /b|), but not file names with relative paths in
%   a simple way. So we take the full paths and trim off the current working
%   directory. Normalization gives the same results as |ls|, hopefully.
%    \begin{macrocode}  
\sys_if_platform_windows:T
  {
    \cs_new_protected:Npe \@@_shell_split_ls:nN #1#2
      {
        \cs_set:Npn \exp_not:N \@@_shell_split_ls:w #1 \c_backslash_str { }
        \seq_set_eq:NN \exp_not:N \l_@@_tmp_seq #2
        \seq_clear:N #2
        \seq_map_inline:Nn \exp_not:N \l_@@_tmp_seq
          {
            \tl_set:Ne \exp_not:N \l_@@_tmp_tl
              { \exp_not:N  \@@_shell_split_ls:w ##1 }
            \tl_replace_all:Nnn \exp_not:N \l_@@_tmp_tl
              { \c_backslash_str } { / }
            \seq_put_right:NV #2 \exp_not:N \l_@@_tmp_tl
          }
      }
    \cs_new:Npn \@@_shell_split_ls:w { }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
%    \begin{macrocode}
%</package>
%    \end{macrocode}
%
% \end{implementation}
%
% \PrintIndex
