2

If a script which should get exited in subroutines without closing the terminal when calling EXIT 1. There for I use this if which calls the script again.

This worked fine until I now discovered some issue with a quoted vertical bar as a parameter "!". I get an error stating that the command is misspelled.

Here is the part of the script that fails:

@ECHO OFF
    SETLOCAL DISABLEDELAYEDEXPANSION

    IF "%selfWrapped%"=="" (
        REM this is necessary so that we can use "exit" to terminate the batch file,
        REM and all subroutines, but not the original cmd.exe
        SET selfWrapped=true
        %ComSpec% /s /c ""%~0" %*"
        GOTO :EOF
    ) 

    echo %*

    ENDLOCAL
EXIT /B 0

Call:

test.cmd "hello world" "|"

Expected Output:

"hello world" "|"

I checked the the value of %* inside the IF but for it seems totally legitimate to use a vertical bar as well as any other quoted string.

So...

  1. Why does the script fails?
  2. How can I fix it?
goulashsoup
  • 2,639
  • 2
  • 34
  • 60
  • 2
    Works ok if you just run the bat directly or use call: `"%~0" %*` or `CALL "%~0" %*` – Squashman Jun 13 '18 at 21:43
  • 1
    Have you tried this: `"%ComSpec%" /S /C "%~0" %*`? – aschipfl Jun 14 '18 at 00:22
  • 2
    @Squashman, as mentioned in the remarks, the new `cmd` instance is needed to be able to terminate the batch script and all its sub-routines using `exit`, so the current instance does not become quit... – aschipfl Jun 14 '18 at 10:19
  • What about this: `"%ComSpec%" /S /C ^^^""%~0" %*^^^"`, or even `"%ComSpec%" /S /C ^^^""%~0" %`? – aschipfl Jun 14 '18 at 10:31

3 Answers3

3

I do not agree with some of the description in the link. See exit /? accurate help description.

  • exit exits the interpreter.
  • exit 1 exits the interpreter with exitcode 1.
  • exit /b has similar behavior as goto :eof which exits the script or called label. Errorlevel is not reset so allows errorlevel from the previous command to be accessable after exit of the script or the called label.
  • exit /b 1 exits the script or the called label with errorlevel 1.

If you oddly use exit /b at a CMD prompt, it is going to exit the interpreter.


Main code:

