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.exeand therefore also in batch files,^only acts as the escape character in unquoted strings; therefore, in general you do not need to escapecmd.exemetacharacters 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 /fstatement:= , ; ( )
"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.exemetacharacters do not need^-escaping, becausecmd.exesees 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 /fexample and how to handle escaping of!whensetlocal enabledelayedexpansionis in effect, see this answer.
