From 82848f3ab39c1ba185e507bf68617e8ef6195f5f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Oct 2026 21:59:29 +0000
Subject: [PATCH 1/8] Initial plan
From ab19dd56b4ff855b94553e9030667a8f1b72281e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 22 Oct 2026 22:06:25 +0000
Subject: [PATCH 2/8] Add Path support to Invoke-Formatter
Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com>
---
Engine/Commands/InvokeFormatterCommand.cs | 107 +++++++++++++++++++++-
Tests/Engine/InvokeFormatter.tests.ps1 | 38 ++++++++
docs/Cmdlets/Invoke-Formatter.md | 35 ++++++-
3 files changed, 171 insertions(+), 9 deletions(-)
diff --git a/Engine/Commands/InvokeFormatterCommand.cs b/Engine/Commands/InvokeFormatterCommand.cs
index 25a2d364e..b1db54604 100644
--- a/Engine/Commands/InvokeFormatterCommand.cs
+++ b/Engine/Commands/InvokeFormatterCommand.cs
@@ -2,7 +2,9 @@
// Licensed under the MIT License.
using System;
+using System.IO;
using System.Globalization;
+using System.Text;
using System.Management.Automation;
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands
@@ -12,20 +14,32 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands
///
/// A cmdlet to format a PowerShell script text.
///
- [Cmdlet(VerbsLifecycle.Invoke, "Formatter")]
+ [Cmdlet(VerbsLifecycle.Invoke, "Formatter", DefaultParameterSetName = ParameterSet_ScriptDefinition, SupportsShouldProcess = true)]
[OutputType(typeof(string))]
public class InvokeFormatterCommand : PSCmdlet, IOutputWriter
{
+ private const string ParameterSet_ScriptDefinition = nameof(ScriptDefinition);
+ private const string ParameterSet_Path = nameof(Path);
+ private const string IndentationRule = "PSUseConsistentIndentation";
private const string defaultSettingsPreset = "CodeFormatting";
private Settings inputSettings;
private Range range;
+ private bool settingsWereSpecified;
+
+ ///
+ /// The path to the file to format in place.
+ ///
+ [Parameter(Mandatory = true, ParameterSetName = ParameterSet_Path, ValueFromPipelineByPropertyName = true, Position = 1)]
+ [ValidateNotNull]
+ [Alias("PSPath")]
+ public string Path { get; set; }
///
/// The script text to be formated.
///
/// *NOTE*: Unlike ScriptBlock parameter, the ScriptDefinition parameter require a string value.
///
- [ParameterAttribute(Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 1)]
+ [ParameterAttribute(Mandatory = true, ParameterSetName = ParameterSet_ScriptDefinition, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 1)]
[ValidateNotNull]
public string ScriptDefinition { get; set; }
@@ -80,6 +94,7 @@ protected override void BeginProcessing()
#endif
this.range = Range == null ? null : new Range(Range[0], Range[1], Range[2], Range[3]);
+ settingsWereSpecified = MyInvocation.BoundParameters.ContainsKey(nameof(Settings));
try
{
inputSettings = PSSASettings.Create(Settings, this.MyInvocation.PSScriptRoot, this, GetResolvedProviderPathFromPSPath);
@@ -108,9 +123,91 @@ protected override void BeginProcessing()
protected override void ProcessRecord()
{
// todo add tests to check range formatting
- string formattedScriptDefinition;
- formattedScriptDefinition = Formatter.Format(ScriptDefinition, inputSettings, range, this);
- this.WriteObject(formattedScriptDefinition);
+ if (ParameterSetName == ParameterSet_Path)
+ {
+ ProviderInfo provider;
+ var resolvedPaths = GetResolvedProviderPathFromPSPath(Path, out provider);
+ if (!String.Equals(provider.Name, "FileSystem", StringComparison.OrdinalIgnoreCase))
+ {
+ ThrowTerminatingError(new ErrorRecord(
+ new ArgumentException("Path must resolve to a file system path.", nameof(Path)),
+ "INVALID_PATH_PROVIDER",
+ ErrorCategory.InvalidArgument,
+ Path));
+ }
+
+ foreach (var path in resolvedPaths)
+ {
+ FormatFile(path);
+ }
+ }
+ else
+ {
+ string formattedScriptDefinition = Formatter.Format(ScriptDefinition, inputSettings, range, this);
+ WriteObject(formattedScriptDefinition);
+ }
+ }
+
+ private void FormatFile(string path)
+ {
+ Encoding encoding;
+ string scriptDefinition;
+ using (var reader = new StreamReader(path, Encoding.UTF8, true))
+ {
+ scriptDefinition = reader.ReadToEnd();
+ encoding = reader.CurrentEncoding;
+ }
+
+ if (inputSettings.RuleArguments.TryGetValue(IndentationRule, out var indentationSettings)
+ && (!settingsWereSpecified || !indentationSettings.ContainsKey("Kind")))
+ {
+ indentationSettings["Kind"] = GetIndentationKind(scriptDefinition);
+ }
+
+ string formattedScriptDefinition = Formatter.Format(scriptDefinition, inputSettings, range, this);
+ if (!String.Equals(scriptDefinition, formattedScriptDefinition, StringComparison.Ordinal)
+ && ShouldProcess(path, "Format file"))
+ {
+ File.WriteAllText(path, formattedScriptDefinition, encoding);
+ }
+
+ WriteObject(formattedScriptDefinition);
+ }
+
+ private static string GetIndentationKind(string scriptDefinition)
+ {
+ int tabIndentedLines = 0;
+ int spaceIndentedLines = 0;
+ string firstIndentationKind = "space";
+ bool foundIndentation = false;
+
+ foreach (var line in scriptDefinition.Split('\n'))
+ {
+ var trimmedLine = line.TrimEnd('\r');
+ if (trimmedLine.Length > 0 && (trimmedLine[0] == '\t' || trimmedLine[0] == ' '))
+ {
+ if (!foundIndentation)
+ {
+ firstIndentationKind = trimmedLine[0] == '\t' ? "tab" : "space";
+ foundIndentation = true;
+ }
+
+ if (trimmedLine[0] == '\t')
+ {
+ tabIndentedLines++;
+ }
+ else
+ {
+ spaceIndentedLines++;
+ }
+ }
+ }
+
+ return tabIndentedLines > spaceIndentedLines
+ ? "tab"
+ : tabIndentedLines < spaceIndentedLines
+ ? "space"
+ : firstIndentationKind;
}
protected override void EndProcessing()
diff --git a/Tests/Engine/InvokeFormatter.tests.ps1 b/Tests/Engine/InvokeFormatter.tests.ps1
index dcc0a6174..9d03cdc73 100644
--- a/Tests/Engine/InvokeFormatter.tests.ps1
+++ b/Tests/Engine/InvokeFormatter.tests.ps1
@@ -27,6 +27,44 @@ Describe "Invoke-Formatter Cmdlet" {
}
}
+ Context 'Formatting files' {
+ It 'Formats a file in place while preserving its encoding, line endings, and tab indentation' {
+ $path = Join-Path $TestDrive 'script.ps1'
+ $scriptDefinition = "function foo {`r`n`t`$x=1`r`n}"
+ $expected = "function foo {`r`n`t`$x = 1`r`n}"
+ $encoding = [System.Text.UTF8Encoding]::new($true)
+ [System.IO.File]::WriteAllText($path, $scriptDefinition, $encoding)
+
+ Invoke-Formatter -Path $path | Should -Be $expected
+
+ [System.IO.File]::ReadAllText($path) | Should -Be $expected
+ [System.IO.File]::ReadAllBytes($path)[0..2] | Should -Be @(239, 187, 191)
+ }
+
+ It 'Preserves a UTF-16 encoding and space indentation' {
+ $path = Join-Path $TestDrive 'unicode.ps1'
+ $scriptDefinition = "function foo {`n `$x=1`n}"
+ $expected = "function foo {`n `$x = 1`n}"
+ $encoding = [System.Text.UnicodeEncoding]::new($false, $true)
+ [System.IO.File]::WriteAllText($path, $scriptDefinition, $encoding)
+
+ Invoke-Formatter -Path $path | Should -Be $expected
+
+ [System.IO.File]::ReadAllText($path) | Should -Be $expected
+ [System.IO.File]::ReadAllBytes($path)[0..1] | Should -Be @(255, 254)
+ }
+
+ It 'Does not modify a file when WhatIf is specified' {
+ $path = Join-Path $TestDrive 'whatif.ps1'
+ $scriptDefinition = '$x=1'
+ [System.IO.File]::WriteAllText($path, $scriptDefinition)
+
+ Invoke-Formatter -Path $path -WhatIf 6>$null
+
+ [System.IO.File]::ReadAllText($path) | Should -Be $scriptDefinition
+ }
+ }
+
Context "When positional parameters are given" {
It "Should use the positional parameters" {
$def = @"
diff --git a/docs/Cmdlets/Invoke-Formatter.md b/docs/Cmdlets/Invoke-Formatter.md
index 80711b0bd..c3f724803 100644
--- a/docs/Cmdlets/Invoke-Formatter.md
+++ b/docs/Cmdlets/Invoke-Formatter.md
@@ -10,20 +10,25 @@ schema: 2.0.0
## SYNOPSIS
-Formats a script text based on the input settings or default settings.
+Formats script text or a file based on the input settings or default settings.
## SYNTAX
```
Invoke-Formatter [-ScriptDefinition] [[-Settings]