Captain Claptrap
Captain Claptrap

Reputation: 3969

Does "&" cause a break in echo command, how to fix?

I'm using the technique in the answer to this question to have a batch file output another file. The problem is that when I reach the "&" symbol, I get errors. Here's my batch file, followed by the error output:

@echo off>zip.vbs
@echo 'Get command-line arguments.>>zip.vbs
@echo Set objArgs = WScript.Arguments>>zip.vbs
@echo InputFolder = objArgs(0)>>zip.vbs
@echo ZipFile = objArgs(1)>>zip.vbs
@echo >>zip.vbs
@echo 'Create empty ZIP file.>>zip.vbs
@echo CreateObject("Scripting.FileSystemObject").CreateTextFile(ZipFile, True).Write "PK" & Chr(5) & Chr(6) & String(18, vbNullChar)>>zip.vbs
@echo >>zip.vbs
@echo Set objShell = CreateObject("Shell.Application")>>zip.vbs
@echo >>zip.vbs
@echo Set source = objShell.NameSpace(InputFolder).Items>>zip.vbs
@echo >>zip.vbs
@echo objShell.NameSpace(ZipFile).CopyHere(source)>>zip.vbs
@echo >>zip.vbs
@echo 'Required!>>zip.vbs
@echo wScript.Sleep 2000>>zip.vbs

which produces this zip2.vbs file:

'Get command-line arguments.
Set objArgs = WScript.Arguments
InputFolder = objArgs(0)
ZipFile = objArgs(1)
ECHO is off.
'Create empty ZIP file.
ECHO is off.
Set objShell = CreateObject("Shell.Application")
ECHO is off.
Set source = objShell.NameSpace(InputFolder).Items
ECHO is off.
objShell.NameSpace(ZipFile).CopyHere(source)
ECHO is off.
'Required!
wScript.Sleep 2000

And this error output:

E:\scripts>zip2
CreateObject("Scripting.FileSystemObject").CreateTextFile(ZipFile, True).Write "
PK"
'Chr' is not recognized as an internal or external command,
operable program or batch file.
'Chr' is not recognized as an internal or external command,
operable program or batch file.
'String' is not recognized as an internal or external command,
operable program or batch file.

Upvotes: 0

Views: 574

Answers (1)

aschipfl
aschipfl

Reputation: 34989

At first, let me provide a quick and dirty correction of your code; explanations follow below:

