(********************************************************************)
(*                                                                  *)
(*  shell.s7i     Support for shell commands                        *)
(*  Copyright (C) 2009 - 2011  Thomas Mertes                        *)
(*                                                                  *)
(*  This file is part of the Seed7 Runtime Library.                 *)
(*                                                                  *)
(*  The Seed7 Runtime Library is free software; you can             *)
(*  redistribute it and/or modify it under the terms of the GNU     *)
(*  Lesser General Public License as published by the Free Software *)
(*  Foundation; either version 2.1 of the License, or (at your      *)
(*  option) any later version.                                      *)
(*                                                                  *)
(*  The Seed7 Runtime Library is distributed in the hope that it    *)
(*  will be useful, but WITHOUT ANY WARRANTY; without even the      *)
(*  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR *)
(*  PURPOSE.  See the GNU Lesser General Public License for more    *)
(*  details.                                                        *)
(*                                                                  *)
(*  You should have received a copy of the GNU Lesser General       *)
(*  Public License along with this program; if not, write to the    *)
(*  Free Software Foundation, Inc., 51 Franklin Street,             *)
(*  Fifth Floor, Boston, MA  02110-1301, USA.                       *)
(*                                                                  *)
(********************************************************************)


include "utf8.s7i";
include "scanstri.s7i";


(**
 *  Use the shell to execute a ''command'' with ''parameters''.
 *  Escape characters and quotations are added to the ''command'' and
 *  all ''parameters'' before they are forwarded to the operating
 *  system shell. This way it is not possible to inject a command in
 *  a parameter. The commands supported and the format of the
 *  ''parameters'' are not covered by the description of the ''shell''
 *  function. Due to the usage of the operating system shell and
 *  external programs, it is hard to write portable programs, which
 *  use the ''shell'' function.
 *  @param command Name of the command to be executed. A path must
 *         use the standard path representation.
 *  @param parameters Array of argument strings passed to the shell
 *         command.
 *  @param redirectStdin Name of a file to be used as standard input
 *         of the shell command. Use "" to do no redirection.
 *  @param redirectStdout Name of a file to be used as standard output
 *         of the shell command. Use "" to do no redirection.
 *  @param redirectStderr Name of a file to be used as standard error
 *         output of the shell command. Use "" to do no redirection.
 *  @return the return code of the executed command or of the shell.
 *)
const func integer: shell (in string: command,
                           in array string: parameters,
                           in string: redirectStdin,
                           in string: redirectStdout,
                           in string: redirectStderr) is action "CMD_SHELL_EXECUTE";


(**
 *  Use the shell to execute a ''command'' with ''parameters''.
 *  Escape characters and quotations are added to the ''command'' and
 *  all ''parameters'' before they are forwarded to the operating
 *  system shell. This way it is not possible to inject a command in
 *  a parameter. The commands supported and the format of the
 *  ''parameters'' are not covered by the description of the ''shell''
 *  function. Due to the usage of the operating system shell and
 *  external programs, it is hard to write portable programs, which
 *  use the ''shell'' function.
 *  @param command Name of the command to be executed. A path must
 *         use the standard path representation.
 *  @param parameters Array of argument strings passed to the shell
 *         command.
 *  @return the return code of the executed command or of the shell.
 *)
const func integer: shell (in string: command,
                           in array string: parameters) is
  return shell(command, parameters, "", "", "");


(**
 *  Use the shell to execute a ''command'' with ''parameters''.
 *  Parameters which contain a space must be either enclosed in
 *  double quotes (E.g.: shell("aCmd", "\"do it\" x", "", "", ""); )
 *  or the spaces in the parameter must be preceded by a backslash
 *  (E.g.: shell("aCmd", "do\\ it x", "", "", ""); ). Escape
 *  characters and quotations are added to the ''command'' and all
 *  ''parameters'' before they are forwarded to the operating system
 *  shell. This way it is not possible to inject a command in a
 *  parameter. The commands supported and the format of the
 *  ''parameters'' are not covered by the description of the
 *  ''shell'' function. Due to the usage of the operating system
 *  shell and external programs, it is hard to write portable
 *  programs, which use the ''shell'' function.
 *  @param command Name of the command to be executed. A path must
 *         use the standard path representation.
 *  @param parameters Space separated list of parameters for the
 *         ''command'', or "" if there are no parameters.
 *  @param redirectStdin Name of a file to be used as standard input
 *         of the shell command. Use "" to do no redirection.
 *  @param redirectStdout Name of a file to be used as standard output
 *         of the shell command. Use "" to do no redirection.
 *  @param redirectStderr Name of a file to be used as standard error
 *         output of the shell command. Use "" to do no redirection.
 *  @exception FILE_ERROR The shell command returns an error.
 *)
