Recent

Author Topic: Escape a command line parameter  (Read 671 times)

LemonParty

  • Sr. Member
  • ****
  • Posts: 391
Escape a command line parameter
« on: November 17, 2025, 07:42:43 pm »
Hello.

I need to escape a command line parameter.
For example for Windows my " param will become "my "" param".
What characters should I escape beside '"'? Also interest how escaping work in Linux.
Is there a ready function for that?
Lazarus v. 4.99. FPC v. 3.3.1. Windows 11

Khrys

  • Sr. Member
  • ****
  • Posts: 367
Re: Escape a command line parameter
« Reply #1 on: November 18, 2025, 06:51:06 am »
For example for Windows my " param will become "my "" param".

Handling of escaped quotes is broken on Windows (for compatibility reasons I assume). There's a Win32 function just for parsing command line arguments, but the RTL doesn't use it, so I wrote a unit that does (to be included in the main program's  uses  section, preferably near the end of the list as per the comments):

Code: Pascal  [Select][+][-]
  1. unit WindowsArgs;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. // WARNING: This unit must be loaded AFTER any other argc/argv-modifying units (such as LazUTF8).
  8.  
  9. uses
  10.   SysUtils, Windows;
  11.  
  12. implementation
  13.  
  14. var
  15.   OriginalArgc: LongInt;
  16.   OriginalArgv: PPChar;
  17.  
  18. function CommandLineToArgvW(lpCmdLine: LPCWSTR; pNumArgs: PINTEGER): PLPWSTR; stdcall; external 'shell32';
  19.  
  20. /// Parses C command line arguments (argc, argv) using the CommandLineToArgv function recommended by Microsoft.
  21. /// FPC uses a custom implementation that doesn't support escaping double quotes inside arguments; for example...
  22. /// running 'prog.exe one "two \"three\""' produces ['prog.exe', 'one', 'two \three\'] instead of...
  23. /// ...the correct argument list ['prog.exe', 'one', 'two "three"'].
  24. /// On success, argv and argc from the System unit are overwritten; on failure, they are left unchanged.
  25. /// This function shall only be called once at program startup and paired with a call to FinalizeArguments (1).
  26. procedure InitializeArguments();
  27. var
  28.   ArgVector: PLPWSTR;
  29.   ArgCount: DWORD;
  30.   Arg: String;
  31.   I: SizeInt;
  32. begin
  33.  
  34.   ArgVector := CommandLineToArgvW(GetCommandLineW(), @ArgCount);
  35.   if ArgVector = Nil then Exit();
  36.  
  37.   OriginalArgc := argc;
  38.   OriginalArgv := argv;
  39.   argc := ArgCount;
  40.   argv := SysGetMem(SizeOf(argv^) * (argc + 1));
  41.  
  42.   for I := 0 to argc - 1 do begin
  43.     Arg := String(WideString(ArgVector[I]));
  44.     argv[I] := SysGetMem(Arg.Length + 1);
  45.     Move(PChar(Arg)[0], argv[I][0], Arg.Length + 1);
  46.   end;
  47.  
  48.   argv[argc] := Nil;
  49.  
  50.   LocalFree(PHANDLE(@ArgVector)^);
  51. end;
  52.  
  53. /// Deallocates the argument vector created by a previous call to InitializeArguments (1) and restores the old values.
  54. /// This function shall only be called once at program exit.
  55. procedure FinalizeArguments();
  56. var
  57.   I: SizeInt;
  58. begin
  59.  
  60.   if OriginalArgv = Nil then Exit();
  61.  
  62.   for I := 0 to argc - 1 do SysFreeMem(argv[I]);
  63.   SysFreeMem(argv);
  64.  
  65.   argc := OriginalArgc;
  66.   argv := OriginalArgv;
  67. end;
  68.  
  69. initialization
  70. begin
  71.   InitializeArguments();
  72. end;
  73.  
  74. finalization
  75. begin
  76.   FinalizeArguments();
  77. end;
  78.  
  79. end.

LemonParty

  • Sr. Member
  • ****
  • Posts: 391
Re: Escape a command line parameter
« Reply #2 on: November 18, 2025, 11:31:31 am »
Thank you, Khrys.

I wrote escaping functions from scratch. Should be crossplatform.
Code: Pascal  [Select][+][-]
  1. uses SysUtils;
  2.  
  3. function ParameterLength(Parameter: AnsiString): SizeUInt;inline;
  4. begin
  5.   Result:= Length(Parameter);
  6.   if Result = 0 then
  7.     Result:= 2
  8.   else begin
  9.     if Pos(' ', Parameter) <> 0 then
  10.       inc(Result, 2);
  11.     {$If Defined(Windows)}
  12.     inc(Result, Parameter.CountChar('"'));
  13.     {$ElseIf Defined(UNIX)}
  14.     inc(Result, Parameter.CountChar('''') * 3);
  15.     {$EndIf}
  16.   end;
  17. end;
  18.  
  19. function ParameterLength(Parameter: UnicodeString): SizeUInt;inline;
  20. begin
  21.   Result:= Length(Parameter);
  22.   if Result = 0 then
  23.     Result:= 2
  24.   else begin
  25.     if Pos(' ', Parameter) <> 0 then
  26.       inc(Result, 2);
  27.     {$If Defined(Windows)}
  28.     inc(Result, Parameter.CountChar('"'));
  29.     {$ElseIf Defined(UNIX)}
  30.     inc(Result, Parameter.CountChar('''') * 3);
  31.     {$EndIf}
  32.   end;
  33. end;
  34.  
  35. function EscapeParameter(Parameter: AnsiString; var OutBuf: AnsiChar): SizeUInt;
  36. var
  37.   Arr: array[Byte]of AnsiChar absolute OutBuf;
  38.   i: SizeUInt;
  39.   HaveSpace: Boolean;
  40. begin
  41.   {$If Defined(Windows)}
  42.   Result:= 0;
  43.   if Parameter = '' then begin
  44.     Arr[0]:= '"';
  45.     Arr[1]:= '"';
  46.     Exit(2);
  47.   end;
  48.   HaveSpace:= Pos(' ', Parameter) <> 0;
  49.   Result:= 0;
  50.   if HaveSpace then begin
  51.     Arr[Result]:= '"';
  52.     inc(Result);
  53.   end;
  54.   for i:= 1 to Length(Parameter) do begin
  55.     if Parameter[i] = '"' then begin
  56.       Arr[Result]:= '"';
  57.       Arr[Result + 1]:= '"';
  58.       inc(Result, 2);
  59.     end else begin
  60.       Arr[Result]:= Parameter[i];
  61.       inc(Result);
  62.     end;
  63.   end;
  64.   if HaveSpace then begin
  65.     Arr[Result]:= '"';
  66.     inc(Result);
  67.   end;
  68.   {$ElseIf Defined(UNIX)}
  69.   if Parameter = '' then begin
  70.     Arr[0]:= '''';
  71.     Arr[1]:= '''';
  72.     Exit(2);
  73.   end;
  74.   HaveSpace:= Pos(' ', Parameter) <> 0;
  75.   Result:= 0;
  76.   if HaveSpace then begin
  77.     Arr[Result]:= '"';
  78.     inc(Result);
  79.   end;
  80.   for i:= 1 to Length(Parameter) do begin
  81.     if Parameter[i] = '''' then begin
  82.       Arr[Result]:= '''';
  83.       Arr[Result + 1]:= '\';
  84.       Arr[Result + 2]:= '''';
  85.       Arr[Result + 3]:= '''';
  86.       inc(Result, 4);
  87.     end else begin
  88.       Arr[Result]:= Parameter[i];
  89.       inc(Result);
  90.     end;
  91.   end;
  92.   if HaveSpace then begin
  93.     Arr[Result]:= '"';
  94.     inc(Result);
  95.   end;
  96.   {$Else}
  97.  
  98.   {$EndIf}
  99. end;
  100.  
  101. function EscapeParameter(Parameter: UnicodeString; var OutBuf: UnicodeChar): SizeUInt;
  102. var
  103.   Arr: array[Byte]of UnicodeChar absolute OutBuf;
  104.   i: SizeUInt;
  105.   HaveSpace: Boolean;
  106. begin
  107.   {$If Defined(Windows)}
  108.   Result:= 0;
  109.   if Parameter = '' then begin
  110.     Arr[0]:= '"';
  111.     Arr[1]:= '"';
  112.     Exit(2);
  113.   end;
  114.   HaveSpace:= Pos(' ', Parameter) <> 0;
  115.   Result:= 0;
  116.   if HaveSpace then begin
  117.     Arr[Result]:= '"';
  118.     inc(Result);
  119.   end;
  120.   for i:= 1 to Length(Parameter) do begin
  121.     if Parameter[i] = '"' then begin
  122.       Arr[Result]:= '"';
  123.       Arr[Result + 1]:= '"';
  124.       inc(Result, 2);
  125.     end else begin
  126.       Arr[Result]:= Parameter[i];
  127.       inc(Result);
  128.     end;
  129.   end;
  130.   if HaveSpace then begin
  131.     Arr[Result]:= '"';
  132.     inc(Result);
  133.   end;
  134.   {$ElseIf Defined(UNIX)}
  135.   if Parameter = '' then begin
  136.     Arr[0]:= '''';
  137.     Arr[1]:= '''';
  138.     Exit(2);
  139.   end;
  140.   HaveSpace:= Pos(' ', Parameter) <> 0;
  141.   Result:= 0;
  142.   if HaveSpace then begin
  143.     Arr[Result]:= '"';
  144.     inc(Result);
  145.   end;
  146.   for i:= 1 to Length(Parameter) do begin
  147.     if Parameter[i] = '''' then begin
  148.       Arr[Result]:= '''';
  149.       Arr[Result + 1]:= '\';
  150.       Arr[Result + 2]:= '''';
  151.       Arr[Result + 3]:= '''';
  152.       inc(Result, 4);
  153.     end else begin
  154.       Arr[Result]:= Parameter[i];
  155.       inc(Result);
  156.     end;
  157.   end;
  158.   if HaveSpace then begin
  159.     Arr[Result]:= '"';
  160.     inc(Result);
  161.   end;
  162.   {$Else}
  163.  
  164.   {$EndIf}
  165. end;
Usage:
Code: Pascal  [Select][+][-]
  1. {...}
  2. var
  3.   Param: String = 'My "parameter"';
  4.   EscapedStr: String;
  5. begin
  6.   SetLength(EscapedStr, ParameterLength(Param));
  7.   EscapeParameter(Param, EscapedStr[1]);
  8.   Writeln(EscapedStr);
  9. end.
  10.  
EscapeParameter writes result into buffer, so this function may be used in cycle.
Not tested well.
Lazarus v. 4.99. FPC v. 3.3.1. Windows 11

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 12571
  • FPC developer.
Re: Escape a command line parameter
« Reply #3 on: November 18, 2025, 01:24:00 pm »
For example for Windows my " param will become "my "" param".

Handling of escaped quotes is broken on Windows (for compatibility reasons I assume). There's a Win32 function just for parsing command line arguments, but the RTL doesn't use it, so I wrote a unit that does (to be included in the main program's  uses  section, preferably near the end of the list as per the comments):

Do you have examples, and which versions did you test? IIRC there are patches by Yuri about this in flight that synchronise behaviour with delphi. I can test RC1 and trunk if needed.

Khrys

  • Sr. Member
  • ****
  • Posts: 367
Re: Escape a command line parameter
« Reply #4 on: November 18, 2025, 03:30:32 pm »
Do you have examples, and which versions did you test? IIRC there are patches by Yuri about this in flight that synchronise behaviour with delphi.

There's an example in the comment above  InitializeArguments:

Quote from: WindowsArgs.pas
[...] for example running  'prog.exe one "two \"three\""'  produces  ['prog.exe', 'one', 'two \three\']  instead of the correct argument list  ['prog.exe', 'one', 'two "three"']

I wrote this unit more than a year ago (back when I was using Lazarus 2.0.12 / FPC 3.2.0) so I could parse JSON arguments.

Grepping for  CommandLineToArgv on current trunk returns only one match (the function declaration) in  winunits-base.

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 12571
  • FPC developer.
Re: Escape a command line parameter
« Reply #5 on: November 18, 2025, 04:51:48 pm »
prog.exe one "two \"three\""'

[/tt]  produces  ['prog.exe', 'one', 'two \three\']  instead of the correct argument list  ['prog.exe', 'one', 'two "three"']

Returns

Code: [Select]
1 one
2 two \three\'

on 3.2.2, 3.2.4-rc1 and delphi.

FPC 3.3.1 produces:

Code: [Select]
1 one
2 two \three\"

(double instead of single quote)


Where do you get the escaping rules? From VS or Winapi/MSDN ?
« Last Edit: November 18, 2025, 04:53:31 pm by marcov »

Khrys

  • Sr. Member
  • ****
  • Posts: 367
Re: Escape a command line parameter
« Reply #6 on: November 19, 2025, 07:15:21 am »
Where do you get the escaping rules? From VS or Winapi/MSDN ?

All my code does is call  CommandLineToArgvW(GetCommandLineW, ...)  and wrangle the result into  argv/argc.  Per MSDN:

Quote from: CommandLineToArgvW function (shellapi.h)
CommandLineToArgvW has a special interpretation of backslash characters when they are followed by a quotation mark character ("). This interpretation assumes that any preceding argument is a valid file system path, or else it may behave unpredictably.

This special interpretation controls the "in quotes" mode tracked by the parser. When this mode is off, whitespace terminates the current argument. When on, whitespace is added to the argument like all other characters.

  • 2n backslashes followed by a quotation mark produce n backslashes followed by begin/end quote. This does not become part of the parsed argument, but toggles the "in quotes" mode.
  • (2n) + 1 backslashes followed by a quotation mark again produce n backslashes followed by a quotation mark literal ("). This does not toggle the "in quotes" mode.
  • n backslashes not followed by a quotation mark simply produce n backslashes.

Thaddy

  • Hero Member
  • *****
  • Posts: 18524
  • Here stood a man who saw the Elbe and jumped it.
Re: Escape a command line parameter
« Reply #7 on: November 19, 2025, 08:09:26 am »
Yes, but, although to my ions of horror argc and argv are directly supported by Freepascal in the system unit, the proper Pascal versions are ParamCount and ParamStr and these are to my knowledge not exact equivalents.
So wrangle it in those two  ;) :D
Due to censorship, I changed this to "Nelly the Elephant". Keeps the message clear.

Khrys

  • Sr. Member
  • ****
  • Posts: 367
Re: Escape a command line parameter
« Reply #8 on: November 19, 2025, 09:09:27 am »
So wrangle it in those two  ;) :D

That's... exactly what I'm doing  ;)
ParamStr / ParamCount  are mere wrappers around  argv/argc.  If I simply redefined/overloaded the former then things like  Application.GetOptionValue  would still use the old/incorrect values.

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 12571
  • FPC developer.
Re: Escape a command line parameter
« Reply #9 on: November 19, 2025, 09:37:57 am »
That is maybe important for C (MSVCRT) programs that have special handling for backslash. Afaik escaping simply works with doubling, and Pascal doesn't interpret backslashing any different or escaped, and its products are native win32, not msvcrt based.

There is no system level argc/argv on win32/64, it is emulated because delphi does (early Kylix influence?) see rtl/win/syswinh.inc, and syswin.inc for how it is filled from getcommandlineA/W.
« Last Edit: November 19, 2025, 09:41:35 am by marcov »

 

TinyPortal © 2005-2018