Created
July 27, 2026 03:22
-
-
Save jborean93/9725cc73fa0ac5515b54cdebf8191277 to your computer and use it in GitHub Desktop.
Calls statx on Linux Platforms from PowerShell.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright: (c) 2026, Jordan Borean (@jborean93) <jborean93@gmail.com> | |
| # MIT License (see LICENSE or https://opensource.org/licenses/MIT) | |
| function Get-StatX { | |
| <# | |
| .SYNOPSIS | |
| Get the statx values for the provided path. | |
| .DESCRIPTION | |
| Gets the statx values for the provided path into a structured object. | |
| This is a Linux only cmdlet and will not work on other platforms. | |
| .PARAMETER Path | |
| The path to get the statx values for. This can be a relative or absolute path and wildcards are supported. | |
| .PARAMETER LiteralPath | |
| The path to get the statx values for. This can be a relative or absolute path and wildcards are not supported. | |
| .PARAMETER Follow | |
| Follow symbolic links when getting the statx values. By default, symbolic links are not followed. | |
| .EXAMPLE | |
| Get-ChildItem -Path ./test.txt | Get-StatX | |
| #> | |
| [CmdletBinding(DefaultParameterSetName = 'Path')] | |
| param ( | |
| [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'Path')] | |
| [string[]] | |
| $Path, | |
| [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'LiteralPath')] | |
| [Alias('PSPath')] | |
| [string[]] | |
| $LiteralPath, | |
| [Parameter()] | |
| [switch] | |
| $Follow | |
| ) | |
| begin { | |
| Add-Type -TypeDefinition @' | |
| using System; | |
| using System.ComponentModel; | |
| using System.Runtime.CompilerServices; | |
| using System.Runtime.InteropServices; | |
| namespace StatxInterop; | |
| [StructLayout(LayoutKind.Sequential)] | |
| public struct statx_timestamp | |
| { | |
| public long tv_sec; | |
| public int tv_nsec; | |
| private int _reserved; | |
| } | |
| [InlineArray(16)] | |
| public struct StatxReserved | |
| { | |
| private long _reserved; | |
| } | |
| [StructLayout(LayoutKind.Sequential)] | |
| public struct statx | |
| { | |
| public int stx_mask; | |
| public int stx_blksize; | |
| public long stx_attributes; | |
| public int stx_nlink; | |
| public int stx_uid; | |
| public int stx_gid; | |
| public short stx_mode; | |
| public long stx_ino; | |
| public long stx_size; | |
| public long stx_blocks; | |
| public long stx_attributes_mask; | |
| public statx_timestamp stx_atime; | |
| public statx_timestamp stx_btime; | |
| public statx_timestamp stx_ctime; | |
| public statx_timestamp stx_mtime; | |
| public StatxReserved _spare; | |
| } | |
| [Flags] | |
| public enum StatxMask | |
| { | |
| Type = 0x00000001, | |
| Mode = 0x00000002, | |
| Nlink = 0x00000004, | |
| Uid = 0x00000008, | |
| Gid = 0x00000010, | |
| Atime = 0x00000020, | |
| Mtime = 0x00000040, | |
| Ctime = 0x00000080, | |
| Ino = 0x00000100, | |
| Size = 0x00000200, | |
| Blocks = 0x00000400, | |
| BasicStats = 0x000007ff, | |
| Btime = 0x00000800, | |
| MntId = 0x00001000, | |
| DioAlign = 0x00002000, | |
| } | |
| public enum FileType : short | |
| { | |
| Unknown = 0x0000, | |
| Fifo = 0x1000, | |
| CharacterDevice = 0x2000, | |
| Directory = 0x4000, | |
| BlockDevice = 0x6000, | |
| RegularFile = unchecked((short)0x8000), | |
| SymbolicLink = unchecked((short)0xA000), | |
| Socket = unchecked((short)0xC000), | |
| } | |
| [Flags] | |
| public enum FilePermission : short | |
| { | |
| None = 0, | |
| OtherExecute = 0x0001, | |
| OtherWrite = 0x0002, | |
| OtherRead = 0x0004, | |
| GroupExecute = 0x0008, | |
| GroupWrite = 0x0010, | |
| GroupRead = 0x0020, | |
| UserExecute = 0x0040, | |
| UserWrite = 0x0080, | |
| UserRead = 0x0100, | |
| Sticky = 0x0200, | |
| SetGid = 0x0400, | |
| SetUid = 0x0800, | |
| } | |
| [Flags] | |
| public enum FileAttributes : long | |
| { | |
| None = 0x00000000, | |
| Compressed = 0x00000004, | |
| Immutable = 0x00000010, | |
| Append = 0x00000020, | |
| NoDump = 0x00000040, | |
| Encrypted = 0x00000800, | |
| AutoMount = 0x00001000, | |
| MountRoot = 0x00002000, | |
| Verify = 0x00100000, | |
| Dax = 0x00200000, | |
| WriteAtomic = 0x00400000, | |
| } | |
| public static class Interop | |
| { | |
| public const int AT_SYMLINK_NOFOLLOW = 0x100; | |
| [DllImport("libc", SetLastError = true)] | |
| private static extern int statx( | |
| int dirfd, | |
| string path, | |
| int flags, | |
| int mask, | |
| ref statx buf); | |
| public static statx GetStatx( | |
| string path, | |
| int flags, | |
| StatxMask mask) | |
| { | |
| statx result = new(); | |
| int ret = statx(-1, path, flags, (int)mask, ref result); | |
| if (ret != 0) | |
| { | |
| throw new Win32Exception(Marshal.GetLastWin32Error()); | |
| } | |
| return result; | |
| } | |
| } | |
| '@ | |
| $statxMask = [StatxInterop.StatxMask]::BasicStats -bor [StatxInterop.StatxMask]::Btime | |
| $statxFlags = if ($Follow) { 0 } else { [StatxInterop.Interop]::AT_SYMLINK_NOFOLLOW } | |
| } | |
| process { | |
| $allPaths = if ($PSCmdlet.ParameterSetName -eq 'Path') { | |
| $Path | ForEach-Object -Process { | |
| $provider = $null | |
| try { | |
| $PSCmdlet.SessionState.Path.GetResolvedProviderPathFromPSPath($_, [ref]$provider) | |
| } | |
| catch [System.Management.Automation.ItemNotFoundException] { | |
| $PSCmdlet.WriteError($_) | |
| } | |
| } | |
| } | |
| elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') { | |
| $LiteralPath | ForEach-Object -Process { | |
| $resolvedPath = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($_) | |
| if (-not (Test-Path -LiteralPath $resolvedPath)) { | |
| $msg = "Cannot find path '$resolvedPath' because it does not exist" | |
| $err = [System.Management.Automation.ErrorRecord]::new( | |
| [System.Management.Automation.ItemNotFoundException]::new($msg), | |
| "PathNotFound", | |
| "ObjectNotFound", | |
| $resolvedPath) | |
| $PSCmdlet.WriteError($err) | |
| return | |
| } | |
| $resolvedPath | |
| } | |
| } | |
| foreach ($p in $allPaths) { | |
| try { | |
| $result = [StatxInterop.Interop]::GetStatx($p, $statxFlags, $statxMask) | |
| $mask = [Enum]::ToObject([StatxInterop.StatxMask], $result.stx_mask) | |
| $obj = [ordered]@{ | |
| Path = $p | |
| BlockSize = $result.stx_blksize | |
| Attributes = [Enum]::ToObject([StatxInterop.FileAttributes], $result.stx_attributes) | |
| FileType = [StatxInterop.FileType]::Unknown | |
| Mode = [StatxInterop.FilePermission]::None | |
| NumberOfLinks = -1 | |
| UserId = -1 | |
| GroupId = -1 | |
| Inode = [long]-1 | |
| Size = [long]-1 | |
| Blocks = [long]-1 | |
| AccessTime = $null | |
| BirthTime = $null | |
| ChangeTime = $null | |
| ModifyTime = $null | |
| } | |
| if ($mask.HasFlag([StatxInterop.StatxMask]::Type)) { | |
| $obj.FileType = [Enum]::ToObject([StatxInterop.FileType], [short]($result.stx_mode -band [short]"0xF000")) | |
| } | |
| if ($mask.HasFlag([StatxInterop.StatxMask]::Mode)) { | |
| $obj.Mode = [Enum]::ToObject([StatxInterop.FilePermission], [short]($result.stx_mode -band 0x0FFF)) | |
| } | |
| if ($mask.HasFlag([StatxInterop.StatxMask]::Nlink)) { | |
| $obj.NumberOfLinks = $result.stx_nlink | |
| } | |
| if ($mask.HasFlag([StatxInterop.StatxMask]::Uid)) { | |
| $obj.UserId = $result.stx_uid | |
| } | |
| if ($mask.HasFlag([StatxInterop.StatxMask]::Gid)) { | |
| $obj.GroupId = $result.stx_gid | |
| } | |
| if ($mask.HasFlag([StatxInterop.StatxMask]::Ino)) { | |
| $obj.Inode = $result.stx_ino | |
| } | |
| if ($mask.HasFlag([StatxInterop.StatxMask]::Size)) { | |
| $obj.Size = $result.stx_size | |
| } | |
| if ($mask.HasFlag([StatxInterop.StatxMask]::Blocks)) { | |
| $obj.Blocks = $result.stx_blocks | |
| } | |
| if ($mask.HasFlag([StatxInterop.StatxMask]::Atime)) { | |
| $obj.AccessTime = [DateTimeOffset]::FromUnixTimeSeconds($result.stx_atime.tv_sec).AddTicks($result.stx_atime.tv_nsec / 100).LocalDateTime | |
| } | |
| if ($mask.HasFlag([StatxInterop.StatxMask]::Btime)) { | |
| $obj.BirthTime = [DateTimeOffset]::FromUnixTimeSeconds($result.stx_btime.tv_sec).AddTicks($result.stx_btime.tv_nsec / 100).LocalDateTime | |
| } | |
| if ($mask.HasFlag([StatxInterop.StatxMask]::Ctime)) { | |
| $obj.ChangeTime = [DateTimeOffset]::FromUnixTimeSeconds($result.stx_ctime.tv_sec).AddTicks($result.stx_ctime.tv_nsec / 100).LocalDateTime | |
| } | |
| if ($mask.HasFlag([StatxInterop.StatxMask]::Mtime)) { | |
| $obj.ModifyTime = [DateTimeOffset]::FromUnixTimeSeconds($result.stx_mtime.tv_sec).AddTicks($result.stx_mtime.tv_nsec / 100).LocalDateTime | |
| } | |
| [PSCustomObject]$obj | |
| } | |
| catch { | |
| $PSCmdlet.WriteError($_) | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment