{
Double Commander
-------------------------------------------------------------------------
This unit contains some functions for open files in associated applications.
Copyright (C) 2006-2019 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, see .
}
unit uShellExecute;
{$mode objfpc}{$H+}
interface
uses
Classes, uFile, uFileView, fMain;
const
ASCII_DLE = #16;
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
%z - last directory of 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
%Z - last directory of path of 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[] - where X is function
is 1..n, where n is number of selected files.
Also can be 0, file under cursor is used in this case.
If there are no selected files, currently active file is nr 1.
If 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[{}][{}]
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 -f )
%f{"}{"} - enclose each name in quotes
(ex.: "" "")
%f1{-first }%f2{ -second }
- if only 1 file selected : -first
- if 2 (or more) files selected: -first -second
*)
function ReplaceVarParams(sSourceStr: string; paramFile: TFile; pbShowCommandLinePriorToExecute: PBoolean; pbRunInTerminal: PBoolean; pbKeepTerminalOpen: PBoolean; pbAbortOperation: PBoolean = nil): string;
type
TFunctType = (ftNone, ftName, ftDir, ftLastDir, ftPath, ftSingleDir, ftLastSingleDir, 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;
leftFile: TFile;
rightFile: TFile;
activeFile: TFile;
inactiveFile: TFile;
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;
fil: TFile;
otherfil: TFile;
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, ftLastDir, ftPath, ftNameNoExt, ftExtension, ftSingleDir, ftLastSingleDir, ftRelativePath, ftSource, ftSourcePath:
begin
case state.funct of
ftName:
Result := aFile.Name;
ftDir:
Result := aFile.Path;
ftLastDir:
Result := GetLastDir(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;
ftLastSingleDir:
Result := GetLastDir(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, ftLastDir, ftSingleDir, ftLastSingleDir:
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;
fil := activeFile;
otherfil := nil;
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 = -2;
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 := -2;
end;
if fileIndex = -1 then
begin
if Assigned(state.fil) then
sOutput := sOutput + BuildName(state.fil);
if Assigned(state.otherfil) then
sOutput := ConcatenateStrWithSpace(sOutput, BuildName(state.otherfil));
end
else 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, ftLastDir, ftNameNoExt, ftSourcePath, ftExtension, ftRelativePath] then
sOutput := sOutput + BuildAllNames
else if state.funct in [ftSingleDir, ftLastSingleDir, 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, ftLastSingleDir: state.pos := spComplete; // Numbers not allowed for %D and %Z
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;
ASCII_DLE:
begin
AddParsedText(state.functStartIndex);
parseStartIndex := Index + 1;
Index := Length(sSourceStr) + 1;
state.pos := spComplete;
Break;
end;
'%', '#':
begin
state.funct := ftProcessPercentSignIndicator;
state.sSubParam := sSourceStr[index];
state.pos := spComplete;
Inc(Index);
end;
'f', 'd', 'z', 'p', 'o', 'e', 'v', 'D', 'Z', 'A', 'a', 'n', 'h', '/', '"', 't':
begin
case sSourceStr[index] of
'f': state.funct := ftName;
'd': state.funct := ftDir;
'z': state.funct := ftLastDir;
'p': state.funct := ftPath;
'o': state.funct := ftNameNoExt;
'e': state.funct := ftExtension;
'v': state.funct := ftRelativePath;
'D': state.funct := ftSingleDir;
'Z': state.funct := ftLastSingleDir;
'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.fil := leftFile;
state.dir := frmMain.FrameLeft.CurrentPath;
state.address := frmMain.FrameLeft.CurrentAddress;
state.pos := spSide;
if sSourceStr[index] = 'b' then
begin
state.otherfiles := rightFiles;
state.otherfil := rightFile;
end;
end;
'r':
begin
state.files := rightFiles;
state.fil := rightFile;
state.dir := frmMain.FrameRight.CurrentPath;
state.address := frmMain.FrameRight.CurrentAddress;
state.pos := spSide;
end;
's', 'p':
begin
state.files := activeFiles;
state.fil := activeFile;
state.dir := activeDir;
state.address := activeAddress;
state.pos := spSide;
if sSourceStr[index] = 'p' then
begin
state.otherfil := inactiveFile;
state.otherfiles := inactiveFiles;
end;
end;
't':
begin
state.files := inactiveFiles;
state.fil := inactiveFile;
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;
leftFile:= frmMain.FrameLeft.CloneActiveFile;
rightFile:= frmMain.FrameRight.CloneActiveFile;
if Assigned(leftFile) and (not leftFile.IsNameValid) then
FreeAndNil(leftFile);
if Assigned(rightFile) and (not rightFile.IsNameValid) then
FreeAndNil(rightFile);
if frmMain.ActiveFrame = frmMain.FrameLeft then
begin
activeFiles := leftFiles;
activeFile:= leftFile;
inactiveFile:= rightFile;
activeDir := frmMain.FrameLeft.CurrentPath;
activeAddress := frmMain.FrameLeft.CurrentAddress;
inactiveFiles := rightFiles;
inactiveDir := frmMain.FrameRight.CurrentPath;
inactiveAddress := frmMain.FrameRight.CurrentAddress;
end
else
begin
activeFiles := rightFiles;
activeFile:= rightFile;
inactiveFile:= leftFile;
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
FreeAndNil(leftFile);
FreeAndNil(rightFile);
FreeAndNil(leftFiles);
FreeAndNil(rightFiles);
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}
// 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.