4
\$\begingroup\$

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]
cpicanco
5093 silver badges19 bronze badges
asked Nov 18, 2015 at 3:28
\$\endgroup\$
6
  • \$\begingroup\$ Your sample usage is broken, you should create the object first. Could you edit your question? \$\endgroup\$ Commented 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\$ Commented Nov 19, 2015 at 17:07
  • \$\begingroup\$ @cpicanco That should be common sense with any object... \$\endgroup\$ Commented Nov 19, 2015 at 17:18
  • \$\begingroup\$ You increase your chances making life easier. Also, IMHO being explicit on usage is preferable. \$\endgroup\$ Commented 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\$ Commented Nov 19, 2015 at 20:28

1 Answer 1

1
\$\begingroup\$

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.
mdfst13
22.4k6 gold badges34 silver badges70 bronze badges
answered Nov 19, 2015 at 18:41
\$\endgroup\$
1
  • \$\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\$ Commented Nov 19, 2015 at 19:23

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.