2

Environment

  • Azure DevOps Classic UI
  • Task: PowerShell@2 (Windows PowerShell, not pwsh)
  • Agent: Windows (Microsoft-hosted and self-hosted both repro)
  • PowerShell version: Windows PowerShell 5.1
  • Script encoding: UTF-8, CRLF line endings
  • Task setting “Run script in separate scope”: tested both true and false
  • The same logic also exists split across multiple inline PowerShell tasks and runs fine

Symptom When I run a single “File Path” PowerShell script (large-ish, ~600 lines), the task fails immediately with:

Missing closing '}' in statement block or type definition.
The Try statement is missing its Catch or Finally block.
CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
FullyQualifiedErrorId : MissingEndCurlyBrace

In the pipeline logs it points to line 1 where the script starts with a top-level try {, and later to a closing }:

At D:\AzureAgentFolder\_work\r1\a\MyApp\install_myapp_script.ps1:1 char:5
+ try {
+     ~
Missing closing '}' in statement block or type definition.
At D:\AzureAgentFolder\_work\r1\a\MyApp\install_myapp_script.ps1:577 char:2
+ }
+  ~
The Try statement is missing its Catch or Finally block.

What’s puzzling

  • The exact same code, when split into multiple inline PowerShell tasks, runs fine.
  • Locally, the single unified script parses and executes under Windows PowerShell 5.1 without errors.
  • VS Code (PowerShell extension) shows no parser errors.
  • I’ve manually checked bracket balance and I’m not missing a }.
  • File is saved with UTF-8 and CRLF. I also tried re-saving as “UTF‑8 with BOM” to satisfy PS 5.1’s encoding quirks.

What the script looks like (high-level)

  • Everything is wrapped in a single top-level try { … } catch { … } to fail fast and emit nice logs.
  • It does some pre-install cleanup, downloads artifacts, installs software, copies an .ssh tree, sets ACLs, and finally does some ssh-keyscan/ssh checks.
  • There are several strings with embedded quotes for cmd.exe/icacls calls (e.g. "icacls "$folder" …").
  • I’ve already fixed ambiguous interpolations like "$sshKeyPath:" to "`${sshKeyPath}:" so the parser doesn’t think the colon is part of the variable name.

What I’ve tried (and observations)

  • Verified in VS Code that the file has CRLF line endings and is UTF‑8. Also re-saved explicitly as UTF‑8 with BOM.
  • Ran the script locally with Windows PowerShell 5.1 (not PowerShell 7): no parser errors, it runs.
  • Grepped for trailing backticks (common cause of accidental line continuation) and removed any accidental ones.
  • Ensured there are no unbalanced quotes in strings (heuristic: checked lines with odd numbers of double quotes).
  • Ensured there aren’t any tasks that transform/replace tokens in the .ps1 file during the pipeline (to the best of my knowledge).
  • Confirmed the PowerShell task is calling the file directly (File Path), not dot-sourcing it (also tried “Run script in separate scope” = true to be safe).

Diagnostic steps I added in the pipeline (to run on the agent against the exact file path the task uses)

  1. Parse the file before running it to get exact parser errors and context:
param([string]$Path)
[System.Management.Automation.Language.Token[]]$t=$null
[System.Management.Automation.Language.ParseError[]]$e=$null
$null=[System.Management.Automation.Language.Parser]::ParseFile($Path,[ref]$t,[ref]$e)
if($e){
  $c=Get-Content $Path
  foreach($err in $e){
    $ln=$err.Extent.StartLineNumber;$col=$err.Extent.StartColumnNumber
    "Error: $($err.Message) at line $ln, col $col"
    $start=[Math]::Max(1,$ln-3);$end=[Math]::Min($c.Count,$ln+3)
    for($i=$start;$i -le $end;$i++){($i -eq $ln?">>":"  ")+("{0,5}: {1}" -f $i,$c[$i-1])}
    ""
  }
  exit 1
}else{"Parser reports no errors for $Path."}
  1. Scan for trailing backticks and lines with odd quote counts (heuristic):
param([string]$Path)
"Lines ending with backtick:"
Select-String -Path $Path -Pattern '`\s*$' | ForEach-Object { "$($_.LineNumber): $($_.Line)" }
"Lines with odd double-quote counts:"
$i=0; Get-Content $Path | % { $i++; $q=($_ -split '"').Count-1; if($q%2 -ne 0){"$i: $_"} }
  1. Verified bytes aren’t changing between my repo and the agent by logging a SHA256 of the file in the pipeline:
(Get-FileHash '$(System.DefaultWorkingDirectory)\MyApp\install_myapp_script.ps1' -Algorithm SHA256).Hash

What I suspect

  • Either the file on the agent is being subtly altered before execution (encoding/line endings/token replacement), or there is an accidental line continuation / unclosed string that the agent environment exposes but local testing doesn’t (e.g., due to CR/LF normalization).
  • PowerShell 5.1 treats UTF‑8 without BOM as ANSI, but the failure persists.