@ECHO OFF
    SETLOCAL DISABLEDELAYEDEXPANSION

    SET args=%*
    SET "self=%~f0"

    IF "%selfWrapped%"=="" (
        @REM this is necessary so that we can use "exit" to terminate the batch file,
        @REM and all subroutines, but not the original cmd.exe
        SET "selfWrapped=true"

        SETLOCAL ENABLEDELAYEDEXPANSION
        ECHO !ComSpec! /s /c ""!self!" !args!"
        "!ComSpec!" /s /c ""!self!" !args!"
        GOTO :EOF
    )

    ECHO(%*
EXIT /B 0

Both use of GOTO :EOF and EXIT /B 0 will exit the script. ENDLOCAL is implied at exit of the script. Explicit use of ENDLOCAL is for when you want to end the current local scope and continue the script. As always, being explicit all the time is a choice.

Setting %* to args keeps the double quoting paired. Quoting i.e. set "args=%*" can cause issue sometimes though not using quotes allow code injection i.e. arguments "arg1" ^& del *.*. If the del *.* is not going to execute at the set line, then it will probably happen at the ComSpec line. For this example, I chose not quote. So, it is a choice.

You are using disabled expansion at start of the script. That saves the ! arguments which is good. Before you execute ComSpec though, enable delayed expansion and use !args! which is now protected from the interpreter now not seeing | or any other special character which may throw an error.

Your script fails as the | argument is exposed.

C:\Windows\system32\cmd.exe /s /c ""test.cmd" "  | ""

The above is echoed evaluation of the ComSpec line with setting @ECHO ON. Notice the pairing of quotes i.e. "", " " and "". Notice the extra spacing inserted around the | character as the interpreter does not consider it as part of a quoted string.

Compared to updated code changes of echoed evaluation...:

"!ComSpec!" /s /c ""!self!" !args!"

The string between the quotes remain intact. No extra spacing inserted into the string. The echoed evalution looks good and executes good.

Disclaimer:

Expressing the workings of CMD is like walking a tight rope. Just when you think you know, fall off the rope you go.

michael_heath
  • 5,262
  • 2
  • 12
  • 22
  • It would be interesting to see what Jeb and Dbenham have to say about the question. They are pretty knowledgeable about the parsing rules. – Squashman Jun 14 '18 at 03:36
  • 1
    Nice, but regard that delayed expansion only helps in the parent `cmd` instance but not in the child one! Anyway, there is still room for improvement: 1. You are expanding `%`-variables while delayed expansion is enabled, which can cause trouble particularly with `!` characters; change all `%` expansions to `!` ones to be safe (like `!ComSpec!`; then do `set "self=%~0F"` before enabling delayed expansion and use `!self!` instead of `%~f0` later). 2. Put `!ComSpec!` in between `""` to allow custom system directories containing white-spaces. 3. Use the safe `echo` syntax `echo(%*`. – aschipfl Jun 14 '18 at 11:05
  • 1
    4. I would escape the outer quotes in the child `cmd` instantiation, like this: `"!ComSpec!" /S /C ^""!self!" !args!^"`, to avoid syntax errors when `cmd` attempts to remove outer quotes. 5. I would remove the quotes at `set args=%*`, because under normal circumstances, arguments are already quoted correctly (e. g., when doing drag-and-drop, arguments with white-spaces and special characters are already quoted, so another pair of quotes exposes special characters to `cmd` unintentionally). In case of unbalanced quotes, both the unquoted and the quoted assignmend variants can fail anyway. – aschipfl Jun 14 '18 at 11:05
  • @aschipf. **1.** Done. **2.** Done. **3.** Done. **4.** CMD will strip the outer quotes anyway as `/s` enforces it and old CMD behavior ensures it, if with using `/c` or `/k` rule #2 applies in `cmd /?`. I just do not see how using `^"` on the arguments outer quotes is a safer option. If it passed to the script OK initially, then delayed expansion should protect it until it executes `ComSpec` and gets passed again. Need further convincing before appling it. **5.** Done. **#.** Thankyou for your advice with keeping me on the tight rope. – michael_heath Jun 14 '18 at 12:44
  • The purpose of **4.** is to present quotes to `cmd` which it will strip, in order to avoid stripping of needed quotes (like `"program.exe" "argument"` becoming `program.exe" "argument`)... You're welcome! – aschipfl Jun 14 '18 at 16:16
  • 1
    @aschipfl **5.** drag-and-drop does only quote arguments with white spaces, but not for special characters like in `Cat&dog.txt` – jeb Jun 14 '18 at 16:31
  • @jeb, thanks for correcting, I had it wrong in mind and didn't check! So the final question is, what is better: `set args=%*` or `set "args=%*"`? I my opinion, it doesn't matter, because dragging-and-dropping `cat&dog.txt` and `cat & dog.txt` results in different quoting, hence one of the `&`s becomes exposed to `cmd` in any case; or did I miss anything? – aschipfl Jun 14 '18 at 16:55
  • @jeb, `cmd`'s auto-completion feature automatically quotes paths containing the special characters listed at the end of `cmd /?` plus `%`, that is what I had in mind... – aschipfl Jun 15 '18 at 12:14
  • @aschipfl 1. `set args=%*` works in more cases than `set "args=%*"`, as `cat&dog.txt` can't fetched with `%*` at all, ithe `&dog.txt is` not part of `%*`, see also [SO:Drag and drop batch file for multiple files? ](https://stackoverflow.com/a/5192427/463115). Your second comment is a good point, that is obviously useful behaviour – jeb Jun 15 '18 at 13:19
2

I don't see the necessity to append the parameter to your %ComSpec% /s /c ""%~0" %*" at all.

As you already use a variable (selfWrapped) to detect, if the wrapper call is necessary, you could also put your arguments into a variable.

set args=%*

Then you can simply use !args! in your child instance.

@ECHO OFF
    setlocal DisableDelayedExpansion

    IF "%selfWrapped%"=="" (
        @REM this is necessary so that we can use "exit" to terminate the batch file,
        @REM and all subroutines, but not the original cmd.exe
        SET "selfWrapped=true"

        SET ^"args=%*"
        "%ComSpec%" /s /c ""%~f0""
        GOTO :EOF
    )

:Main

    setlocal EnableDelayedExpansion
    ECHO(!args!
EXIT /B 0

Now the only problem left, is the set args=%*.
If you can't control the content, then there is no way to access %* in a simple safe way.
Think of this batch invokations

myBatch.bat "abc|"
myBatch.bat abc^|
myBatch.bat abc^|--"|"

But you could use How to receive even the strangest command line parameters?
or Get arguments without temporary file

Btw. You could spare your child process, you can also exit from a function
Look at Exit batch script from inside a function

jeb
  • 78,592
  • 17
  • 171
  • 225
1

One correction to above answers. Yes, ENDLOCAL is implied at the end of the script, but there's a catch.

I've found that with nested scripts, if you don't ENDLOCAL before you EXIT /B 1 you will not get your return code of 1 at the next level out script.

If you only ever EXIT /B 0, then this will not matter as the default return code is 0.