param( [Parameter(Position = 0)] [string]$Ip, [Parameter(Position = 1, ValueFromRemainingArguments = $true)] [string[]]$HostName, [string]$Path = "$env:SystemRoot\System32\drivers\etc\hosts", [switch]$DryRun, [switch]$Check, [switch]$List, [switch]$Remove, [switch]$Help ) $ErrorActionPreference = 'Stop' $ScriptName = Split-Path -Leaf $PSCommandPath if ($Help) { @' Usage: hosts-add.ps1 [-Path PATH] -List hosts-add.ps1 [-Path PATH] [-DryRun] [-Check] [-Remove] IP HOSTNAME... Manage hosts entries with duplicate checks and timestamped backups. '@ exit 0 } function Test-Admin { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Get-HostsLines { param([string]$File) if (Test-Path -LiteralPath $File) { return @(Get-Content -LiteralPath $File) } return @() } if ($List) { if ($Ip -or $HostName) { throw '-List does not take IP or hostname arguments.' } Get-HostsLines -File $Path | Where-Object { $_ -match '\S' -and $_ -notmatch '^\s*#' } exit 0 } if (-not $Ip -or -not $HostName -or $HostName.Count -eq 0) { throw 'IP and at least one hostname are required.' } if ($Ip -notmatch '^[0-9A-Fa-f:.]+$') { throw "Invalid IP address format: $Ip" } foreach ($hostEntry in $HostName) { if ($hostEntry -notmatch '^[A-Za-z0-9._-]+$') { throw "Invalid hostname: $hostEntry" } } if ($Check -and $Remove) { throw '-Check and -Remove cannot be used together.' } $lines = Get-HostsLines -File $Path if ($Check) { $missing = $false foreach ($hostEntry in $HostName) { $found = $false foreach ($line in $lines) { if ($line -match '^\s*#' -or $line -notmatch '\S') { continue } $parts = $line -split '\s+' if ($parts.Count -ge 2 -and $parts[0] -eq $Ip -and $parts[1..($parts.Count - 1)] -contains $hostEntry) { $found = $true break } } if ($found) { Write-Host "[$ScriptName] OK $hostEntry -> $Ip" } else { Write-Host "[$ScriptName] MISSING $hostEntry -> $Ip" $missing = $true } } if ($missing) { exit 1 } exit 0 } $target = New-Object System.Collections.Generic.List[string] foreach ($line in $lines) { if ($line -match '^\s*#' -or $line -notmatch '\S') { $target.Add($line) continue } $parts = @($line -split '\s+' | Where-Object { $_ }) if ($parts.Count -lt 2) { $target.Add($line) continue } $keptHosts = @($parts[1..($parts.Count - 1)] | Where-Object { $HostName -notcontains $_ }) if ($keptHosts.Count -gt 0) { $target.Add(($parts[0] + ' ' + ($keptHosts -join ' '))) } } if (-not $Remove) { $missingHosts = @() foreach ($hostEntry in $HostName) { $exists = $false foreach ($line in $target) { if ($line -match '^\s*#' -or $line -notmatch '\S') { continue } $parts = @($line -split '\s+' | Where-Object { $_ }) if ($parts.Count -ge 2 -and $parts[1..($parts.Count - 1)] -contains $hostEntry) { $exists = $true break } } if (-not $exists) { $missingHosts += $hostEntry } } if ($missingHosts.Count -gt 0) { $target.Add("$Ip $($missingHosts -join ' ')") } } $currentText = ($lines -join "`n") $targetText = ($target.ToArray() -join "`n") if ($currentText -eq $targetText) { Write-Host "[$ScriptName] Hosts file is already current: $Path" exit 0 } if ($DryRun) { Write-Host "[$ScriptName] Would update: $Path" $target.ToArray() exit 0 } $defaultHosts = "$env:SystemRoot\System32\drivers\etc\hosts" if (([IO.Path]::GetFullPath($Path) -eq [IO.Path]::GetFullPath($defaultHosts)) -and -not (Test-Admin)) { throw 'Administrator privileges are required to edit the system hosts file.' } if (Test-Path -LiteralPath $Path) { Copy-Item -LiteralPath $Path -Destination "$Path.bak.$(Get-Date -Format yyyyMMddHHmmss)" -Force } else { New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Path) | Out-Null } Set-Content -LiteralPath $Path -Value $target.ToArray() -Encoding ASCII Write-Host "[$ScriptName] Updated: $Path"