Hashbrown
Hashbrown

Reputation: 13023

Can't echo close parenthesis only when piping

I have a pretty peculiar problem, echo writes "ECHO is on." even if it has an argument, but only when printing to a pipe, for certain arguments.

My use case is I'm writing a script file to feed into an interpreter. On Linux I use heredocs to achieve this very nicely.

prog arg1 - arg3 arg4 \
<< EOM
    start server load (
        foreground
        initial database '$database'
        directory $directory
        from file '${database}.ext'
    );
EOM

For Windows' batch I recently discovered something very similar;

(
    echo start server load ^(
    echo     foreground
    echo     initial database '%%d'
    echo     directory %directory%
    echo     from file '%%d.ext'
    echo ^);
    echo.
) | prog arg1 - arg3 arg4

But I kept getting the following error:

E7511-ACE: '-' line 6: An error has occurred while processing the DDM file.
Caused by
E2420-ACE: Expected punctuation character ')'.
4:     directory 'C:\Users\redacted\AppData\Local\Temp'
5:     from file 'testing.ext'
6: ECHO is on.
---^---
7:  )

But if I instead, begrudgingly, use a temporary file, it all runs fine!

(
    echo start server load ^(
    echo     foreground
    echo     initial database '%%d'
    echo     directory %directory%
    echo     from file '%%d.ext'
    echo ^);
    echo.
) > tmp.txt
prog arg1 tmp.txt arg3 arg4

Contents of tmp.txt;

start server load (
    foreground
    initial database 'testing'
    directory 'C:\Users\redacted\AppData\Local\Temp'
    from file 'testing.ext'
);

I even tried mocking up a .bat to try and show this but all it did was make me think I am crazy

@echo off
set databases=a
for %%d in (%databases%) do (
    (
        echo bob ^(
        echo     %%d
        echo ^);
    )
)
pause

output:

bob (
    a
);
Press any key to continue . . .

I don't know any .exes that accept stdin and just write it out to the screen/to a file for me to test my 'it's broken when piping' hypothesis (Windows' type & echo are less versatile than Linux' cat & echo),
do you guys have any ideas?

Upvotes: 3

Views: 324

Answers (4)

Aacini
Aacini

Reputation: 67216

Another method is to use the LF macro variable to store a multi-line value in a variable:

@echo off

rem Define \n ("new line") variable:
set LF=^
%empty line 1/2, don't remove%
%empty line 2/2, don't remove%
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"

rem Define your multi-line value:
set multiLine=%\n%
start server load (%\n%
    foreground%\n%
    initial database '%%d'%\n%
    directory %directory%%\n%
    from file '%%d.ext'%\n%
);%\n%


cmd /V:ON /C echo !multiLine! | prog arg1 - arg3 arg4


rem If Delayed Expansion is enabled, use it this way:

setlocal EnableDelayedExpansion
cmd /V:ON /C echo ^^^!multiLine^^^! | prog arg1 - arg3 arg4

Upvotes: 1

dbenham
dbenham

Reputation: 130849

Another option is to embed the text within your script with a label prefix like :::, and use FINDSTR to extract the lines. The FOR /F command can be used to strip off the ::: prefix, and the variables can be expanded if you use delayed expansion. I'm assuming your code is already within some type of FOR loop since you are using %%d. The %%d value must be transferred to an environment variable so that it can be expanded properly. I've added a bogus FOR loop just so that I have a %%d value.

The piped commands are run within a new cmd.exe process, so delayed expansion must be enabled via an extra CMD command with the /v:on option.

@echo off
setlocal

:::start server load (
:::    foreground
:::    initial database '!database!'
:::    directory !directory!
:::    from file '!database!.ext'
:::);

set "directory=c:\test"
for %%d in (test) do (
  set "database=%%d"
  cmd /v:on /c for /f "tokens=* delims=:" %%A in ('findstr "^:::" "%~f0"'^) do @echo %%A|prog arg1 - arg3 arg4
)

You can include multiple blocks of text with unique labels, and simply modify the FINDSTR and the FOR /F slightly. Below I still have just one text block, but I show how the label can be handled.

@echo off
setlocal

::1:start server load (
::1:    foreground
::1:    initial database '!database!'
::1:    directory !directory!
::1:    from file '!database!.ext'
::1:);

set "directory=c:\test"
for %%d in (test) do (
  set "database=%%d"
  cmd /v:on /c for /f "tokens=1* delims=:" %%A in ('findstr "^::1:" "%~f0"'^) do @echo %%B|prog arg1 - arg3 arg4
)

Upvotes: 1

jeb
jeb

Reputation: 82307

For the most cases the solution of MC ND (using three carets) should be used.

But when you want to output many special charaters in a line it gets annoying to add always the carets.

Like

(
    echo amp^^^& caret^^^^ single quote^^"
    echo ^^^<html^^^>^^^<body^^^>^^^</body^^^>^^^</html^^^>
) | more

The you could use the disappearing quotes technic.

SET "FOR=FOR /F "tokens=1,2" %%^! IN ("a") DO @("

(
    %%FOR%% ^
    echo %%"amp& caret^ single quote"
    echo %%"<html><body></body></html>%%"
    rem.^)
) |  more

The technic itself is complex but using it is easy.
It uses the fact that the parser will see the quotes in %%" and will quote the following charaters, but later it will replace %%" with nothing.

Upvotes: 2

MC ND
MC ND

Reputation: 70933

Pipes are created between two processes. The code in your left side of the pipe is batch code so a new cmd instance is started to create the process for the left side of the pipe and the code is passed to this instance.

Your escape character is properly written for the current cmd instance, but when the code is passed to the new instance there is no escape codes as they were consumed when preparing the code to execute in the new cmd instance.

You will need to double escape your closing parenthesis, escaping the escape character and escaping the parenthesis so the new cmd instance receives a properly escaped parenthesis.

(
    echo start server load (
    echo     foreground
    echo     initial database '%%d'
    echo     directory %directory%
    echo     from file '%%d.ext'
    echo ^^^);
    echo.
) | prog arg1 - arg3 arg4

The redirection and the for code in the question work without any problem because the code is being executed in the current cmd instance.

Upvotes: 3

Related Questions