{
    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; pbAbortOperation: 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; pbAbortOperation: 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,
  DCUnicodeUtils,

  //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
   -------------------------------------------------------------------------------
   %[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.
   %[Enter with required;1024] - This is an example. The text following the ";" indicates that default value is 1024
   %[First name;%o] - The text proposed in the parameter value may be parsed prior to be displayed to user. For example here, the %o will be substituted to the filename with no extension prior to be displayed to user.

   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; pbAbortOperation: PBoolean = nil): 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);

  var
    leftFiles: TFiles = nil;
    rightFiles: TFiles = nil;
    singleFileFiles: TFiles = nil;
    activeFiles: TFiles;
    inactiveFiles: TFiles;
    activeDir: string;
    inactiveDir: string;
    activeAddress: string;
    inactiveAddress: string;
    bTrailingDelimiter: boolean = False;
    bQuoteOrNot: boolean = True;
    CurrentPercentIndicator: char = '%';
    bKeepProcessParameter:boolean = true;

  // There is a inside recursive function because of the %[ that could have a parameter that could be parsed using the same function.
  // It would have been possible to simply call again "ReplaceVarParams" without an inner function...
  // ...but there are a few things that would have not work as what the user would expect.
  // For example, if user would have wrote previously %"0 to have the following not include the quote, by simply recalling "ReplaceVarParams" itself, if he would have used then a %o in the default parameter value for the %[ , the filename would have been quoted again since it's the default when entering into the "ReplaceVarParams" function originally...
  // Same similar problem with the bTrailingDelimiter, etc.
  // So that's why there is an inner recursive functions where the kind of local-global flag like the ones mentionned above have to be global for the current parsed string.
  function InnerRecursiveReplaceVarParams(sSourceStr: string; paramFile: TFile; pbShowCommandLinePriorToExecute: PBoolean; pbRunInTerminal: PBoolean; pbKeepTerminalOpen: PBoolean; pbAbortOperation: PBoolean = nil): string;
  type
    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;
    state: Tstate;
    sOutput: string = '';
    parseStartIndex: integer;

    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, just 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, just 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
      state.sUserMessage := InnerRecursiveReplaceVarParams(state.sUserMessage, paramFile, pbShowCommandLinePriorToExecute, pbRunInTerminal, pbKeepTerminalOpen, pbAbortOperation);
      msgOK(state.sUserMessage);
    end;

    procedure AskUserParamAndReplace;
    begin
      state.sSubParam := InnerRecursiveReplaceVarParams(state.sSubParam, paramFile, pbShowCommandLinePriorToExecute, pbRunInTerminal, pbKeepTerminalOpen, pbAbortOperation);
      if ShowInputQuery(rsMsgCofirmUserParam, state.sUserMessage, state.sSubParam) then
      begin
        sOutput := sOutput + state.sSubParam;
      end
      else
      begin
        bKeepProcessParameter:=False;
      end;
    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
    index := 1;
    parseStartIndex := index;

    ResetState(state);

    while (index <= Length(sSourceStr)) AND (bKeepProcessParameter) 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 bKeepProcessParameter then
    begin
      if state.pos in [spFunction, spSide, spIndex, spGotPrefix] then
        DoFunction
      else
        AddParsedText(index);
    end;

    if bKeepProcessParameter then
      Result := sOutput
    else
      if pbAbortOperation<>nil then
        pbAbortOperation^ := True;
  end;

begin
  result := '';
  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;

    result:=InnerRecursiveReplaceVarParams(sSourceStr, paramFile, pbShowCommandLinePriorToExecute, pbRunInTerminal, pbKeepTerminalOpen, pbAbortOperation);

  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; pbAbortOperation: PBoolean = nil): string; overload;
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,pbAbortOperation);

  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;
  bAbortOperationFlag: 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, @bAbortOperationFlag);
  if not bAbortOperationFlag then sWorkPath := PrepareParameter(sWorkPath, paramFile, [ppoNormalizePathDelims, ppoReplaceTilde]);

  // 2. If working directory has been specified, let's switch to it.
  if not bAbortOperationFlag then
  begin
    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 := [poWaitOnExit];
          Process.ShowWindow := swoHide;
          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; //if not bAbortOperationFlag
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.














