public
imalexnord
read
ms.open
Adds Mac "Open ." command to Windows
Languages
Repository composition by tracked source files.
PowerShell
100%
Create file
Wiki Documentation
Clone
https://nobgit.com/user/imalexnord/ms.open.git
ssh://[email protected]:2222/user/imalexnord/ms.open.git
[CmdletBinding()]
param(
[Parameter(Position = 0, Mandatory = $true)]
[string]$Command,
[Parameter(Position = 1, ValueFromRemainingArguments = $true)]
[string[]]$Arguments
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$Arguments = @($Arguments)
function Resolve-MSOpenPath {
param(
[Parameter(Mandatory)]
[string]$Path
)
$resolved = Resolve-Path -LiteralPath $Path -ErrorAction Stop
return $resolved.Path
}
function Invoke-Open {
param([string[]]$Args)
$target = if ($Args.Count -gt 0) {
$Args[0]
}
else {
(Get-Location).Path
}
if (Test-Path -LiteralPath $target) {
$target = Resolve-MSOpenPath -Path $target
}
Start-Process -FilePath $target
}
function Invoke-Reveal {
param([string[]]$Args)
if ($Args.Count -lt 1) {
throw 'Usage: reveal <file-or-folder>'
}
$target = Resolve-MSOpenPath -Path $Args[0]
if (Test-Path -LiteralPath $target -PathType Container) {
Start-Process -FilePath 'explorer.exe' -ArgumentList "`"$target`""
return
}
Start-Process -FilePath 'explorer.exe' -ArgumentList "/select,`"$target`""
}
function Invoke-CopyPath {
param([string[]]$Args)
$target = if ($Args.Count -gt 0) {
Resolve-MSOpenPath -Path $Args[0]
}
else {
(Get-Location).Path
}
Set-Clipboard -Value $target
Write-Output $target
}
function Invoke-Paths {
$entries = $env:Path -split ';' |
ForEach-Object { $_.Trim() } |
Where-Object { $_ }
$index = 1
foreach ($entry in $entries) {
[pscustomobject]@{
Number = $index
Path = $entry
Exists = Test-Path -LiteralPath $entry
}
$index++
}
}
function Invoke-Which {
param([string[]]$Args)
if ($Args.Count -lt 1) {
throw 'Usage: which <command>'
}
$commands = Get-Command -Name $Args[0] -All -ErrorAction Stop
foreach ($item in $commands) {
$location = $null
if ($item.PSObject.Properties.Name -contains 'Path' -and $item.Path) {
$location = $item.Path
}
elseif ($item.Source) {
$location = $item.Source
}
elseif ($item.Definition) {
$location = $item.Definition
}
[pscustomobject]@{
CommandType = $item.CommandType
Name = $item.Name
Location = $location
}
}
}
function Get-ListeningPorts {
$connections = Get-NetTCPConnection -State Listen -ErrorAction Stop |
Sort-Object LocalPort, LocalAddress
foreach ($connection in $connections) {
$processName = '<unknown>'
if ($connection.OwningProcess -eq 4) {
$processName = 'System'
}
else {
try {
$processName = (Get-Process -Id $connection.OwningProcess -ErrorAction Stop).ProcessName
}
catch {
$processName = '<unavailable>'
}
}
[pscustomobject]@{
Protocol = 'TCP'
Address = $connection.LocalAddress
Port = $connection.LocalPort
PID = $connection.OwningProcess
Process = $processName
}
}
}
function Invoke-Ports {
Get-ListeningPorts | Format-Table -AutoSize
}
function Invoke-KillPort {
param([string[]]$Args)
if ($Args.Count -lt 1) {
throw 'Usage: killport <port> [--force]'
}
$port = 0
if (-not [int]::TryParse($Args[0], [ref]$port) -or $port -lt 1 -or $port -gt 65535) {
throw "Invalid port: $($Args[0])"
}
$force = $Args -contains '--force'
$connections = Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction SilentlyContinue
$processIds = @($connections | Select-Object -ExpandProperty OwningProcess -Unique)
if ($processIds.Count -eq 0) {
throw "Nothing is listening on port $port."
}
$targets = foreach ($processId in $processIds) {
$name = '<unknown>'
try {
$name = (Get-Process -Id $processId -ErrorAction Stop).ProcessName
}
catch {
$name = '<unavailable>'
}
[pscustomobject]@{
Port = $port
PID = $processId
Process = $name
}
}
$targets | Format-Table -AutoSize
$protected = @($targets | Where-Object { $_.PID -in 0, 4 })
if ($protected.Count -gt 0) {
throw 'Refusing to terminate a protected Windows system process.'
}
if (-not $force) {
$answer = Read-Host "Terminate the process owning port $port? [y/N]"
if ($answer -notmatch '^(y|yes)$') {
Write-Host 'Cancelled.'
return
}
}
foreach ($target in $targets) {
Stop-Process -Id $target.PID -Force -ErrorAction Stop
Write-Host "Stopped $($target.Process) (PID $($target.PID))."
}
}
function Invoke-Sha256 {
param([string[]]$Args)
if ($Args.Count -lt 1) {
throw 'Usage: sha256 <file>'
}
$target = Resolve-MSOpenPath -Path $Args[0]
if (-not (Test-Path -LiteralPath $target -PathType Leaf)) {
throw "Not a file: $target"
}
$hash = Get-FileHash -LiteralPath $target -Algorithm SHA256
Write-Output "$($hash.Hash.ToLowerInvariant()) $target"
}
function Invoke-Pbcopy {
param([string[]]$Args)
$text = if ($Args.Count -gt 0) {
$Args -join ' '
}
elseif ([Console]::IsInputRedirected) {
[Console]::In.ReadToEnd()
}
else {
''
}
Set-Clipboard -Value $text
}
function Invoke-Pbpaste {
$value = Get-Clipboard -Raw
if ($null -ne $value) {
[Console]::Out.Write($value)
}
}
function Get-MimeType {
param([string]$Path)
switch ([IO.Path]::GetExtension($Path).ToLowerInvariant()) {
'.html' { 'text/html; charset=utf-8' }
'.htm' { 'text/html; charset=utf-8' }
'.css' { 'text/css; charset=utf-8' }
'.js' { 'text/javascript; charset=utf-8' }
'.json' { 'application/json; charset=utf-8' }
'.xml' { 'application/xml; charset=utf-8' }
'.txt' { 'text/plain; charset=utf-8' }
'.md' { 'text/markdown; charset=utf-8' }
'.svg' { 'image/svg+xml' }
'.png' { 'image/png' }
'.jpg' { 'image/jpeg' }
'.jpeg' { 'image/jpeg' }
'.gif' { 'image/gif' }
'.webp' { 'image/webp' }
'.ico' { 'image/x-icon' }
'.pdf' { 'application/pdf' }
'.wasm' { 'application/wasm' }
'.zip' { 'application/zip' }
default { 'application/octet-stream' }
}
}
function Send-TextResponse {
param(
[Parameter(Mandatory)]
[System.Net.HttpListenerResponse]$Response,
[Parameter(Mandatory)]
[int]$StatusCode,
[Parameter(Mandatory)]
[string]$Text,
[string]$ContentType = 'text/plain; charset=utf-8'
)
$bytes = [Text.Encoding]::UTF8.GetBytes($Text)
$Response.StatusCode = $StatusCode
$Response.ContentType = $ContentType
$Response.ContentLength64 = $bytes.Length
$Response.OutputStream.Write($bytes, 0, $bytes.Length)
}
function Get-DirectoryListingHtml {
param(
[Parameter(Mandatory)]
[string]$Directory,
[Parameter(Mandatory)]
[string]$RequestPath
)
$encodedTitle = [Net.WebUtility]::HtmlEncode("Index of $RequestPath")
$builder = [Text.StringBuilder]::new()
[void]$builder.AppendLine('<!doctype html>')
[void]$builder.AppendLine('<html lang="en"><head><meta charset="utf-8">')
[void]$builder.AppendLine("<title>$encodedTitle</title>")
[void]$builder.AppendLine('<style>body{font-family:system-ui,sans-serif;max-width:900px;margin:40px auto;padding:0 20px}li{margin:8px 0}a{text-decoration:none}a:hover{text-decoration:underline}</style>')
[void]$builder.AppendLine('</head><body>')
[void]$builder.AppendLine("<h1>$encodedTitle</h1><ul>")
if ($RequestPath -ne '/') {
[void]$builder.AppendLine('<li><a href="../">../</a></li>')
}
foreach ($item in Get-ChildItem -LiteralPath $Directory | Sort-Object @{ Expression = { -not $_.PSIsContainer } }, Name) {
$displayName = $item.Name + $(if ($item.PSIsContainer) { '/' } else { '' })
$hrefName = [Uri]::EscapeDataString($item.Name) + $(if ($item.PSIsContainer) { '/' } else { '' })
$encodedName = [Net.WebUtility]::HtmlEncode($displayName)
[void]$builder.AppendLine("<li><a href=`"$hrefName`">$encodedName</a></li>")
}
[void]$builder.AppendLine('</ul></body></html>')
return $builder.ToString()
}
function Invoke-Serve {
param([string[]]$Args)
$port = 8000
if ($Args.Count -gt 0) {
if (-not [int]::TryParse($Args[0], [ref]$port) -or $port -lt 1 -or $port -gt 65535) {
throw "Invalid port: $($Args[0])"
}
}
$root = [IO.Path]::GetFullPath((Get-Location).Path)
$rootWithSeparator = $root.TrimEnd('\') + '\'
$prefix = "http://127.0.0.1:$port/"
$listener = [System.Net.HttpListener]::new()
$listener.Prefixes.Add($prefix)
try {
$listener.Start()
Write-Host "Serving $root"
Write-Host $prefix
Write-Host 'Press Ctrl+C to stop.'
while ($listener.IsListening) {
$context = $listener.GetContext()
$response = $context.Response
try {
$requestPath = [Uri]::UnescapeDataString($context.Request.Url.AbsolutePath)
$relativePath = $requestPath.TrimStart('/').Replace('/', '\')
$candidate = [IO.Path]::GetFullPath((Join-Path $root $relativePath))
$insideRoot = $candidate.Equals($root, [StringComparison]::OrdinalIgnoreCase) -or
$candidate.StartsWith($rootWithSeparator, [StringComparison]::OrdinalIgnoreCase)
if (-not $insideRoot) {
Send-TextResponse -Response $response -StatusCode 403 -Text 'Forbidden'
continue
}
if (Test-Path -LiteralPath $candidate -PathType Container) {
$indexFile = @('index.html', 'index.htm') |
ForEach-Object { Join-Path $candidate $_ } |
Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
Select-Object -First 1
if ($indexFile) {
$candidate = $indexFile
}
else {
$html = Get-DirectoryListingHtml -Directory $candidate -RequestPath $requestPath
Send-TextResponse -Response $response -StatusCode 200 -Text $html -ContentType 'text/html; charset=utf-8'
continue
}
}
if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
Send-TextResponse -Response $response -StatusCode 404 -Text 'Not found'
continue
}
$fileInfo = Get-Item -LiteralPath $candidate
$response.StatusCode = 200
$response.ContentType = Get-MimeType -Path $candidate
$response.ContentLength64 = $fileInfo.Length
$stream = [IO.File]::OpenRead($candidate)
try {
$stream.CopyTo($response.OutputStream)
}
finally {
$stream.Dispose()
}
}
catch {
if ($response.OutputStream.CanWrite) {
Send-TextResponse -Response $response -StatusCode 500 -Text 'Internal server error'
}
}
finally {
$response.OutputStream.Close()
}
}
}
finally {
if ($listener.IsListening) {
$listener.Stop()
}
$listener.Close()
}
}
function Show-Help {
@'
MS.Open commands
open [path] Open a folder, file, or URL
reveal <path> Show a file in Explorer
copypath [path] Copy a full path to the clipboard
which <command> Explain which command will run
paths Print PATH entries one per row
serve [port] Serve the current folder at 127.0.0.1
ports Show listening TCP ports
killport <port> Stop the process listening on a port
sha256 <file> Calculate a SHA-256 hash
pbcopy [text] Copy arguments or piped input
pbpaste Print clipboard text
PowerShell functions installed separately:
mkcd <folder> Create a directory and enter it
reload Reload the current PowerShell profile
'@
}
try {
switch ($Command.ToLowerInvariant()) {
'open' { Invoke-Open -Args $Arguments }
'reveal' { Invoke-Reveal -Args $Arguments }
'copypath' { Invoke-CopyPath -Args $Arguments }
'which' { Invoke-Which -Args $Arguments }
'paths' { Invoke-Paths }
'serve' { Invoke-Serve -Args $Arguments }
'ports' { Invoke-Ports }
'killport' { Invoke-KillPort -Args $Arguments }
'sha256' { Invoke-Sha256 -Args $Arguments }
'pbcopy' { Invoke-Pbcopy -Args $Arguments }
'pbpaste' { Invoke-Pbpaste }
'help' { Show-Help }
'--help' { Show-Help }
'-h' { Show-Help }
default { throw "Unknown command: $Command`n`n$(Show-Help)" }
}
}
catch {
Write-Error $_.Exception.Message
exit 1
}