#!/usr/bin/env bash # # __ __ # / /_ ____ _____/ /______ # / __ \/ __ \/ ___/ __/ ___/ # / / / / /_/ (__ ) /_(__ ) # /_/ /_/\____/____/\__/____/ # # A program for managing host file entries. # # https://github.com/xwmx/hosts # # Based on Bash Boilerplate: https://github.com/xwmx/bash-boilerplate # # Updates copyright (c) 2015 William Melody • hi@williammelody.com ############################################################################### # Strict Mode ############################################################################### set -o nounset set -o errexit set -o pipefail set -o noglob IFS=$'\n\t' ############################################################################### # Globals ############################################################################### # $_VERSION # # Manually set this to to current version of the program. Adhere to the # semantic versioning specification: http://semver.org _VERSION="3.3.0" # $HOSTS_DEFAULT_COMMAND # # The command to be run by default, when no command name is specified. If the # environment has an existing `$HOSTS_DEFAULT_COMMAND` set, then that value is # used. HOSTS_DEFAULT_COMMAND="${HOSTS_DEFAULT_COMMAND:-list}" # $HOSTS_PATH # # The path to the hosts file. This will almost always be /etc/hosts HOSTS_PATH="${HOSTS_PATH:-/etc/hosts}" # Space and tab for regular expressions # # sed regular expressions have slightly different behaviors dependending on # the environment, and POSIX [[:space:]] matches whitespace characters other # than just space and tab. These variables provide an easier, portable way to # test for just these two characters. export _TAB_=$'\t' export _SPACE_=$' ' export _TAB_SPACE_="${_TAB_}${_SPACE_}" export _TAB_SPACE_CC_="[${_TAB_SPACE_}]" # $_SED_I_COMMAND # # `sed -i` takes an extension on macOS, but that extension can cause errors in # GNU `sed`. # # NOTE: To use this command, call it with `"${_SED_I_COMMAND[@]}"` # # https://stackoverflow.com/q/43171648 # http://stackoverflow.com/a/16746032 if sed --help >/dev/null 2>&1 then # GNU export _SED_I_COMMAND=(sed -i) else # macOS export _SED_I_COMMAND=(sed -i '') fi ############################################################################### # Debug ############################################################################### # _debug() # # Usage: # _debug printf "Debug info. Variable: %s\\n" "$0" # # A simple function for executing a specified command if the `$_USE_DEBUG` # variable has been set. The command is expected to print a message and # should typically be either `echo`, `printf`, or `cat`. __DEBUG_COUNTER=0 _debug() { if [[ "${_USE_DEBUG:-"0"}" -eq 1 ]] then __DEBUG_COUNTER=$((__DEBUG_COUNTER+1)) # Prefix debug message with "bug (U+1F41B)" printf "🐛 %s " "${__DEBUG_COUNTER}" "${@}" printf "――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\\n" fi } # debug() # # Usage: # debug "Debug info. Variable: $0" # # Print the specified message if the `$_USE_DEBUG` variable has been set. # # This is a shortcut for the _debug() function that simply echos the message. debug() { _debug echo "${@}" } ############################################################################### # Die ############################################################################### # _die() # # Usage: # _die printf "Error message. Variable: %s\\n" "$0" # # A simple function for exiting with an error after executing the specified # command. The command is expected to print a message and should typically # be either `echo`, `printf`, or `cat`. _die() { # Prefix die message with "cross mark (U+274C)", often displayed as a red x. printf "❌ " "${@}" 1>&2 exit 1 } # die() # # Usage: # die "Error message. Variable: $0" # # Exit with an error and print the specified message. # # This is a shortcut for the _die() function that simply echos the message. die() { _die echo "${@}" } ############################################################################### # Environment ############################################################################### # $_ME # # Set to the program's basename. _ME=$(basename "${0}") _debug printf "\${_ME}: %s\\n" "${_ME}" ############################################################################### # Load Commands ############################################################################### # Initialize $_DEFINED_COMMANDS array. _DEFINED_COMMANDS=() # _load_commands() # # Usage: # _load_commands # # Loads all of the commands sourced in the environment. _load_commands() { _debug printf "_load_commands(): entering...\\n" _debug printf "_load_commands() declare -F:\\n%s\\n" "$(declare -F)" # declare is a bash built-in shell function that, when called with the '-F' # option, displays all of the functions with the format # `declare -f function_name`. These are then assigned as elements in the # $function_list array. local _function_list _function_list=($(declare -F)) _debug printf \ "_load_commands() \${_function_list[@]}: %s\\n" \ "${_function_list[@]}" for __name in "${_function_list[@]}" do _debug printf \ "_load_commands() \${__name}: %s\\n" \ "${__name}" # Each element has the format `declare -f function_name`, so set the name # to only the 'function_name' part of the string. local _function_name _function_name=$(printf "%s" "${__name}" | awk '{ print $3 }') _debug printf \ "_load_commands() \${_function_name}: %s\\n" \ "${_function_name}" # Add the function name to the $_DEFINED_COMMANDS array unless it starts # with an underscore or is one of the desc(), debug(), or die() functions, # since these are treated as having 'private' visibility. if ! { [[ "${_function_name}" =~ ^_(.*) ]] || \ [[ "${_function_name}" == "desc" ]] || \ [[ "${_function_name}" == "debug" ]] || \ [[ "${_function_name}" == "die" ]] } then _DEFINED_COMMANDS+=("${_function_name}") fi done _debug printf \ "commands() \${_DEFINED_COMMANDS[*]:-}:\\n%s\\n" \ "${_DEFINED_COMMANDS[*]:-}" } ############################################################################### # Main ############################################################################### # _main() # # Usage: # _main # # The primary function for starting the program. # # NOTE: must be called at end of program after all commands have been defined. _main() { _debug printf "main(): entering...\\n" _debug printf "main() \${_CMD} (upon entering): %s\\n" "${_CMD}" if [[ -z "${_CMD}" ]] then _CMD="${HOSTS_DEFAULT_COMMAND}" fi # Load all of the commands. _load_commands # If the command is defined, run it, otherwise return an error. if _contains "${_CMD}" "${_DEFINED_COMMANDS[*]:-}" then # Pass all comment arguments to the program except for the first ($0). "${_CMD}" "${_COMMAND_PARAMETERS[@]:-}" else _die printf "Unknown command: %s\\n" "${_CMD}" fi } ############################################################################### # Utility Functions ############################################################################### # _function_exists() # # Usage: # _function_exists "possible_function_name" # # Returns: # 0 If a function with the given name is defined in the current environment. # 1 If not. # # Other implementations, some with better performance: # http://stackoverflow.com/q/85880 _function_exists() { [ "$(type -t "${1}")" == 'function' ] } # _command_exists() # # Usage: # _command_exists "possible_command_name" # # Returns: # 0 If a command with the given name is defined in the current environment. # 1 If not. # # Information on why `hash` is used here: # http://stackoverflow.com/a/677212 _command_exists() { hash "${1}" 2>/dev/null } # _contains() # # Usage: # _contains "$item" "${list[*]}" # # Returns: # 0 If the item is included in the list. # 1 If not. _contains() { local _test_list=(${*:2}) for __test_element in "${_test_list[@]:-}" do _debug printf "_contains() \${__test_element}: %s\\n" "${__test_element}" if [[ "${__test_element}" == "${1}" ]] then _debug printf "_contains() match: %s\\n" "${1}" return 0 fi done return 1 } # _join() # # Usage: # _join # # Examples: # _join , a "b c" d => a,b c,d # _join / var local tmp => var/local/tmp # _join , "${FOO[@]}" => a,b,c # # More Information: # http://stackoverflow.com/a/17841619 _join() { local IFS="${1}" shift printf "%s\\n" "${*}" } # _command_argv_includes() # # Usage: # _command_argv_includes "an_argument" # # Returns: # 0 If the argument is included in `$_COMMAND_ARGV`, the program's command # argument list. # 1 If not. # # This is a shortcut for simple cases where a command wants to check for the # presence of options quickly without parsing the options again. _command_argv_includes() { _contains "${1}" "${_COMMAND_ARGV[*]}" } # _blank() # # Usage: # _blank "$an_argument" # # Returns: # 0 If the argument is not present or null. # 1 If the argument is present and not null. _blank() { [[ -z "${1:-}" ]] } # _present() # # Usage: # _present "$an_argument" # # Returns: # 0 If the argument is present and not null. # 1 If the argument is not present or null. _present() { [[ -n "${1:-}" ]] } # _interactive_input() # # Usage: # _interactive_input # # Returns: # 0 If the current input is interactive (eg, a shell). # 1 If the current input is stdin / piped input. _interactive_input() { [[ -t 0 ]] } # _piped_input() # # Usage: # _piped_input # # Returns: # 0 If the current input is stdin / piped input. # 1 If the current input is interactive (eg, a shell). _piped_input() { ! _interactive_input } # _print_entries() # # Usage: # _print_entries _print_entries() { local _input="${1:-}" [[ -n "${_input}" ]] || return 0 local _newline=$'\n' if [[ -n "${2:-}" ]] then _input+="${_newline}${2:-}" fi local _disabled=0 local _max_length=0 while IFS=$'\t ' read -r -a _parts do if [[ "${_parts[0]}" =~ disabled ]] then _parts=("${_parts[@]:1}") fi if [[ "${_max_length}" -lt "${#_parts[0]}" ]] then _max_length="${#_parts[0]}" fi done <<< "${_input}" _max_divided="$((${_max_length} / 8))" while IFS=$'\t ' read -r -a _parts do if [[ "${_parts[0]}" =~ disabled ]] then _parts=("${_parts[@]:1}") printf "disabled:\n" fi _current_divided=$((${#_parts[0]} / 8)) _tab_count=$((${_max_divided} - ${_current_divided} + 1)) _tabs="$(printf "%*s" ${_tab_count} | tr " " '\t')" if [[ -n "${_parts[2]:-}" ]] then printf "%s%s %s\t%s\\n" \ "${_parts[0]}" \ "${_tabs}" \ "${_parts[1]}" \ "$(printf "%s" "${_parts[*]:2}" | tr '\r\n' ' ')" else printf "%s%s %s\\n" \ "${_parts[0]}" \ "${_tabs}" \ "${_parts[1]}" fi done <<< "${_input}" } # _verify_write_permissions # # Print a helpful error message when the specified operation can't be # performed due to the lack of write permissions. If `$_AUTO_SUDO` is enabled, # then run the command with sudo and exit with the command's exit value. _verify_write_permissions() { if ! test -w "${HOSTS_PATH}" then if ((_AUTO_SUDO)) then local _my_path _my_path="$(cd "$(dirname "$0")"; pwd)/${_ME}" sudo "${_my_path}" "${_CMD}" "${_COMMAND_PARAMETERS[@]:-}" exit $? else _die printf \ "You don't have permission to perform this operation. Try again with: sudo !!\\n" fi fi } ############################################################################### # desc ############################################################################### # desc() # # Usage: # desc # desc --get # # Options: # --get Print the description for if one has been set. # # Examples: # ``` # desc "list" <. The # text can be passed as the second argument or as standard input. # # To make the text available to other functions, `desc()` assigns # the text to a variable with the format `$___desc_`. # # When the `--get` option is used, the description for is printed, if # one has been set. desc() { _debug printf "desc() \${*}: %s\\n" "$@" [[ -z "${1:-}" ]] && _die printf "desc(): No command name specified.\\n" if [[ "${1}" == "--get" ]] then # get ------------------------------------------------------------------ [[ -z "${2:-}" ]] && _die printf "desc(): No command name specified.\\n" local _name="${2:-}" local _desc_var="___desc_${_name}" if [[ -n "${!_desc_var:-}" ]] then printf "%s\\n" "${!_desc_var}" else printf "No additional information for \`%s\`\\n" "${_name}" fi else # set ------------------------------------------------------------------ if [[ -n "${2:-}" ]] then # argument is present read -r -d '' "___desc_${1}" <] Description: Display help information for ${_ME} or a specified command. HEREDOC help() { if [[ -n "${1:-}" ]] then desc --get "${1}" else cat <] ${_ME} add [] ${_ME} backups [create | [compare | delete | restore | show] ] ${_ME} block ... ${_ME} disable ( | | ) ${_ME} disabled ${_ME} edit ${_ME} enable ( | | ) ${_ME} enabled ${_ME} file ${_ME} list [enabled | disabled | ] ${_ME} search ${_ME} show ( | | ) ${_ME} remove ( | | ) [--force] ${_ME} unblock ... ${_ME} --auto-sudo ${_ME} -h | --help ${_ME} --version Options: --auto-sudo Run write commands with \`sudo\` automatically. -h --help Display this help information. --version Display version information. Help: ${_ME} help [] More Information: https://github.com/xwmx/hosts HEREDOC fi } # Command List ################################################################ desc "commands" < [] Description: Add a given IP address and hostname pair, along with an optional comment. HEREDOC add() { _debug printf "add() \${1}: %s\\n" "${1:-}" _debug printf "add() \${2}: %s\\n" "${2:-}" _debug printf "add() \${3}: %s\\n" "${3:-}" _verify_write_permissions "$@" local _ip="${1:-}" local _hostname="${2:-}" local _comment="${*:3}" local _tabs=$'\t' if [[ -z "${_ip:-}" ]] then help add exit 1 elif [[ -z "${_hostname:-}" ]] then printf "Please include a hostname\\n" help add exit 1 elif grep \ -e "^${_ip}[[:space:]]\+${_hostname}$" \ -e "^${_ip}[[:space:]]\+${_hostname}\\t.*$" "${HOSTS_PATH}" then _die printf \ "Duplicate address/host combination, %s unchanged.\\n" \ "${HOSTS_PATH}" else if [[ "${#_ip}" -lt 8 ]] then _tabs=$'\t\t' fi if [[ -n "${_comment:-}" ]] then local _formatted_comment _formatted_comment=$(_join " " "${_comment[@]}") printf "%s%s%s\\t# %s\\n" \ "${_ip}" \ "${_tabs}" \ "${_hostname}" \ "${_formatted_comment}" >> "${HOSTS_PATH}" printf "Added:\\n%s%s%s\\t# %s\\n" \ "${_ip}" \ "${_tabs}" \ "${_hostname}" \ "${_formatted_comment}" else printf "%s%s%s\\n" \ "${_ip}" \ "${_tabs}" \ "${_hostname}" >> "${HOSTS_PATH}" printf "Added:\\n%s%s%s\\n" \ "${_ip}" \ "${_tabs}" \ "${_hostname}" fi fi } # --------------------------------------------------------------------- backups desc "backups" < ${_ME} backups delete ${_ME} backups restore [--skip-backup] ${_ME} backups show Subcommands: backups List available backups. backups create Create a new backup of the hosts file. backups compare Compare a backup file with the current hosts file. The diff tool configured for git will be used if one is set. backups delete Delete the specified backup. backups restore Replace the contents of the hosts file with a specified backup. The hosts file is automatically backed up before being overwritten unless the '--skip-backup' flag is specified. backups show Show the contents of the specified backup file. HEREDOC backups() { local _subcommand="${1:-}" shift local _hosts_dirname _hosts_dirname="$(dirname "${HOSTS_PATH}")" case "${_subcommand}" in create) _verify_write_permissions "$@" local _timestamp _timestamp="$(date +"%Y%m%d%H%M%S")" cp "${HOSTS_PATH}" "${HOSTS_PATH}--backup-${_timestamp}" && \ printf "Backed up to %s--backup-%s\n" "${HOSTS_PATH}" "${_timestamp}" ;; compare) local _filename local _use_diff=0 for __arg in "${@:-}" do case "${__arg}" in --diff) _use_diff=1 ;; *) if [[ -z "${_filename:-}" ]] then _filename="${__arg}" fi ;; esac done if [[ -z "${_filename:-}" ]] then help backups exit 1 elif [[ ! -e "${_hosts_dirname}/${_filename}" ]] then printf "Backup not found: %s\n" "${_filename:-}" exit 1 fi local _difftool="diff" if ! ((_use_diff)) then if command -v git &>/dev/null then local _git_merge_tool _git_merge_tool="$(git config --get merge.tool)" [[ -n "${_git_merge_tool}" ]] && _difftool="${_git_merge_tool}" fi fi "${_difftool}" "${HOSTS_PATH}" "${_hosts_dirname}/${_filename}" ;; delete) if [[ -z "${1:-}" ]] then help backups exit 1 fi _verify_write_permissions "$@" if [[ "${HOSTS_PATH}" != "${_hosts_dirname}/${1:-}" ]] && [[ -e "${_hosts_dirname}/${1:-}" ]] then rm "${_hosts_dirname}/${1:-}" && printf "Backup deleted: %s\n" "${_hosts_dirname}/${1:-}" else printf "Backup not found: %s\n" "${1:-}" exit 1 fi ;; restore) if [[ -z "${1:-}" ]] then help backups exit 1 elif [[ ! -e "${_hosts_dirname}/${1}" ]] then printf "Backup not found: %s\n" "${1:-}" exit 1 fi _verify_write_permissions "$@" if [[ "${2:-}" != "--skip-backup" ]] then backups create fi cat "${_hosts_dirname}/${1}" > "${HOSTS_PATH}" && printf "Restored from backup: %s\n" "${1}" ;; show) if [[ -z "${1:-}" ]] then help backups exit 1 elif [[ ! -e "${_hosts_dirname}/${1}" ]] then printf "Backup not found: %s\n" "${1:-}" exit 1 fi less "${_hosts_dirname}/${1:-}" ;; *) local _filenames=() set +f for __filename in $(cd "${_hosts_dirname}" && ls -1 hosts* 2> /dev/null) do if [[ "${__filename:-}" != "hosts" ]] && \ [[ ! "${__filename:-}" =~ ^hosts_test..{6}$ ]] then _filenames+=("${__filename:-}") fi done set -f if ((${#_filenames[@]})) then for __filename in "${_filenames[@]:-}" do printf "%s\n" "${__filename}" done else printf \ "No backups found. Create a new backup:\n %s backups create\n" \ "${_ME}" fi ;; esac } # ----------------------------------------------------------------------- block desc "block" <... Description: Block one or more hostnames by adding new entries assigned to \`127.0.0.1\` for IPv4 and both \`fe80::1%lo0\` and \`::1\` for IPv6. HEREDOC block() { _verify_write_permissions "$@" if [[ -z "${1:-}" ]] then help block exit 1 fi for __hostname in "${@}" do add 127.0.0.1 "${__hostname}" # block IPv6 add "fe80::1%lo0" "${__hostname}" add "::1" "${__hostname}" done } # --------------------------------------------------------------------- disable desc "disable" < | | ) Description: Disable one or more records based on a given ip address, hostname, or search string. HEREDOC disable() { _verify_write_permissions "$@" local _search_string="${1:-}" if [[ -z "${_search_string:-}" ]] then help disable exit 1 else _debug printf "disable() \${_search_string}: %s\\n" "${_search_string}" local _regex_ip _regex_ip="^\\(${_search_string}[${_TAB_SPACE_}]..*\\)$" local _regex_commented_hostname _regex_commented_hostname="^\\([^#]..*[${_TAB_SPACE_}]${_search_string}[${_TAB_SPACE_}]..*\\)$" local _regex_hostname _regex_hostname="^\\([^#]..*[${_TAB_SPACE_}]${_search_string}\\)$" # Regular Expression Notes # # - Note double periods in regular expression in order to emulate /.+/, # which apparently doesn't work properly with all versions of sed. local _targets _targets=$( sed -n \ -e "s/${_regex_ip}/\\1/p" \ -e "s/${_regex_commented_hostname}/\\1/p" \ -e "s/${_regex_hostname}/\\1/p" \ "${HOSTS_PATH}" ) _debug printf "disable() \${_targets}: %s\\n" "${_targets}" if [[ -z "${_targets:-}" ]] then _die printf "Not found: %s\\n" "${_search_string}" fi printf "Disabling:\\n%s\\n" "${_targets}" "${_SED_I_COMMAND[@]}" \ -e "s/${_regex_ip}/\\#disabled: \\1/g" \ -e "s/${_regex_commented_hostname}/\\#disabled: \\1/g" \ -e "s/${_regex_hostname}/\\#disabled: \\1/g" \ "${HOSTS_PATH}" fi } # -------------------------------------------------------------------- disabled desc "disabled" < | | ) Description: Enable one or more disabled records based on a given ip address, hostname, or search string. HEREDOC enable() { _verify_write_permissions "$@" local _search_string="${1:-}" if [[ -z "${_search_string:-}" ]] then help enable exit 1 else _debug printf "enable() \${_search_string}: %s\\n" "${_search_string}" local _regex_ip _regex_ip="^\\#disabled: \\(${_search_string}[${_TAB_SPACE_}]..*\\)$" local _regex_commented_hostname _regex_commented_hostname="^\\#disabled: \\(..*[${_TAB_SPACE_}]${_search_string}[${_TAB_SPACE_}]..*\\)$" local _regex_hostname _regex_hostname="^\\#disabled: \\(..*[${_TAB_SPACE_}]${_search_string}\\)$" # Regular Expression Notes # # - Note double periods in regular expression in order to emulate /.+/, # which apparently doesn't work properly with all versions of sed. local _targets _targets=$( sed -n \ -e "s/${_regex_ip}/\\1/p" \ -e "s/${_regex_commented_hostname}/\\1/p" \ -e "s/${_regex_hostname}/\\1/p" \ "${HOSTS_PATH}" ) _debug printf "enable() \${targets}: %s\\n" "${_targets}" if [[ -z "${_targets:-}" ]] then _die printf "Not found: %s\\n" "${_search_string}" fi printf "Enabling:\\n%s\\n" "${_targets}" "${_SED_I_COMMAND[@]}" \ -e "s/${_regex_ip}/\\1/g" \ -e "s/${_regex_commented_hostname}/\\1/g" \ -e "s/${_regex_hostname}/\\1/g" \ "${HOSTS_PATH}" fi } # --------------------------------------------------------------------- enabled desc "enabled" <] Description: List the existing IP / hostname pairs, optionally limited to a specified state. When provided with a seach string, all matching enabled records will be printed. HEREDOC list() { # Get the disabled records up front for the two cases where they are needed. local _disabled_records _disabled_records=$( sed -n "s/^\\#disabled: \\(.*\\)$/\\1/p" "${HOSTS_PATH}" ) if [[ -n "${1:-}" ]] then if [[ "${1}" == "disabled" ]] then _print_entries "${_disabled_records}" elif [[ "${1}" == "enabled" ]] then _print_entries "$(grep -v -e '^$' -e '^\s*\#' "${HOSTS_PATH}")" else show "${1}" fi else # NOTE: use separate expressions since using a | for the or results in # inconsistent behavior. local _enabled_records _enabled_records="$(grep -v -e '^$' -e '^\s*\#' "${HOSTS_PATH}")" _print_entries "${_enabled_records:-}" if [[ -n "${_disabled_records:-}" ]] then [[ -n "${_enabled_records:-}" ]] && printf "\\n" printf "Disabled:\\n" printf "%s\\n" "---------" _print_entries "${_disabled_records}" fi fi } # ---------------------------------------------------------------------- remove desc "remove" < | | ) [--force] ${_ME} remove Options: --force Skip the confirmation prompt. Description: Remove one or more records based on a given IP address, hostname, or search string. If an IP and hostname are both provided, only records matching the IP and hostname pair will be removed. HEREDOC remove() { _verify_write_permissions "$@" local _is_search_pair=0 local _force_skip_prompt=0 local _arguments=() local _search_ip="" local _search_hostname="" local _search_string="" _debug printf "remove() \${1}: %s\\n" "${1:-}" _debug printf "remove() \${2}: %s\\n" "${2:-}" for __arg in "${@:-}" do case "${__arg}" in --force) _force_skip_prompt=1 ;; *) _arguments+=("${__arg}") ;; esac done _debug printf "remove() \${arguments[1]}: %s\\n" "${_arguments[0]:-}" _debug printf "remove() \${arguments[2]}: %s\\n" "${_arguments[1]:-}" if [[ -z "${_arguments[0]:-}" ]] then help remove exit 1 elif [[ -n "${_arguments[1]:-}" ]] then _search_ip="${_arguments[0]}" _search_hostname="${_arguments[1]}" _is_search_pair=1 _debug printf "remove() \${_is_search_pair}: %s\\n" "${_is_search_pair}" else _search_string="${_arguments[0]:-}" _debug printf "remove() \${_search_string}: %s\\n" "${_search_string}" fi # Regular Expression Notes # # Note double periods in regular expression in order to emulate /.+/, # which apparently doesn't work properly with all versions of sed. # # IP / Hostname pair regular expressions: local _regex_ip_hostname_commented _regex_ip_hostname_commented="^\\(${_search_ip}[${_TAB_SPACE_}]*${_search_hostname}[${_TAB_SPACE_}]..*\\)$" local _regex_ip_hostname _regex_ip_hostname="^\\(${_search_ip}[${_TAB_SPACE_}]*${_search_hostname}\\)$" # Search string regular expressions: local _regex_ip _regex_ip="^\\(${_search_string}[${_TAB_SPACE_}]..*\\)$" local _regex_commented_hostname _regex_commented_hostname="^\\(..*[${_TAB_SPACE_}]${_search_string}[${_TAB_SPACE_}]..*\\)$" local _regex_hostname _regex_hostname="^\\(..*[${_TAB_SPACE_}]${_search_string}\\)$" local _target_records if ((_is_search_pair)) then _target_records=$( sed -n \ -e "s/${_regex_ip_hostname_commented}/\\1/p" \ -e "s/${_regex_ip_hostname}/\\1/p" \ "${HOSTS_PATH}" ) else _target_records=$( sed -n \ -e "s/${_regex_ip}/\\1/p" \ -e "s/${_regex_commented_hostname}/\\1/p" \ -e "s/${_regex_hostname}/\\1/p" \ "${HOSTS_PATH}" ) fi if [[ -z "${_target_records:-}" ]] then printf "No matching records found.\\n" exit 1 fi if ! ((_force_skip_prompt)) then printf "Removing the following records:\\n%s\\n" "${_target_records}" while true do read -r -p "Are you sure you want to proceed? [y/N] " _yn case "${_yn}" in [Yy]* ) break ;; * ) printf "Exiting...\\n" exit 0 ;; esac done fi if ((_is_search_pair)) then "${_SED_I_COMMAND[@]}" \ -e "/${_regex_ip_hostname_commented}/d" \ -e "/${_regex_ip_hostname}/d" \ "${HOSTS_PATH}" else "${_SED_I_COMMAND[@]}" \ -e "/${_regex_ip}/d" \ -e "/${_regex_commented_hostname}/d" \ -e "/${_regex_hostname}/d" \ "${HOSTS_PATH}" fi printf "Removed:\\n%s\\n" "${_target_records}" } desc "delete" "$(desc --get 'remove')" delete() { remove "${@}"; } # ---------------------------------------------------------------------- search desc "search" < Description: Search entries for . HEREDOC search() { if _blank "${_COMMAND_ARGV[1]:-}" then help "search" return 1 fi list "$@" } # ------------------------------------------------------------------------ show desc "show" < | | ) Description: Print entries matching a given IP address, hostname, or search string. HEREDOC show() { if [[ -n "${1:-}" ]] then # Run `sed` before `grep` to avoid conflict that supresses `sed` output. local _disabled_records _disabled_records="$( sed -n "s/^\\#disabled: \\(.*${1}.*\\)$/\\1/p" "${HOSTS_PATH}" )" local _enabled_records _enabled_records="$( grep --invert-match "^#" "${HOSTS_PATH}" | grep "${1}" || true )" _print_entries "${_enabled_records}" if [[ -n "${_disabled_records}" ]] then [[ -n "${_enabled_records}" ]] && printf "\\n" printf "Disabled:\\n" printf "%s\\n" "---------" _print_entries "${_disabled_records}" fi else help show exit 1 fi } # --------------------------------------------------------------------- unblock desc "unblock" <... Description: Unblock one or more hostnames by removing the entries from the hosts file. HEREDOC unblock() { _verify_write_permissions "$@" if [[ -z "${1:-}" ]] then help unblock exit 1 fi for __hostname in "${@}" do remove 127.0.0.1 "${__hostname}" --force # unblock IPv6 remove "fe80::1%lo0" "${__hostname}" --force remove "::1" "${__hostname}" --force done } ############################################################################### # Options # # NOTE: The `getops` builtin command only parses short options and BSD `getopt` # does not support long arguments (GNU `getopt` does), so the most portable # and clear way to parse options is often to just use a `while` loop. # # For a pure bash `getopt` function, try pure-getopt: # https://github.com/agriffis/pure-getopt # # More info: # http://wiki.bash-hackers.org/scripting/posparams # http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html # http://stackoverflow.com/a/14203146 # http://stackoverflow.com/a/7948533 # https://stackoverflow.com/a/12026302 # https://stackoverflow.com/a/402410 ############################################################################### # Get raw options for any commands that expect them. _RAW_OPTIONS="${*:-}" # Parse Options ############################################################### # Initialize $_COMMAND_ARGV array # # This array contains all of the arguments that get passed along to each # command. This is essentially the same as the program arguments, minus those # that have been filtered out in the program option parsing loop. This array # is initialized with $0, which is the program's name. _COMMAND_ARGV=("${0}") # Initialize $_CMD and `$_USE_DEBUG`, which can continue to be blank depending # on what the program needs. _CMD="" _USE_DEBUG=0 _AUTO_SUDO=0 _SUBCOMMANDS=( add backups block commands disable disabled edit enable enabled file help list remove delete search show unblock version ) _SUBCOMMANDS_PATTERN="$(_join '|' "${_SUBCOMMANDS[@]}")" while [[ ${#} -gt 0 ]] do __opt="${1}" shift case "${__opt}" in -h|--help) _CMD="help" ;; --version) _CMD="version" ;; --debug) _USE_DEBUG=1 ;; --auto-sudo|--sudo) _AUTO_SUDO=1 ;; *) # The first non-option argument is assumed to be the command name. # All subsequent arguments are added to $_COMMAND_ARGV. if [[ -z "${_CMD:-}" ]] && [[ "${__opt:-}" =~ ${_SUBCOMMANDS_PATTERN} ]] then _CMD="${__opt}" else _COMMAND_ARGV+=("${__opt}") fi ;; esac done # Set $_COMMAND_PARAMETERS to $_COMMAND_ARGV, minus the initial element, $0. This # provides an array that is equivalent to $* and $@ within each command # function, though the array is zero-indexed, which could lead to confusion. # # Use `unset` to remove the first element rather than slicing (e.g., # `_COMMAND_PARAMETERS=("${_COMMAND_ARGV[@]:1}")`) because under bash 3.2 the # resulting slice is treated as a quoted string and doesn't easily get coaxed # into a new array. _COMMAND_PARAMETERS=("${_COMMAND_ARGV[@]}") unset "_COMMAND_PARAMETERS[0]" _debug printf \ "\${_CMD}: %s\\n" \ "${_CMD}" _debug printf \ "\${_RAW_OPTIONS} (one per line):\\n%s\\n" \ "${_RAW_OPTIONS}" _debug printf \ "\${_COMMAND_ARGV[*]}: %s\\n" \ "${_COMMAND_ARGV[*]}" _debug printf \ "\${_COMMAND_PARAMETERS[*]:-}: %s\\n" \ "${_COMMAND_PARAMETERS[*]:-}" ############################################################################### # Run Program ############################################################################### # Call the `_main` function after everything has been defined. _main