const func integer: shell (in string: command,
                           in var string: parameters,
                           in string: redirectStdin,
                           in string: redirectStdout,
                           in string: redirectStderr) is func
  result
    var integer: returnCode is 0;
  local
    var string: parameter is "";
    var array string: parameterArray is 0 times "";
  begin
    parameter := getCommandLineWord(parameters);
    while parameter <> "" do
      parameterArray &:= parameter;
      parameter := getCommandLineWord(parameters);
    end while;
    returnCode := shell(command, parameterArray, redirectStdin,
                        redirectStdin, redirectStdin);
  end func;


(**
 *  Use the shell to execute a ''command'' with ''parameters''.
 *  Parameters which contain a space must be either enclosed in
 *  double quotes (E.g.: shell("aCommand", "\"do it\" param2"); )
 *  or the spaces in the parameter must be preceded by a backslash
 *  (E.g.: shell("aCommand", "do\\ it param2"); ). Escape
 *  characters and quotations are added to the ''command'' and all
 *  ''parameters'' before they are forwarded to the operating system
 *  shell. This way it is not possible to inject a command in a
 *  parameter. The commands supported and the format of the
 *  ''parameters'' are not covered by the description of the
 *  ''shell'' function. Due to the usage of the operating system
 *  shell and external programs, it is hard to write portable
 *  programs, which use the ''shell'' function.
 *  @param command Name of the command to be executed. A path must
 *         use the standard path representation.
 *  @param parameters Space separated list of parameters for the
 *         ''command'', or "" if there are no parameters.
 *  @exception FILE_ERROR The shell command returns an error.
 *)
const func integer: shell (in string: command,
                           in string: parameters) is
  return shell(command, parameters, "", "", "");


(**
 *  Executes a command using the shell of the operating system.
 *  The command path must use the standard path representation.
 *  If the command or a parameter contains a space it must be either
 *  enclosed in double quotes (E.g.: shell("aCmd \"do it\" par2"); )
 *  or the spaces in the command or parameter must be preceded by a
 *  backslash (E.g.: shell("aCommand do\\ it param2"); ). Escape
 *  characters and quotations are added to the command and all
 *  parameters before they are forwarded to the operating system
 *  shell. This way it is not possible to inject a command in a
 *  parameter. The commands supported and the format of the
 *  parameters are not covered by the description of the ''shell''
 *  function. Due to the usage of the operating system shell and
 *  external programs, it is hard to write portable programs, which
 *  use the ''shell'' function.
 *  @param cmdAndParams Command to be executed and optional space
 *         separated list of parameters. Command and parameters
 *         must be space separated.
 *  @return the return code of the executed command or of the shell.
 *)
const func integer: shell (in string: cmdAndParams) is func
  result
    var integer: returnCode is 0;
  local
    var string: command is "";
    var string: parameters is "";
  begin
    parameters := cmdAndParams;
    command := getCommandLineWord(parameters);
    returnCode := shell(command, parameters);
  end func;


(**
 *  Use the shell to execute a ''command'' with ''parameters''.
 *  Parameters which contain a space must be either enclosed in
 *  double quotes (E.g.: shell("aCommand", "\"do it\" param2"); )
 *  or the spaces in the parameter must be preceded by a backslash
 *  (E.g.: shell("aCommand", "do\\ it param2"); ). Escape
 *  characters and quotations are added to the ''command'' and all
 *  ''parameters'' before they are forwarded to the operating system
 *  shell. This way it is not possible to inject a command in a
 *  parameter. The commands supported and the format of the
 *  ''parameters'' are not covered by the description of the
 *  ''shellCmd'' function. Due to the usage of the operating system
 *  shell and external programs, it is hard to write portable
 *  programs, which use the ''shellCmd'' function.
 *  @param command Name of the command to be executed. A path must
 *         use the standard path representation.
 *  @param parameters Space separated list of parameters for the
 *         ''command'', or "" if there are no parameters.
 *  @exception FILE_ERROR The shell command returns an error.
 *)
