1

Why the string with question mark is not printed (from command prompt)?

A code example: FOR %A IN (--eval string?) DO ECHO %A

The code will print only --eval but ignores string?

Is there a workaround to get it printed?

I have tried to:

  • enclose it in double quotes "string?"
  • tried to escape it "string^?" or "string^^^?" like with bang !
  • tried to double the ??
  • tried combination of these

Nothing seems to work. This seems to be a bug.

Edit base on the answer and question at comment.

  • What do I expect? I would expect having both options --eval string? printed.

  • What I'm trying to achieve? I tried to give a simplification of my final goal.

In the end, I wish to have both strings in the !temp_string! variable (in the below test.bat example).

An example based on the real code:

test.bat example:

@ECHO OFF
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION

ECHO "First argument outside FOR statement: %~1"
ECHO "Second argument outside FOR statement: %~2"

FOR %%A IN (%*) DO (
    ECHO "First inside FOR argument: %~1"
    ECHO "Second inside FOR  argument: %~2"

    SET "temp_string=%%~A"
    ECHO !temp_string!
)

If you execute the text.bat without the ? in question like:

test.bat --eval --test it will correctly show the both strings.

The result:

"First argument outside FOR statement: --eval"
"Second argument outside FOR statement: --test"
"First inside FOR argument: --eval"
"Second inside FOR  argument: --test"
--eval
"First inside FOR argument: --eval"
"Second inside FOR  argument: --test"
--test

However, if you run it with the same parameter, but containing ?

test.bat --eval --test?

The result is incorrect:

"First argument outside FOR statement: --eval"
"Second argument outside FOR statement: --test?"
"First inside FOR argument: --eval"
"Second inside FOR  argument: --test?"
--eval

If you wish to see the real code, which is here simplified, you can find it here around line 783.

Second edit

I'm now trying to get the value of the %%A from the proposed solution by @dbenham.

The %test% variable is still empty in both cases:

@echo off

setlocal disableDelayedExpansion

