(********************************************************************)
(*                                                                  *)
(*  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''.
 *  Parameters which contain a space must be enclosed in double
 *  quotes (E.g.: shell("aCommand", "\"par 1\" par2"); ). 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.
 *  @return the return code of the executed command or of the shell.
 *)
const func integer: shell (in string: command,
                           in string: parameters) is action "CMD_SHELL";


(**
 *  Use the shell to execute a ''command'' with ''parameters''.
 *  Parameters which contain a space must be enclosed in double
 *  quotes (E.g.: shellCmd("aCommand", "\"par 1\" par2"); ). 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;


# The function cmd_sh() is deprecated. Use shellCmd() instead.
const proc: cmd_sh (in string: command, in string: parameters) is func
  begin
    ignore(shell(command, parameters));
  end func;


(**
 *  Executes a command using the shell of the operating system.
 *  The command path must use the standard path representation.
 *  Spaces in the command must be preceded by a backslash
 *  (E.g.: shell("do\\ it"); ). Alternatively the command can
 *  be enclosed in double quotes (E.g.: shell("\"do it\""); ).
 *  Command parameters containing a space must be enclosed in double
 *  quotes (E.g.: shell("do_it \"par 1\" par2"); ). 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;


(**
 *  Executes a command using the shell of the operating system.
 *  The command path must use the standard path representation.
 *  Spaces in the command must be preceded by a backslash
 *  (E.g.: shellCmd("do\\ it"); ). Alternatively the command can
 *  be enclosed in double quotes (E.g.: shellCmd("\"do it\""); ).
 *  Command parameters containing a space must be enclosed in double
 *  quotes (E.g.: shellCmd("do_it \"par 1\" par2"); ). 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;


# The function cmd_sh() is deprecated. Use shellCmd() instead.
const proc: cmd_sh (in string: cmdAndParams) is func
  begin
    ignore(shell(cmdAndParams));
  end func;


(**
 *  Convert a [[string]], such that it can be used as shell parameter.
 *  The function adds escape characters or quotations to a string.
 *  The result is useable as parameter for the functions ''shell'',
 *  ''shellCmd'', ''popen'' and ''popen8''. Shell parameters must be
 *  escaped individually. Afterwards escaped parameters are
 *  joined to a space separated list of parameters.
 *  @return a string which can be used as shell parameter.
 *  @exception MEMORY_ERROR Not enough memory to convert '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 must be escaped with ''shellEscape'' to be useable as
 *  parameter for the functions ''shell'', ''shellCmd'', ''popen'' and
 *  ''popen8''.
 *  @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 such that it can be used as shell parameter.
 *  The result is useable as parameter for the functions ''shell'',
 *  ''shellCmd'', ''popen'' and ''popen8''. Shell parameters must be
 *  converted individually. Afterwards converted parameters are
 *  joined to a space separated list of parameters.
 *  @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 integer: shell (in string: command, in array string: paramList) is
  return shell(command, shellParameters(paramList));


const func clib_file: popenClibFile (in string: command,
    in string: parameters, in string: mode)       is action "FIL_POPEN";
const proc: pclose (in clib_file: fileToClose)    is action "FIL_PCLOSE";


(**
 *  [[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.
 *  Parameters which contain a space must be enclosed in double
 *  quotes (E.g.: popen("aCommand", "\"par 1\" par2", "r"); ). The
 *  function [[#shellEscape(in_string)|shellEscape]] converts a
 *  [[string]], such that it can be used as parameter for ''popen''
 *  (E.g.: popen("aCmd", shellEscape("par 1") & " par2", "r"); ). 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 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.
 *  The command reads, respectively writes with Latin-1 encoding.
 *  Spaces in the command must be preceded by a backslash
 *  (E.g.: popen("do\\ it"); ). Alternatively the command can
 *  be enclosed in double quotes (E.g.: popen("\"do it\""); ).
 *  Command parameters containing a space must be enclosed in
 *  double quotes (E.g.: popen("do_it \"par 1\" par2"); ).
 *  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;


(**
 *  Open a pipe to a shell ''command'' with a ''paramList''.
 *  The command reads, respectively writes with Latin-1 encoding.
 *  @param command Name of the command to be executed. A path must
 *         use the standard path representation.
 *  @param paramList Array of argument strings passed to the command.
 *         It is not necessary to quote the parameters, since this
 *         function takes care of that.
 *  @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: paramList,
    in string: mode) is func
  result
    var file: newPipe is STD_NULL;
  local
    var string: parameter is "";
    var string: parameters is "";
  begin
    for parameter range paramList do
      if parameters <> "" then
        parameters &:= " ";
      end if;
      parameters &:= shellEscape(parameter);
    end for;
    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
    pclose(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.
 *  Parameters which contain a space must be enclosed in double
 *  quotes (E.g.: popen8("aCommand", "\"par 1\" par2", "r"); ). The
 *  function [[#shellEscape(in_string)|shellEscape]] converts a
 *  [[string]], such that it can be used as parameter for ''popen''
 *  (E.g.: popen8("aCmd", shellEscape("par 1") & " par2", "r"); ). 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 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.
 *  Spaces in the command must be preceded by a backslash
 *  (E.g.: popen8("do\\ it"); ). Alternatively the command can
 *  be enclosed in double quotes (E.g.: popen8("\"do it\""); ).
 *  Command parameters containing a space must be enclosed in
 *  double quotes (E.g.: popen8("do_it \"par 1\" par2"); ).
 *  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;


(**
 *  Open an UTF-8 encoded pipe to a shell ''command'' with a ''paramList''.
 *  The command reads, respectively writes with UTF-8 encoding.
 *  @param command Name of the command to be executed. A path must
 *         use the standard path representation.
 *  @param paramList Array of argument strings passed to the command.
 *         It is not necessary to quote the parameters, since this
 *         function takes care of that.
 *  @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: paramList,
    in string: mode) is func
  result
    var file: newPipe is STD_NULL;
  local
    var string: parameter is "";
    var string: parameters is "";
  begin
    for parameter range paramList do
      if parameters <> "" then
        parameters &:= " ";
      end if;
      parameters &:= shellEscape(parameter);
    end for;
    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
    pclose(aPipe.ext_file);
  end func;