Last active
June 23, 2023 10:26
-
-
Save aadje/a906790b4b111c03acd81d07bc446756 to your computer and use it in GitHub Desktop.
Have fun editing your local hostsfile
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#Requires -PSEdition Core | |
#Requires -Version 7 | |
function hf { | |
<# | |
.SYNOPSIS | |
Update your local hostsfile | |
.DESCRIPTION | |
Have Fun editing your local hostsfile in High Frequency using minimal keystrokes | |
.NOTES | |
This script needs to run as Administrator to be able to write the hostsfile. | |
This script works on Windows, Linux and Osx (untested). | |
Import this Module or copy the function into your Powershell Core profile. | |
code $PROFILE.CurrentUserAllHosts # Edit your Powershell Profile in VsCode | |
ipmo .\hf.psm1 -Force # Import the module | |
It might be worth to clean up the default comments added by your OS in your hostsfile when using this script. | |
As 'hf' outputs the raw hostsfile content. Your hostsfile can be opened in a text editor using 'hf code' or 'hf open' commands. | |
This single Powershell cmdlet script has multiple commands which are specified in the first positional parameter. | |
These commands can have a parameters value in the second positional string array parameter, which is a basic regex filter for most commands. | |
This 'hf <command> <values> -param' syntax is faster than the overly verbose classic powershell <verb>-<noun> syntax. | |
.PARAMETER Command | |
show / cat -> Output the raw hostsfile content (default command) | |
list -> Parse hostsfile en list entries including links in a table | |
set-localhost / localhost / local -> Update hostsfile entries to 127.0.0.1 | |
set-cluster / cluster / set -> Update hostsfile entries to the value of the -Ip param | |
set-public / public / disable / comment -> Add a # in front of the hostsfile entries | |
enable / uncomment -> Remove the # in front of the hostsfile entries | |
add -> Comma separated array of domains for which to create new hostsfile entries | |
remove / rm -> Remove entries matching a value | |
sort -> Sort hostsfile alphabetically | |
code -> Open hostsfile in VsCode | |
open / start -> Open hostsfile in you default associated editor | |
help -> Output this structured help content | |
.PARAMETER Value | |
A Regex which matches hostnames or ip's to operate on. | |
Or a comma separated list of (sub)domains for the add command. | |
.PARAMETER Ip | |
The ip to use when updating entries. Can have a default value by setting a HOSTSFILE_DEFAULT_NEW_IP environment variable. This can be a local k8s cluster ingress ip. | |
.PARAMETER DefaultFilter | |
Pre filter the hostsfile entries to operate on | |
.PARAMETER DefaultDomain | |
The default domain which will be appended when adding new entries. This allow for only specifying subdomain. | |
.PARAMETER Padding | |
The indentation to use for the updated hostsfile entries | |
.PARAMETER Dry | |
Output the hostsfile without saving | |
.PARAMETER Force | |
Skip confirmations | |
.EXAMPLE | |
PS> hf | |
Shows the raw hostsfile. Same command as show and cat | |
.EXAMPLE | |
PS> hf disable . | |
Provide a . as the value to update all hostsfile entries | |
.EXAMPLE | |
PS> hf disable | |
Running a command without a value will trigger a prompt | |
.EXAMPLE | |
PS> hf disable myservice | |
Filter entries matching *myservice* | |
.EXAMPLE | |
PS> hf local . -f -d | |
Update all entries to localhost without confirmation and output without saving | |
.EXAMPLE | |
PS> hf add test1, test2 | |
Add multiple subdomains concatenated by the DefaultDomain | |
.EXAMPLE | |
PS> hf add test1.domain.com -ip 183.34.34.34 | |
Add a subdomain with an explicit domain and an explicit ip | |
.EXAMPLE | |
PS> hf rm test1.d | |
Remove all entries matching test1.d | |
.EXAMPLE | |
PS> sudo pwsh | |
Open a nested pwsh terminal in which hf can save the hostsfile. | |
Requires the sudo cli to be installed on windows. | |
.LINK | |
Original source at https://gist.github.com/aadje/a906790b4b111c03acd81d07bc446756 | |
#> | |
[CmdLetBinding()] | |
param ( | |
[ValidateSet('show','cat', | |
'list', | |
'set-localhost','localhost','local', | |
'set-cluster','cluster','set', | |
'set-public','public','disable','comment', | |
'enable','uncomment', | |
'add', | |
'remove','rm', | |
'sort', | |
'code', | |
'open','start', | |
'help')] | |
[string] $Command = 'show', | |
[string[]] $Value, | |
[string] $Ip = $env:HOSTSFILE_DEFAULT_NEW_IP ?? '172.18.23.96', | |
[string] $DefaultFilter = $env:HOSTSFILE_DEFAULT_FILTER ?? 'local|zone', | |
[string] $DefaultDomain = $env:HOSTSFILE_DEFAULT_NEW_DOMAIN ?? 'k8slocal.com', | |
[int] $Padding = 15, | |
[Alias("f")] | |
[switch] $Force, | |
[Alias("d")] | |
[switch] $Dry | |
) | |
if($Command -in 'cat') { $Command = 'show' } | |
if($Command -in 'set-localhost', 'localhost', 'local') { $Command = 'set'; $Ip = [System.Net.IPAddress]::Loopback.IPAddressToString } | |
if($Command -in 'set-cluster', 'cluster') { $Command = 'set' } | |
if($Command -in 'set-public', 'public', 'disable') { $Command = 'comment' } | |
if($Command -in 'enable') { $Command = 'uncomment' } | |
if($Command -in 'remove', 'rm') { $Command = 'remove' } | |
if($IsWindows){ | |
$hostsFilePath = Join-Path $Env:WinDir '\system32\Drivers\etc\hosts' | |
if($Command -in 'open', 'start') { $Command = 'start' } | |
} elseif($IsLinux) { | |
$hostsFilePath = '/etc/hosts' | |
if($Command -in 'open', 'start') { $Command = 'open' } | |
} elseif($IsMacOS) { | |
$hostsFilePath = '/private/etc/hosts' | |
if($Command -in 'open', 'start') { $Command = 'open' } | |
} else{ | |
throw [NotImplementedException] "OS $([System.Environment]::OSVersion)" | |
} | |
function ParseHostsFile($match = $DefaultFilter) { | |
$hostsFileContent = Get-Content $hostsFilePath | |
$entries = @() | |
[regex]$lineRegex = "(?<IP>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+(?<HOSTNAME>\S+)" | |
for ($i = 0; $i -lt $hostsFileContent.Count; $i++) { | |
$line = $hostsFileContent[$i] | |
$matched = $lineRegex.Match($line) | |
if($matched.Success){ | |
$entries += [PSCustomObject]@{ | |
Entry = $line | |
Ip = $matched.Groups['IP'].Value | |
Hostname = $matched.Groups['HOSTNAME'].Value | |
Enabled = !$line.StartsWith('#') | |
Index = $i | |
} | |
} | |
} | |
return $entries -match $match | |
} | |
function SaveHostsFile($fileContent){ | |
if($Dry){ | |
Write-Host ($"Not writing content to $hostsFilePath in DryRun: `n") | |
$fileContent | |
} else { | |
if((($IsLinux -or $IsMacOS) -and $env:USER -ne 'root') -or ($IsWindows -and -not | |
([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) { | |
if($Force){ | |
Write-Host "Force write to $hostsFilePath" | |
} else { | |
throw "Script is not running as administrator and didn't write to $hostsFilePath" | |
} | |
} | |
$fileContent | Set-Content $hostsFilePath | |
} | |
} | |
function ShowEntries() { | |
ParseHostsFile | Select Entry, @{Name="Link" ;Expression={ "https://$($_.Hostname)"}} | Format-Table | |
} | |
function FilterEntries($entries) { | |
if($Value){ | |
Write-host "Hosts entries matching '$Value'" | |
} else { | |
$entries | Format-Table Entry | |
$Value = Read-Host -Prompt "Provide filter for entries to update" | |
} | |
$entries | where { $_.Entry -match $Value } | foreach { | |
$_ | Add-Member -MemberType NoteProperty -Name "Update" -Value $true | |
} | |
$updateEntries = $entries | where Update | |
if(-not $updateEntries){ | |
return "No entries matching '$filter'" | |
} | |
} | |
function ConfirmEntries($entries) { | |
$count = ($entries | where Update).Length | |
if($count -eq 0){ | |
"`nNo hostsfile changes needed" | |
Break Script | |
} | |
switch ($Command) { | |
'add' { | |
$message = "Add these $count hosts entries to $hostsFilePath ?" | |
} | |
'remove' { | |
$message = "Delete these $count hosts entries in $hostsFilePath ?" | |
} | |
default { | |
$message = "Update these $count hosts entries in $hostsFilePath ?" | |
} | |
} | |
if (-not $Force -and -not ($PSCmdlet.ShouldContinue($message, "Confirm"))) { | |
break | |
} | |
} | |
function UpdateEntries($entries) { | |
$entries | where Update | foreach { | |
$entry = $_ | |
switch ($Command) { | |
'set' { | |
$newEntry = "$($Ip.PadRight($Padding)) $($entry.Hostname)" | |
} | |
'uncomment' { | |
$newEntry = "$($entry.Ip.PadRight($Padding)) $($entry.Hostname)" | |
} | |
'comment' { | |
$newEntry = "# $($entry.Ip.PadRight($Padding)) $($entry.Hostname)" | |
} | |
'remove' { | |
$entry | Add-Member -MemberType NoteProperty -Name "Remove" -Value $true | |
} | |
} | |
$entry | Add-Member -MemberType NoteProperty -Name "NewEntry" -Value $newEntry | |
} | |
} | |
function AddEntries($entries) { | |
if(-not $Value){ | |
$Value = Read-Host -Prompt "Provide comma separated domain names for $IP" | |
} | |
$newEntries = @() | |
$Value | foreach { | |
if(-not $DefaultDomain -or $_ -contains '.'){ | |
$newDomain = $_ | |
} else { | |
$newDomain = "$_.$DefaultDomain" | |
} | |
$existingEntries = $entries | where { $newDomain -eq $_.Hostname} | |
if($existingEntries){ | |
Write-Host "Skip existing entry $($existingEntries.Ip) $newDomain" | |
if($existingEntries.Ip -ne $Ip){ | |
throw "Existing $($existingEntries.Ip) and $Ip ip's for $newDomain do not match" | |
} | |
} else { | |
$newEntries += [PSCustomObject]@{ | |
NewEntry = "$($Ip.PadRight($Padding)) $newDomain" | |
Update = $true | |
} | |
} | |
} | |
return $entries += $newEntries | |
} | |
function WriteEntries($entries) { | |
$hostsFileContent = Get-Content $hostsFilePath | |
$entries | where Update | foreach { | |
if($_.Index -eq $null){ | |
$hostsFileContent += $_.NewEntry | |
} else { | |
$hostsFileContent[$_.Index] = $_.NewEntry | |
} | |
} | |
$lines = [System.Collections.ArrayList]$hostsFileContent | |
$entries | where Remove | foreach { | |
$hostsFileContent | where { $_ -eq $entry.Entry } | foreach { | |
$lines.Remove($_) | |
} | |
} | |
SaveHostsFile($lines) | |
} | |
function SortEntries($entries) { | |
$hostsFileContent = Get-Content $hostsFilePath | |
$lines = [System.Collections.ArrayList]$hostsFileContent | |
$entries | sort Hostname | foreach { | |
$entry = $_.Entry | |
$hostsFileContent | where { $_ -eq $entry } | foreach { | |
$lines.Remove($_) | |
$lines.Add($entry) | Out-Null | |
} | |
} | |
Write-Host ("Sorted $hostsFilePath content: `n") | |
$lines | |
if (-not $Force -and -not ($PSCmdlet.ShouldContinue("Save sorted hostsfile?`n", "Confirm"))) { | |
break | |
} else{ | |
SaveHostsFile($lines) | |
} | |
} | |
switch ($Command) { | |
'list' { | |
ShowEntries | |
} | |
'show' { | |
Get-Content $hostsFilePath | |
} | |
({$Command -in 'set','comment','uncomment','remove'}) { | |
$entries = ParseHostsFile | |
FilterEntries $entries | |
UpdateEntries $entries | |
$entries | where Update | Format-Table Entry, NewEntry | |
ConfirmEntries $entries | |
WriteEntries $entries | |
ShowEntries | |
} | |
'add' { | |
$entries = ParseHostsFile | |
$entries | Format-Table Entry | |
$entries = AddEntries $entries | |
$entries | where { $_.Update } | Format-Table NewEntry | |
ConfirmEntries $entries | |
WriteEntries $entries | |
ShowEntries | |
} | |
'sort' { | |
$entries = ParseHostsFile | |
SortEntries $entries | |
ShowEntries | |
} | |
'code' { | |
code $hostsFilePath | |
} | |
'start' { | |
start $hostsFilePath | |
} | |
'open' { | |
open $hostsFilePath | |
} | |
'help' { | |
Get-Help hf -Full | |
} | |
default { throw [NotImplementedException] "Todo $Command" } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
sudo pwsh -NoProfile -c "ipmo .\hf.psm1; hf $@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment