diff --git a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs index d466663631..6ce956bf26 100644 --- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs +++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs @@ -276,6 +276,10 @@ namespace System.Management.Automation.Runspaces "System.Management.Automation.PSStyle+ProgressConfiguration", ViewsOf_System_Management_Automation_PSStyleProgressConfiguration()); + yield return new ExtendedTypeDefinition( + "System.Management.Automation.PSStyle+FileInfoFormatting", + ViewsOf_System_Management_Automation_PSStyleFileInfoFormat()); + yield return new ExtendedTypeDefinition( "System.Management.Automation.PSStyle+ForegroundColor", ViewsOf_System_Management_Automation_PSStyleForegroundColor()); @@ -2060,6 +2064,10 @@ namespace System.Management.Automation.Runspaces .AddItemScriptBlock(@"""$($_.Progress.MaxWidth)""", label: "Progress.MaxWidth") .AddItemScriptBlock(@"""$($_.Progress.View)""", label: "Progress.View") .AddItemScriptBlock(@"""$($_.Progress.UseOSCIndicator)""", label: "Progress.UseOSCIndicator") + .AddItemScriptBlock(@"""$($_.FileInfo.Directory)$($_.FileInfo.Directory.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "FileInfo.Directory") + .AddItemScriptBlock(@"""$($_.FileInfo.SymbolicLink)$($_.FileInfo.SymbolicLink.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "FileInfo.SymbolicLink") + .AddItemScriptBlock(@"""$($_.FileInfo.Executable)$($_.FileInfo.Executable.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "FileInfo.Executable") + .AddItemScriptBlock(@"""$([string]::Join(',',$_.FileInfo.Extension.Keys))""", label: "FileInfo.Extension") .AddItemScriptBlock(@"""$($_.Foreground.Black)$($_.Foreground.Black.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Foreground.Black") .AddItemScriptBlock(@"""$($_.Foreground.White)$($_.Foreground.White.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Foreground.White") .AddItemScriptBlock(@"""$($_.Foreground.DarkGray)$($_.Foreground.DarkGray.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Foreground.DarkGray") @@ -2124,6 +2132,39 @@ namespace System.Management.Automation.Runspaces .EndList()); } + private static IEnumerable ViewsOf_System_Management_Automation_PSStyleFileInfoFormat() + { + yield return new FormatViewDefinition("System.Management.Automation.PSStyle+FileInfoFormatting", + ListControl.Create() + .StartEntry() + .AddItemScriptBlock(@"""$($_.Directory)$($_.Directory.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Directory") + .AddItemScriptBlock(@"""$($_.SymbolicLink)$($_.SymbolicLink.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "SymbolicLink") + .AddItemScriptBlock(@"""$($_.Executable)$($_.Executable.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Executable") + .AddItemScriptBlock(@" + $sb = [System.Text.StringBuilder]::new() + $maxKeyLength = 0 + foreach ($key in $_.Extension.Keys) { + if ($key.Length -gt $maxKeyLength) { + $maxKeyLength = $key.Length + } + } + + foreach ($key in $_.Extension.Keys) { + $null = $sb.Append($key.PadRight($maxKeyLength)) + $null = $sb.Append(' = ""') + $null = $sb.Append($_.Extension[$key]) + $null = $sb.Append($_.Extension[$key].Replace(""`e"",'`e')) + $null = $sb.Append($PSStyle.Reset) + $null = $sb.Append('""') + $null = $sb.Append([Environment]::NewLine) + } + + $sb.ToString()", + label: "Extension") + .EndEntry() + .EndList()); + } + private static IEnumerable ViewsOf_System_Management_Automation_PSStyleForegroundColor() { yield return new FormatViewDefinition("System.Management.Automation.PSStyle+ForegroundColor", diff --git a/src/System.Management.Automation/FormatAndOutput/common/PSStyle.cs b/src/System.Management.Automation/FormatAndOutput/common/PSStyle.cs index b8e1890b37..2d7aaa7f13 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/PSStyle.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/PSStyle.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; + namespace System.Management.Automation { #region OutputRendering @@ -34,7 +36,7 @@ namespace System.Management.Automation /// Classic rendering of progress. Classic = 1, } - + #region PSStyle /// /// Contains configuration for how PowerShell renders text. @@ -333,6 +335,55 @@ namespace System.Management.Automation public string Debug { get; set; } = "\x1b[33;1m"; } + /// + /// Contains formatting styles for FileInfo objects. + /// + public sealed class FileInfoFormatting + { + /// + /// Gets or sets the style for directories. + /// + public string Directory { get; set; } = "\x1b[44;1m"; + + /// + /// Gets or sets the style for symbolic links. + /// + public string SymbolicLink { get; set; } = "\x1b[36;1m"; + + /// + /// Gets or sets the style for executables. + /// + public string Executable { get; set; } = "\x1b[32;1m"; + + /// + /// Gets the style for archive. + /// + public Dictionary Extension { get; } + + /// + /// Initializes a new instance of the class. + /// + public FileInfoFormatting() + { + Extension = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // archives + Extension.Add(".zip", "\x1b[31;1m"); + Extension.Add(".tgz", "\x1b[31;1m"); + Extension.Add(".gz", "\x1b[31;1m"); + Extension.Add(".tar", "\x1b[31;1m"); + Extension.Add(".nupkg", "\x1b[31;1m"); + Extension.Add(".cab", "\x1b[31;1m"); + Extension.Add(".7z", "\x1b[31;1m"); + + // powershell + Extension.Add(".ps1", "\x1b[33;1m"); + Extension.Add(".psd1", "\x1b[33;1m"); + Extension.Add(".psm1", "\x1b[33;1m"); + Extension.Add(".ps1xml", "\x1b[33;1m"); + } + } + /// /// Gets or sets the rendering mode for output. /// @@ -444,6 +495,11 @@ namespace System.Management.Automation /// public BackgroundColor Background { get; } + /// + /// Gets FileInfo colors. + /// + public FileInfoFormatting FileInfo { get; } + private static readonly PSStyle s_psstyle = new PSStyle(); private PSStyle() @@ -452,6 +508,7 @@ namespace System.Management.Automation Progress = new ProgressConfiguration(); Foreground = new ForegroundColor(); Background = new BackgroundColor(); + FileInfo = new FileInfoFormatting(); } /// diff --git a/src/System.Management.Automation/engine/CommandDiscovery.cs b/src/System.Management.Automation/engine/CommandDiscovery.cs index fb9f9f5b58..6d7fb77eba 100644 --- a/src/System.Management.Automation/engine/CommandDiscovery.cs +++ b/src/System.Management.Automation/engine/CommandDiscovery.cs @@ -1410,7 +1410,7 @@ namespace System.Management.Automation lock (s_lockObject) { s_cachedPathExtCollection = pathExt != null - ? pathExt.Split(Utils.Separators.PathSeparator, StringSplitOptions.RemoveEmptyEntries) + ? pathExt.ToLower().Split(Utils.Separators.PathSeparator, StringSplitOptions.RemoveEmptyEntries) : Array.Empty(); s_cachedPathExtCollectionWithPs1 = new string[s_cachedPathExtCollection.Length + 1]; s_cachedPathExtCollectionWithPs1[0] = StringLiterals.PowerShellScriptFileExtension; diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index 70d69ba35b..84aa78534a 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -140,6 +140,9 @@ namespace System.Management.Automation new ExperimentalFeature( name: "PSLoadAssemblyFromNativeCode", description: "Expose an API to allow assembly loading from native code"), + new ExperimentalFeature( + name: "PSAnsiRenderingFileInfo", + description: "Enable coloring for FileInfo objects"), }; EngineExperimentalFeatures = new ReadOnlyCollection(engineFeatures); diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs index c81991b95e..f4473d7fbf 100644 --- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs +++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs @@ -2058,11 +2058,43 @@ namespace Microsoft.PowerShell.Commands /// Name if a file or directory, Name -> Target if symlink. public static string NameString(PSObject instance) { - return instance?.BaseObject is FileSystemInfo fileInfo - ? InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo) - ? $"{fileInfo.Name} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}" - : fileInfo.Name - : string.Empty; + if (ExperimentalFeature.IsEnabled("PSAnsiRendering") && ExperimentalFeature.IsEnabled("PSAnsiRenderingFileInfo")) + { + if (instance?.BaseObject is FileSystemInfo fileInfo) + { + if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo)) + { + return $"{PSStyle.Instance.FileInfo.SymbolicLink}{fileInfo.Name}{PSStyle.Instance.Reset} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}"; + } + else if (fileInfo.Attributes.HasFlag(FileAttributes.Directory)) + { + return $"{PSStyle.Instance.FileInfo.Directory}{fileInfo.Name}{PSStyle.Instance.Reset}"; + } + else if (PSStyle.Instance.FileInfo.Extension.ContainsKey(fileInfo.Extension)) + { + return $"{PSStyle.Instance.FileInfo.Extension[fileInfo.Extension]}{fileInfo.Name}{PSStyle.Instance.Reset}"; + } + else if ((Platform.IsWindows && CommandDiscovery.PathExtensions.Contains(fileInfo.Extension.ToLower())) || + (!Platform.IsWindows && Platform.NonWindowsIsExecutable(fileInfo.FullName))) + { + return $"{PSStyle.Instance.FileInfo.Executable}{fileInfo.Name}{PSStyle.Instance.Reset}"; + } + else + { + return fileInfo.Name; + } + } + + return string.Empty; + } + else + { + return instance?.BaseObject is FileSystemInfo fileInfo + ? InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo) + ? $"{fileInfo.Name} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}" + : fileInfo.Name + : string.Empty; + } } /// diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Get-Item.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Get-Item.Tests.ps1 index c04412e91b..35c05b1bb1 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Get-Item.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Get-Item.Tests.ps1 @@ -205,3 +205,62 @@ Describe "Get-Item environment provider on Windows with accidental case-variant } } } + +Describe 'Formatting for FileInfo objects' -Tags 'CI' { + BeforeAll { + $PSDefaultParameterValues.Add('It:Skip', (-not $EnabledExperimentalFeatures.Contains('PSAnsiRenderingFileInfo'))) + $extensionTests = [System.Collections.Generic.List[HashTable]]::new() + foreach ($extension in @('.zip', '.tgz', '.tar', '.gz', '.nupkg', '.cab', '.7z', '.ps1', '.psd1', '.psm1', '.ps1xml')) { + $extensionTests.Add(@{extension = $extension}) + } + } + + AfterAll { + $PSDefaultParameterValues.Remove('It:Skip') + } + + It 'File type should have correct color' -TestCases $extensionTests { + param($extension) + + $testFile = Join-Path -Path $TestDrive -ChildPath "test$extension" + $file = New-Item -ItemType File -Path $testFile + $file.NameString | Should -BeExactly "$($PSStyle.FileInfo.Extension[$extension] + $file.Name + $PSStyle.Reset)" + } + + It 'Directory should have correct color' { + $dirPath = Join-Path -Path $TestDrive -ChildPath 'myDir' + $dir = New-Item -ItemType Directory -Path $dirPath + $dir.NameString | Should -BeExactly "$($PSStyle.FileInfo.Directory + $dir.Name + $PSStyle.Reset)" + } + + It 'Executable should have correct color' { + if ($IsWindows) { + $exePath = Join-Path -Path $TestDrive -ChildPath 'myExe.exe' + $exe = New-Item -ItemType File -Path $exePath + } + else { + $exePath = Join-Path -Path $TestDrive -ChildPath 'myExe' + $null = New-Item -ItemType File -Path $exePath + chmod +x $exePath + $exe = Get-Item -Path $exePath + } + + $exe.NameString | Should -BeExactly "$($PSStyle.FileInfo.Executable + $exe.Name + $PSStyle.Reset)" + } +} + +Describe 'Formatting for FileInfo requiring admin' -Tags 'CI','RequireAdminOnWindows' { + BeforeAll { + $PSDefaultParameterValues.Add('It:Skip', (-not $EnabledExperimentalFeatures.Contains('PSAnsiRenderingFileInfo'))) + } + + AfterAll { + $PSDefaultParameterValues.Remove('It:Skip') + } + + It 'Symlink should have correct color' { + $linkPath = Join-Path -Path $TestDrive -ChildPath 'link' + $link = New-Item -ItemType SymbolicLink -Name 'link' -Value $TestDrive -Path $TestDrive + $link.NameString | Should -BeExactly "$($PSStyle.FileInfo.SymbolicLink + $link.Name + $PSStyle.Reset) -> $TestDrive" + } +}