Home > Software design >  .ps1 script reported as "Missing closing '}'" in .bat file script
.ps1 script reported as "Missing closing '}'" in .bat file script

Time:10-05

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 escape cmd.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, because cmd.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 ! when setlocal enabledelayedexpansion is in effect, see this answer.

  • Related