{
    Double Commander
    -------------------------------------------------------------------------
    This unit contains some functions for open files in associated applications.

    Copyright (C) 2006-2015 Alexander Koblov (alexx2000@mail.ru)

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
}

unit uShellExecute;

{$mode objfpc}{$H+}

interface

uses
  Classes, uFile, uFileView, fMain;

type
  TPrepareParameterOption = (ppoNormalizePathDelims, ppoReplaceTilde);
  TPrepareParameterOptions = set of TPrepareParameterOption;

function PrepareParameter(sParam: string; paramFile: TFile = nil; options: TPrepareParameterOptions = []; pbShowCommandLinePriorToExecute: PBoolean = nil; pbRunInTerminal: PBoolean = nil; pbKeepTerminalOpen: PBoolean = nil): string; overload;

{en
   Replace variable parameters that depend on files in panels.
}
function ReplaceVarParams(sSourceStr: string; paramFile: TFile = nil; pbShowCommandLinePriorToExecute: PBoolean = nil; pbRunInTerminal: PBoolean = nil; pbKeepTerminalOpen: PBoolean = nil): string; overload;
{en
   Replace variable parameters that depend on the file in active dir.
}

function ProcessExtCommandFork(sCmd: string; sParams: string = ''; sWorkPath: string = ''; paramFile: TFile = nil; bTerm: boolean = False; bKeepTerminalOpen: boolean = False): boolean;

function ShellExecuteEx(sActionName, sFileName, sActiveDir: string): boolean;

implementation

uses
  //Notes: StrUtils is here first, so because of visibility rules, if a called
  //       routine name is present both in "StrUtils" and one of the following
  //       it will be one of the following that will be used and not the one
  //       "StrUtils". Make sure to let "StrUtils" at the first position.
  //       "StrUtils" is here to have the "PosEx".
  //Lazarus, Free-Pascal, etc.
  StrUtils, Dialogs, SysUtils, Process, UTF8Process, LazUTF8, LConvEncoding,
  UnicodeUtils,

  //DC
  uShowMsg, uDCUtils, uLng, uFormCommands, fViewer, fEditor, uShowForm, uGlobs,
  uOSUtils, uFileSystemFileSource, DCOSUtils, DCStrUtils, DCClassesUtf8;

//Dialogs,
//LConvEncoding;