set "string=--eval string? ;hello <&|>! "^<^&^|^>!""
set string
echo(

setlocal enableDelayedExpansion
for %%n in (^"^
%= This results in a quoted newline character =%
^") do for /f delims^=^ eol^= %%A in ("!string: =%%~n!") do (
    if "!!" equ "" endlocal set "test=%%~A"
    echo "A value:" %%A
    echo "Test value:" %test%
)

echo %test%

The output:

string=--eval string? ;hello <&|>! "<&|>!"

"A value:" --eval
"Test value:"
"A value:" string?
"Test value:"
"A value:" ;hello
"Test value:"
"A value:" <&|>!
"Test value:"
"A value:" "<&|>!"
"Test value:"
ECHO is off.

Third and Forth Edit - The answer.

Based the dbenham && Stephan I'm posting working code with which I'm satisfied. Note: The dbenham's nasty "^<^&^|^>!" are not working here, but I decided I don't needed it.

@echo off
REM works for all!!! --> *.bat -string? :hello "<&|>!"

setlocal disableDelayedExpansion

:process_arguments
:: Process all arguments in the order received
if defined %1 (
    ECHO %1
    SET "string=%string% %~1"
    shift
    goto:process_arguments
)

echo(

setlocal enableDelayedExpansion
for %%n in (^"^
%= This results in a quoted newline character =%
^") do for /f "eol= delims=" %%A in ("!string: =%%~n!") do (
    if "!!" equ "" endlocal 
    set "test=%%~A"
    echo "In for loop - a value: %%A"
    CALL :testing_value "%%A"
)
:: END
goto :eof

:: echo "Outside test:" %test%
setlocal disableDelayedExpansion

:testing_value
  set "test=%~1"
  set test
  echo "Calling Subroutine: %test%"
  echo:

Thank you your guidance!

tukan
  • 17,050
  • 1
  • 20
  • 48

3 Answers3

4

Stephan's last answer works, but there can be complications:

  • The delayed expansion will cause problems with ! literals.
    • solved by conditionally toggling delayed expansion off within the loop
  • Poison characters can be problematic, especially if some are quoted, and some are not.
    • solved by expanding with delayed expansion, coupled with FOR variable for the <LF> replacement term
  • EOL could be an issue, depending on the starting character of each string.
    • solved by disabling both DELIMS and EOL using some arcane syntax

Here is a robust technique that should solve all the issues:

@echo off
setlocal disableDelayedExpansion

set "string=--eval string? ;hello <&|>! "^<^&^|^>!""
set string
echo(

setlocal enableDelayedExpansion
for %%n in (^"^
%= This results in a quoted newline character =%
^") do for /f delims^=^ eol^= %%A in ("!string: =%%~n!") do (
    if "!!" equ "" endlocal
    echo %%A
)

--OUTPUT--

string=--eval string? ;hello <&|>! "<&|>!"

--eval
string?
;hello
<&|>!
"<&|>!"

EDIT

Since we replaced all [space] with [LF], we know that a line can never start with a [space].

So it is simpler if we set EOL to [space]. The FOR /F options can be simplified to:

for /f "eol= delims=" ...
dbenham
  • 127,446
  • 28
  • 251
  • 390
  • sorry to say, but my output differs: the `!` are missing (copy-pasted your code 1:1) – Stephan Jan 18 '18 at 17:18
  • @Stephan - Your command session must have delayed expansion enabled before you enter the batch script. I've restructured my code so that it should always work, regardless how your command session is configured. – dbenham Jan 18 '18 at 17:26
  • yup, works now. (also the old code works with a fresh `cmd` window) I guess, I will never stop to be surprised how strange and weird things can get. – Stephan Jan 18 '18 at 17:32
  • @dbenham quite a brutal solution :). Isn't there a problem with context if I want to save the last `%%A` into a variable and use it outside `echo(...)`? E.g. I would do `set "test=%%~A"` after your `echo %%A` and try to use it ouside the `echo(...)` block with `echo !test!` would give me an empty variable. – tukan Jan 19 '18 at 12:06
  • @tukan: use `echo "%test%" instead of `echo !test!`. Delayed expansion is turned off at this moment. – Stephan Jan 19 '18 at 12:18
  • yes, I had it in the test file, but wrote it here incorrectly. It will return nothing -> only gives `ECHO is off.` – tukan Jan 19 '18 at 12:38
  • 1
    @tukan - The SET must occur after the conditional ENDLOCAL if you want the value to survive. That should be done anyway if you want to preserve values with `!`. – dbenham Jan 19 '18 at 13:14
  • @dbenham Argh, I can't get it working :(. Could you please check the edit at top? I have tried your suggestion but it doesn't seem to work for me. – tukan Jan 19 '18 at 14:02
  • @tukan - Simply put the SET command on the next line after the ENDLOCAL, or else you need `&` before the SET if you want to keep it on the same line. Your `echo "Test value:" %test%` cannot work within the loop because the value is expanded before it has been set (standard delayed expansion issue with loops). If you want to work with normal %VAR% expansion within the loop, then your loop should CALL a :subroutine. – dbenham Jan 19 '18 at 15:13
  • @tukan - I'm wondering if you need the complexity of my solution. It is only needed if you are likely to run into any of the potential problems that I list for Stephan's answer. – dbenham Jan 19 '18 at 15:16
  • @dbenham I'm now testing your solution with your commentary (that will take probably some time). Well the input can be arbitrary so the additional complexity, alias robust solution is welcome and needed. – tukan Jan 22 '18 at 14:11
  • @dbenham I have finally managed to get it working, except your last `"^<^&^|^>!"`. I don't need those escaping chars. Out of curiosity how would you handle those with CALL? I'm editing my question with the working code. Thank you! – tukan Feb 21 '18 at 09:42
  • @dbenham how could adjust the code to work directly for user input? Not stored variable `string` as in your case? – tukan Feb 23 '18 at 13:10
  • @tukan - If you have user input via SET /P, then you are dealing with a variable. So I don't understand your question. – dbenham Feb 23 '18 at 13:50
  • @dbenham Ah,thank you for poking me. I'm not using `SET /P`. I'm gathering the parameters batch files parameters. e.g. `smalltalk.bat -string? :hello "<&|>!"`. When I'm using simple `%*` then it fails. I've just now succeeded with using `SHIFT`, looping, string concatenation and switching off and on `DelayedExpansion`. Thank you very much, I really appreciated it! – tukan Feb 23 '18 at 14:15
3

for tries to find matching files, when you use a wildcard. And ? happens to be a wildcard for "one character". And probably, you don't have a matching file...
To process such a string, use a for /f:

FOR /f "delims=" %A IN ("--eval string?") DO ECHO %A

Edit found a way to split, but with a temporary file:

@echo off
setlocal enabledelayedexpansion

REM create a linefeed:
set ^"LF=^

^" The above empty line is critical - DO NOT REMOVE

set "string=--eval string? hello"
(echo %string: =!LF!%) >tmp
for /f "delims=" %%A in (tmp) do echo %%A

Edit2 dbenham showed me a way to do it without a temporary file (sometimes I tend to think around too many corners):

@echo off
setlocal enabledelayedexpansion

REM create a linefeed:
set ^"LF=^

^" The above empty line is critical - DO NOT REMOVE

set "string=--eval string? hello"
for /f "delims=" %%A in ("%string: =!LF!%") do echo %%A
Stephan
  • 53,940
  • 10
  • 58
  • 91
  • Good explanation, but the solution dramatically changes the output. Simple FOR performs two ECHO, one for each space delimited string. But FOR /F only does one ECHO for the entire string. – dbenham Jan 17 '18 at 20:52
  • @dbenham: can you explain, why `for /f "delims=" %%A IN ('echo %string: =!LF!%') DO ECHO %%A` doesn't work? (also tried with `!CR!` an!d `CR!!LF!`) – Stephan Jan 18 '18 at 08:32
  • @Stephan thank you for your answer. I'm not trying to find matching files. I'm trying to use the user input and save it into a variable. I'm sorry for not being clear here, it is my first (second if you count the deleted one) question on stackoverflow. – tukan Jan 18 '18 at 11:10
  • @dbenham is it because [ stops the parsing immediately (unless there is a caret in front), and the remainder of the line is ignored](https://stackoverflow.com/a/4095133/2152082)? If yes, why does a caret (or two or three) not help? – Stephan Jan 18 '18 at 13:28
  • I think that plays some role, but the more critical aspect is the `'command'` is executed in a new cmd session, so it must be packaged up in a way that can be passed as an argument to `cmd /c`. It is easy to get your desired result if you process a string instead of a command. `for /f "delims=" %%A in ("%string: =!LF!%") do echo %%A`. The `` does not stop the parsing in this case because the stop parse occurs in phase 2, but the `` does not appear until phase 5. – dbenham Jan 18 '18 at 14:45
  • @dbenham Argh - why didn't I think to try just `... in ("%string...`... – Stephan Jan 18 '18 at 16:19
  • Last complications: 1) delayed expansion strips `!` in content, 2) combination of quoted and unquoted poison characters can be a problem, and 3) EOL could be a problem depending on content. I'll post a robust answer. – dbenham Jan 18 '18 at 16:55
  • 1
    @tukan I completely understood you. The "file" thing is just an explanation, what happened with your code. For simple strings like your example, my solution works, but if you need a robust solution, use dbenhams modified version (yes, it looks ugly, but in batch, that's what happens...) – Stephan Jan 18 '18 at 18:19
  • @Stephan yes, it look ugly but if that is the only way, why not? I'm now thinking how to integrate the solution. How does the variable scope look like here. – tukan Jan 19 '18 at 12:08
1

A loop using a label and shifting the arguments can handle ? as a literal. To prevent shifting the main script arguments, call a label such as :args below.

@ECHO OFF
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION

CALL :args %*

ECHO "First outside argument: %~1"
ECHO "Second outside argument: %~2"

GOTO :eof

:args
ECHO "First inside argument: %~1"
ECHO "Second inside argument: %~2"
:args_loop
SET "temp_string=%~1"
IF NOT DEFINED temp_string GOTO :end_args_loop
ECHO !temp_string!
SHIFT
GOTO :args_loop
:end_args_loop
SET "temp_string="
GOTO :eof

A known issue is if %* has /? in it then call may output the help for call.

michael_heath
  • 5,262
  • 2
  • 12
  • 22