Skip to content

Instantly share code, notes, and snippets.

@jborean93
Created July 27, 2026 03:22
Show Gist options
  • Select an option

  • Save jborean93/9725cc73fa0ac5515b54cdebf8191277 to your computer and use it in GitHub Desktop.

Select an option

Save jborean93/9725cc73fa0ac5515b54cdebf8191277 to your computer and use it in GitHub Desktop.
Calls statx on Linux Platforms from PowerShell.
# 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