I am having difficuty converting a short PowerShell script into a cmd.exe .bat file script. The error message (see below) complains of a `Missing closing '}'. The .ps1 script runs successfully as expected.
I used SEMICOLON characters at the end of assignment statements. I escaped the VERTICAL LINE (pipe) character with a CARET (^). What am I missing?
Here is the .bat script and error message output.
PS C:\src\t> Get-Content -Path .\DistributeFiles2.bat
powershell -NoLogo -NoProfile -Command ^
"$ProjectPath = Join-Path -Path $Env:USERPROFILE -ChildPath 'Desktop\Project';" ^
"$NFilesPerDirectory = 400;" ^
"Get-ChildItem -File -Path (Join-Path -Path $Env:USERPROFILE -ChildPath 'Desktop\images') -Filter '*.jpeg' ^|" ^
"ForEach-Object {" ^
"# Check to see if the filename starts with four (4) digits.;" ^
"if ($_.BaseName -match '^(\d{4}).*') {" ^
"$FolderNumber = [math]::Floor([int]$Matches[1] / $NFilesPerDirectory);" ^
"$FolderName = 'Folder' $FolderNumber.ToString();" ^
"$FolderPath = Join-Path -Path $ProjectPath -ChildPath $FolderName;" ^
"# If the destination directory does not exist, create it.;" ^
"if (-not (Test-Path -Path $FolderPath)) { mkdir $FolderPath -WhatIf ^| Out-Null }" ^
"# Move the file to the destination directory.;" ^
"Move-Item -Path $_.FullName -Destination $FolderPath -WhatIf" ^
"}" ^
"}"
Back in a cmd.exe shell...
C:>DistributeFiles2.bat
9:55:41.67 C:\src\t
C:>powershell -NoLogo -NoProfile -Command "$ProjectPath = Join-Path -Path $Env:USERPROFILE -ChildPath 'Desktop\Project';" "$NFilesPerDirectory = 400;" "Get-ChildItem -File -Path (Join-Path -Path $Env:USERPROFILE -ChildPath 'Desktop\images') -Filter '*.jpeg' ^|" "ForEach-Object {" "# Check to see if the filename starts with four (4) digits.;" "if ($_.BaseName -match '^(\d{4}).*') {" "$FolderNumber = [math]::Floor([int]$Matches[1] / $NFilesPerDirectory);" "$FolderName = 'Folder' $FolderNumber.ToString();" "$FolderPath = Join-Path -Path $ProjectPath -ChildPath $FolderName;" "# If the destination directory does not exist, create it.;" "if (-not (Test-Path -Path $FolderPath)) { mkdir $FolderPath -WhatIf ^| Out-Null }" "# Move the file to the destination directory.;" "Move-Item -Path $_.FullName -Destination $FolderPath -WhatIf" "}" "}"
Missing closing '}' in statement block or type definition.
CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
FullyQualifiedErrorId : MissingEndCurlyBrace
This is the original .ps1 script which is working as expected.
PS C:\src\t> Get-Content -Path .\DistributeFiles.ps1
$ProjectPath = Join-Path -Path $Env:USERPROFILE -ChildPath 'Desktop\Project';
$NFilesPerDirectory = 400;
Get-ChildItem -File -Path (Join-Path -Path $Env:USERPROFILE -ChildPath 'Desktop\images') -Filter '*.jpeg' |
ForEach-Object {
# Check to see if the filename starts with four (4) digits.;
if ($_.BaseName -match '^(\d{4}).*') {
$FolderNumber = [math]::Floor([int]$Matches[1] / $NFilesPerDirectory);
$FolderName = 'Folder' $FolderNumber.ToString();
$FolderPath = Join-Path -Path $ProjectPath -ChildPath $FolderName;
# If the destination directory does not exist, create it.;
if (-not (Test-Path -Path $FolderPath)) { mkdir $FolderPath -WhatIf | Out-Null }
# Move the file to the destination directory.;
Move-Item -Path $_.FullName -Destination $FolderPath -WhatIf
}
}
PS C:\src\t> .\DistributeFiles.ps1
What if: Performing the operation "Create Directory" on target "Destination: C:\Users\lit\Desktop\Project\Folder0".
What if: Performing the operation "Move File" on target "Item: C:\Users\lit\Desktop\images\0000.jpeg Destination: C:\Users\lit\Desktop\Project\Folder0".
What if: Performing the operation "Create Directory" on target "Destination: C:\Users\lit\Desktop\Project\Folder1".
What if: Performing the operation "Move File" on target "Item: C:\Users\lit\Desktop\images\0401.jpeg Destination: C:\Users\lit\Desktop\Project\Folder1".
Running the .ps1 script from a .bat file script also works as expected.
PS C:\src\t> Get-Content -Path .\DistributeFiles.bat
@powershell -NoLogo -NoProfile -File "%~dp0%~n0.ps1"
PS C:\src\t> .\DistributeFiles.bat
What if: Performing the operation "Create Directory" on target "Destination: C:\Users\lit\Desktop\Project\Folder0".
What if: Performing the operation "Move File" on target "Item: C:\Users\lit\Desktop\images\0000.jpeg Destination: C:\Users\lit\Desktop\Project\Folder0".
What if: Performing the operation "Create Directory" on target "Destination: C:\Users\lit\Desktop\Project\Folder1".
What if: Performing the operation "Move File" on target "Item: C:\Users\lit\Desktop\images\0401.jpeg Destination: C:\Users\lit\Desktop\Project\Folder1".
Update:
Taking @mklement0's advice, I have removed QUOTATION MARK characters. The two (2) VERTICAL LINE (pipe) characters are escaped and the beginning-of-line CARET in the -match
regex is escaped. I avoided using QUOTATION MARK characters in the .ps1 script from the beginning. The "Missing closing '}'" failure still occurs. What am I missing?
C:>powershell -NoLogo -NoProfile -Command ^
More? $ProjectPath = Join-Path -Path $Env:USERPROFILE -ChildPath 'Desktop\Project'; ^
More? $NFilesPerDirectory = 400; ^
More? Get-ChildItem -File -Path (Join-Path -Path $Env:USERPROFILE -ChildPath 'Desktop\images') -Filter '*.jpeg' ^| ^
More? ForEach-Object { ^
More? # Check to see if the filename starts with four (4) digits.; ^
More? if ($_.BaseName -match '^^(\d{4}).*') { ^
More? $FolderNumber = [math]::Floor([int]$Matches[1] / $NFilesPerDirectory); ^
More? $FolderName = 'Folder' $FolderNumber.ToString(); ^
More? $FolderPath = Join-Path -Path $ProjectPath -ChildPath $FolderName; ^
More? # If the destination directory does not exist, create it.; ^
More? if (-not (Test-Path -Path $FolderPath)) { mkdir $FolderPath -WhatIf ^| Out-Null }; ^
More? # Move the file to the destination directory.; ^
More? Move-Item -Path $_.FullName -Destination $FolderPath -WhatIf; ^
More? }; ^
More? };
Missing closing '}' in statement block or type definition.
CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
FullyQualifiedErrorId : MissingEndCurlyBrace
Update 2:
With @mklement0's consistently good advice, here is the working code.
powershell -NoLogo -NoProfile -Command ^
$ProjectPath = Join-Path -Path $Env:USERPROFILE -ChildPath 'Desktop\Project'; ^
$NFilesPerDirectory = 400; ^
Get-ChildItem -File -Path (Join-Path -Path $Env:USERPROFILE -ChildPath 'Desktop\images') -Filter '*.jpeg' ^| ^
ForEach-Object { ^
^<# Check to see if the filename starts with four (4) digits.#^> ^
if ($_.BaseName -match '^^(\d{4}).*') { ^
$FolderNumber = [math]::Floor([int]$Matches[1] / $NFilesPerDirectory); ^
$FolderName = 'Folder' $FolderNumber.ToString(); ^
$FolderPath = Join-Path -Path $ProjectPath -ChildPath $FolderName; ^
^<# If the destination directory does not exist, create it.#^> ^
if (-not (Test-Path -Path $FolderPath)) { mkdir $FolderPath -WhatIf ^| Out-Null }; ^
^<# Move the file to the destination directory.#^> ^
Move-Item -Path $_.FullName -Destination $FolderPath -WhatIf; ^
}; ^
};
CodePudding user response:
In
cmd.exe
and therefore also in batch files,^
only acts as the escape character in unquoted strings; therefore, in general you do not need to escapecmd.exe
metacharacters such as|
inside"..."
strings as^|
- if you do, the^
characters are retained.However, it looks like using
cmd.exe
's line continuation (a _line-ending^
) with double-quoted-per-line strings doesn't work robustly (and having a single double-quoted string span multiple lines is fundamentally unsupported), so the solution is to use unquoted lines, on which you do need to escape|
as^|
, among other characters.
Here's an example that exercises various PowerShell syntax constructs:
powershell -NoProfile -Command ^
[Environment]::CommandLine; ^
$ProjectPath = Join-Path -Path $Env:USERPROFILE -ChildPath 'Desktop\Project'; ^
^<# This is a comment - note the required *inline* comment syntax #^> ^
$ProjectPath ^| ^
ForEach-Object { \"[$ProjectPath]\" }; ^
(42).ToString(); ^
\"A & B\"
Note the inclusion of [Environment]::CommandLine;
as the first statement, which will echo the command line as seen by PowerShell, which can help with troubleshooting. As an aside: When using PowerShell (Core) 7 's CLI, pwsh.exe
, rather than Windows PowerShell's powershell.exe
, the command line reported is a reconstructed form that is not guaranteed to reflect the actual command line used; notably, ""
sequences turn to \"
.
Note:
Each internal line must have
^
as the very last character on the line.Because line continuation doesn't include the newline,
;
must be used to explicitly terminate each PowerShell statement (except the last).Note: Inserting a blank line (without ending it in
^
) between statements does not work: while it does technically result in an actual newline when passed to PowerShell, the absence of overall double-quoting makes PowerShell treat such newlines the same as spaces, which therefore still necessitates;
between statements.It is for this reason that single-line comments (
# ...
) are not supported with this invocation method, given that such comments invariably span the rest of the line, with no support for;
to end them - see next point.
In order to include comments, the form
^<# ... #^>
- i.e. (escaped) inline comments must be used - normal single-line comments (# ....
) are not supported (see previous point).cmd.exe
's metacharacters need individual^
-escaping, namely:& | < > ^
- Additionally, if called from a
for /f
statement:= , ; ( )
"
characters must be escaped as\"
so that PowerShell treats them as part of the command(s) to execute (otherwise they get stripped during command-line parsing).Inside
\"...\"
,cmd.exe
metacharacters do not need^
-escaping, becausecmd.exe
sees such an escaped-for-PowerShell string as a regular double-quoted string.However, whitespace normalization is applied to what is inside
\"...\"
; that is, runs of multiple spaces are folded into one space each; if that is a concern, use^"\"...\"^"
(sic).
For additional information, including a
for /f
example and how to handle escaping of!
whensetlocal enabledelayedexpansion
is in effect, see this answer.