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.
Why Get-ChildItem Falls Short
Section titled “Why Get-ChildItem Falls Short”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.
Three-Tier Architecture
Section titled “Three-Tier Architecture”ps-bash’s Invoke-BashLs tries three strategies in order:
1. Custom providers (Register-BashLsProvider) └─ Any registered provider whose Detect block returns $true2. System.IO fast path └─ [System.IO.Directory]::Exists($path) → DirectoryInfo.EnumerateFileSystemInfos3. Get-ChildItem fallback └─ Registry:, Cert:, custom PSDrives, and any other PS providerTier 1: Custom Providers
Section titled “Tier 1: Custom Providers”Any code can register a provider for paths that it owns:
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 }}Tier 2: System.IO Fast Path
Section titled “Tier 2: System.IO Fast Path”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 Win32FindFirstFilecall, already cached) - Reserved names handled: the underlying Win32 enumeration handles
nul,con, etc. correctly — it never callsGetFileSecurityon them - Bash-compatible permissions on Windows: read/write/execute bits are derived from
FileAttributes.ReadOnlyand 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.
Tier 3: Get-ChildItem Fallback
Section titled “Tier 3: Get-ChildItem Fallback”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:
ls Registry:/HKLM/SOFTWAREls Cert:/CurrentUser/MyBash Compatibility Details
Section titled “Bash Compatibility Details”| Behavior | bash | ps-bash (old) | ps-bash (new) |
|---|---|---|---|
| Sort order | alpha mixed, case-insensitive | dirs-first | alpha mixed, case-insensitive |
Trailing / on dirs | only with -p | always | only with -p |
nul in directory | works | Get-Acl error | works |
| Memory for 100k files | streaming | full load | streaming |
-a (show hidden) | .-prefixed files | works | works |
-R (recursive) | all depths | works | works |
-l (long format) | permissions, owner, size | works | works |
Get-LsEntryFromFsi vs Get-BashFileInfo
Section titled “Get-LsEntryFromFsi vs Get-BashFileInfo”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.
Extending with Register-BashLsProvider
Section titled “Extending with Register-BashLsProvider”The provider API is designed for ps-bash integrations that need to expose non-standard paths as first-class directories. The List scriptblock receives:
| Parameter | Type | Description |
|---|---|---|
$path | string | The path passed to ls |
$options | hashtable | Parsed 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:
$script:BashLsProviders = $script:BashLsProviders | Where-Object { $_.Name -ne 'MyProvider' }