Skip to content

ls Provider Architecture

ls is deceptively complex to emulate correctly on Windows. This page explains the problems with the naive Get-ChildItem approach, how ps-bash solves them with a three-tier architecture, and how to register custom providers for non-filesystem paths.

The natural PowerShell way to list a directory is Get-ChildItem. It works for simple cases, but has several problems that affect bash compatibility:

1. ACL calls on every entry

Get-ChildItem automatically calls Get-Acl to populate the UnixStat and Mode properties for each file system item. On directories with hundreds of files this is measurably slow. More critically, it fails on directories that contain files with Windows reserved device names (nul, con, prn, aux, com1-com9, lpt1-lpt9). These names are legal on most filesystems via Win32 extended paths, but Get-Acl resolves them as device names and throws an error.

2. Full load before first result

Get-ChildItem loads all entries into memory before streaming any to the pipeline. In a directory with 100k files (e.g., node_modules), this causes significant memory pressure and delay before the first line appears.

3. Dirs-first sort order

Get-ChildItem returns directories before files within each sort group. Bash ls sorts all entries together alphabetically, case-insensitively (a.txt before B.txt before c/).

4. Automatic trailing slash on directories

Get-ChildItem always adds / to directory names in list output. Bash only adds it with ls -p.

ps-bash’s Invoke-BashLs tries three strategies in order:

1. Custom providers (Register-BashLsProvider)
└─ Any registered provider whose Detect block returns $true
2. System.IO fast path
└─ [System.IO.Directory]::Exists($path) → DirectoryInfo.EnumerateFileSystemInfos
3. Get-ChildItem fallback
└─ Registry:, Cert:, custom PSDrives, and any other PS provider

Any code can register a provider for paths that it owns:

Terminal window
Register-BashLsProvider -Name 'MyProvider' -Detect {
param($path)
$path -like 'MyScheme:*'
} -List {
param($path, $options)
# $options.ShowHidden, $options.LongFormat, $options.Recursive
# Return PSCustomObjects with PSTypeName = 'PsBash.LsEntry'
[PSCustomObject]@{
PSTypeName = 'PsBash.LsEntry'
Name = 'example'
FullPath = "$path/example"
IsDirectory = $false
IsSymlink = $false
SizeBytes = 1024
Permissions = '-rw-r--r--'
LinkCount = 1
Owner = 'user'
Group = 'group'
LastModified = [DateTime]::Now
BashText = '' # filled in by ls before output
}
}

For real filesystem paths, ps-bash uses System.IO.DirectoryInfo.EnumerateFileSystemInfos:

  • Streaming: entries are yielded to the pipeline as they are enumerated — no memory accumulation
  • No ACL calls: file attributes come from FileSystemInfo.Attributes (a single Win32 FindFirstFile call, already cached)
  • Reserved names handled: the underlying Win32 enumeration handles nul, con, etc. correctly — it never calls GetFileSecurity on them
  • Bash-compatible permissions on Windows: read/write/execute bits are derived from FileAttributes.ReadOnly and the file extension (.exe, .bat, .cmd, .ps1, .sh, .com)

Sorting is done with System.Linq.Enumerable.OrderBy using StringComparer.OrdinalIgnoreCase, which matches bash’s default sort order: all entries together, case-insensitive alphabetical.

When the path is not a real filesystem directory (e.g., Registry:, Cert:, custom PSDrives), the System.IO tier is skipped and ps-bash falls back to Get-ChildItem. Each item is wrapped into a PsBash.LsEntry via Get-LsEntryFromPsItem, which reads properties from the PS provider item rather than FileSystemInfo.

This tier preserves the ability to use ls naturally on any PowerShell path:

Terminal window
ls Registry:/HKLM/SOFTWARE
ls Cert:/CurrentUser/My
Behaviorbashps-bash (old)ps-bash (new)
Sort orderalpha mixed, case-insensitivedirs-firstalpha mixed, case-insensitive
Trailing / on dirsonly with -palwaysonly with -p
nul in directoryworksGet-Acl errorworks
Memory for 100k filesstreamingfull loadstreaming
-a (show hidden).-prefixed filesworksworks
-R (recursive)all depthsworksworks
-l (long format)permissions, owner, sizeworksworks

Get-LsEntryFromFsi is the new function that creates PsBash.LsEntry from a System.IO.FileSystemInfo. It is the canonical source of truth for file metadata in the System.IO tier.

Get-BashFileInfo still exists as a thin wrapper for backward compatibility — Invoke-BashFind and Invoke-BashStat call it internally. It now delegates to Get-LsEntryFromFsi.

The provider API is designed for ps-bash integrations that need to expose non-standard paths as first-class directories. The List scriptblock receives:

ParameterTypeDescription
$pathstringThe path passed to ls
$optionshashtableParsed flags: ShowHidden, LongFormat, Recursive, SortByTime, SortBySize, ReverseSort, ClassifyDirs

The List block should return PsBash.LsEntry objects. The BashText property is filled in by Invoke-BashLs after the provider returns, so the provider can leave it empty.

To remove a provider:

Terminal window
$script:BashLsProviders = $script:BashLsProviders | Where-Object { $_.Name -ne 'MyProvider' }