Last active 1748532636

Setup-CoreutilsLinks.ps1 Raw
<#
.SYNOPSIS
Automates the setup of uutils/coreutils on Windows by creating batch file shims
for its commands and assisting with PATH configuration.
.DESCRIPTION
This script attempts to locate an existing uutils/coreutils installation.
If not found, it can optionally install it via winget.
It then dynamically fetches the list of available utilities from coreutils.exe
and creates batch file shims (e.g., ls.bat, cat.bat) in a specified directory.
These shims call the main coreutils.exe with the appropriate command.
Finally, it can help add this shims directory to the User's PATH environment variable.
.PARAMETER ShimsDirectory
The directory where the batch file shims for coreutils commands will be created.
Defaults to "$env:LOCALAPPDATA\uutils-shims".
.PARAMETER ForceInstall
If specified, and coreutils is not found, the script will attempt to install
it via winget without prompting for confirmation.
.PARAMETER ForcePathAddition
If specified, and the ShimsDirectory is not in the PATH, the script will
attempt to add it to the User PATH without prompting for confirmation.
.PARAMETER SkipPathCheck
If specified, the script will not check or offer to modify the PATH environment variable.
.EXAMPLE
.\Setup-CoreutilsShims.ps1
Runs the script with default settings and interactive prompts.
.EXAMPLE
.\Setup-CoreutilsShims.ps1 -ShimsDirectory "C:\myCLItools" -ForceInstall -ForcePathAddition
Runs the script, creates shims in "C:\myCLItools", and attempts to install
coreutils and add the directory to PATH without prompting.
.EXAMPLE
.\Setup-CoreutilsShims.ps1 -Verbose
Runs the script with detailed verbose output.
.NOTES
- Changes to the PATH environment variable typically require restarting terminal
sessions or logging out and back in to take effect.
- If you want coreutils commands (like 'dir', 'sort') to override built-in
Windows commands, the ShimsDirectory may need to be placed *before*
C:\Windows\System32 in your PATH environment variable. This script adds
it to the end of the User PATH by default if modification is chosen.
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$ShimsDirectory = (Join-Path $env:LOCALAPPDATA "uutils-shims"),
[switch]$ForceInstall,
[switch]$ForcePathAddition,
[switch]$SkipPathCheck
)
# Script-level error action preference
$ErrorActionPreference = "Stop" # Stop on terminating errors
# --- Helper Functions ---
function Show-Menu ([string]$Title, [string]$PromptMessage) {
Write-Host "`n--- $Title ---"
$options = @(
[System.Management.Automation.Host.ChoiceDescription]::new("&Yes", "Proceed with this action.")
[System.Management.Automation.Host.ChoiceDescription]::new("&No", "Skip this action.")
)
$decision = $host.UI.PromptForChoice($Title, $PromptMessage, $options, 0)
return ($decision -eq 0) # 0 is Yes
}
# --- Core Logic Functions ---
function Find-CoreutilsExecutable {
Write-Verbose "Attempting to find coreutils.exe..."
# 1. Try Get-Command (if already in PATH or an alias exists)
Write-Verbose "Checking PATH for coreutils..."
$coreutilsCmd = Get-Command coreutils -ErrorAction SilentlyContinue
if ($coreutilsCmd -and $coreutilsCmd.Source -and (Test-Path $coreutilsCmd.Source -PathType Leaf)) {
Write-Host "Found coreutils via Get-Command at: $($coreutilsCmd.Source)" -ForegroundColor Green
return $coreutilsCmd.Source
}
# 2. Try known winget installation path pattern
Write-Verbose "Checking common winget installation paths..."
$wingetPackagesBase = Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Packages"
$coreutilsPackageDirPattern = "uutils.coreutils_Microsoft.Winget.Source_8wekyb3d8bbwe" # Common package identifier
if (Test-Path $wingetPackagesBase) {
$packageInstallDir = Get-ChildItem -Path $wingetPackagesBase -Directory -Filter $coreutilsPackageDirPattern -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($packageInstallDir) {
Write-Verbose "Found potential coreutils package directory: $($packageInstallDir.FullName)"
# Find the latest versioned subdirectory (often contains version string or arch)
$latestVersionDir = Get-ChildItem -Path $packageInstallDir.FullName -Directory -ErrorAction SilentlyContinue |
Sort-Object -Property Name -Descending | # Sort by name, hoping newer versions list first
Select-Object -First 1
if ($latestVersionDir) {
$potentialPath = Join-Path $latestVersionDir.FullName "coreutils.exe"
Write-Verbose "Checking for coreutils.exe in: $potentialPath"
if (Test-Path $potentialPath -PathType Leaf) {
Write-Host "Found coreutils in winget package directory: $potentialPath" -ForegroundColor Green
return $potentialPath
}
} else {
Write-Verbose "No version subdirectories found under $($packageInstallDir.FullName)"
}
} else {
Write-Verbose "Coreutils package directory pattern not found under $wingetPackagesBase"
}
} else {
Write-Verbose "Winget packages base directory not found: $wingetPackagesBase"
}
# 3. Not found, offer to install via winget
Write-Warning "coreutils.exe not found on this system."
$shouldInstall = $false
if ($ForceInstall) {
$shouldInstall = $true
} else {
if ($PSCmdlet.ShouldProcess("Install uutils/coreutils via winget", "Coreutils not found. Do you want to attempt installation?")) {
$shouldInstall = Show-Menu -Title "Install Coreutils" -PromptMessage "coreutils.exe was not found. Would you like to attempt to install it using winget? (This will run 'winget install uutils.coreutils --source winget -e --accept-package-agreements --accept-source-agreements')"
}
}
if ($shouldInstall) {
Write-Host "Attempting to install uutils/coreutils via winget..."
$wingetCmd = Get-Command winget -ErrorAction SilentlyContinue
if (-not $wingetCmd) {
Write-Error "winget command not found. Cannot install coreutils automatically."
return $null
}
try {
$installArgs = "install uutils.coreutils --source winget -e --accept-package-agreements --accept-source-agreements"
Write-Verbose "Running: winget $installArgs"
$process = Start-Process winget -ArgumentList $installArgs -Wait -PassThru -WindowStyle Minimized
if ($process.ExitCode -eq 0) {
Write-Host "coreutils installed successfully via winget." -ForegroundColor Green
# Re-attempt find after installation
Write-Verbose "Re-checking for coreutils.exe after installation..."
$coreutilsCmdAfterInstall = Get-Command coreutils -ErrorAction SilentlyContinue
if ($coreutilsCmdAfterInstall -and $coreutilsCmdAfterInstall.Source -and (Test-Path $coreutilsCmdAfterInstall.Source -PathType Leaf)) {
Write-Host "Found coreutils after install at: $($coreutilsCmdAfterInstall.Source)" -ForegroundColor Green
return $coreutilsCmdAfterInstall.Source
}
# If Get-Command doesn't find it immediately, try the winget path again
if (Test-Path $wingetPackagesBase) {
$packageInstallDir = Get-ChildItem -Path $wingetPackagesBase -Directory -Filter $coreutilsPackageDirPattern -ErrorAction SilentlyContinue | Select-Object -First 1
if ($packageInstallDir) {
$latestVersionDir = Get-ChildItem -Path $packageInstallDir.FullName -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1
if ($latestVersionDir) {
$potentialPath = Join-Path $latestVersionDir.FullName "coreutils.exe"
if (Test-Path $potentialPath -PathType Leaf) {
Write-Host "Found coreutils in winget package directory after install: $potentialPath" -ForegroundColor Green
return $potentialPath
}
}
}
}
Write-Warning "coreutils installed, but could not immediately locate coreutils.exe. You might need to restart your terminal or find it manually."
return $null
} else {
Write-Error "winget installation failed with exit code $($process.ExitCode)."
return $null
}
} catch {
Write-Error "An error occurred during winget installation: $($_.Exception.Message)"
return $null
}
}
Write-Error "coreutils.exe could not be found or installed."
return $null
}
function Get-CoreutilsCommands ([string]$CoreutilsExePath) {
Write-Verbose "Attempting to get command list from '$CoreutilsExePath --list'..."
$Commands = $null
try {
$processInfo = New-Object System.Diagnostics.ProcessStartInfo
$processInfo.FileName = $CoreutilsExePath
$processInfo.Arguments = "--list"
$processInfo.RedirectStandardOutput = $true
$processInfo.RedirectStandardError = $true
$processInfo.UseShellExecute = $false
$processInfo.CreateNoWindow = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $processInfo
$process.Start() | Out-Null
$output = $process.StandardOutput.ReadToEnd()
$errors = $process.StandardError.ReadToEnd()
$process.WaitForExit()
$exitCode = $process.ExitCode
if ($exitCode -ne 0 -or [string]::IsNullOrWhiteSpace($output)) {
Write-Warning "Command '$CoreutilsExePath --list' failed or returned empty stdout."
Write-Warning "Exit code: $exitCode"
if (-not [string]::IsNullOrWhiteSpace($errors)) {
Write-Warning "Error output from coreutils --list:`n$errors"
}
} else {
Write-Verbose "Raw output from coreutils --list:`n$output"
$CommandLines = $output.Split([System.Environment]::NewLine)
$ProcessingCommands = $false
$CombinedCommandLines = ""
if ($output -match "Currently defined functions:") {
Write-Verbose "Parsing based on 'Currently defined functions:' marker."
foreach ($Line in $CommandLines) {
if ($ProcessingCommands) {
$TrimmedLine = $Line.Trim()
if (-not [string]::IsNullOrWhiteSpace($TrimmedLine)) {
$CombinedCommandLines += $TrimmedLine
}
}
if ($Line -match "Currently defined functions:") {
$ProcessingCommands = $true
}
}
if (-not [string]::IsNullOrWhiteSpace($CombinedCommandLines)) {
$Commands = $CombinedCommandLines -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
}
} else {
Write-Verbose "Attempting to parse as one command per line (fallback)."
$Commands = $CommandLines | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and $_ -notmatch "^Usage:" -and $_ -notmatch "^Options:" -and $_ -notmatch "^Currently defined functions:" -and $_ -notmatch "multi-call binary" }
}
if ($Commands -and $Commands.Length -gt 0) {
Write-Host "Successfully parsed $($Commands.Length) commands dynamically from coreutils --list." -ForegroundColor Green
} else {
Write-Warning "Dynamically parsed 0 commands. The output format might have changed or was unexpected."
}
}
} catch {
Write-Warning "An error occurred while trying to execute or parse '$CoreutilsExePath --list': $($_.Exception.Message)"
}
if (-not $Commands -or $Commands.Length -eq 0) {
Write-Warning "Dynamic command fetching failed or yielded no commands. Using a predefined fallback list."
$Commands = @(
"arch", "b2sum", "b3sum", "base32", "base64", "basename", "basenc", "cat", "cksum", "comm", "cp",
"csplit", "cut", "date", "dd", "df", "dir", "dircolors", "dirname", "du", "echo", "env",
"expand", "expr", "factor", "false", "fmt", "fold", "hashsum", "head", "hostname", "join",
"link", "ln", "ls", "md5sum", "mkdir", "mktemp", "more", "mv", "nl", "nproc", "numfmt",
"od", "paste", "pr", "printenv", "printf", "ptx", "pwd", "readlink", "realpath", "rm",
"rmdir", "seq", "sha1sum", "sha224sum", "sha256sum", "sha3-224sum", "sha3-256sum",
"sha3-384sum", "sha3-512sum", "sha384sum", "sha3sum", "sha512sum", "shake128sum",
"shake256sum", "shred", "shuf", "sleep", "sort", "split", "sum", "sync", "tac", "tail",
"tee", "test", "touch", "tr", "true", "truncate", "tsort", "uname", "unexpand", "uniq",
"unlink", "vdir", "wc", "whoami", "yes", "["
)
Write-Host "Using fallback list with $($Commands.Length) commands."
}
return $Commands
}
function Create-UtilityShims ([string]$CoreutilsExePath, [array]$Commands, [string]$TargetShimsDirectory) {
Write-Host "`n--- Creating Utility Batch File Shims ---"
Write-Host "Coreutils Executable: $CoreutilsExePath"
Write-Host "Shims Directory: $TargetShimsDirectory"
if (-not (Test-Path $TargetShimsDirectory)) {
Write-Verbose "Shims directory '$TargetShimsDirectory' does not exist. Attempting to create it."
if ($PSCmdlet.ShouldProcess("Create directory '$TargetShimsDirectory'")) {
try {
New-Item -ItemType Directory -Path $TargetShimsDirectory -Force | Out-Null
Write-Host "Created directory '$TargetShimsDirectory'" -ForegroundColor Green
} catch {
Write-Error "Failed to create directory '$TargetShimsDirectory': $($_.Exception.Message)"
return
}
} else {
Write-Warning "Directory creation skipped by user. Cannot create shims."
return
}
}
Write-Host "Creating batch file shims..."
$CreatedCount = 0
$SkippedCount = 0
$FailedCount = 0
foreach ($CommandNameInList in $Commands) {
$ShimmedCommandName = $CommandNameInList # This is the command to pass to coreutils.exe
$BatchFileName = "$CommandNameInList.bat"
if ($CommandNameInList -eq "[") {
$BatchFileName = "bracket.bat" # Use a valid filename for the '[' command
# $ShimmedCommandName is already "[" which is correct
}
$BatchPath = Join-Path $TargetShimsDirectory $BatchFileName
if (Test-Path $BatchPath) {
Write-Verbose "Skipping: Batch file '$BatchPath' already exists."
$SkippedCount++
continue
}
# Content of the batch file: @echo off, then "path/to/coreutils.exe" command %*
# Ensure $CoreutilsExePath is quoted in case it has spaces.
# $ShimmedCommandName is the command name (e.g., "ls", "uname", "[")
$BatchContent = "@echo off`r`n`"$CoreutilsExePath`" $ShimmedCommandName %*"
if ($PSCmdlet.ShouldProcess("Create batch file shim '$BatchPath' for command '$ShimmedCommandName'")) {
try {
Set-Content -Path $BatchPath -Value $BatchContent -Encoding Ascii -Force
Write-Verbose "Successfully created batch file: $BatchPath"
$CreatedCount++
} catch {
Write-Warning "Failed to create batch file '$BatchPath': $($_.Exception.Message)"
$FailedCount++
}
} else {
Write-Verbose "Skipped creating batch file '$BatchPath' due to ShouldProcess."
$SkippedCount++
}
}
Write-Host "Shim creation summary: $CreatedCount created, $SkippedCount skipped, $FailedCount failed." -ForegroundColor Cyan
}
function Manage-PathEnvironmentVariable ([string]$DirectoryToAdd) {
Write-Host "`n--- PATH Environment Variable Check ---"
$currentUserPath = [System.Environment]::GetEnvironmentVariable("Path", "User")
$pathParts = $currentUserPath -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
if ($pathParts -contains $DirectoryToAdd) {
Write-Host "Directory '$DirectoryToAdd' is already in your User PATH." -ForegroundColor Green
return
}
Write-Warning "Directory '$DirectoryToAdd' is NOT in your User PATH."
$shouldAddToPath = $false
if ($ForcePathAddition) {
$shouldAddToPath = $true
} else {
if ($PSCmdlet.ShouldProcess("Add '$DirectoryToAdd' to User PATH", "This will make the commands available in new terminal sessions.")) {
$shouldAddToPath = Show-Menu -Title "Update User PATH" -PromptMessage "Would you like to add '$DirectoryToAdd' to your User PATH environment variable?"
}
}
if ($shouldAddToPath) {
if ($PSCmdlet.ShouldProcess("Set User PATH = '$currentUserPath;$DirectoryToAdd'")) {
try {
$newPath = ($pathParts + $DirectoryToAdd) -join ';'
[System.Environment]::SetEnvironmentVariable("Path", $newPath, "User")
Write-Host "Successfully added '$DirectoryToAdd' to your User PATH." -ForegroundColor Green
Write-Host "IMPORTANT: You need to RESTART your terminal (PowerShell, Command Prompt, etc.) or log out and back in for this change to take full effect." -ForegroundColor Yellow
Write-Host "Note: For coreutils commands like 'dir' or 'sort' to override built-in Windows commands, '$DirectoryToAdd' may need to be placed *before* C:\Windows\System32 in your PATH. This script added it to the end of your User PATH." -ForegroundColor Yellow
} catch {
Write-Error "Failed to add '$DirectoryToAdd' to User PATH: $($_.Exception.Message)"
}
} else {
Write-Warning "PATH modification skipped by user (ShouldProcess)."
}
} else {
Write-Host "Skipped adding '$DirectoryToAdd' to User PATH."
}
}
# --- Main Script Execution ---
Write-Host "Starting uutils/coreutils Batch Shim Setup Script..." -ForegroundColor Magenta
# Step 1: Find Coreutils Executable
$coreutilsExe = Find-CoreutilsExecutable
if (-not $coreutilsExe) {
Write-Error "Could not locate or install coreutils.exe. Script cannot continue."
exit 1
}
Write-Host "Using coreutils at: $coreutilsExe" -ForegroundColor Cyan
# Step 2: Get Command List
$commandsToShim = Get-CoreutilsCommands -CoreutilsExePath $coreutilsExe
if (-not $commandsToShim -or $commandsToShim.Length -eq 0) {
Write-Error "Could not determine the list of coreutils commands. Script cannot continue."
exit 1
}
# Step 3: Create Utility Shims
Create-UtilityShims -CoreutilsExePath $coreutilsExe -Commands $commandsToShim -TargetShimsDirectory $ShimsDirectory
# Step 4: Manage PATH (unless skipped)
if (-not $SkipPathCheck) {
Manage-PathEnvironmentVariable -DirectoryToAdd $ShimsDirectory
} else {
Write-Host "`nSkipping PATH environment variable check as per -SkipPathCheck parameter."
}
Write-Host "`n--- Script Finished ---" -ForegroundColor Magenta
Write-Host "If PATH was modified, remember to restart your terminal sessions."
Write-Host "You can now try running commands like 'ls', 'cat', 'uname', 'bracket ... ]', etc."