@echo off
> "zip.vbs" echo('Get command-line arguments.
>>"zip.vbs" echo(Set objArgs = WScript.Arguments
>>"zip.vbs" echo(InputFolder = objArgs(0)
>>"zip.vbs" echo(ZipFile = objArgs(1)
>>"zip.vbs" echo(
>>"zip.vbs" echo('Create empty ZIP file.
>>"zip.vbs" echo(CreateObject("Scripting.FileSystemObject").CreateTextFile(ZipFile, True).Write "PK" ^& Chr(5) ^& Chr(6) ^& String(18, vbNullChar)
>>"zip.vbs" echo(
>>"zip.vbs" echo(Set objShell = CreateObject("Shell.Application")
>>"zip.vbs" echo(
>>"zip.vbs" echo(Set source = objShell.NameSpace(InputFolder).Items
>>"zip.vbs" echo(
>>"zip.vbs" echo(objShell.NameSpace(ZipFile).CopyHere(source)
>>"zip.vbs" echo(
>>"zip.vbs" echo('Required!
>>"zip.vbs" echo(wScript.Sleep 2000

The ampersand has a special meaning to cmd, it is the command concatenation operator; for instance, type echo abc & echo def to put two echo commands in a single line.

To output a literal & you need to escape it by the caret like ^&. The same is true for <, > and | and the ^ itself. In case the echo command lies within a parenthesised block of code, you also need to escape ( and ), otherwise it is optional (so I suggest to escape them anyway).

To output an empty line use echo(, because echo returns the on/off state of command-echoing, like ECHO is on/off.. There are widely spread methods like echo. and echo/ to achieve the same, but there are situations where those behave strangely. echo( is the only secure way (although its syntax looks quite odd, I have to admit). By the way, echo( can be used for echoing non-empty text also.

If you switch command-echoing off by a non-redirected @echo off globally, you do not have to precede every further command line with @.

I recommend to put the redirection part (>>zip.vbs) to the beginning of the line in order to avoid weird behaviour in case the echoed string ends with a single numeral, because this may be treated as the stream identifier for redirection (0 is STDIN, 1 is STDOUT, 2 is STDERR and 3 to 9 are undefined; < is the same as 0< and >/>> is the same as 1>/1>>). For example, echo X=2>"zip.vbs" would display X= on the console as STDOUT is not redirected, and write nothing to zip.vbs as STDERR is empty here. You could avoid this by inserting a SPACE in front of the >, but it would be echoed also. A better way is to put the entire echo command line in between (), so the numeral becomes separated from >. But the best solution should be this: >"zip.vbs" echo X=2.


You can also avoid multiple redirections and so many file accesses when you place all the echos inside of a parenthesised block, which is redirected once only (but as described above, do not forget to escape the parentheses here now):

@echo off
> "zip.vbs" (
    echo('Get command-line arguments.
    echo(Set objArgs = WScript.Arguments
    echo(InputFolder = objArgs^(0^)
    echo(ZipFile = objArgs^(1^)
    echo(
    echo('Create empty ZIP file.
    echo(CreateObject^("Scripting.FileSystemObject"^).CreateTextFile^(ZipFile, True^).Write "PK" ^& Chr^(5^) ^& Chr^(6^) ^& String^(18, vbNullChar^)
    echo(
    echo(Set objShell = CreateObject^("Shell.Application"^)
    echo(
    echo(Set source = objShell.NameSpace^(InputFolder^).Items
    echo(
    echo(objShell.NameSpace^(ZipFile^).CopyHere^(source^)
    echo(
    echo('Required!
    echo(wScript.Sleep 2000
)

Here is a completely different approach, where no more escaping needs to be done. The VBScript script zip.vbs is embedded in the batch file directly, after the executable batch code. For this to work you need to place exit /B before the VBScript portion. The VBScript part is identified by being in between a pair of lines that consist of # only (this implies that the entire batch file including the VBScript part must not contain such line on its own; alternatively each VBScript line could be preceded by a certain identifyer prefix that is removed later, but the principle is the same). With this method the VBScript lines are output literally, there is absolutely no escaping required. So here we go:

@echo off
setlocal EnableExtensions DisableDelayedExpansion

set "FLAG="
> "zip.vbs" (
    for /F "delims=" %%L in ('findstr /N /R "^" "%~f0"') do (
        set "LINE=%%L"
        setlocal EnableDelayedExpansion
        set "LINE=!LINE:*:=!"
        if not "!LINE:#=!"=="" (
            if defined FLAG (echo(!LINE!)
        ) else (
            endlocal
            if defined FLAG (set "FLAG=") else (set "FLAG=#")
            setlocal EnableDelayedExpansion
        )
        endlocal
    )
)

endlocal
exit /B


################################################################################
'Get command-line arguments.
Set objArgs = WScript.Arguments
InputFolder = objArgs(0)
ZipFile = objArgs(1)

'Create empty ZIP file.
CreateObject("Scripting.FileSystemObject").CreateTextFile(ZipFile, True).Write "PK" & Chr(5) & Chr(6) & String(18, vbNullChar)

Set objShell = CreateObject("Shell.Application")

Set source = objShell.NameSpace(InputFolder).Items

objShell.NameSpace(ZipFile).CopyHere(source)

'Required!
wScript.Sleep 2000
################################################################################

The entire batch file is read by a for /F loop. Since this ignores empty lines, findstr is used to precede every line with its line number and a colon to not appear empty to for /F. This prefix is removed in the loop later. The variable FLAG is cleared at the beginning and is toggled as soon as a line has been found that consists of # characters only. If FLAG is non-empty, the current line is output, otherwise not. Delayed expansion is toggled to not lose any exclamation marks in the text.

Upvotes: 2

Related Questions