const proc: shellCmd (in string: command, in string: parameters) is func
  begin
    if shell(command, parameters) <> 0 then
      raise FILE_ERROR;
    end if;
  end func;


(**
 *  Executes a command using the shell of the operating system.
 *  The command path must use the standard path representation.
 *  If the command or a parameter contains a space it must be either
 *  enclosed in double quotes (E.g.: shell("aCmd \"do it\" par2"); )
 *  or the spaces in the command or parameter must be preceded by a
 *  backslash (E.g.: shell("aCommand do\\ it param2"); ). Escape
 *  characters and quotations are added to the command and all
 *  parameters before they are forwarded to the operating system
 *  shell. This way it is not possible to inject a command in a
 *  parameter. The commands supported and the format of the
 *  ''parameters'' are not covered by the description of the
 *  ''shellCmd'' function. Due to the usage of the operating system
 *  shell and external programs, it is hard to write portable
 *  programs, which use the ''shellCmd'' function.
 *  @param cmdAndParams Command to be executed and optional space
 *         separated list of parameters. Command and parameters
 *         must be space separated.
 *  @exception FILE_ERROR The shell command returns an error.
 *)
const proc: shellCmd (in string: cmdAndParams) is func
  begin
    if shell(cmdAndParams) <> 0 then
      raise FILE_ERROR;
    end if;
  end func;


(**
 *  Change a [[string]] in a way the operating system shell would require.
 *  The function adds escape characters or quotations to a string.
 *  Note that the functions ''shell'', ''shellCmd'', ''popen'' and
 *  ''popen8'' already add escape characters and quotations to their
 *  command and all their parameters. Don't use ''shellEscape'' for the
 *  commands and parameters of ''shell'', ''shellCmd'', ''popen'' and
 *  ''popen8''. This would lead to wrong results.
 *  @return a string which is escaped or quoted in a shell specific way.
 *  @exception MEMORY_ERROR Not enough memory to convert 'stri'.
 *  @exception RANGE_ERROR An illegal character is in 'stri'.
 *)
const func string: shellEscape (in string: stri)   is action "CMD_SHELL_ESCAPE";


(**
 *  Convert a standard path to the path of the operating system.
 *  The result can be used as parameter for the functions ''shell'',
 *  ''shellCmd'', ''popen'', ''popen8'', ''startProcess'' and ''startPipe''.
 *  The function ''toOsPath'' should only be used for parameters which
 *  represent a path. Don't use ''toOsPath'' for the command of a shell
 *  or process function. Note that ''shellEscape'' should never be used
 *  for a command or parameter of a shell or process function. Using
 *  ''shellEscape'' for shell and process functions leads to wrong
 *  results.
 *  @param standardPath Path in the standard path representation.
 *  @return a string containing an operating system path.
 *  @exception MEMORY_ERROR Not enough memory to convert ''standardPath''.
 *  @exception RANGE_ERROR ''standardPath'' is not representable as operating
 *             system path.
 *)
const func string: toOsPath (in string: standardPath)   is action "CMD_TO_OS_PATH";


(**
 *  Convert a standard path in a way the operating system shell would require.
 *  Note that the functions ''shell'', ''shellCmd'', ''popen'' and
 *  ''popen8'' already add escape characters and quotations to their
 *  command and all their parameters. Don't use ''toShellPath'' for the
 *  commands and parameters of ''shell'', ''shellCmd'', ''popen'' and
 *  ''popen8''. This would lead to wrong results. Instead of ''toShellPath''
 *  you should use ''toOsPath'' for parameters which represent a path.
 *  @param standardPath Path in the standard path representation.
 *  @return a string containing an escaped operating system path.
 *  @exception MEMORY_ERROR Not enough memory to convert ''standardPath''.
 *  @exception RANGE_ERROR ''standardPath'' is not representable as operating
 *             system path.
 *)
