Skip to content

Instantly share code, notes, and snippets.

@matchy233
Last active October 26, 2024 19:50
Show Gist options
  • Select an option

  • Save matchy233/0c8baaad8af9aa12838ded428d6f8e31 to your computer and use it in GitHub Desktop.

Select an option

Save matchy233/0c8baaad8af9aa12838ded428d6f8e31 to your computer and use it in GitHub Desktop.
Bash-like ls for PowerShell
Function Get-ColorChildItem {
[CmdletBinding()]
param(
[Parameter(
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
Position = 0
)]
[string[]]$Path = ".",
[string]$Filter,
[switch]$Recurse,
[switch]$a,
[switch]$l,
[switch]$h,
[switch]$la,
[switch]$al,
[switch]$lh,
[switch]$hl,
[switch]$ah,
[switch]$ha,
[switch]$lah,
[switch]$lha,
[switch]$alh,
[switch]$ahl,
[switch]$hal,
[switch]$hla
)
begin {
# Process switches
if ($a -or $la -or $al -or $ah -or $ha -or $lah -or $lha -or $alh -or $ahl -or $hal -or $hla) {
$a = $true
}
if ($l -or $al -or $la -or $lh -or $hl -or $lah -or $lha -or $alh -or $ahl -or $hal -or $hla) {
$l = $true
}
if ($h -or $lh -or $hl -or $ah -or $ha -or $lah -or $lha -or $alh -or $ahl -or $hal -or $hla) {
$h = $true
}
# Initialize regex patterns
$regex_opts = ([System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Compiled)
$hidden = New-Object System.Text.RegularExpressions.Regex('^\.', $regex_opts)
$compressed = New-Object System.Text.RegularExpressions.Regex('\.(zip|tar|gz|rar)$', $regex_opts)
$executable = New-Object System.Text.RegularExpressions.Regex('\.(exe|bat|cmd|ps1|psm1|vbs|rb|reg|dll|o|lib|py|sh)$', $regex_opts)
# Check if the function is being called in a pipeline
$isPipeline = -not $MyInvocation.ExpectingInput -and ($MyInvocation.PipelinePosition -lt $MyInvocation.PipelineLength)
$width = $Host.UI.RawUI.WindowSize.Width
$cols = 5
if ($width -le 82) {
$cols = 3
}
function Get-HumanReadableSize {
param($size)
if ($size -lt 1kb) {
return $size.toString()
}
elseif ($size -lt 1mb) {
if ($size / 1kb -lt 10) {
return [string]::Format("{0:0.0}K", [math]::Round($size / 1kb, 2))
}
else {
return [string]::Format("{0:0}K", [math]::Round($size / 1kb, 2))
}
}
elseif ($size -lt 1gb) {
if ($size / 1mb -lt 10) {
return [string]::Format("{0:0.0}M", [math]::Round($size / 1mb, 2))
}
else {
return [string]::Format("{0:0}M", [math]::Round($size / 1mb, 2))
}
}
elseif ($size -lt 1tb) {
if ($size / 1gb -lt 10) {
return [string]::Format("{0:0.0}G", [math]::Round($size / 1gb, 2))
}
else {
return [string]::Format("{0:0}G", [math]::Round($size / 1gb, 2))
}
}
else {
if ($size / 1tb -lt 10) {
return [string]::Format("{0:0.0}T", [math]::Round($size / 1tb, 2))
}
else {
return [string]::Format("{0:0}T", [math]::Round($size / 1tb, 2))
}
}
}
# Function to test for full-width characters
function Test-FullWidth {
param (
[char]$Char
)
$eastAsianWidth = [System.Globalization.CharUnicodeInfo]::GetUnicodeCategory($Char)
# East Asian Wide (W) and East Asian Full-width (F) are considered full-width
return $eastAsianWidth -in @(
[System.Globalization.UnicodeCategory]::OtherLetter,
[System.Globalization.UnicodeCategory]::LetterNumber,
[System.Globalization.UnicodeCategory]::OtherNumber,
[System.Globalization.UnicodeCategory]::OtherSymbol
)
}
function Get-Substring {
param (
[string]$String,
[int]$StartIndex,
[int]$Length
)
$curr = 0
for ($i = 0; $i -lt $String.Length; $i++) {
if (Test-FullWidth $String[$i]) {
$curr += 2
}
else {
$curr++
}
if ($curr -ge $StartIndex) {
$startIndex = $i
break
}
}
for ($i = $startIndex; $i -lt $String.Length; $i++) {
if (Test-FullWidth $String[$i]) {
$curr += 2
}
else {
$curr++
}
if ($curr -ge $StartIndex + $Length) {
return $String.Substring($startIndex, $i - $startIndex)
}
}
return $String.Substring($startIndex, $String.Length - $startIndex)
}
function Get-Color {
param($item)
# return foregroundcolor and backgroundcolor based on item type
$ForegroundColor = 'White'
# don't set background color by default
$BackgroundColor = $null
$isHidden = $hidden.IsMatch($item.Name)
if ($item.PSIsContainer) {
$BackgroundColor = if ($isHidden) { 'DarkBlue' } else { 'Blue' }
$ForegroundColor = if ($isHidden) { 'DarkGray' } else { 'White' }
}
elseif ($isHidden) {
$ForegroundColor = 'DarkGray'
}
elseif ($compressed.IsMatch($item.Name)) {
$ForegroundColor = 'Red'
}
elseif ($executable.IsMatch($item.Name)) {
$ForegroundColor = 'Green'
}
else {
$ForegroundColor = 'White'
}
return $ForegroundColor, $BackgroundColor
}
}
process {
# Build parameter hashtable
$gciParams = @{
Path = $Path
}
if ($PSBoundParameters.ContainsKey('Filter')) {
$gciParams['Filter'] = $Filter
}
if ($PSBoundParameters.ContainsKey('Recurse')) {
$gciParams['Recurse'] = $Recurse
}
$items = Get-ChildItem @gciParams
if ($l) {
Write-Output "`n Directory: $((Get-Location).Path)`n"
$format_title = "{0} {1,29} {2,15} {3}"
Write-Host ($format_title -f "Mode", "LastWriteTime", "Length", "Name") -ForegroundColor Green
Write-Host ($format_title -f "----", "-------------", "------", "----") -ForegroundColor Green
foreach ($item in $items) {
$out = $item.Name
$isHidden = $hidden.IsMatch($out)
# Skip hidden items unless -a is specified
if ($isHidden -and -not $a) { continue }
if (!$isPipeline) {
# Calculate length display
if ($item.PSIsContainer) {
$len = ""
}
else {
$len = if ($h) { Get-HumanReadableSize $item.Length } else { $item.Length.ToString() }
}
# Write item info
if ($h) {
$len = $len.PadLeft(8)
}
else {
$len = $len.ToString().PadLeft(15)
}
Write-Host ("{0} {1,28:dd-MMM-yy hh:mm} {2}" -f $item.Mode, $item.LastWriteTime, $len) -NoNewline
$defaultBackground = $Host.UI.RawUI.BackgroundColor
# Write item name
$color, $bgColor = Get-Color $item
Write-Host " " -NoNewline # Write the leading space without color
if ($bgColor) {
Write-Host $out -ForegroundColor $color -BackgroundColor $bgColor -NoNewline
# Reset to default background color for the newline
Write-Host "" -BackgroundColor $defaultBackground
}
else {
Write-Host $out -ForegroundColor $color
}
}
else {
$item
}
}
}
else {
$i = 0
$colwid = [int]($width / $cols)
$pad = [int]($width / $cols) - 1
foreach ($item in $items) {
$out = $item.Name
$isHidden = $hidden.IsMatch($item.Name)
# Skip hidden items unless -a is specified
if ($isHidden -and -not $a) { continue }
$outLen = 0
$shrinkVal = 0
# iterate over chars in $out, counting full-width chars
for ($j = 0; $j -lt $out.Length; $j++) {
if (Test-FullWidth $out[$j]) {
$outLen += 2
$shrinkVal++
}
else {
$outLen++
}
}
$isHidden = $hidden.IsMatch($out)
if ($outLen -ge $colwid - 1) {
$out = "$(Get-Substring -String $out -StartIndex 0 -Length ($colwid - 5))…"
$shrinkVal = 0
for ($j = 0; $j -lt $out.Length; $j++) {
if (Test-FullWidth $out[$j]) {
$shrinkVal++
}
}
}
$nnl = ++$i % $cols -ne 0
$color, $bgColor = Get-Color $item
if (!$isPipeline) {
# If background color is specified
if ($bgColor) {
Write-Host $out -ForegroundColor $color -BackgroundColor $bgColor -NoNewline
# Write padding separately with no background color
$padding = " " * ($pad - $shrinkVal - $out.Length)
if ($padding.Length -gt 0) {
Write-Host $padding -NoNewline:$nnl
}
else {
Write-Host "" -NoNewline:$nnl
}
}
else {
# Original behavior without background color
Write-Host $out -ForegroundColor $color -NoNewline
$padding = " " * ($pad - $shrinkVal - $out.Length)
if ($padding.Length -gt 0) {
Write-Host $padding -NoNewline:$nnl
}
else {
Write-Host "" -NoNewline:$nnl
}
}
}
else {
$item.Name
}
}
}
}
end {
if ($l) {
Write-Output ""
}
}
}
# Create aliases
if (Test-Path alias:ls) { Remove-Item alias:ls -Force }
# Create wrapper functions for all aliases
Set-Alias -Name ls -Value Get-ColorChildItem -Option AllScope
@matchy233
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment