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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.