(*
  Functions (without parameters they give output for all selected files):

  Miscellaneous:
    %? - as first parameter only, it will show report to show command line prior to execute
    %% - to use one time the percent sign

  "File related":
  ------------------------------------------------------------------------------
    %f - only filename
    %d - only path of the file
    %p - path + filename
    %o - only the filename with no extension
    %e - only the file extension
    %v - only relative path + filename

    %D - current path in active or chosen panel
    %a - address + path + filename
    %A - current address in active or chosen panel
    %F - file list with file name only
    %L - file list with full file name
    %R - file list with relative path + file name
    %F, %L and %R - create a list file in the TEMP directory with the names of the selected
                    files and directories, and appends the name of the list file to the command line

  "Choosing encoding" for %F, $L and %R (if not given, system encoding used):
  ---------------------------------------------------------------------------
  %X[U|W|Q] - where X is function %F, %L or %R
    U - UTF-8,
    W - UTF-16 (with byte order marker),
    Q - quote file name by double quotes

  "Choosing panel" (if not given, active panel is used):
  ------------------------------------------------------------------------------
  %X[l|r|s|t] - where X is function (l - left, r - right, s - source, t - target)
    s - source or active panel (no matter if it's left or right)
    t - target or inactive panel (no matter if it's left or right)
    l - left panel
    r - right panel
    b - both panels, left first, right second
    p - both panels, active first, inactive second

  "Choosing selected files" (only for %f, %d, %p, %o and %e):
  ------------------------------------------------------------------------------
  %X[<nr>] - where X is function
    <nr> is 1..n, where n is number of selected files.
    If there are no selected files, currently active file is nr 1.
    If <nr> is invalid or there is no selected file by that number the result for the whole function will be empty string.

  "Adding prefix, postfix before or after the result string":
  ------------------------------------------------------------------------------
  %X[{<prefix>}][{<postfix>}]
    If applied to multiple files, each name is prefixed/postfixed.

  Control if %f, %d, etc... will return name between quotes or not
  ----------------------------------------------------------------
   %" - will set default. For DC legacy is quoted
   %"0 - will make the result unquoted
   %"1 - will make the result quoted

  Control if %D, %d etc... will return path name with the ending delimiter or not
  -------------------------------------------------------------------------------
   %/ - will set default. For DC legacy it was without ending delimited
   %/0 - will exclude the ending delimiter
   %/1 - will include the ending delimiter

   Prompt the user with a sentence, propose a default, and use what the user typed
   -------------------------------------------------------------------------------
   %[Enter with required;1024] - This is an example. The text following the ";" indicates that default value is 1024
   %[This required the \\DB-2010\ server to be online!] - if no default value is given, DC will simply shows the message, assuming it's simply to echo a message.

   Control what will be the effective "%" char (for situation where we want the "%" to be the "#" sign instead
   -----------------------------------------------------------------------------------------------------------
   %# - Will set the percent-variable indicator to be the "#" character from now on when evaluating the line.
        Note that it will be evaluated -only- when the current percent-variable indicator is "%".
   #% - Will set the percent-variable indicator to be the "%" character from now on when evaluating the line.
        Note that it will be evaluated -only- when the current percent-variable indicator is "#".

   Control if it run in terminal, if it close it at the end or not
   ---------------------------------------------------------------
   %t - Will have it run in terminal for sure, close or not depend of the action requested
   %t0 - Will run in terminal AND request to close it at the end
   %t1 - Will run in terminal AND let it run at the end

  Above parameters can be combined together.
  ------------------------------------------------------------------------------
  Order of params:
  - %function
  - quoting and encoding (only for %F, %L and %R)
  - left or right or source or target panel (optional)
  - nr of file (optional)
  - prefix, postfix (optional)

  Examples:
    %f1       - first selected file in active panel
    %pr2      - full path of second selected file in right panel
    %fl       - only filenames from left panel
    %pr       - full filenames from right panel
    %Dl       - current path in left panel
    %f{-f }   - prepend each name with "-f "
                (ex.: -f <file_1> -f <file_2>)
    %f{"}{"}  - enclose each name in quotes
                (ex.: "<file_1>" "<file_2>")
    %f1{-first }%f2{ -second }
         - if only 1 file selected      : -first <file_1>
         - if 2 (or more) files selected: -first <file_1> -second <file_2>
*)
function ReplaceVarParams(sSourceStr: string; paramFile: TFile; pbShowCommandLinePriorToExecute: PBoolean; pbRunInTerminal: PBoolean; pbKeepTerminalOpen: PBoolean): string;
type
  TFunctType = (ftNone, ftName, ftDir, ftPath, ftSingleDir, ftSource, ftSourcePath,
    ftFileFullList, ftFileNameList, ftRelativeFileNameList,
    ftNameNoExt, ftExtension, ftRelativePath,
    ftProcessPercentSignIndicator, ftJustSetTheShowFlag,
    ftSetTrailingDelimiter, ftSetQuoteOrNot, ftSetTerminalOptions,
    ftEchoSimpleMessage, ftPromptUserForParam, ftExecuteConsoleCommand);
  TFuncModifiers = set of (fmQuote, fmUTF8, fmUTF16);
  TStatePos = (spNone, spPercent, spFunction, spPrefix, spPostfix,
    spGotPrefix, spSide, spIndex, spUserInputOrEcho,
    spGotInputHintWaitEndDefaultVal, spGetExecuteConsoleCommand,
    spComplete);

  Tstate = record
    pos: TStatePos;
    functStartIndex: integer;
    funct: TFunctType;
    functMod: TFuncModifiers;
    files: TFiles;
    otherfiles: TFiles;
    dir: string;
    address: string;
    sFileIndex: string;
    prefix, postfix: string; // a string to add before/after each output
    // (for functions giving output of multiple strings)
    sSubParam: string;
    sUserMessage: string;
  end;

var
  index: integer;
  leftFiles: TFiles = nil;
  rightFiles: TFiles = nil;
  activeFiles: TFiles;
  inactiveFiles: TFiles;
  singleFileFiles: TFiles = nil;
  activeDir: string;
  inactiveDir: string;
  activeAddress: string;
  inactiveAddress: string;
  state: Tstate;
  sOutput: string = '';
  parseStartIndex: integer;
  bTrailingDelimiter: boolean = False;
  bQuoteOrNot: boolean = True;
  CurrentPercentIndicator: char = '%';

  function BuildName(aFile: TFile): string;
  begin
    //1. Processing according to function requested
    case state.funct of
      ftName, ftDir, ftPath, ftNameNoExt, ftExtension, ftRelativePath, ftSingleDir, ftSource, ftSourcePath:
      begin
        ;
        case state.funct of
          ftName:
            Result := aFile.Name;

          ftDir:
            Result := aFile.Path;

          ftPath:
            Result := aFile.FullPath;

          ftNameNoExt:
            Result := aFile.NameNoExt;

          ftExtension:
            Result := aFile.Extension;

          ftRelativePath:
            Result := ExtractRelativepath(state.dir, aFile.FullPath);

          ftSingleDir:
            Result := state.dir;

          ftSource:
            Result := state.address;

          ftSourcePath:
            Result := state.address + aFile.FullPath;
        end;
      end;
      else
        Exit('');
    end;

    //2. Processing the prefix/postfix requested
    Result := state.prefix + Result + state.postfix;

    //3. Processing the trailing path delimiter requested
    case state.funct of
      ftDir, ftSingleDir:
      begin
        if bTrailingDelimiter then
          Result := IncludeTrailingPathDelimiter(Result)
        else
          Result := ExcludeBackPathDelimiter(Result);
      end;
    end;

    //4. Processing the quotes requested
    if bQuoteOrNot then
      Result := QuoteStr(Result);
  end;

  function BuildAllNames: string;
  var
    i: integer;
  begin
    Result := '';

    if Assigned(state.files) then
      for i := 0 to pred(state.files.Count) do
        Result := ConcatenateStrWithSpace(Result, BuildName(state.files[i]));

    if Assigned(state.otherfiles) then
      for i := 0 to pred(state.otherfiles.Count) do
        Result := ConcatenateStrWithSpace(Result, BuildName(state.otherfiles[i]));
  end;

  function BuildFile(aFile: TFile): string;
  begin
    case state.funct of
      ftFileFullList: Result := aFile.FullPath;
      ftFileNameList: Result := aFile.Name;
      ftRelativeFileNameList: Result := ExtractRelativepath(state.dir, aFile.FullPath);
      else
        Result := aFile.Name;
    end;

    if aFile.isDirectory then
    begin
      if bTrailingDelimiter then
        Result := IncludeTrailingPathDelimiter(Result)
      else
        Result := ExcludeBackPathDelimiter(Result);
    end;

    if (fmQuote in state.functMod) then
      Result := '"' + Result + '"';

    if (fmUTF16 in state.functMod) then
      Result := Utf8ToUtf16LE(Result)
    else if not (fmUTF8 in state.functMod) then
      Result := UTF8ToSys(Result);
  end;

  function BuildFileList: String;
  var
    I: integer;
    FileName: ansistring;
    FileList: TFileStreamEx;
    LineEndingA: ansistring = LineEnding;
  begin
    Result := GetTempName(GetTempFolderDeletableAtTheEnd + 'Filelist') + '.lst';
    try
      FileList := TFileStreamEx.Create(Result, fmCreate);
      try
        if fmUTF16 in state.functMod then
        begin
          FileName := UTF16LEBOM;
          LineEndingA := Utf8ToUtf16LE(LineEnding);
        end;

        if Assigned(state.files) then
        begin
          if state.files.Count > 0 then
          begin
            for I := 0 to state.files.Count - 1 do
              FileName += BuildFile(state.files[I]) + LineEndingA;
          end;
        end;

        if Assigned(state.otherfiles) then
        begin
          if state.otherfiles.Count > 0 then
          begin
            FileName += LineEndingA;

            for I := 0 to state.otherfiles.Count - 1 do
              FileName += BuildFile(state.otherfiles[I]) + LineEndingA;
          end;
        end;

        FileList.Write(FileName[1], Length(FileName));
      finally
        FileList.Free;
      end;
    except
      Result := EmptyStr;
    end;
  end;

  procedure ResetState(var aState: TState);
  begin
    with aState do
    begin
      pos := spNone;
      if paramFile <> nil then
        files := singleFileFiles
      else
        files := activeFiles;
      otherfiles := nil;
      dir := activeDir;
      address := activeAddress;
      sFileIndex := '';
      funct := ftNone;
      functMod := [];
      functStartIndex := 0;
      prefix := '';
      postfix := '';
      sSubParam := '';
      sUserMessage := '';
    end;
  end;

  procedure AddParsedText(limit: integer);
  begin
    // Copy [parseStartIndex .. limit - 1].
    if limit > parseStartIndex then
      sOutput := sOutput + Copy(sSourceStr, parseStartIndex, limit - parseStartIndex);
    parseStartIndex := index;
  end;

  procedure SetTrailingPathDelimiter;
  begin
    bTrailingDelimiter := state.sSubParam = '1';
    // Currently in the code, anything else than "0" will include the trailing delimiter.
    // BUT, officially, in the documentation, juste state that 0 or 1 is required.
    // This could give room for future addition maybe.
  end;

  procedure SetQuoteOrNot;
  begin
    bQuoteOrNot := not (state.sSubParam = '0');
    // Currently in the code, anything else than "0" will indicate we want to quote
    // BUT, officially, in the documentation, juste state that 0 or 1 is required.
    // This could give room for future addition maybe.
  end;

  procedure SetTerminalOptions;
  begin
    if pbRunInTerminal <> nil then
    begin
      pbRunInTerminal^ := True;
      if pbKeepTerminalOpen <> nil then
        pbKeepTerminalOpen^ := not (state.sSubParam = '0');
    end;
  end;

  procedure JustEchoTheMessage;
  begin
    msgOK(state.sUserMessage);
  end;

  procedure AskUserParamAndReplace;
  begin
    ShowInputQuery(state.sUserMessage, state.sUserMessage, state.sSubParam);
    sOutput := sOutput + state.sSubParam;
  end;

  procedure ExecuteConsoleCommand;
  var
    sTmpFilename, sShellCmdLine: string;
    Process: TProcessUTF8;
  begin
    sTmpFilename := GetTempName(GetTempFolderDeletableAtTheEnd) + '.tmp';
    //sShellCmdLine := Copy(state.sSubParam, 3, length(state.sSubParam)-2) + ' > ' + QuoteStr(sTmpFilename);
    sShellCmdLine := state.sSubParam + ' > ' + QuoteStr(sTmpFilename);
    Process := TProcessUTF8.Create(nil);
    try
      Process.CommandLine := FormatShell(sShellCmdLine);
      Process.Options := [poNoConsole, poWaitOnExit];
      Process.Execute;
    finally
      Process.Free;
    end;
    sOutput := sOutput + sTmpFilename;
  end;

  procedure ProcessPercentSignIndicator;
  begin
    if CurrentPercentIndicator = state.sSubParam then
      sOutput := sOutput + state.sSubParam
    else
    if CurrentPercentIndicator = '%' then
      CurrentPercentIndicator := '#'
    else
      CurrentPercentIndicator := '%';
  end;

  procedure DoFunction;
  var
    fileIndex: integer = -1;
    OffsetFromStart: integer = 0;
  begin
    AddParsedText(state.functStartIndex);

    if state.sFileIndex <> '' then
      try
        fileIndex := StrToInt(state.sFileIndex);
        fileIndex := fileIndex - 1; // Files are counted from 0, but user enters 1..n.
      except
        on EConvertError do
          fileIndex := -1;
      end;

    if fileIndex <> -1 then
    begin
      if (fileIndex >= 0) and Assigned(state.files) then
      begin
        if fileIndex < state.files.Count then
          sOutput := sOutput + BuildName(state.files[fileIndex]);
        OffsetFromStart := state.files.Count;
      end;

      if ((fileIndex - OffsetFromStart) >= 0) and Assigned(state.otherfiles) then
        if (fileIndex - OffsetFromStart) < state.otherfiles.Count then
          sOutput := sOutput + BuildName(state.otherfiles[fileIndex - OffsetFromStart]);
    end
    else
    begin
      if state.funct in [ftName, ftPath, ftDir, ftNameNoExt, ftSourcePath, ftExtension, ftRelativePath] then
        sOutput := sOutput + BuildAllNames
      else if state.funct in [ftSingleDir, ftSource] then // only single current dir
        sOutput := sOutput + BuildName(nil)
      else if state.funct in [ftFileFullList, ftFileNameList, ftRelativeFileNameList] then // for list of file
        sOutput := sOutput + BuildFileList
      else if state.funct in [ftProcessPercentSignIndicator] then // only add % sign
        ProcessPercentSignIndicator
      else if state.funct in [ftJustSetTheShowFlag] then //only set the flag to show the params prior to execute
      begin
        if pbShowCommandLinePriorToExecute <> nil then
          pbShowCommandLinePriorToExecute^ := True;
      end
      else if state.funct in [ftSetTrailingDelimiter] then //set the trailing path delimiter
        SetTrailingPathDelimiter
      else if state.funct in [ftSetQuoteOrNot] then
        SetQuoteOrNot
      else if state.funct in [ftEchoSimpleMessage] then
        JustEchoTheMessage
      else if state.funct in [ftPromptUserForParam] then
        AskUserParamAndReplace
      else if state.funct in [ftSetTerminalOptions] then
        SetTerminalOptions
      else if state.funct in [ftExecuteConsoleCommand] then
        ExecuteConsoleCommand;
    end;

    ResetState(state);
  end;

  procedure ProcessNumber;
  begin
    case state.funct of
      ftSingleDir: state.pos := spComplete; // Numbers not allowed for %D
      ftSetTrailingDelimiter, ftSetQuoteOrNot, ftSetTerminalOptions:
      begin
        state.sSubParam := state.sSubParam + sSourceStr[index];
        state.pos := spComplete;
        Inc(Index);
      end;
      else
      begin
        state.sFileIndex := state.sFileIndex + sSourceStr[index];
        state.pos := spIndex;
      end;
    end;
  end;

  procedure ProcessOpenBracket; // '{'
  begin
    if state.pos <> spGotPrefix then
      state.pos := spPrefix
    else
      state.pos := spPostfix;
  end;

begin
  try
    leftFiles := frmMain.FrameLeft.CloneSelectedOrActiveFiles;
    rightFiles := frmMain.FrameRight.CloneSelectedOrActiveFiles;
    if paramFile <> nil then
    begin
      singleFileFiles := TFiles.Create(paramFile.Path);
      singleFileFiles.Add(paramFile.Clone);
    end;

    if frmMain.ActiveFrame = frmMain.FrameLeft then
    begin
      activeFiles := leftFiles;
      activeDir := frmMain.FrameLeft.CurrentPath;
      activeAddress := frmMain.FrameLeft.CurrentAddress;
      inactiveFiles := rightFiles;
      inactiveDir := frmMain.FrameRight.CurrentPath;
      inactiveAddress := frmMain.FrameRight.CurrentAddress;
    end
    else
    begin
      activeFiles := rightFiles;
      activeDir := frmMain.FrameRight.CurrentPath;
      activeAddress := frmMain.FrameRight.CurrentAddress;
      inactiveFiles := leftFiles;
      inactiveDir := frmMain.FrameLeft.CurrentPath;
      inactiveAddress := frmMain.FrameLeft.CurrentAddress;
    end;

    index := 1;
    parseStartIndex := index;

    ResetState(state);

    while index <= Length(sSourceStr) do
    begin
      case state.pos of
        spNone:
          if sSourceStr[index] = CurrentPercentIndicator then
          begin
            state.pos := spPercent;
            state.functStartIndex := index;
          end;

        spPercent:
          case sSourceStr[index] of
            '?':
            begin
              state.funct := ftJustSetTheShowFlag;
              state.pos := spComplete;
              Inc(Index);
            end;

            '%', '#':
            begin
              state.funct := ftProcessPercentSignIndicator;
              state.sSubParam := sSourceStr[index];
              state.pos := spComplete;
              Inc(Index);
            end;

            'f', 'd', 'p', 'o', 'e', 'v', 'D', 'A', 'a', 'n', 'h', '/', '"', 't':
            begin
              case sSourceStr[index] of
                'f': state.funct := ftName;
                'd': state.funct := ftDir;
                'p': state.funct := ftPath;
                'o': state.funct := ftNameNoExt;
                'e': state.funct := ftExtension;
                'v': state.funct := ftRelativePath;
                'D': state.funct := ftSingleDir;
                'A': state.funct := ftSource;
                'a': state.funct := ftSourcePath;
                '/': state.funct := ftSetTrailingDelimiter;
                '"': state.funct := ftSetQuoteOrNot;
                't': state.funct := ftSetTerminalOptions;
              end;
              state.pos := spFunction;
            end;

            'L', 'F', 'R':
            begin
              case sSourceStr[index] of
                'L': state.funct := ftFileFullList;
                'F': state.funct := ftFileNameList;
                'R': state.funct := ftRelativeFileNameList;
              end;
              state.pos := spFunction;
            end;

            '[':
            begin
              state.pos := spUserInputOrEcho;
            end;

            '<':
            begin
              state.pos := spGetExecuteConsoleCommand;
            end;
            else
              ResetState(state);
          end;

        spFunction:
          case sSourceStr[index] of
            'l', 'b':
            begin
              state.files := leftFiles;
              state.dir := frmMain.FrameLeft.CurrentPath;
              state.address := frmMain.FrameLeft.CurrentAddress;
              state.pos := spSide;
              if sSourceStr[index] = 'b' then
                state.otherfiles := rightFiles;
            end;

            'r':
            begin
              state.files := rightFiles;
              state.dir := frmMain.FrameRight.CurrentPath;
              state.address := frmMain.FrameRight.CurrentAddress;
              state.pos := spSide;
            end;

            's', 'p':
            begin
              state.files := activeFiles;
              state.dir := activeDir;
              state.address := activeAddress;
              state.pos := spSide;
              if sSourceStr[index] = 'p' then
                state.otherfiles := inactiveFiles;
            end;

            't':
            begin
              state.files := inactiveFiles;
              state.dir := inactiveDir;
              state.address := inactiveAddress;
              state.pos := spSide;
            end;

            'U':
            begin
              state.functMod += [fmUTF8];
              state.pos := spFunction;
            end;

            'W':
            begin
              state.functMod += [fmUTF16];
              state.pos := spFunction;
            end;

            'Q':
            begin
              state.functMod += [fmQuote];
              state.pos := spFunction;
            end;

            '0'..'9':
              ProcessNumber;

            '{':
              ProcessOpenBracket;

            else
              state.pos := spComplete;
          end;

        spSide:
          case sSourceStr[index] of
            '0'..'9':
              ProcessNumber;
            '{':
              ProcessOpenBracket;
            else
              state.pos := spComplete;
          end;

        spIndex:
          case sSourceStr[index] of
            '0'..'9':
              ProcessNumber;
            '{':
              ProcessOpenBracket;
            else
              state.pos := spComplete;
          end;

        spPrefix, spPostfix:
          case sSourceStr[index] of
            '}':
            begin
              if state.pos = spPostfix then
              begin
                Inc(index); // include closing bracket in the function
                state.pos := spComplete;
              end
              else
                state.pos := spGotPrefix;
            end;
            else
            begin
              case state.pos of
                spPrefix:
                  state.prefix := state.prefix + sSourceStr[index];
                spPostfix:
                  state.postfix := state.postfix + sSourceStr[index];
              end;
            end;
          end;

        spGotPrefix:
          case sSourceStr[index] of
            '{':
              ProcessOpenBracket;
            else
              state.pos := spComplete;
          end;

        spUserInputOrEcho:
        begin
          case sSourceStr[index] of
            ';':
            begin
              state.pos := spGotInputHintWaitEndDefaultVal;
            end;

            ']':
            begin
              state.funct := ftEchoSimpleMessage;
              state.pos := spComplete;
              Inc(Index);
            end;
            else
              State.sUserMessage := State.sUserMessage + sSourceStr[index];
          end;
        end;

        spGotInputHintWaitEndDefaultVal:
        begin
          case sSourceStr[index] of
            ']':
            begin
              state.funct := ftPromptUserForParam;
              state.pos := spComplete;
              Inc(Index);
            end;
            else
              State.sSubParam := State.sSubParam + sSourceStr[index];
          end;
        end;

        spGetExecuteConsoleCommand:
        begin
          case sSourceStr[index] of
            '>':
            begin
              state.funct := ftExecuteConsoleCommand;
              state.pos := spComplete;
              Inc(Index);
            end;
            else
              State.sSubParam := State.sSubParam + sSourceStr[index];
          end;
        end;
      end;

      if state.pos <> spComplete then
        Inc(index) // check next character
      else
        // Process function and then check current character again after resetting state.
        DoFunction;
    end;

    // Finish current parse.
    if state.pos in [spFunction, spSide, spIndex, spGotPrefix] then
      DoFunction
    else
      AddParsedText(index);

    Result := sOutput;

  finally
    if Assigned(leftFiles) then
      FreeAndNil(leftFiles);
    if Assigned(rightFiles) then
      FreeAndNil(rightFiles);
    if Assigned(singleFileFiles) then
      FreeAndNil(singleFileFiles);
  end;
end;

{ PrepareParameter }
function PrepareParameter(sParam: string; paramFile: TFile; options: TPrepareParameterOptions; pbShowCommandLinePriorToExecute: PBoolean; pbRunInTerminal: PBoolean; pbKeepTerminalOpen: PBoolean): string;
begin
  Result := sParam;

  if ppoNormalizePathDelims in Options then
    Result := NormalizePathDelimiters(Result);

  if ppoReplaceTilde in Options then
    Result := ReplaceTilde(Result);

  Result := ReplaceEnvVars(Result);

  Result := ReplaceVarParams(Result, paramFile, pbShowCommandLinePriorToExecute, pbRunInTerminal, pbKeepTerminalOpen);

  Result := Trim(Result);
end;

{ ProcessExtCommandFork }
function ProcessExtCommandFork(sCmd, sParams, sWorkPath: string; paramFile: TFile; bTerm: boolean; bKeepTerminalOpen: boolean): boolean;
var
  sTmpFile, sShellCmdLine: string;
  iStart, iCount: integer;
  iLastConsoleCommandPos: integer = 0;
  Process: TProcessUTF8;
  sl: TStringList;
  bShowCommandLinePriorToExecute: boolean = False;
begin
  Result := False;

  // 1. Parse the command, parameters and working directory for the percent-variable substitution.
  sCmd := PrepareParameter(sCmd, paramFile, [ppoReplaceTilde]);
  sParams := PrepareParameter(sParams, paramFile, [], @bShowCommandLinePriorToExecute, @bTerm, @bKeepTerminalOpen);
  sWorkPath := PrepareParameter(sWorkPath, paramFile, [ppoNormalizePathDelims, ppoReplaceTilde]);

  // 2. If working directory has been specified, let's switch to it.
  if sWorkPath <> '' then
    mbSetCurrentDir(sWorkPath);

  // 3. If user has command-line to execute and get the result to a file, let's execute it.
  // Check for <? ?> command.
  // This command is used to put output of some console program to a file so
  //   that the file can then be viewed. The command is between '<?' and '?>'.
  // The whole <?...?> expression is replaced with a path to the temporary file
  //   containing output of the command.
  // For example:
  // {!VIEWER} <?rpm -qivlp --scripts %p?>
  //  Show in Viewer information about RPM package
  repeat
    iStart := Posex('<?', sParams, (iLastConsoleCommandPos + 1)) + 2;
    iCount := Posex('?>', sParams, iStart) - iStart;
    if (iStart <> 0) and (iCount >= 0) then
    begin
      sTmpFile := GetTempName(GetTempFolderDeletableAtTheEnd) + '.tmp';
      sShellCmdLine := Copy(sParams, iStart, iCount) + ' > ' + QuoteStr(sTmpFile);
      Process := TProcessUTF8.Create(nil);
      try
        Process.CommandLine := FormatShell(sShellCmdLine);
        Process.Options := [poNoConsole, poWaitOnExit];
        Process.Execute;
      finally
        Process.Free;
      end;
      sParams := Copy(sParams, 1, iStart - 3) + sTmpFile + Copy(sParams, iStart + iCount + 2, MaxInt);
      iLastConsoleCommandPos := iStart;
    end;
  until ((iStart = 0) or (iCount < 0));

  //4. If user user wanted to execute an internal command, let's do it.
  if frmMain.Commands.Commands.ExecuteCommand(sCmd, [sParams]) = cfrSuccess then
  begin
    Result := True;
    exit;
  end;

  //5. From legacy, invoking shell seems to be similar to "run in terminal with stay open" with param as-is
  if Pos('{!SHELL}', sCmd) > 0 then
  begin
    sCmd := sParams;
    sParams := '';
    bTerm := True;
    bKeepTerminalOpen := True;
  end;

  //6. If user wants to process via terminal (and close at the end), let's flag it.
  if Pos('{!TERMANDCLOSE}', sCmd) > 0 then
  begin
    sCmd := RemoveQuotation(sParams);
    sParams := '';
    bTerm := True;
  end;

  //7. If user wants to process via terminal (and close at the end), let's flag it.
  if Pos('{!TERMSTAYOPEN}', sCmd) > 0 then
  begin
    sCmd := RemoveQuotation(sParams);
    sParams := '';
    bTerm := True;
    bKeepTerminalOpen := True;
  end;

  //8. If our end-job is to EDIT a file via what's configured as editor, let's do it.
  if Pos('{!EDITOR}', sCmd) > 0 then
  begin
    uShowForm.ShowEditorByGlob(RemoveQuotation(sParams));
    Result := True;
    Exit;
  end;

  //9. If our end-job is to EDIT a file via internal editor, let's do it.
  if Pos('{!DC-EDITOR}', sCmd) > 0 then
  begin
    fEditor.ShowEditor(RemoveQuotation(sParams));
    Result := True;
    Exit;
  end;

  //10. If our end-job is to VIEW a file via what's configured as viewer, let's do it.
  if Pos('{!VIEWER}', sCmd) > 0 then
  begin
    uShowForm.ShowViewerByGlob(RemoveQuotation(sParams));
    Result := True;
    Exit;
  end;

  //11. If our end-job is to VIEW a file or files via internal viewer, let's do it.
  if Pos('{!DC-VIEWER}', sCmd) > 0 then
  begin
    sl := TStringList.Create;
    try
      sl.Add(RemoveQuotation(sParams));
      fViewer.ShowViewer(sl);
      Result := True;
    finally
      FreeAndNil(sl);
    end;
    Exit;
  end;

  //12. Ok. If we're here now it's to execute something external so let's launch it!
  try
    Result := ExecCmdFork(sCmd, sParams, sWorkPath, bShowCommandLinePriorToExecute, bTerm, bKeepTerminalOpen);
  except
    on e: EInvalidCommandLine do
    begin
      MessageDlg(rsMsgInvalidCommandLine, rsMsgInvalidCommandLine + ': ' + e.Message, mtError, [mbOK], 0);
      Result := False;
    end;
  end;
end;

function ShellExecuteEx(sActionName, sFileName, sActiveDir: string): boolean;
var
  aFile: TFile;
  sCmd, sParams, sStartPath: string;
begin
  Result := False;

  // Executing files directly only works for FileSystem.
  aFile := TFileSystemFileSource.CreateFileFromFile(sFileName);
  try
    if gExts.GetExtActionCmd(aFile, sActionName, sCmd, sParams, sStartPath) then
    begin
      Result := ProcessExtCommandFork(sCmd, sParams, sStartPath, aFile);
    end;

    if not Result then
    begin
      mbSetCurrentDir(sActiveDir);
      Result := ShellExecute(sFileName);
    end;

  finally
    FreeAndNil(aFile);
  end;
end;



end.