Questions

  1. Is there a known issue with Azure DevOps “PowerShell@2” File Path task + Windows PowerShell 5.1 causing “MissingEndCurlyBrace” when the same script parses locally and as inline steps?
  2. What else should I check to find what the agent’s parser is actually choking on, given that the file appears correct locally?
  3. Any best practices to harden large File Path scripts for ADO?
    • Example: always save as UTF‑8 with BOM + CRLF, avoid trailing backticks, use ${var} or $() when interpolating punctuation, add preflight parse step, set “Run script in separate scope” = true, disable token-replacement steps on .ps1 files, etc.

Any guidance or pointers to known pitfalls would be greatly appreciated. Happy to post the parser preflight output (exact failing line + 6-line context) from the agent if that helps.

Thank you!

3
  • if its a self hosted. can you store it as a script instead of inline, and then attempt to RDP into the box and run it? Commented Oct 25 at 1:36
  • Hi @Ctznkane525, yes its agent locally installed in a server, we deploy towards several servers that each has a ADO agent installed as a service, and I'm deploying towards one of those devices this pipeline as a testing, before using it in production... But this erros shouldn't take place it should read any script stores as in Azure DevOps Repo Commented Oct 27 at 13:21
  • Without seeing the script, is there possibly a non-printing character somewhere in your script? Try saving as ANSI / ASCII and look for an unexpected "?" character. Also, try removing chunks of code with matching braces (e.g. if() { ... }) until it starts running without an error - that'll help track down where it's "losing" a brace and you can focus on that bit of the code. Commented Oct 29 at 8:58

1 Answer 1

0

Looking at the source code, the PowerShell@2 task does not alter the contents of your script.

Under the hood, the task is a nodejs script that generates a temporary file and then executes a shell process to invoke it.

For example, the following task:

- task: PowerShell@2
  inputs:
    filePath: './myscript.ps1'
    arguments: '-argument1 "x"'
    warningPreference: 'Continue'
    informationPreference: 'Continue'
    verbosePreference: 'Continue'
    debugPreference: 'Continue'
    failOnStderr: true
    showWarnings: true
    ignoreLASTEXITCODE: false
    pwsh: false
    workingDirectory: 'src'
    runScriptInSeparateScope: true

...would produce this auto-generated script:

$ErrorActionPreference = 'Stop'
$WarningPreference = 'Continue'
$InformationPreference = 'Continue'
$VerbosePreference = 'Continue'
$DebugPreference = 'Continue'

$warnings = New-Object System.Collections.ObjectModel.ObservableCollection[System.Management.Automation.WarningRecord];
Register-ObjectEvent -InputObject $warnings -EventName CollectionChanged -Action {
  if($Event.SourceEventArgs.Action -like "Add"){
    $Event.SourceEventArgs.NewItems | ForEach-Object {
      Write-Host "##vso[task.logissue type=warning;]$_";
    }
  }
};
Invoke-Command { . /myscript.ps1 -argument1 "x" } -WarningVariable +warnings;
if (!Test-Path -LiteralPath variable:\LASTEXITCODE)) {
  Write-Host '##vso[task.debug]$LASTEXITCODE is not set.'
} else {
  Write-Host ('##vso[task.debug]$LASTEXITCODE: {0}' -f $LASTEXITCODE)
  exit $LASTEXITCODE
}

Then based on the runScriptInSeparateScope input, the generated script would either be executed as:

# using separate scope
<path-to:pwsh|powershell> -NoLogo -NoProfile -NonInteractive -Command & generated-file-name.ps1

# using same scope
<path-to:pwsh|powershell> -NoLogo -NoProfile -NonInteractive -Command . generated-file-name.ps1

Few things to try:

  • Ensure that the script is executing with Windows Powershell 5.1 and not PowerShell Core.
  • Try saving the file as UTF-8 (no BOM) as the BOM is likely being interpreted as an invisible character try { which could trip up the parser. Windows PowerShell is more forgiving in this manner, but PowerShell Core is not.
Sign up to request clarification or add additional context in comments.

2 Comments

Both Windows PowerShell and PowerShell (Core) 7 recognize BOMs when reading source code from files; the only difference is what encoding is assumed in the absence of a BOM (ANSI vs. UTF-8). As an aside: The existence of the runScriptInSeparateScope option is curious, given that in a CLI invocation (that doesn't also use -NoExit so as to enter an interactive session after executing the given command), the distinction between executing directly in the global scope vs. in a child scope is mostly moot, except for obscure edge cases (that are arguably bugs).
there are some other aspects of the nodejs script that aren't shown here, such as waiting for the task to complete and reading off the stderr stream, but agreed - running in a separate scope would have no impact. My point was to show that the tasl doesn't alter the user's provided script. Definitely looks like a parser error though. The OP doesn't show the syntax of his pipeline task, indicate the target OS or which versions of powershell/pwsh are installed. Regardless, it looks like a parser error.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.