<# .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."