1

Let's say I have the following in my AutoRun script:

@doskey cd = @( ^
    for /f usebackq^^ delims^^=^^ eol^^= %%a in (^
        '$* ' ^
    ) do @( ^
        if "%%~a"==" " ( ^
            if /i not "%%CD%%"=="%%USERPROFILE%%" ( ^
                chdir /d "%%USERPROFILE%%" ^&^& ^
                set "OLDPWD=%%CD%%" ^
            ) ^
        ) else if "%%~a"=="- " ( ^
            if /i not "%%CD%%"=="%%OLDPWD%%" ( ^
                chdir /d "%%OLDPWD%%" ^&^& ^
                set "OLDPWD=%%CD%%" ^
            ) ^
        ) else ( ^
            if /i not "%%CD%%"=="%%~a" ( ^
                chdir /d "%%~a" ^&^& ^
                set "OLDPWD=%%CD%%" ^
            ) ^
        ) ^
    ) ^
)

This is supposed to emulate the behaviour of POSIX cd in cmd.exe, and for the most part it works great:

C:\Users\user>cd C:\

C:\>cd

C:\Users\user>cd -

C:\>

However, all it takes is an incomplete pair of double quotes to make that house of cards fall:

C:\>cd "
2> was unexpected at this time.

Is there a way of sanitizing macro input inline so that it can handle misplaced quotes properly?

I've tried/thought of the following:

  • creative use of for /f with single quotes - what you can see in the provided example: it doesn't break on a list of arguments during string comparison, but " is all it takes
  • turning the whole thing into a compound macro (i.e. for /l (1, 1, 2)... combined with set args=, at the end) - nope, can't enable delayed expansion from command line context
  • ignore $* and just use $1, $2 etc. and hope for the best - doesn't solve the problem as DOSKEY doesn't care about double quotes when delimiting arguments and not applicable to chdir anyway when command extensions are enabled, as it does not treat spaces (or anything) as delimiters
mataha
  • 155
  • 7

1 Answers1

2

One simple way is to use the definition batch file for some code, too.
The macro uses set args= at the end and call then a batch file for the main code.
With this technique the code is also much more readable.

@echo off
REM *** Trampoline jump for function calls of the form ex. "C:\:function:\..\MyBatchFile.bat"
FOR /F "tokens=3 delims=:" %%L in ("%~0") DO goto :%%L


doskey cd=for %%# in ( 1 1 2) do @if %%#==2 ( "%~d0\:__cd:\..\%~pnx0" ) else set args=$*

exit /b

:__cd

setlocal EnableDelayedExpansion

for /F "delims=" %%a in (""!args!"") DO (
  endlocal
  if "%%~a"=="" (
      if /i not "%CD%"=="%USERPROFILE%" (
          chdir /d "%USERPROFILE%" && set "OLDPWD=%CD%"
      ) 
  ) else if "%%~a"=="-" (
       if /i not "%CD%"=="%OLDPWD%" (
            chdir /d "%OLDPWD%" && set "OLDPWD=%CD%"
       ) 
  ) else (
        if /i not "%CD%"=="%%~a" (
            chdir /d %%~a && set "OLDPWD=%CD%"
        )
  )
)

Doskey only:
But if you insist to solve it only with a doskey macro, you could use disappearing carets.

Line2 do the trick:
FOR %%^^^^ in ("") do defines an empty %%^ FOR meta variable.

This is used in for /f "delims=" %%a in (^^""$*%%~^^"^")
The %%~^^ escapes the next quote, but only if $* contains unbalanced quotes, and after the expansion the %%~^^ disappears.
The quote escaping solves the problem of unbalanced quotes in $*, because in the case of none or balanced quotes in $* it expands to a balanced quote string, but this works even for unbalanced quotes in $*

doskey cd=( ^
    FOR %%^^^^ in ("") do @for /f "delims=" %%a in (^^""$*%%~^^"^") do @( ^
        if "%%~a"=="" ( ^
            if /i not "%%CD%%"=="%%USERPROFILE%%" ( ^
                chdir /d "%%USERPROFILE%%" ^&^& ^
                set "OLDPWD=%%CD%%" ^
            ) ^
        ) else if "%%~a"=="-" ( ^
            if /i not "%%CD%%"=="%%OLDPWD%%" ( ^
                chdir /d "%%OLDPWD%%" ^&^& ^
                set "OLDPWD=%%CD%%" ^
            ) ^
        ) else ( ^
            if /i not "%%CD%%"=="%%~a" ( ^
                chdir /d %%~a  ^&^& ^
                set "OLDPWD=%%CD%%" ^
            ) ^
        ) ^
    ) ^
)

Batch file only version:

The great advantage of using a batch file, it can be used in batch files, too, but doskey macros can't.
That's because doskey macros has to be typed in via keyboard, they can't be activated in any other way.

cd.bat (the file should be in the path)

@echo off
setlocal EnableDelayedExpansion

for /F "delims=" %%a in (""!args!"") DO (
  endlocal
  if "%~1"=="" (
      if /i not "%CD%"=="%USERPROFILE%" (
          chdir /d "%USERPROFILE%" && set "OLDPWD=%CD%"
      ) 
  ) else if "%~1"=="-" (
       if /i not "%CD%"=="%OLDPWD%" (
            chdir /d "%OLDPWD%" && set "OLDPWD=%CD%"
       ) 
  ) else (
        if /i not "%CD%"=="%~1" (
            chdir /d "%~1" && set "OLDPWD=%CD%"
        )
  )
)
jeb
  • 78,592
  • 17
  • 171
  • 225
  • Exactly what I had in mind, but couldn't figure out how to do it. Very clever. – mataha May 09 '23 at 22:15
  • I've went a bit further and made that part into a separate macro: `@set i_as_$*=%%^^^^^^^^ in ("") do @for /f "delims=" %%i in (^^^^""$*%%~^^^^"^")`. And I just do `for %i_as_$*% do @...` to write succinct one-liners. Beauty is in the eye of the beholder. – mataha May 10 '23 at 18:15
  • Re: your edit - DOSKEY macros work inside interactive shells as well, i.e. `set /p`. – mataha May 21 '23 at 17:06
  • @mataha I don't understand your comment. I know `set /p` expands a macro, if you type the macro name at the beginning, but I tried to say, that doskey macros doesn't work in batch scripts or even in a more complex command line, like `echo hi & cd -` - there they are not detected nor expanded – jeb May 21 '23 at 18:25
  • Yes, that's what I meant about `set /p`. I know that doskey macros don't work in scripts, which is a shame! That said, I feel it's a bit of an overkill to put `cd` in a script instead of inlining it using a macro. One notices that it's slower than running `chdir` directly, and then there's `PATH` lookup... – mataha May 21 '23 at 20:01