Skip to content

Commit bfb4a81

Browse files
authored
Add support to colorize FileInfo file names (#14403)
1 parent adc236c commit bfb4a81

File tree

6 files changed

+199
-7
lines changed

6 files changed

+199
-7
lines changed

src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ internal static IEnumerable<ExtendedTypeDefinition> GetFormatData()
276276
"System.Management.Automation.PSStyle+ProgressConfiguration",
277277
ViewsOf_System_Management_Automation_PSStyleProgressConfiguration());
278278

279+
yield return new ExtendedTypeDefinition(
280+
"System.Management.Automation.PSStyle+FileInfoFormatting",
281+
ViewsOf_System_Management_Automation_PSStyleFileInfoFormat());
282+
279283
yield return new ExtendedTypeDefinition(
280284
"System.Management.Automation.PSStyle+ForegroundColor",
281285
ViewsOf_System_Management_Automation_PSStyleForegroundColor());
@@ -2060,6 +2064,10 @@ private static IEnumerable<FormatViewDefinition> ViewsOf_System_Management_Autom
20602064
.AddItemScriptBlock(@"""$($_.Progress.MaxWidth)""", label: "Progress.MaxWidth")
20612065
.AddItemScriptBlock(@"""$($_.Progress.View)""", label: "Progress.View")
20622066
.AddItemScriptBlock(@"""$($_.Progress.UseOSCIndicator)""", label: "Progress.UseOSCIndicator")
2067+
.AddItemScriptBlock(@"""$($_.FileInfo.Directory)$($_.FileInfo.Directory.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "FileInfo.Directory")
2068+
.AddItemScriptBlock(@"""$($_.FileInfo.SymbolicLink)$($_.FileInfo.SymbolicLink.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "FileInfo.SymbolicLink")
2069+
.AddItemScriptBlock(@"""$($_.FileInfo.Executable)$($_.FileInfo.Executable.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "FileInfo.Executable")
2070+
.AddItemScriptBlock(@"""$([string]::Join(',',$_.FileInfo.Extension.Keys))""", label: "FileInfo.Extension")
20632071
.AddItemScriptBlock(@"""$($_.Foreground.Black)$($_.Foreground.Black.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Foreground.Black")
20642072
.AddItemScriptBlock(@"""$($_.Foreground.White)$($_.Foreground.White.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Foreground.White")
20652073
.AddItemScriptBlock(@"""$($_.Foreground.DarkGray)$($_.Foreground.DarkGray.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Foreground.DarkGray")
@@ -2124,6 +2132,39 @@ private static IEnumerable<FormatViewDefinition> ViewsOf_System_Management_Autom
21242132
.EndList());
21252133
}
21262134

2135+
private static IEnumerable<FormatViewDefinition> ViewsOf_System_Management_Automation_PSStyleFileInfoFormat()
2136+
{
2137+
yield return new FormatViewDefinition("System.Management.Automation.PSStyle+FileInfoFormatting",
2138+
ListControl.Create()
2139+
.StartEntry()
2140+
.AddItemScriptBlock(@"""$($_.Directory)$($_.Directory.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Directory")
2141+
.AddItemScriptBlock(@"""$($_.SymbolicLink)$($_.SymbolicLink.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "SymbolicLink")
2142+
.AddItemScriptBlock(@"""$($_.Executable)$($_.Executable.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Executable")
2143+
.AddItemScriptBlock(@"
2144+
$sb = [System.Text.StringBuilder]::new()
2145+
$maxKeyLength = 0
2146+
foreach ($key in $_.Extension.Keys) {
2147+
if ($key.Length -gt $maxKeyLength) {
2148+
$maxKeyLength = $key.Length
2149+
}
2150+
}
2151+
2152+
foreach ($key in $_.Extension.Keys) {
2153+
$null = $sb.Append($key.PadRight($maxKeyLength))
2154+
$null = $sb.Append(' = ""')
2155+
$null = $sb.Append($_.Extension[$key])
2156+
$null = $sb.Append($_.Extension[$key].Replace(""`e"",'`e'))
2157+
$null = $sb.Append($PSStyle.Reset)
2158+
$null = $sb.Append('""')
2159+
$null = $sb.Append([Environment]::NewLine)
2160+
}
2161+
2162+
$sb.ToString()",
2163+
label: "Extension")
2164+
.EndEntry()
2165+
.EndList());
2166+
}
2167+
21272168
private static IEnumerable<FormatViewDefinition> ViewsOf_System_Management_Automation_PSStyleForegroundColor()
21282169
{
21292170
yield return new FormatViewDefinition("System.Management.Automation.PSStyle+ForegroundColor",

src/System.Management.Automation/FormatAndOutput/common/PSStyle.cs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Collections.Generic;
5+
46
namespace System.Management.Automation
57
{
68
#region OutputRendering
@@ -34,7 +36,7 @@ public enum ProgressView
3436
/// <summary>Classic rendering of progress.</summary>
3537
Classic = 1,
3638
}
37-
39+
3840
#region PSStyle
3941
/// <summary>
4042
/// Contains configuration for how PowerShell renders text.
@@ -333,6 +335,55 @@ public sealed class FormattingData
333335
public string Debug { get; set; } = "\x1b[33;1m";
334336
}
335337

338+
/// <summary>
339+
/// Contains formatting styles for FileInfo objects.
340+
/// </summary>
341+
public sealed class FileInfoFormatting
342+
{
343+
/// <summary>
344+
/// Gets or sets the style for directories.
345+
/// </summary>
346+
public string Directory { get; set; } = "\x1b[44;1m";
347+
348+
/// <summary>
349+
/// Gets or sets the style for symbolic links.
350+
/// </summary>
351+
public string SymbolicLink { get; set; } = "\x1b[36;1m";
352+
353+
/// <summary>
354+
/// Gets or sets the style for executables.
355+
/// </summary>
356+
public string Executable { get; set; } = "\x1b[32;1m";
357+
358+
/// <summary>
359+
/// Gets the style for archive.
360+
/// </summary>
361+
public Dictionary<string, string> Extension { get; }
362+
363+
/// <summary>
364+
/// Initializes a new instance of the <see cref="FileInfoFormatting"/> class.
365+
/// </summary>
366+
public FileInfoFormatting()
367+
{
368+
Extension = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
369+
370+
// archives
371+
Extension.Add(".zip", "\x1b[31;1m");
372+
Extension.Add(".tgz", "\x1b[31;1m");
373+
Extension.Add(".gz", "\x1b[31;1m");
374+
Extension.Add(".tar", "\x1b[31;1m");
375+
Extension.Add(".nupkg", "\x1b[31;1m");
376+
Extension.Add(".cab", "\x1b[31;1m");
377+
Extension.Add(".7z", "\x1b[31;1m");
378+
379+
// powershell
380+
Extension.Add(".ps1", "\x1b[33;1m");
381+
Extension.Add(".psd1", "\x1b[33;1m");
382+
Extension.Add(".psm1", "\x1b[33;1m");
383+
Extension.Add(".ps1xml", "\x1b[33;1m");
384+
}
385+
}
386+
336387
/// <summary>
337388
/// Gets or sets the rendering mode for output.
338389
/// </summary>
@@ -444,6 +495,11 @@ public string FormatHyperlink(string text, Uri link)
444495
/// </summary>
445496
public BackgroundColor Background { get; }
446497

498+
/// <summary>
499+
/// Gets FileInfo colors.
500+
/// </summary>
501+
public FileInfoFormatting FileInfo { get; }
502+
447503
private static readonly PSStyle s_psstyle = new PSStyle();
448504

449505
private PSStyle()
@@ -452,6 +508,7 @@ private PSStyle()
452508
Progress = new ProgressConfiguration();
453509
Foreground = new ForegroundColor();
454510
Background = new BackgroundColor();
511+
FileInfo = new FileInfoFormatting();
455512
}
456513

457514
/// <summary>

src/System.Management.Automation/engine/CommandDiscovery.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1410,7 +1410,7 @@ private static void InitPathExtCache(string pathExt)
14101410
lock (s_lockObject)
14111411
{
14121412
s_cachedPathExtCollection = pathExt != null
1413-
? pathExt.Split(Utils.Separators.PathSeparator, StringSplitOptions.RemoveEmptyEntries)
1413+
? pathExt.ToLower().Split(Utils.Separators.PathSeparator, StringSplitOptions.RemoveEmptyEntries)
14141414
: Array.Empty<string>();
14151415
s_cachedPathExtCollectionWithPs1 = new string[s_cachedPathExtCollection.Length + 1];
14161416
s_cachedPathExtCollectionWithPs1[0] = StringLiterals.PowerShellScriptFileExtension;

src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ static ExperimentalFeature()
140140
new ExperimentalFeature(
141141
name: "PSLoadAssemblyFromNativeCode",
142142
description: "Expose an API to allow assembly loading from native code"),
143+
new ExperimentalFeature(
144+
name: "PSAnsiRenderingFileInfo",
145+
description: "Enable coloring for FileInfo objects"),
143146
};
144147

145148
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);

src/System.Management.Automation/namespaces/FileSystemProvider.cs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,11 +2058,43 @@ string ToModeString(FileSystemInfo fileSystemInfo)
20582058
/// <returns>Name if a file or directory, Name -> Target if symlink.</returns>
20592059
public static string NameString(PSObject instance)
20602060
{
2061-
return instance?.BaseObject is FileSystemInfo fileInfo
2062-
? InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo)
2063-
? $"{fileInfo.Name} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}"
2064-
: fileInfo.Name
2065-
: string.Empty;
2061+
if (ExperimentalFeature.IsEnabled("PSAnsiRendering") && ExperimentalFeature.IsEnabled("PSAnsiRenderingFileInfo"))
2062+
{
2063+
if (instance?.BaseObject is FileSystemInfo fileInfo)
2064+
{
2065+
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo))
2066+
{
2067+
return $"{PSStyle.Instance.FileInfo.SymbolicLink}{fileInfo.Name}{PSStyle.Instance.Reset} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}";
2068+
}
2069+
else if (fileInfo.Attributes.HasFlag(FileAttributes.Directory))
2070+
{
2071+
return $"{PSStyle.Instance.FileInfo.Directory}{fileInfo.Name}{PSStyle.Instance.Reset}";
2072+
}
2073+
else if (PSStyle.Instance.FileInfo.Extension.ContainsKey(fileInfo.Extension))
2074+
{
2075+
return $"{PSStyle.Instance.FileInfo.Extension[fileInfo.Extension]}{fileInfo.Name}{PSStyle.Instance.Reset}";
2076+
}
2077+
else if ((Platform.IsWindows && CommandDiscovery.PathExtensions.Contains(fileInfo.Extension.ToLower())) ||
2078+
(!Platform.IsWindows && Platform.NonWindowsIsExecutable(fileInfo.FullName)))
2079+
{
2080+
return $"{PSStyle.Instance.FileInfo.Executable}{fileInfo.Name}{PSStyle.Instance.Reset}";
2081+
}
2082+
else
2083+
{
2084+
return fileInfo.Name;
2085+
}
2086+
}
2087+
2088+
return string.Empty;
2089+
}
2090+
else
2091+
{
2092+
return instance?.BaseObject is FileSystemInfo fileInfo
2093+
? InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo)
2094+
? $"{fileInfo.Name} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}"
2095+
: fileInfo.Name
2096+
: string.Empty;
2097+
}
20662098
}
20672099

20682100
/// <summary>

test/powershell/Modules/Microsoft.PowerShell.Management/Get-Item.Tests.ps1

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,62 @@ Describe "Get-Item environment provider on Windows with accidental case-variant
205205
}
206206
}
207207
}
208+
209+
Describe 'Formatting for FileInfo objects' -Tags 'CI' {
210+
BeforeAll {
211+
$PSDefaultParameterValues.Add('It:Skip', (-not $EnabledExperimentalFeatures.Contains('PSAnsiRenderingFileInfo')))
212+
$extensionTests = [System.Collections.Generic.List[HashTable]]::new()
213+
foreach ($extension in @('.zip', '.tgz', '.tar', '.gz', '.nupkg', '.cab', '.7z', '.ps1', '.psd1', '.psm1', '.ps1xml')) {
214+
$extensionTests.Add(@{extension = $extension})
215+
}
216+
}
217+
218+
AfterAll {
219+
$PSDefaultParameterValues.Remove('It:Skip')
220+
}
221+
222+
It 'File type <extension> should have correct color' -TestCases $extensionTests {
223+
param($extension)
224+
225+
$testFile = Join-Path -Path $TestDrive -ChildPath "test$extension"
226+
$file = New-Item -ItemType File -Path $testFile
227+
$file.NameString | Should -BeExactly "$($PSStyle.FileInfo.Extension[$extension] + $file.Name + $PSStyle.Reset)"
228+
}
229+
230+
It 'Directory should have correct color' {
231+
$dirPath = Join-Path -Path $TestDrive -ChildPath 'myDir'
232+
$dir = New-Item -ItemType Directory -Path $dirPath
233+
$dir.NameString | Should -BeExactly "$($PSStyle.FileInfo.Directory + $dir.Name + $PSStyle.Reset)"
234+
}
235+
236+
It 'Executable should have correct color' {
237+
if ($IsWindows) {
238+
$exePath = Join-Path -Path $TestDrive -ChildPath 'myExe.exe'
239+
$exe = New-Item -ItemType File -Path $exePath
240+
}
241+
else {
242+
$exePath = Join-Path -Path $TestDrive -ChildPath 'myExe'
243+
$null = New-Item -ItemType File -Path $exePath
244+
chmod +x $exePath
245+
$exe = Get-Item -Path $exePath
246+
}
247+
248+
$exe.NameString | Should -BeExactly "$($PSStyle.FileInfo.Executable + $exe.Name + $PSStyle.Reset)"
249+
}
250+
}
251+
252+
Describe 'Formatting for FileInfo requiring admin' -Tags 'CI','RequireAdminOnWindows' {
253+
BeforeAll {
254+
$PSDefaultParameterValues.Add('It:Skip', (-not $EnabledExperimentalFeatures.Contains('PSAnsiRenderingFileInfo')))
255+
}
256+
257+
AfterAll {
258+
$PSDefaultParameterValues.Remove('It:Skip')
259+
}
260+
261+
It 'Symlink should have correct color' {
262+
$linkPath = Join-Path -Path $TestDrive -ChildPath 'link'
263+
$link = New-Item -ItemType SymbolicLink -Name 'link' -Value $TestDrive -Path $TestDrive
264+
$link.NameString | Should -BeExactly "$($PSStyle.FileInfo.SymbolicLink + $link.Name + $PSStyle.Reset) -> $TestDrive"
265+
}
266+
}

0 commit comments

Comments
 (0)