mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Experimental Feature: Provide Unix stat information in filesystem output (#11042)
This commit is contained in:
committed by
Aditya Patwardhan
parent
f32ae64b8b
commit
fe712f8de2
@@ -554,27 +554,284 @@ namespace System.Management.Automation
|
||||
return IsMacOS ? Unix.NativeMethods.GetPPid(pid) : Unix.GetProcFSParentPid(pid);
|
||||
}
|
||||
|
||||
// Unix specific implementations of required functionality
|
||||
//
|
||||
// Please note that `Win32Exception(Marshal.GetLastWin32Error())`
|
||||
// works *correctly* on Linux in that it creates an exception with
|
||||
// the string perror would give you for the last set value of errno.
|
||||
// No manual mapping is required. .NET Core maps the Linux errno
|
||||
// to a PAL value and calls strerror_r underneath to generate the message.
|
||||
|
||||
/// <summary>Unix specific implementations of required functionality.</summary>
|
||||
internal static class Unix
|
||||
{
|
||||
private static Dictionary<int, string> usernameCache = new Dictionary<int, string>();
|
||||
private static Dictionary<int, string> groupnameCache = new Dictionary<int, string>();
|
||||
|
||||
/// <summary>The type of a Unix file system item.</summary>
|
||||
public enum ItemType
|
||||
{
|
||||
/// <summary>The item is a Directory.</summary>
|
||||
Directory,
|
||||
|
||||
/// <summary>The item is a File.</summary>
|
||||
File,
|
||||
|
||||
/// <summary>The item is a Symbolic Link.</summary>
|
||||
SymbolicLink,
|
||||
|
||||
/// <summary>The item is a Block Device.</summary>
|
||||
BlockDevice,
|
||||
|
||||
/// <summary>The item is a Character Device.</summary>
|
||||
CharacterDevice,
|
||||
|
||||
/// <summary>The item is a Named Pipe.</summary>
|
||||
NamedPipe,
|
||||
|
||||
/// <summary>The item is a Socket.</summary>
|
||||
Socket,
|
||||
}
|
||||
|
||||
/// <summary>The mask to use to retrieve specific mode bits from the mode value in the stat class.</summary>
|
||||
public enum StatMask
|
||||
{
|
||||
/// <summary>The mask to collect the owner mode.</summary>
|
||||
OwnerModeMask = 0x1C0,
|
||||
|
||||
/// <summary>The mask to get the owners read bit.</summary>
|
||||
OwnerRead = 0x100,
|
||||
|
||||
/// <summary>The mask to get the owners write bit.</summary>
|
||||
OwnerWrite = 0x080,
|
||||
|
||||
/// <summary>The mask to get the owners execute bit.</summary>
|
||||
OwnerExecute = 0x040,
|
||||
|
||||
/// <summary>The mask to get the group mode.</summary>
|
||||
GroupModeMask = 0x038,
|
||||
|
||||
/// <summary>The mask to get the group mode.</summary>
|
||||
GroupRead = 0x20,
|
||||
|
||||
/// <summary>The mask to get the group mode.</summary>
|
||||
GroupWrite = 0x10,
|
||||
|
||||
/// <summary>The mask to get the group mode.</summary>
|
||||
GroupExecute = 0x8,
|
||||
|
||||
/// <summary>The mask to get the "other" mode.</summary>
|
||||
OtherModeMask = 0x007,
|
||||
|
||||
/// <summary>The mask to get the "other" read bit.</summary>
|
||||
OtherRead = 0x004,
|
||||
|
||||
/// <summary>The mask to get the "other" write bit.</summary>
|
||||
OtherWrite = 0x002,
|
||||
|
||||
/// <summary>The mask to get the "other" execute bit.</summary>
|
||||
OtherExecute = 0x001,
|
||||
|
||||
/// <summary>The mask to retrieve the sticky bit.</summary>
|
||||
SetStickyMask = 0x200,
|
||||
|
||||
/// <summary>The mask to retrieve the setgid bit.</summary>
|
||||
SetGidMask = 0x400,
|
||||
|
||||
/// <summary>The mask to retrieve the setuid bit.</summary>
|
||||
SetUidMask = 0x800,
|
||||
}
|
||||
|
||||
/// <summary>The Common Stat class.</summary>
|
||||
public class CommonStat
|
||||
{
|
||||
/// <summary>The inode of the filesystem item.</summary>
|
||||
public long Inode;
|
||||
|
||||
/// <summary>The Mode of the filesystem item.</summary>
|
||||
public int Mode;
|
||||
|
||||
/// <summary>The user id of the filesystem item.</summary>
|
||||
public int UserId;
|
||||
|
||||
/// <summary>The group id of the filesystem item.</summary>
|
||||
public int GroupId;
|
||||
|
||||
/// <summary>The number of hard links for the filesystem item.</summary>
|
||||
public int HardlinkCount;
|
||||
|
||||
/// <summary>The size in bytes of the filesystem item.</summary>
|
||||
public long Size;
|
||||
|
||||
/// <summary>The last access time of the filesystem item.</summary>
|
||||
public DateTime AccessTime;
|
||||
|
||||
/// <summary>The last modified time for the filesystem item.</summary>
|
||||
public DateTime ModifiedTime;
|
||||
|
||||
/// <summary>The last time the status changes for the filesystem item.</summary>
|
||||
public DateTime StatusChangeTime;
|
||||
|
||||
/// <summary>The block size of the filesystem.</summary>
|
||||
public long BlockSize;
|
||||
|
||||
/// <summary>The device id of the filesystem item.</summary>
|
||||
public int DeviceId;
|
||||
|
||||
/// <summary>The number of blocks used by the filesystem item.</summary>
|
||||
public int NumberOfBlocks;
|
||||
|
||||
/// <summary>The type of the filesystem item.</summary>
|
||||
public ItemType ItemType;
|
||||
|
||||
/// <summary>Whether the filesystem item has the setuid bit enabled.</summary>
|
||||
public bool IsSetUid;
|
||||
|
||||
/// <summary>Whether the filesystem item has the setgid bit enabled.</summary>
|
||||
public bool IsSetGid;
|
||||
|
||||
/// <summary>Whether the filesystem item has the sticky bit enabled. This is only available for directories.</summary>
|
||||
public bool IsSticky;
|
||||
|
||||
private const char CanRead = 'r';
|
||||
private const char CanWrite = 'w';
|
||||
private const char CanExecute = 'x';
|
||||
|
||||
// helper for getting unix mode
|
||||
private Dictionary<StatMask, char> modeMap = new Dictionary<StatMask, char>()
|
||||
{
|
||||
{ StatMask.OwnerRead, CanRead },
|
||||
{ StatMask.OwnerWrite, CanWrite },
|
||||
{ StatMask.OwnerExecute, CanExecute },
|
||||
{ StatMask.GroupRead, CanRead },
|
||||
{ StatMask.GroupWrite, CanWrite },
|
||||
{ StatMask.GroupExecute, CanExecute },
|
||||
{ StatMask.OtherRead, CanRead },
|
||||
{ StatMask.OtherWrite, CanWrite },
|
||||
{ StatMask.OtherExecute, CanExecute },
|
||||
};
|
||||
|
||||
private StatMask[] permissions = new StatMask[]
|
||||
{
|
||||
StatMask.OwnerRead,
|
||||
StatMask.OwnerWrite,
|
||||
StatMask.OwnerExecute,
|
||||
StatMask.GroupRead,
|
||||
StatMask.GroupWrite,
|
||||
StatMask.GroupExecute,
|
||||
StatMask.OtherRead,
|
||||
StatMask.OtherWrite,
|
||||
StatMask.OtherExecute
|
||||
};
|
||||
|
||||
// The item type and the character representation for the first element in the stat string
|
||||
private Dictionary<ItemType, char> itemTypeTable = new Dictionary<ItemType, char>()
|
||||
{
|
||||
{ ItemType.BlockDevice, 'b' },
|
||||
{ ItemType.CharacterDevice, 'c' },
|
||||
{ ItemType.Directory, 'd' },
|
||||
{ ItemType.File, '-' },
|
||||
{ ItemType.NamedPipe, 'p' },
|
||||
{ ItemType.Socket, 's' },
|
||||
{ ItemType.SymbolicLink, 'l' },
|
||||
};
|
||||
|
||||
/// <summary>Convert the mode to a string which is usable in our formatting.</summary>
|
||||
/// <returns>The mode converted into a Unix style string similar to the output of ls.</returns>
|
||||
public string GetModeString()
|
||||
{
|
||||
int offset = 0;
|
||||
char[] modeCharacters = new char[10];
|
||||
modeCharacters[offset++] = itemTypeTable[ItemType];
|
||||
|
||||
foreach (StatMask permission in permissions)
|
||||
{
|
||||
// determine whether we are setuid, sticky, or the usual rwx.
|
||||
if ((Mode & (int)permission) == (int)permission)
|
||||
{
|
||||
if ((permission == StatMask.OwnerExecute && IsSetUid) || (permission == StatMask.GroupExecute && IsSetGid))
|
||||
{
|
||||
// Check for setuid and add 's'
|
||||
modeCharacters[offset] = 's';
|
||||
}
|
||||
else if (permission == StatMask.OtherExecute && IsSticky && (ItemType == ItemType.Directory))
|
||||
{
|
||||
// Directories are sticky, rather than setuid
|
||||
modeCharacters[offset] = 't';
|
||||
}
|
||||
else
|
||||
{
|
||||
modeCharacters[offset] = modeMap[permission];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
modeCharacters[offset] = '-';
|
||||
}
|
||||
|
||||
offset++;
|
||||
}
|
||||
|
||||
return new string(modeCharacters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the user name. This is used in formatting, but we shouldn't
|
||||
/// do the pinvoke this unless we're going to use it.
|
||||
/// </summary>
|
||||
/// <returns>The user name.</returns>
|
||||
public string GetUserName()
|
||||
{
|
||||
if (usernameCache.TryGetValue(UserId, out string username))
|
||||
{
|
||||
return username;
|
||||
}
|
||||
|
||||
// Get and add the user name to the cache so we don't need to
|
||||
// have a pinvoke for each file.
|
||||
username = NativeMethods.GetPwUid(UserId);
|
||||
usernameCache.Add(UserId, username);
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the group name. This is used in formatting, but we shouldn't
|
||||
/// do the pinvoke this unless we're going to use it.
|
||||
/// </summary>
|
||||
/// <returns>The name of the group.</returns>
|
||||
public string GetGroupName()
|
||||
{
|
||||
if (groupnameCache.TryGetValue(GroupId, out string groupname))
|
||||
{
|
||||
return groupname;
|
||||
}
|
||||
|
||||
// Get and add the group name to the cache so we don't need to
|
||||
// have a pinvoke for each file.
|
||||
groupname = NativeMethods.GetGrGid(GroupId);
|
||||
groupnameCache.Add(GroupId, groupname);
|
||||
|
||||
return groupname;
|
||||
}
|
||||
}
|
||||
|
||||
// This is a helper that attempts to map errno into a PowerShell ErrorCategory
|
||||
internal static ErrorCategory GetErrorCategory(int errno)
|
||||
{
|
||||
return (ErrorCategory)Unix.NativeMethods.GetErrorCategory(errno);
|
||||
}
|
||||
|
||||
/// <summary>Is this a hardlink.</summary>
|
||||
/// <param name="handle">The handle to a file.</param>
|
||||
/// <returns>A boolean that represents whether the item is a hardlink.</returns>
|
||||
public static bool IsHardLink(ref IntPtr handle)
|
||||
{
|
||||
// TODO:PSL implement using fstat to query inode refcount to see if it is a hard link
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Determine if the item is a hardlink.</summary>
|
||||
/// <param name="fs">A FileSystemInfo to check to determine if it is a hardlink.</param>
|
||||
/// <returns>A boolean that represents whether the item is a hardlink.</returns>
|
||||
public static bool IsHardLink(FileSystemInfo fs)
|
||||
{
|
||||
if (!fs.Exists || (fs.Attributes & FileAttributes.Directory) == FileAttributes.Directory)
|
||||
@@ -589,19 +846,105 @@ namespace System.Management.Automation
|
||||
{
|
||||
return count > 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a managed replica of the native stat structure.
|
||||
/// </summary>
|
||||
/// <param name="css">The common stat structure from which we copy.</param>
|
||||
/// <returns>A managed common stat class instance.</returns>
|
||||
private static CommonStat CopyStatStruct(NativeMethods.CommonStatStruct css)
|
||||
{
|
||||
CommonStat cs = new CommonStat();
|
||||
cs.Inode = css.Inode;
|
||||
cs.Mode = css.Mode;
|
||||
cs.UserId = css.UserId;
|
||||
cs.GroupId = css.GroupId;
|
||||
cs.HardlinkCount = css.HardlinkCount;
|
||||
cs.Size = css.Size;
|
||||
cs.AccessTime = DateTime.UnixEpoch.AddSeconds(css.AccessTime).ToLocalTime();
|
||||
cs.ModifiedTime = DateTime.UnixEpoch.AddSeconds(css.ModifiedTime).ToLocalTime();
|
||||
cs.StatusChangeTime = DateTime.UnixEpoch.AddSeconds(css.StatusChangeTime).ToLocalTime();
|
||||
cs.BlockSize = css.BlockSize;
|
||||
cs.DeviceId = css.DeviceId;
|
||||
cs.NumberOfBlocks = css.NumberOfBlocks;
|
||||
|
||||
if (css.IsDirectory == 1)
|
||||
{
|
||||
cs.ItemType = ItemType.Directory;
|
||||
}
|
||||
else if (css.IsFile == 1)
|
||||
{
|
||||
cs.ItemType = ItemType.File;
|
||||
}
|
||||
else if (css.IsSymbolicLink == 1)
|
||||
{
|
||||
cs.ItemType = ItemType.SymbolicLink;
|
||||
}
|
||||
else if (css.IsBlockDevice == 1)
|
||||
{
|
||||
cs.ItemType = ItemType.BlockDevice;
|
||||
}
|
||||
else if (css.IsCharacterDevice == 1)
|
||||
{
|
||||
cs.ItemType = ItemType.CharacterDevice;
|
||||
}
|
||||
else if (css.IsNamedPipe == 1)
|
||||
{
|
||||
cs.ItemType = ItemType.NamedPipe;
|
||||
}
|
||||
else
|
||||
{
|
||||
cs.ItemType = ItemType.Socket;
|
||||
}
|
||||
|
||||
cs.IsSetUid = css.IsSetUid == 1;
|
||||
cs.IsSetGid = css.IsSetGid == 1;
|
||||
cs.IsSticky = css.IsSticky == 1;
|
||||
|
||||
return cs;
|
||||
}
|
||||
|
||||
/// <summary>Get the lstat info from a path.</summary>
|
||||
/// <param name="path">The path to the lstat information.</param>
|
||||
/// <returns>An instance of the CommonStat for the path.</returns>
|
||||
public static CommonStat GetLStat(string path)
|
||||
{
|
||||
NativeMethods.CommonStatStruct css;
|
||||
if (NativeMethods.GetCommonLStat(path, out css) == 0)
|
||||
{
|
||||
return CopyStatStruct(css);
|
||||
}
|
||||
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
/// <summary>Get the stat info from a path.</summary>
|
||||
/// <param name="path">The path to the stat information.</param>
|
||||
/// <returns>An instance of the CommonStat for the path.</returns>
|
||||
public static CommonStat GetStat(string path)
|
||||
{
|
||||
NativeMethods.CommonStatStruct css;
|
||||
if (NativeMethods.GetCommonStat(path, out css) == 0)
|
||||
{
|
||||
return CopyStatStruct(css);
|
||||
}
|
||||
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
/// <summary>Read the /proc file system for information about the parent.</summary>
|
||||
/// <param name="pid">The process id used to get the parent process.</param>
|
||||
/// <returns>The process id.</returns>
|
||||
public static int GetProcFSParentPid(int pid)
|
||||
{
|
||||
const int invalidPid = -1;
|
||||
|
||||
// read /proc/<pid>/stat
|
||||
// 4th column will contain the ppid, 92 in the example below
|
||||
// ex: 93 (bash) S 92 93 2 4294967295 ...
|
||||
|
||||
var path = $"/proc/{pid}/stat";
|
||||
try
|
||||
{
|
||||
@@ -620,12 +963,12 @@ namespace System.Management.Automation
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The native methods class.</summary>
|
||||
internal static class NativeMethods
|
||||
{
|
||||
private const string psLib = "libpsl-native";
|
||||
|
||||
// Ansi is a misnomer, it is hardcoded to UTF-8 on Linux and macOS
|
||||
|
||||
// C bools are 1 byte and so must be marshaled as I1
|
||||
|
||||
[DllImport(psLib, CharSet = CharSet.Ansi)]
|
||||
@@ -644,21 +987,39 @@ namespace System.Management.Automation
|
||||
[DllImport(psLib, CharSet = CharSet.Ansi)]
|
||||
internal static extern uint GetCurrentThreadId();
|
||||
|
||||
// This is a struct tm from <time.h>
|
||||
// This is a struct tm from <time.h>.
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal unsafe struct UnixTm
|
||||
{
|
||||
public int tm_sec; /* Seconds (0-60) */
|
||||
public int tm_min; /* Minutes (0-59) */
|
||||
public int tm_hour; /* Hours (0-23) */
|
||||
public int tm_mday; /* Day of the month (1-31) */
|
||||
public int tm_mon; /* Month (0-11) */
|
||||
public int tm_year; /* Year - 1900 */
|
||||
public int tm_wday; /* Day of the week (0-6, Sunday = 0) */
|
||||
public int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */
|
||||
public int tm_isdst; /* Daylight saving time */
|
||||
/// <summary>Seconds (0-60).</summary>
|
||||
internal int tm_sec;
|
||||
|
||||
/// <summary>Minutes (0-59).</summary>
|
||||
internal int tm_min;
|
||||
|
||||
/// <summary>Hours (0-23).</summary>
|
||||
internal int tm_hour;
|
||||
|
||||
/// <summary>Day of the month (1-31).</summary>
|
||||
internal int tm_mday;
|
||||
|
||||
/// <summary>Month (0-11).</summary>
|
||||
internal int tm_mon;
|
||||
|
||||
/// <summary>The year - 1900.</summary>
|
||||
internal int tm_year;
|
||||
|
||||
/// <summary>Day of the week (0-6, Sunday = 0).</summary>
|
||||
internal int tm_wday;
|
||||
|
||||
/// <summary>Day in the year (0-365, 1 Jan = 0).</summary>
|
||||
internal int tm_yday;
|
||||
|
||||
/// <summary>Daylight saving time.</summary>
|
||||
internal int tm_isdst;
|
||||
}
|
||||
|
||||
// We need a way to convert a DateTime to a unix date.
|
||||
internal static UnixTm DateTimeToUnixTm(DateTime date)
|
||||
{
|
||||
UnixTm tm;
|
||||
@@ -701,6 +1062,94 @@ namespace System.Management.Automation
|
||||
[DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)]
|
||||
internal static extern int GetInodeData([MarshalAs(UnmanagedType.LPStr)]string path,
|
||||
out UInt64 device, out UInt64 inode);
|
||||
|
||||
/// <summary>
|
||||
/// This is a struct from getcommonstat.h in the native library.
|
||||
/// It presents each member of the stat structure as the largest type of that member across
|
||||
/// all stat structures on the platforms we support. This allows us to present a common
|
||||
/// stat structure for all our platforms.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct CommonStatStruct
|
||||
{
|
||||
/// <summary>The inode of the filesystem item.</summary>
|
||||
internal long Inode;
|
||||
|
||||
/// <summary>The mode of the filesystem item.</summary>
|
||||
internal int Mode;
|
||||
|
||||
/// <summary>The user id of the filesystem item.</summary>
|
||||
internal int UserId;
|
||||
|
||||
/// <summary>The group id of the filesystem item.</summary>
|
||||
internal int GroupId;
|
||||
|
||||
/// <summary>The number of hard links to the filesystem item.</summary>
|
||||
internal int HardlinkCount;
|
||||
|
||||
/// <summary>The size in bytes of the filesystem item.</summary>
|
||||
internal long Size;
|
||||
|
||||
/// <summary>The time of the last access for the filesystem item.</summary>
|
||||
internal long AccessTime;
|
||||
|
||||
/// <summary>The time of the last modification for the filesystem item.</summary>
|
||||
internal long ModifiedTime;
|
||||
|
||||
/// <summary>The time of the last status change for the filesystem item.</summary>
|
||||
internal long StatusChangeTime;
|
||||
|
||||
/// <summary>The size in bytes of the file system.</summary>
|
||||
internal long BlockSize;
|
||||
|
||||
/// <summary>The device id for the filesystem item.</summary>
|
||||
internal int DeviceId;
|
||||
|
||||
/// <summary>The number of filesystem blocks that the filesystem item uses.</summary>
|
||||
internal int NumberOfBlocks;
|
||||
|
||||
/// <summary>This filesystem item is a directory.</summary>
|
||||
internal int IsDirectory;
|
||||
|
||||
/// <summary>This filesystem item is a file.</summary>
|
||||
internal int IsFile;
|
||||
|
||||
/// <summary>This filesystem item is a symbolic link.</summary>
|
||||
internal int IsSymbolicLink;
|
||||
|
||||
/// <summary>This filesystem item is a block device.</summary>
|
||||
internal int IsBlockDevice;
|
||||
|
||||
/// <summary>This filesystem item is a character device.</summary>
|
||||
internal int IsCharacterDevice;
|
||||
|
||||
/// <summary>This filesystem item is a named pipe.</summary>
|
||||
internal int IsNamedPipe;
|
||||
|
||||
/// <summary>This filesystem item is a socket.</summary>
|
||||
internal int IsSocket;
|
||||
|
||||
/// <summary>This filesystem item will run as the the owner if executed.</summary>
|
||||
internal int IsSetUid;
|
||||
|
||||
/// <summary>This filesystem item will run as the the group if executed.</summary>
|
||||
internal int IsSetGid;
|
||||
|
||||
/// <summary>Whether the sticky bit is set on the filesystem item.</summary>
|
||||
internal int IsSticky;
|
||||
}
|
||||
|
||||
[DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)]
|
||||
internal static extern unsafe int GetCommonLStat(string filePath, [Out] out CommonStatStruct cs);
|
||||
|
||||
[DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)]
|
||||
internal static extern unsafe int GetCommonStat(string filePath, [Out] out CommonStatStruct cs);
|
||||
|
||||
[DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)]
|
||||
internal static extern string GetPwUid(int id);
|
||||
|
||||
[DllImport(psLib, CharSet = CharSet.Ansi, SetLastError = true)]
|
||||
internal static extern string GetGrGid(int id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -41,6 +41,30 @@ namespace System.Management.Automation.Runspaces
|
||||
|
||||
private static IEnumerable<FormatViewDefinition> ViewsOf_FileSystemTypes(CustomControl[] sharedControls)
|
||||
{
|
||||
#if UNIX
|
||||
if (ExperimentalFeature.IsEnabled("PSUnixFileStat"))
|
||||
{
|
||||
yield return new FormatViewDefinition("childrenWithUnixStat",
|
||||
TableControl.Create()
|
||||
.GroupByProperty("PSParentPath", customControl: sharedControls[0])
|
||||
.AddHeader(Alignment.Left, label: "UnixMode", width: 10)
|
||||
.AddHeader(Alignment.Left, label: "User", width: 16)
|
||||
.AddHeader(Alignment.Left, label: "Group", width: 16)
|
||||
.AddHeader(Alignment.Right, label: "LastWriteTime", width: 18)
|
||||
.AddHeader(Alignment.Right, label: "Size", width: 14)
|
||||
.AddHeader(Alignment.Left, label: "Name")
|
||||
.StartRowDefinition(wrap: true)
|
||||
.AddPropertyColumn("UnixMode")
|
||||
.AddPropertyColumn("User")
|
||||
.AddPropertyColumn("Group")
|
||||
.AddScriptBlockColumn(scriptBlock: @"'{0:d} {0:HH}:{0:mm}' -f $_.LastWriteTime")
|
||||
.AddPropertyColumn("Size")
|
||||
.AddPropertyColumn("NameString")
|
||||
.EndRowDefinition()
|
||||
.EndTable());
|
||||
}
|
||||
#endif
|
||||
|
||||
yield return new FormatViewDefinition("children",
|
||||
TableControl.Create()
|
||||
.GroupByProperty("PSParentPath", customControl: sharedControls[0])
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0-preview3.19551.4" />
|
||||
<!-- the following package(s) are from the powershell org -->
|
||||
<PackageReference Include="Microsoft.Management.Infrastructure" Version="2.0.0-preview.2" />
|
||||
<PackageReference Include="Microsoft.PowerShell.Native" Version="7.0.0-preview.3" />
|
||||
<PackageReference Include="Microsoft.PowerShell.Native" Version="7.0.0-preview.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -127,6 +127,11 @@ namespace System.Management.Automation
|
||||
new ExperimentalFeature(
|
||||
name: "PSPipelineChainOperators",
|
||||
description: "Allow use of && and || as operators between pipeline invocations"),
|
||||
#if UNIX
|
||||
new ExperimentalFeature(
|
||||
name: "PSUnixFileStat",
|
||||
description: "Provide unix permission information for files and directories"),
|
||||
#endif
|
||||
new ExperimentalFeature(
|
||||
name: "PSNullConditionalOperators",
|
||||
description: "Support the null conditional member access operators in PowerShell language")
|
||||
|
||||
@@ -9198,6 +9198,53 @@ namespace System.Management.Automation.Runspaces
|
||||
|
||||
#endregion System.Version#IncludeLabel
|
||||
|
||||
#if UNIX
|
||||
#region UnixStat
|
||||
|
||||
|
||||
if (ExperimentalFeature.IsEnabled("PSUnixFileStat"))
|
||||
{
|
||||
typeName = @"System.IO.FileSystemInfo";
|
||||
typeMembers = _extendedMembers.GetOrAdd(typeName, GetValueFactoryBasedOnInitCapacity(capacity: 1));
|
||||
|
||||
// Where we have a method to invoke below, first check to be sure that the object is present
|
||||
// to avoid null reference issues
|
||||
newMembers.Add(@"UnixMode");
|
||||
AddMember(
|
||||
errors,
|
||||
typeName,
|
||||
new PSScriptProperty(@"UnixMode", GetScriptBlock(@"if ($this.UnixStat) { $this.UnixStat.GetModeString() }")),
|
||||
typeMembers,
|
||||
isOverride: false);
|
||||
|
||||
newMembers.Add(@"User");
|
||||
AddMember(
|
||||
errors,
|
||||
typeName,
|
||||
new PSScriptProperty(@"User", GetScriptBlock(@" if ($this.UnixStat) { $this.UnixStat.GetUserName() } ")),
|
||||
typeMembers,
|
||||
isOverride: false);
|
||||
|
||||
newMembers.Add(@"Group");
|
||||
AddMember(
|
||||
errors,
|
||||
typeName,
|
||||
new PSScriptProperty(@"Group", GetScriptBlock(@" if ($this.UnixStat) { $this.UnixStat.GetGroupName() } ")),
|
||||
typeMembers,
|
||||
isOverride: false);
|
||||
|
||||
newMembers.Add(@"Size");
|
||||
AddMember(
|
||||
errors,
|
||||
typeName,
|
||||
new PSScriptProperty(@"Size", GetScriptBlock(@"$this.UnixStat.Size")),
|
||||
typeMembers,
|
||||
isOverride: false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endif
|
||||
|
||||
// Update binder version for newly added members.
|
||||
foreach (string memberName in newMembers)
|
||||
{
|
||||
|
||||
@@ -1862,6 +1862,27 @@ namespace System.Management.Automation.Provider
|
||||
|
||||
result.AddOrSetProperty("PSChildName", childName);
|
||||
providerBaseTracer.WriteLine("Attaching {0} = {1}", "PSChildName", childName);
|
||||
#if UNIX
|
||||
|
||||
// Add a commonstat structure to file system objects
|
||||
if (ExperimentalFeature.IsEnabled("PSUnixFileStat") && ProviderInfo.ImplementingType == typeof(Microsoft.PowerShell.Commands.FileSystemProvider))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use LStat because if you get a link, you want the information about the
|
||||
// link, not the file.
|
||||
var commonStat = Platform.Unix.GetLStat(path);
|
||||
result.AddOrSetProperty("UnixStat", commonStat);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If there is *any* problem in retrieving the stat information
|
||||
// set the property to null. There is no specific exception which
|
||||
// would result in different behavior.
|
||||
result.AddOrSetProperty("UnixStat", value: null);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// PSDriveInfo
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
Describe "UnixFileSystem additions" -Tag "CI" {
|
||||
# if PSUnixFileStat is converted from an experimental feature, these tests will need to be changed
|
||||
BeforeAll {
|
||||
$experimentalFeatureName = "PSUnixFileStat"
|
||||
$skipTest = -not $EnabledExperimentalFeatures.Contains($experimentalFeatureName)
|
||||
$PSDefaultParameterValues.Add('It:Skip', $skipTest)
|
||||
}
|
||||
AfterAll {
|
||||
$PSDefaultParameterValues.Remove('It:Skip')
|
||||
}
|
||||
Context "Basic Validation" {
|
||||
|
||||
It "Should be an experimental feature on non-Windows systems" {
|
||||
$feature = Get-ExperimentalFeature -Name $experimentalFeatureName
|
||||
if ( $IsWindows ) {
|
||||
$feature | Should -BeNullOrEmpty
|
||||
}
|
||||
else {
|
||||
$feature.Name | Should -Be $experimentalFeatureName
|
||||
}
|
||||
}
|
||||
|
||||
It "Should include a UnixStat property" {
|
||||
$i = Get-Item ${TestDrive}
|
||||
$i.UnixStat | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It "The UnixStat property should be the correct type" {
|
||||
$expected = "System.Management.Automation.Platform+Unix+CommonStat"
|
||||
$i = (get-item /).psobject.properties['UnixStat'].TypeNameOfValue
|
||||
$i | Should -Be $expected
|
||||
}
|
||||
}
|
||||
|
||||
Context "Validation of additional properties on file system objects" {
|
||||
BeforeAll {
|
||||
if ( $IsWindows ) {
|
||||
return
|
||||
}
|
||||
|
||||
$testDir = "${TestDrive}/TestDir"
|
||||
$testFile = "${testDir}/TestFile"
|
||||
|
||||
$testCase = @{ Mode = '000'; Perm = '----------'; Item = "${testFile}" },
|
||||
@{ Mode = '111'; Perm = '---x--x--x'; Item = "${testFile}" },
|
||||
@{ Mode = '222'; Perm = '--w--w--w-'; Item = "${testFile}" },
|
||||
@{ Mode = '333'; Perm = '--wx-wx-wx'; Item = "${testFile}" },
|
||||
@{ Mode = '444'; Perm = '-r--r--r--'; Item = "${testFile}" },
|
||||
@{ Mode = '555'; Perm = '-r-xr-xr-x'; Item = "${testFile}" },
|
||||
@{ Mode = '666'; Perm = '-rw-rw-rw-'; Item = "${testFile}" },
|
||||
@{ Mode = '777'; Perm = '-rwxrwxrwx'; Item = "${testFile}" },
|
||||
@{ Mode = '4777'; Perm = '-rwsrwxrwx'; Item = "${testFile}" },
|
||||
@{ Mode = '1777'; Perm = 'drwxrwxrwt'; Item = "${testDir}" }
|
||||
}
|
||||
|
||||
BeforeEach {
|
||||
$null = New-Item -ItemType Directory -Path "${testDir}"
|
||||
$null = New-Item -ItemType File -Path "${testFile}"
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
Remove-Item -Path "${testFile}" -Force
|
||||
Remove-Item -Path "${testDir}" -Recurse -Force
|
||||
}
|
||||
|
||||
It "Should present filemode '<Mode>' string correctly as '<Perm>'" -testCase $testCase {
|
||||
param ($Mode, $Perm, $Item )
|
||||
chmod "$Mode" "${Item}"
|
||||
$i = Get-Item $Item
|
||||
$i.UnixMode | Should -Be $Perm
|
||||
}
|
||||
|
||||
It "Should retrieve the user name for the file" {
|
||||
$i = Get-Item ${testFile}
|
||||
$user = (/bin/ls -ld $testFile).split(" ",[System.StringSplitOptions]"RemoveEmptyEntries")[2]
|
||||
$i.User | Should -Be $user
|
||||
}
|
||||
|
||||
It "Should retrieve the group name for the file" {
|
||||
$i = Get-Item ${testFile}
|
||||
$group = (/bin/ls -ld $testFile).split(" ",[System.StringSplitOptions]"RemoveEmptyEntries")[3]
|
||||
$i.Group | Should -Be $Group
|
||||
}
|
||||
}
|
||||
|
||||
Context "Other properties of UnixStat object" {
|
||||
BeforeAll {
|
||||
if ( $IsWindows ) {
|
||||
return
|
||||
}
|
||||
|
||||
$testDir = "${TestDrive}/TestDir"
|
||||
$testFile = "${testDir}/TestFile"
|
||||
|
||||
$null = New-Item -Type Directory -Path $testDir
|
||||
$null = New-Item -Type File -Path $testFile
|
||||
Set-Content -Path ${testFile} -Value "abc"
|
||||
|
||||
$expectedFileInode,$permission,$expectedFileLinkCount,$user,$group,$expectedFileSize,$unused =
|
||||
(/bin/ls -ldi ${testFile}).Split(" ", 7, [System.StringSplitOptions]"RemoveEmptyEntries")
|
||||
$file = Get-Item ${testFile}
|
||||
|
||||
$expectedDirInode,$permission,$expectedDirLinkCount,$user,$group,$expectedDirSize,$unused =
|
||||
(/bin/ls -ldi ${testDir}).Split(" ", 7, [System.StringSplitOptions]"RemoveEmptyEntries")
|
||||
$Dir = Get-Item ${testDir}
|
||||
|
||||
$testCases =
|
||||
@{ Expected = $expectedFileInode; Observed = $File.UnixStat.Inode; Title = "FileInode" },
|
||||
@{ Expected = $expectedFileLinkCount; Observed = $File.UnixStat.HardlinkCount; Title = "FileHardlinkCount" },
|
||||
@{ Expected = $expectedFileSize; Observed = $File.UnixStat.Size; Title = "FileSize" },
|
||||
@{ Expected = $expectedDirInode; Observed = $Dir.UnixStat.Inode; Title = "DirInode" },
|
||||
@{ Expected = $expectedDirLinkCount; Observed = $Dir.UnixStat.HardlinkCount; Title = "DirHardlinkCount" },
|
||||
@{ Expected = $expectedDirSize; Observed = $Dir.UnixStat.Size; Title = "DirSize" }
|
||||
}
|
||||
|
||||
It "Should have correct values in UnixStat property for '<Title>'" -TestCases $testCases {
|
||||
param ( $Title, $expected, $observed )
|
||||
|
||||
$observed | Should -Be $expected
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@ Describe "Get-FormatData" -Tags "CI" {
|
||||
$format.TypeNames | Should -HaveCount 2
|
||||
$format.TypeNames[0] | Should -BeExactly "System.IO.DirectoryInfo"
|
||||
$format.TypeNames[1] | Should -BeExactly "System.IO.FileInfo"
|
||||
$format.FormatViewDefinition | Should -HaveCount 4
|
||||
$format.FormatViewDefinition | Should -HaveCount ($IsWindows ? 4 : 5)
|
||||
}
|
||||
|
||||
It "Should return nothing for format data requiring '-PowerShellVersion 5.1' and not provided" {
|
||||
|
||||
@@ -18,7 +18,7 @@ Describe "Built-in type information tests" -Tag "CI" {
|
||||
}
|
||||
|
||||
It "Should have correct number of built-in type items in type table" {
|
||||
$types.Count | Should -BeExactly ($IsWindows ? 273 : 271)
|
||||
$types.Count | Should -BeExactly ($IsWindows ? 273 : 272)
|
||||
}
|
||||
|
||||
It "Should have expected member info for 'System.Diagnostics.ProcessModule'" {
|
||||
|
||||
Reference in New Issue
Block a user