Windows: spack.bat CLI handling robustness (#36281)

* Current develop spack.bat file cannot handle any reserved characters
  being passed via the CLI, particularly '=' and '?'. To address this,
  re-do the CLI parsing for loop to use custom logic to allow for more
  granular handling of CLI args.
* We take a less-than-ideal approach to escaping local scope and
  handling unset variables as well as the actual parsing of CL
  arguments. To address this, don't quote the args and then try to
  parse the quotes we just added (resulting in spack flags being
  undefined). Instead, leverage batch script features. Since we are
  not unnecessarily quoting things, we don't need to think about
  removing them, and in the case of paths with spaces, we should _not_
  be removing the quotes as we currently do.
This commit is contained in:
John W. Parent 2023-03-28 19:50:37 -04:00 committed by GitHub
parent 7ffe2fadfe
commit 6b03c9f285
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -50,24 +50,69 @@ setlocal enabledelayedexpansion
:: flags will always start with '-', e.g. --help or -V
:: subcommands will never start with '-'
:: everything after the subcommand is an arg
for %%x in (%*) do (
set t="%%~x"
:: we cannot allow batch "for" loop to directly process CL args
:: a number of batch reserved characters are commonly passed to
:: spack and allowing batch's "for" method to process the raw inputs
:: results in a large number of formatting issues
:: instead, treat the entire CLI as one string
:: and split by space manually
:: capture cl args in variable named cl_args
set cl_args=%*
:process_cl_args
:: tokens=1* returns the first processed token produced
:: by tokenizing the input string cl_args on spaces into
:: the named variable %%g
:: While this make look like a for loop, it only
:: executes a single time for each of the cl args
:: the actual iterative loop is performed by the
:: goto process_cl_args stanza
:: we are simply leveraging the "for" method's string
:: tokenization
for /f "tokens=1*" %%g in ("%cl_args%") do (
set t=%%~g
:: remainder of string is composed into %%h
:: these are the cl args yet to be processed
:: assign cl_args var to only the args to be processed
:: effectively discarding the current arg %%g
:: this will be nul when we have no further tokens to process
set cl_args=%%h
:: process the first space delineated cl arg
:: of this iteration
if "!t:~0,1!" == "-" (
if defined _sp_subcommand (
:: We already have a subcommand, processing args now
if not defined _sp_args (
set "_sp_args=!t!"
) else (
set "_sp_args=!_sp_args! !t!"
)
) else (
if not defined _sp_flags (
set "_sp_flags=!t!"
shift
) else (
set "_sp_flags=!_sp_flags! !t!"
shift
)
)
) else if not defined _sp_subcommand (
set "_sp_subcommand=!t!"
shift
) else (
if not defined _sp_args (
set "_sp_args=!t!"
shift
) else (
set "_sp_args=!_sp_args! !t!"
shift
)
)
)
:: if this is not nil, we have more tokens to process
:: start above process again with remaining unprocessed cl args
if defined cl_args goto :process_cl_args
:: --help, -h and -V flags don't require further output parsing.
:: If we encounter, execute and exit
@ -95,31 +140,21 @@ if not defined _sp_subcommand (
:: pass parsed variables outside of local scope. Need to do
:: this because delayedexpansion can only be set by setlocal
echo %_sp_flags%>flags
echo %_sp_args%>args
echo %_sp_subcommand%>subcmd
endlocal
set /p _sp_subcommand=<subcmd
set /p _sp_flags=<flags
set /p _sp_args=<args
if "%_sp_subcommand%"=="ECHO is off." (set "_sp_subcommand=")
if "%_sp_subcommand%"=="ECHO is on." (set "_sp_subcommand=")
if "%_sp_flags%"=="ECHO is off." (set "_sp_flags=")
if "%_sp_flags%"=="ECHO is on." (set "_sp_flags=")
if "%_sp_args%"=="ECHO is off." (set "_sp_args=")
if "%_sp_args%"=="ECHO is on." (set "_sp_args=")
del subcmd
del flags
del args
endlocal & (
set "_sp_flags=%_sp_flags%"
set "_sp_args=%_sp_args%"
set "_sp_subcommand=%_sp_subcommand%"
)
:: Filter out some commands. For any others, just run the command.
if %_sp_subcommand% == "cd" (
if "%_sp_subcommand%" == "cd" (
goto :case_cd
) else if %_sp_subcommand% == "env" (
) else if "%_sp_subcommand%" == "env" (
goto :case_env
) else if %_sp_subcommand% == "load" (
) else if "%_sp_subcommand%" == "load" (
goto :case_load
) else if %_sp_subcommand% == "unload" (
) else if "%_sp_subcommand%" == "unload" (
goto :case_load
) else (
goto :default_case
@ -154,20 +189,20 @@ goto :end_switch
if NOT defined _sp_args (
goto :default_case
)
set args_no_quote=%_sp_args:"=%
if NOT "%args_no_quote%"=="%args_no_quote:--help=%" (
if NOT "%_sp_args%"=="%_sp_args:--help=%" (
goto :default_case
) else if NOT "%args_no_quote%"=="%args_no_quote: -h=%" (
) else if NOT "%_sp_args%"=="%_sp_args: -h=%" (
goto :default_case
) else if NOT "%args_no_quote%"=="%args_no_quote:--bat=%" (
) else if NOT "%_sp_args%"=="%_sp_args:--bat=%" (
goto :default_case
) else if NOT "%args_no_quote%"=="%args_no_quote:deactivate=%" (
) else if NOT "%_sp_args%"=="%_sp_args:deactivate=%" (
for /f "tokens=* USEBACKQ" %%I in (
`call python %spack% %_sp_flags% env deactivate --bat %args_no_quote:deactivate=%`
`call python %spack% %_sp_flags% env deactivate --bat %_sp_args:deactivate=%`
) do %%I
) else if NOT "%args_no_quote%"=="%args_no_quote:activate=%" (
) else if NOT "%_sp_args%"=="%_sp_args:activate=%" (
for /f "tokens=* USEBACKQ" %%I in (
`python %spack% %_sp_flags% env activate --bat %args_no_quote:activate=%`
`python %spack% %_sp_flags% env activate --bat %_sp_args:activate=%`
) do %%I
) else (
goto :default_case
@ -188,7 +223,7 @@ if defined _sp_args (
for /f "tokens=* USEBACKQ" %%I in (
`python "%spack%" %_sp_flags% %_sp_subcommand% --bat %_sp_args%`) do %%I
)
goto :end_switch
:case_unload