Skip to content

Instantly share code, notes, and snippets.

@aadje
Last active June 23, 2023 10:26
Show Gist options
  • Save aadje/a906790b4b111c03acd81d07bc446756 to your computer and use it in GitHub Desktop.
Save aadje/a906790b4b111c03acd81d07bc446756 to your computer and use it in GitHub Desktop.
Have fun editing your local hostsfile
#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" }
}
}
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