const func string: toShellPath (in string: path) is
  return shellEscape(toOsPath(path));


const func string: shellParameters (in array string: paramList) is func
  result
    var string: parameters is "";
  local
    var string: parameter is "";
  begin
    for parameter range paramList do
      if parameters <> "" then
        parameters &:= " ";
      end if;
      parameters &:= shellEscape(parameter);
    end for;
  end func;


const func clib_file: popenClibFile (in string: command,
    in array string: parameters, in string: mode) is action "FIL_POPEN";


(**
 *  [[file|File]] implementation type for operating system pipes.
 *)
const type: popenFile is sub external_file struct
  end struct;

type_implements_interface(popenFile, file);


(**
 *  Open a pipe to a shell ''command'' with ''parameters''.
 *  The command reads, respectively writes with Latin-1 encoding.
 *  Escape characters and quotations are added to the ''command'' and
 *  all ''parameters'' before they are forwarded to the operating
 *  system shell. This way it is not possible to inject a command in
 *  a parameter. The commands supported and the format of the
 *  ''parameters'' are not covered by the description of the ''popen''
 *  function. Due to the usage of the operating system shell and
 *  external programs, it is hard to write portable programs, which
 *  use the ''popen'' function.
 *  @param command Name of the command to be executed. A path must
 *         use the standard path representation.
 *  @param parameters Array of argument strings passed to the shell
 *         command.
 *  @param mode A pipe can be opened with the binary modes
 *         "r" (read) and "w" (write) or with the text modes
 *         "rt" (read) and "wt" (write).
 *  @return the pipe file opened, or [[null_file#STD_NULL|STD_NULL]]
 *          if it could not be opened.
 *  @exception RANGE_ERROR ''command'' is not representable as
 *             operating system path, or ''mode'' is illegal.
 *)
const func file: popen (in string: command, in array string: parameters,
    in string: mode) is func
  result
    var file: newPipe is STD_NULL;
  local
    var clib_file: open_file is CLIB_NULL_FILE;
    var popenFile: new_file is popenFile.value;
  begin
    open_file := popenClibFile(command, parameters, mode);
    if open_file <> CLIB_NULL_FILE then
      new_file.ext_file := open_file;
      new_file.name := command;
      newPipe := toInterface(new_file);
    end if;
  end func;


(**
 *  Open a pipe to a shell ''command'' with ''parameters''.
 *  The command reads, respectively writes with Latin-1 encoding.
 *  Parameters which contain a space must be either enclosed in
 *  double quotes (E.g.: popen("aCommand", "\"do it\" par2", "r"); )
 *  or the spaces in the parameter must be preceded by a backslash
 *  (E.g.: popen("aCommand", "do\\ it par2", "r"); ). Escape
 *  characters and quotations are added to the ''command'' and all
 *  ''parameters'' before they are forwarded to the operating system
 *  shell. This way it is not possible to inject a command in a
 *  parameter. The commands supported and the format of the
 *  ''parameters'' are not covered by the description of the
 *  ''popen'' function. Due to the usage of the operating system
 *  shell and external programs, it is hard to write portable
 *  programs, which use the ''popen'' function.
 *  @param command Name of the command to be executed. A path must
 *         use the standard path representation.
 *  @param parameters Space separated list of parameters for
 *         the ''command'', or "" if there are no parameters.
 *  @param mode A pipe can be opened with the binary modes
 *         "r" (read) and "w" (write) or with the text modes
 *         "rt" (read) and "wt" (write).
 *  @return the pipe file opened, or [[null_file#STD_NULL|STD_NULL]]
 *          if it could not be opened.
 *  @exception RANGE_ERROR ''command'' is not representable as
 *             operating system path, or ''mode'' is illegal.
 *)
const func file: popen (in string: command, in var string: parameters,
    in string: mode) is func
  result
    var file: newPipe is STD_NULL;
  local
    var string: parameter is "";
    var array string: parameterArray is 0 times "";
    var clib_file: open_file is CLIB_NULL_FILE;
    var popenFile: new_file is popenFile.value;
  begin
    parameter := getCommandLineWord(parameters);
    while parameter <> "" do
      parameterArray &:= parameter;
      parameter := getCommandLineWord(parameters);
    end while;
    open_file := popenClibFile(command, parameterArray, mode);
    if open_file <> CLIB_NULL_FILE then
      new_file.ext_file := open_file;
      new_file.name := command;
      newPipe := toInterface(new_file);
    end if;
  end func;


(**
 *  Open a pipe to a shell command.
 *  The command reads, respectively writes with Latin-1 encoding.
 *  The command path must use the standard path representation.
 *  If the command or a parameter contains a space it must be either
 *  enclosed in double quotes (E.g.: popen("aCmd \"do it\" x", "r"); )
 *  or the spaces in the command or parameter must be preceded by a
 *  backslash (E.g.: popen("aCommand do\\ it x", "r"); ). Escape
 *  characters and quotations are added to the command and all
 *  parameters before they are forwarded to the operating system
 *  shell. This way it is not possible to inject a command in a
 *  parameter. The commands supported and the format of the
 *  parameters are not covered by the description of the ''popen''
 *  function. Due to the usage of the operating system shell and
 *  external programs, it is hard to write portable programs, which
 *  use the ''popen'' function.
 *  @param cmdAndParams Command to be executed and optional space
 *         separated list of parameters. Command and parameters
 *         must be space separated.
 *  @param mode A pipe can be opened with the binary modes
 *         "r" (read) and "w" (write) or with the text modes
 *         "rt" (read) and "wt" (write).
 *  @return the pipe file opened, or [[null_file#STD_NULL|STD_NULL]]
 *          if it could not be opened.
 *  @exception RANGE_ERROR The command is not representable as
 *             operating system path, or ''mode'' is illegal.
 *)
const func file: popen (in string: cmdAndParams, in string: mode) is func
  result
    var file: newPipe is STD_NULL;
  local
    var string: command is "";
    var string: parameters is "";
  begin
    parameters := cmdAndParams;
    command := getCommandLineWord(parameters);
    newPipe := popen(command, parameters, mode);
  end func;


(**
 *  Wait for the process associated with aPipe to terminate.
 *  @param aFile Pipe to be closed (created by 'popen').
 *  @exception FILE_ERROR A system function returned an error.
 *)
const proc: close (in popenFile: aPipe) is func
  begin
    close(aPipe.ext_file);
  end func;


(**
 *  [[file|File]] implementation type for UTF-8 encoded operating system pipes.
 *)
const type: popen8File is sub utf8File struct
  end struct;

type_implements_interface(popen8File, file);


(**
 *  Open an UTF-8 encoded pipe to a shell ''command'' with ''parameters''.
 *  The command reads, respectively writes with UTF-8 encoding.
 *  Escape characters and quotations are added to the ''command'' and
 *  all ''parameters'' before they are forwarded to the operating
 *  system shell. This way it is not possible to inject a command in
 *  a parameter. The commands supported and the format of the
 *  ''parameters'' are not covered by the description of the ''popen8''
 *  function. Due to the usage of the operating system shell and
 *  external programs, it is hard to write portable programs, which
 *  use the ''popen8'' function.
 *  @param command Name of the command to be executed. A path must
 *         use the standard path representation.
 *  @param parameters Array of argument strings passed to the shell
 *         command.
 *  @param mode A pipe can be opened with the binary modes
 *         "r" (read) and "w" (write) or with the text modes
 *         "rt" (read) and "wt" (write).
 *  @return the pipe file opened, or [[null_file#STD_NULL|STD_NULL]]
 *          if it could not be opened.
 *  @exception RANGE_ERROR ''command'' is not representable as
 *             operating system path, or ''mode'' is illegal.
 *)
const func file: popen8 (in string: command, in array string: parameters,
    in string: mode) is func
  result
    var file: newPipe is STD_NULL;
  local
    var clib_file: open_file is CLIB_NULL_FILE;
    var popen8File: new_file is popen8File.value;
  begin
    open_file := popenClibFile(command, parameters, mode);
    if open_file <> CLIB_NULL_FILE then
      new_file.ext_file := open_file;
      new_file.name := command;
      newPipe := toInterface(new_file);
    end if;
  end func;


(**
 *  Open an UTF-8 encoded pipe to a shell command.
 *  The command reads, respectively writes with UTF-8 encoding.
 *  Parameters which contain a space must be either enclosed in
 *  double quotes (E.g.: popen8("aCommand", "\"do it\" par2", "r"); )
 *  or the spaces in the parameter must be preceded by a backslash
 *  (E.g.: popen8("aCommand", "do\\ it par2", "r"); ). Escape
 *  characters and quotations are added to the ''command'' and all
 *  ''parameters'' before they are forwarded to the operating system
 *  shell. This way it is not possible to inject a command in a
 *  parameter. The commands supported and the format of the
 *  ''parameters'' are not covered by the description of the
 *  ''popen8'' function. Due to the usage of the operating system
 *  shell and external programs, it is hard to write portable
 *  programs, which use the ''popen8'' function.
 *  @param command Name of the command to be executed. A path must
 *         use the standard path representation.
 *  @param parameters Space separated list of parameters for
 *         the ''command'', or "" if there are no parameters.
 *  @param mode A pipe can be opened with the binary modes
 *         "r" (read) and "w" (write) or with the text modes
 *         "rt" (read) and "wt" (write).
 *  @return the pipe file opened, or [[null_file#STD_NULL|STD_NULL]]
 *          if it could not be opened.
 *  @exception RANGE_ERROR ''command'' is not representable as
 *             operating system path, or ''mode'' is illegal.
 *)
const func file: popen8 (in string: command, in var string: parameters,
    in string: mode) is func
  result
    var file: newPipe is STD_NULL;
  local
    var string: parameter is "";
    var array string: parameterArray is 0 times "";
    var clib_file: open_file is CLIB_NULL_FILE;
    var popen8File: new_file is popen8File.value;
  begin
    parameter := getCommandLineWord(parameters);
    while parameter <> "" do
      parameterArray &:= parameter;
      parameter := getCommandLineWord(parameters);
    end while;
    open_file := popenClibFile(command, parameterArray, mode);
    if open_file <> CLIB_NULL_FILE then
      new_file.ext_file := open_file;
      new_file.name := command;
      newPipe := toInterface(new_file);
    end if;
  end func;


(**
 *  Open an UTF-8 encoded pipe to a shell command.
 *  The command reads, respectively writes with UTF-8 encoding.
 *  The command path must use the standard path representation.
 *  If the command or a parameter contains a space it must be either
 *  enclosed in double quotes (E.g.: popen8("aCmd \"do it\" x", "r"); )
 *  or the spaces in the command or parameter must be preceded by a
 *  backslash (E.g.: popen8("aCommand do\\ it x", "r"); ). Escape
 *  characters and quotations are added to the command and all
 *  parameters before they are forwarded to the operating system
 *  shell. This way it is not possible to inject a command in a
 *  parameter. The commands supported and the format of the
 *  parameters are not covered by the description of the ''popen8''
 *  function. Due to the usage of the operating system shell and
 *  external programs, it is hard to write portable programs, which
 *  use the ''popen8'' function.
 *  @param cmdAndParams Command to be executed and optional space
 *         separated list of parameters. Command and parameters
 *         must be space separated.
 *  @param mode A pipe can be opened with the binary modes
 *         "r" (read) and "w" (write) or with the text modes
 *         "rt" (read) and "wt" (write).
 *  @return the pipe file opened, or [[null_file#STD_NULL|STD_NULL]]
 *          if it could not be opened.
 *  @exception RANGE_ERROR The command is not representable as
 *             operating system path, or ''mode'' is illegal.
 *)
const func file: popen8 (in string: cmdAndParams, in string: mode) is func
  result
    var file: newPipe is STD_NULL;
  local
    var string: command is "";
    var string: parameters is "";
  begin
    parameters := cmdAndParams;
    command := getCommandLineWord(parameters);
    newPipe := popen8(command, parameters, mode);
  end func;


(**
 *  Wait for the process associated with aPipe to terminate.
 *  @param aPipe UTF-8 encoded pipe to be closed (created by 'popen8').
 *  @exception FILE_ERROR A system function returned an error.
 *)
const proc: close (in popen8File: aPipe) is func
  begin
    close(aPipe.ext_file);
  end func;