I've written an object which allows parsing and serializing a command line. I don't in any way consider this done, but this is the beginning of it. I know there are other implementations out there like this, but they tend to be either too primitive or too heavy. This is an attempt to simplify parsing and serializing a command line in Windows, consisting of the application, any file(s) to be opened, and any additional parameters.
The reason I cannot use the built-in param switches is because I need to be able to feed a string from one application instance to another, while enforcing only one instance of the application. For example, lets say there's already an application instance open, and the user chooses to open an existing file, and provide specific parameters to control how it's opened. The new instance will detect that there's already an instance open, and forward the command line to it before terminating itself. Then, the existing instance receives and processes that command line using this parser.
Before I move much further with this object, do you see anything critically wrong with it?
unit CmdLine;
(*
Command Line Parser
by Jerry Dodge
Class: TCmdLine
- Parses out a command line into individual name/value pairs
- Concatenates name/value pairs into a command line string
- Property "ModuleFilename" for the current executable path
- Property "OpenFilename" for the file to be opened, if any
- Default property "Values" to read/write name/value pairs
*)
interface
uses
System.Classes, System.SysUtils;
type
TCmdLine = class(TObject)
private
FItems: TStringList;
FModuleFilename: String;
FOpenFilename: String;
function GetAsString: String;
procedure SetAsString(const Value: String);
procedure SetModuleFilename(const Value: String);
procedure SetOpenFilename(const Value: String);
function GetValue(const Name: String): String;
procedure SetValue(const Name, Value: String);
function GetName(const Index: Integer): String;
public
constructor Create;
destructor Destroy; override;
function Count: Integer;
function Exists(const N: String; const IgnoreCase: Boolean = False): Boolean;
property ModuleFilename: String read FModuleFilename write SetModuleFilename;
property OpenFilename: String read FOpenFilename write SetOpenFilename;
property AsString: String read GetAsString write SetAsString;
property Names[const Index: Integer]: String read GetName;
property Values[const Name: String]: String read GetValue write SetValue; default;
end;
implementation
{ TCmdLine }
constructor TCmdLine.Create;
begin
FItems:= TStringList.Create;
end;
destructor TCmdLine.Destroy;
begin
FItems.Free;
inherited;
end;
function TCmdLine.Count: Integer;
begin
Result:= FItems.Count;
end;
function TCmdLine.Exists(const N: String; const IgnoreCase: Boolean = False): Boolean;
var
X: Integer;
begin
Result:= False;
for X := 0 to FItems.Count-1 do begin
if IgnoreCase then begin
if SameText(N, FItems.Names[X]) then begin
Result:= True;
Break;
end;
end else begin
if N = FItems.Names[X] then begin
Result:= True;
Break;
end;
end;
end;
end;
procedure TCmdLine.SetModuleFilename(const Value: String);
begin
FModuleFilename:= Value;
end;
procedure TCmdLine.SetOpenFilename(const Value: String);
begin
FOpenFilename:= Value;
end;
function TCmdLine.GetValue(const Name: String): String;
begin
Result:= FItems.Values[Name];
end;
procedure TCmdLine.SetValue(const Name, Value: String);
begin
FItems.Values[Name]:= Value;
end;
function TCmdLine.GetAsString: String;
var
X: Integer;
Cmd: String;
Val: String;
begin
Result:= '"'+FModuleFilename+'"';
if Trim(FOpenFilename) <> '' then
Result:= Result + ' "'+FOpenFilename+'"';
for X := 0 to FItems.Count-1 do begin
Cmd:= FItems.Names[X];
Val:= FItems.Values[Cmd];
Result:= Result + ' -'+Cmd;
if Trim(Val) <> '' then begin
Result:= Result + ' ';
if Pos(' ', Val) > 0 then
Result:= Result + '"'+Val+'"'
else
Result:= Result + Val;
end;
end;
end;
function TCmdLine.GetName(const Index: Integer): String;
begin
Result:= FItems.Names[Index];
end;
procedure TCmdLine.SetAsString(const Value: String);
var
Str: String;
Tmp: String;
Cmd: String;
Val: String;
P: Integer;
begin
FItems.Clear;
FModuleFilename:= '';
FOpenFilename:= '';
Str:= Trim(Value) + ' ';
//Extract module filename
P:= Pos('"', Str);
if P = 1 then begin
//Module filename is wrapped in ""
Delete(Str, 1, 1);
P:= Pos('"', Str);
Tmp:= Copy(Str, 1, P-1);
Delete(Str, 1, P);
FModuleFilename:= Tmp;
end else begin
//Module filename is not wrapped in ""
P:= Pos(' ', Str);
Tmp:= Copy(Str, 1, P-1);
Delete(Str, 1, P);
FModuleFilename:= Tmp;
end;
Str:= Trim(Str) + ' ';
//Extract open filename
P:= Pos('"', Str);
if P = 1 then begin
//Open filename is wrapped in ""
Delete(Str, 1, 1);
P:= Pos('"', Str);
Tmp:= Copy(Str, 1, P-1);
Delete(Str, 1, P);
FOpenFilename:= Tmp;
end else begin
//Open filename is not wrapped in ""
P:= Pos('-', Str);
if P < 1 then
P:= Pos('/', 'Str');
if P < 1 then begin
//Param does not have switch name
P:= Pos(' ', Str);
Tmp:= Copy(Str, 1, P-1);
Delete(Str, 1, P);
FOpenFilename:= Tmp;
end;
end;
Str:= Trim(Str) + ' ';
//Extract remaining param switches/values
while Length(Trim(Str)) > 0 do begin
P:= Pos('-', Str);
if P < 1 then
P:= Pos('/', 'Str');
if P > 0 then begin
//Param switch prefix found
Delete(Str, 1, 1);
P:= Pos(' ', Str);
Tmp:= Trim(Copy(Str, 1, P-1)); //Switch name
Delete(Str, 1, P);
Cmd:= Tmp;
Str:= Trim(Str) + ' ';
if (Pos('-', Str) <> 1) and (Pos('/', Str) <> 1) then begin
//This parameter has a value associated with it
P:= Pos('"', Str);
if P = 1 then begin
//Value is wrapped in ""
Delete(Str, 1, 1);
P:= Pos('"', Str);
Tmp:= Copy(Str, 1, P-1);
Delete(Str, 1, P);
end else begin
//Value is not wrapped in ""
P:= Pos(' ', Str);
Tmp:= Copy(Str, 1, P-1);
Delete(Str, 1, P);
end;
Val:= Tmp;
end else begin
Val:= '';
end;
//If blank, add space to ensure at least name gets added
if Val = '' then
Val:= ' ';
FItems.Values[Cmd]:= Val;
end else begin
Str:= '';
raise Exception.Create('Command line parameters malformed ('+Str+')');
end;
Str:= Trim(Str) + ' ';
end;
end;
end.
NOTE: The primary procedure to be reviewed is TCmdLine.SetAsString
.
Sample Usage
CmdLine.AsString := '"C:\MyApp.exe" "C:\SomeFile.txt" -n -o "Some Value With Spaces" -f SomeOtherValueWithNoSpaces -p';
Result
ModuleFilename
=C:\MyApp.exe
OpenFilename
=C:\SomeFile.txt
- Param
n
= [BLANK] - Param
o
=Some Value With Spaces
- Param
f
=SomeOtherValueWithNoSpaces
- Param
p
= [BLANK]
-
\$\begingroup\$ Your sample usage is broken, you should create the object first. Could you edit your question? \$\endgroup\$cpicanco– cpicanco2015年11月19日 16:41:33 +00:00Commented Nov 19, 2015 at 16:41
-
\$\begingroup\$ FCmdLine := TCmdLine.Create; FCmdLine.AsString := '"C:\MyApp.exe" "C:\SomeFile.txt" -n -o "Some Value With Spaces" -f SomeOtherValueWithNoSpaces -p'; WriteLn(FCmdLine.AsString); ReadLn; \$\endgroup\$cpicanco– cpicanco2015年11月19日 17:07:32 +00:00Commented Nov 19, 2015 at 17:07
-
\$\begingroup\$ @cpicanco That should be common sense with any object... \$\endgroup\$Jerry Dodge– Jerry Dodge2015年11月19日 17:18:35 +00:00Commented Nov 19, 2015 at 17:18
-
\$\begingroup\$ You increase your chances making life easier. Also, IMHO being explicit on usage is preferable. \$\endgroup\$cpicanco– cpicanco2015年11月19日 20:10:59 +00:00Commented Nov 19, 2015 at 20:10
-
\$\begingroup\$ @cpicanco Never had that problem on Stack Overflow. They insist on making examples as short as possible. That would be "beating around the bush". \$\endgroup\$Jerry Dodge– Jerry Dodge2015年11月19日 20:28:46 +00:00Commented Nov 19, 2015 at 20:28
1 Answer 1
I think this should work, but I think you could write a class such that the code that used it was more readable. Why not narrow it down to just three functions -
IsArg
(return true if a switch is present, else false)
GetArg
(take a switch, return the value if any)
GetDelimitedArg
(take a switch and a delimiter, return an array result)
Example:
unit CLArgParser;
//this class makes it easier to parse command line arguments
interface
uses
Classes;
type
strarr = array of string;
type
TCLArgParser = class
private
FPermitTags : array of string;
FTrimAll: boolean;
public
function IsArg(argtag : string) : boolean;
function GetArg(argtag : string) : string;
function GetDelimtedArg(argtag, delimiter : string) : TStringList;
constructor Create(ArgTags : array of string); overload;
constructor Create; overload;
property TrimAll: boolean read FTrimAll write FTrimAll;
end;
implementation
uses
SysUtils;
const
cDefaultTags : array[0..1] of string = ('-','/');
constructor TCLArgParser.Create(ArgTags : array of string);
var i : integer;
begin
try
SetLength(FPermitTags,High(ArgTags)+1);
for i := 0 to High(ArgTags) do begin
FPermitTags[i] := ArgTags[i];
end; //for i
except on e : exception do
raise;
end; //try-except
end;
constructor TCLArgParser.Create;
begin
FTrimAll := False; //default value
inherited Create;
Create(cDefaultTags);
end;
function TCLArgParser.GetArg(argtag: string): string;
var i,j,n : integer;
begin
try
Result := '';
n := High(FPermitTags);
for i := 1 to ParamCount do
for j := 0 to n do
if Uppercase(ParamStr(i)) = (FPermitTags[j] + Uppercase(argtag)) then
Result := ParamStr(i+1);
if FTrimAll then begin
Result := Trim(Result);
end;
except on e : exception do
raise;
end; //try-except
end;
function TCLArgParser.GetDelimtedArg(argtag, delimiter: string): TStringList;
var i : integer;
argval, tmp : string;
begin
try
Result := TStringList.Create;
argval := GetArg(argtag);
for i := 1 to Length(argval) do begin
if ((i = Length(argval)) or ((argval[i] = delimiter) and (tmp <> '')))
then begin
if i = Length(argval) then begin
tmp := tmp + argval[i];
if FTrimAll then begin
tmp := Trim(tmp);
end;
end;
Result.Add(tmp);
tmp := '';
end //if we found a delimted value
else begin
tmp := tmp + argval[i];
end; //else we just keep looking
end; //for ea. character
except on e : exception do
raise;
end; //try-except
end;
function TCLArgParser.IsArg(argtag: string): boolean;
var i,j,n : integer;
begin
try
Result := False;
n := High(FPermitTags);
for i := 1 to ParamCount do begin
for j := 0 to n do begin
if Uppercase(ParamStr(i)) = (FPermitTags[j] + Uppercase(argtag))
then begin
Result := True;
Exit;
end; //if we found it
end; //for j
end; //for i
except on e : exception do
raise;
end; //try-except
end;
end.
-
\$\begingroup\$ We tend to prefer answers that include more about why to do things. Why just three? Why not two? Or four? Why implement those three the way that you did? \$\endgroup\$mdfst13– mdfst132015年11月19日 19:23:47 +00:00Commented Nov 19, 2015 at 19:23
Explore related questions
See similar questions with these tags.