repo_description: 🔒 Consolidating and extending hosts files from several well-curated sources. Optionally pick extensions for porn, social media, and other categories.
repo_topics: ad-blocker anti-virus curated-sources gambling-filter hosts malware porn-filter pornblocker privacy protection python ransomware security social-media-filter trojans unified-hosts
code: #!/usr/bin/env python3
  
  # Script by Ben Limmer
  # https://github.com/l1m5
  #
  # This Python script will combine all the host files you provide
  # as sources into one, unique host file to keep your internet browsing happy.
  
  import argparse
  import fnmatch
  import ipaddress
  import json
  import locale
  import os
  import platform
  from pathlib import Path
  import re
  import shutil
  import socket
  import subprocess
  import sys
  import tempfile
  import time
  from glob import glob
  from typing import Optional, Tuple
  
  # Detecting Python 3 for version-dependent implementations
  PY3 = sys.version_info >= (3, 0)
  
  if not PY3:
      raise Exception("We do not support Python 2 anymore.")
  
  
  try:
      import requests
  except ImportError:
      raise ImportError(
          "This project's dependencies have changed. The Requests library ("
          "https://docs.python-requests.org/en/latest/) is now required."
      )
  
  
  # Syntactic sugar for "sudo" command in UNIX / Linux
  if platform.system() == "OpenBSD":
      SUDO = ["/usr/bin/doas"]
  elif platform.system() == "Windows":
      SUDO = ["powershell", "Start-Process", "powershell", "-Verb", "runAs"]
  else:
      SUDO = ["/usr/bin/env", "sudo"]
  
  
  # Project Settings
  BASEDIR_PATH = os.path.dirname(os.path.realpath(__file__))
  
  
  def get_defaults():
      """
      Helper method for getting the default settings.
  
      Returns
      -------
      default_settings : dict
          A dictionary of the default settings when updating host information.
      """
  
      return {
          "numberofrules": 0,
          "datapath": path_join_robust(BASEDIR_PATH, "data"),
          "freshen": True,
          "replace": False,
          "backup": False,
          "skipstatichosts": False,
          "keepdomaincomments": True,
          "extensionspath": path_join_robust(BASEDIR_PATH, "extensions"),
          "extensions": [],
          "nounifiedhosts": False,
          "compress": False,
          "minimise": False,
          "outputsubfolder": "",
          "hostfilename": "hosts",
          "targetip": "0.0.0.0",
          "sourcedatafilename": "update.json",
          "sourcesdata": [],
          "readmefilename": "readme.md",
          "readmetemplate": path_join_robust(BASEDIR_PATH, "readme_template.md"),
          "readmedata": {},
          "readmedatafilename": path_join_robust(BASEDIR_PATH, "readmeData.json"),
          "exclusionpattern": r"([a-zA-Z\d-]+\.){0,}",
          "exclusionregexes": [],
          "exclusions": [],
          "commonexclusions": ["hulu.com"],
          "blacklistfile": path_join_robust(BASEDIR_PATH, "blacklist"),
          "whitelistfile": path_join_robust(BASEDIR_PATH, "whitelist"),
      }
  
  
  # End Project Settings
  
  
  def main():
      parser = argparse.ArgumentParser(
          description="Creates a unified hosts "
          "file from hosts stored in the data subfolders."
      )
      parser.add_argument(
          "--auto",
          "-a",
          dest="auto",
          default=False,
          action="store_true",
          help="Run without prompting.",
      )
      parser.add_argument(
          "--backup",
          "-b",
          dest="backup",
          default=False,
          action="store_true",
          help="Backup the hosts files before they are overridden.",
      )
      parser.add_argument(
          "--extensions",
          "-e",
          dest="extensions",
          default=[],
          nargs="*",
          help="Host extensions to include in the final hosts file.",
      )
      parser.add_argument(
          "--nounifiedhosts",
          dest="nounifiedhosts",
          default=False,
          action="store_true",
          help="Do not include the unified hosts file in the final hosts file. Usually used together with `--extensions`.",
      )
      parser.add_argument(
          "--ip",
          "-i",
          dest="targetip",
          default="0.0.0.0",
          help="Target IP address. Default is 0.0.0.0.",
      )
      parser.add_argument(
          "--keepdomaincomments",
          "-k",
          dest="keepdomaincomments",
          action="store_false",
          default=True,
          help="Do not keep domain line comments.",
      )
      parser.add_argument(
          "--noupdate",
          "-n",
          dest="noupdate",
          default=False,
          action="store_true",
          help="Don't update from host data sources.",
      )
      parser.add_argument(
          "--skipstatichosts",
          "-s",
          dest="skipstatichosts",
          default=False,
          action="store_true",
          help="Skip static localhost entries in the final hosts file.",
      )
      parser.add_argument(
          "--nogendata",
          "-g",
          dest="nogendata",
          default=False,
          action="store_true",
          help="Skip generation of readmeData.json",
      )
      parser.add_argument(
          "--output",
          "-o",
          dest="outputsubfolder",
          default="",
          help="Output subfolder for generated hosts file.",
      )
      parser.add_argument(
          "--replace",
          "-r",
          dest="replace",
          default=False,
          action="store_true",
          help="Replace your active hosts file with this new hosts file.",
      )
      parser.add_argument(
          "--flush-dns-cache",
          "-f",
          dest="flushdnscache",
          default=False,
          action="store_true",
          help="Attempt to flush DNS cache after replacing the hosts file.",
      )
      parser.add_argument(
          "--compress",
          "-c",
          dest="compress",
          default=False,
          action="store_true",
          help="Compress the hosts file ignoring non-necessary lines "
          "(empty lines and comments) and putting multiple domains in "
          "each line. Improve the performance under Windows.",
      )
      parser.add_argument(
          "--minimise",
          "-m",
          dest="minimise",
          default=False,
          action="store_true",
          help="Minimise the hosts file ignoring non-necessary lines "
          "(empty lines and comments).",
      )
      parser.add_argument(
          "--whitelist",
          "-w",
          dest="whitelistfile",
          default=path_join_robust(BASEDIR_PATH, "whitelist"),
          help="Whitelist file to use while generating hosts files.",
      )
      parser.add_argument(
          "--blacklist",
          "-x",
          dest="blacklistfile",
          default=path_join_robust(BASEDIR_PATH, "blacklist"),
          help="Blacklist file to use while generating hosts files.",
      )
  
      global settings
  
      options = vars(parser.parse_args())
  
      options["outputpath"] = path_join_robust(BASEDIR_PATH, options["outputsubfolder"])
      options["freshen"] = not options["noupdate"]
  
      settings = get_defaults()
      settings.update(options)
  
      datapath = settings["datapath"]
      extensionspath = settings["extensionspath"]
  
      settings["sources"] = list_dir_no_hidden(datapath)
      settings["extensionsources"] = list_dir_no_hidden(extensionspath)
  
      # All our extensions folders...
      settings["extensions"] = [
          os.path.basename(item) for item in list_dir_no_hidden(extensionspath)
      ]
      # ... intersected with the extensions passed-in as arguments, then sorted.
      settings["extensions"] = sorted(
          list(set(options["extensions"]).intersection(settings["extensions"]))
      )
  
      auto = settings["auto"]
      exclusionregexes = settings["exclusionregexes"]
      sourcedatafilename = settings["sourcedatafilename"]
      nounifiedhosts = settings["nounifiedhosts"]
  
      updatesources = prompt_for_update(freshen=settings["freshen"], updateauto=auto)
      if updatesources:
          update_all_sources(sourcedatafilename, settings["hostfilename"])
  
      gatherexclusions = prompt_for_exclusions(skipprompt=auto)
  
      if gatherexclusions:
          commonexclusions = settings["commonexclusions"]
          exclusionpattern = settings["exclusionpattern"]
          exclusionregexes = display_exclusion_options(
              commonexclusions=commonexclusions,
              exclusionpattern=exclusionpattern,
              exclusionregexes=exclusionregexes,
          )
  
      extensions = settings["extensions"]
      sourcesdata = update_sources_data(
          settings["sourcesdata"],
          datapath=datapath,
          extensions=extensions,
          extensionspath=extensionspath,
          sourcedatafilename=sourcedatafilename,
          nounifiedhosts=nounifiedhosts,
      )
  
      mergefile = create_initial_file(
          nounifiedhosts=nounifiedhosts,
      )
      remove_old_hosts_file(settings["outputpath"], "hosts", settings["backup"])
      if settings["compress"]:
          finalfile = open(path_join_robust(settings["outputpath"], "hosts"), "w+b")
          compressedfile = tempfile.NamedTemporaryFile()
          remove_dups_and_excl(mergefile, exclusionregexes, compressedfile)
          compress_file(compressedfile, settings["targetip"], finalfile)
      elif settings["minimise"]:
          finalfile = open(path_join_robust(settings["outputpath"], "hosts"), "w+b")
          minimisedfile = tempfile.NamedTemporaryFile()
          remove_dups_and_excl(mergefile, exclusionregexes, minimisedfile)
          minimise_file(minimisedfile, settings["targetip"], finalfile)
      else:
          finalfile = remove_dups_and_excl(mergefile, exclusionregexes)
  
      numberofrules = settings["numberofrules"]
      outputsubfolder = settings["outputsubfolder"]
      skipstatichosts = settings["skipstatichosts"]
  
      write_opening_header(
          finalfile,
          extensions=extensions,
          numberofrules=numberofrules,
          outputsubfolder=outputsubfolder,
          skipstatichosts=skipstatichosts,
          nounifiedhosts=nounifiedhosts,
      )
      finalfile.close()
  
      if not settings["nogendata"]:
          update_readme_data(
              settings["readmedatafilename"],
              extensions=extensions,
              numberofrules=numberofrules,
              outputsubfolder=outputsubfolder,
              sourcesdata=sourcesdata,
              nounifiedhosts=nounifiedhosts,
          )
  
      print_success(
          "Success! The hosts file has been saved in folder "
          + "./"
          + outputsubfolder
          + "\nIt contains "
          + "{:,}".format(numberofrules)
          + " unique entries."
      )
  
      movefile = prompt_for_move(
          finalfile,
          auto=auto,
          replace=settings["replace"],
          skipstatichosts=skipstatichosts,
      )
  
      # We only flush the DNS cache if we have
      # moved a new hosts file into place.
      if movefile:
          prompt_for_flush_dns_cache(
              flushcache=settings["flushdnscache"], promptflush=not auto
          )
  
  
  # Prompt the User
  def prompt_for_update(freshen, updateauto):
      """
      Prompt the user to update all hosts files.
  
      If requested, the function will update all data sources after it
      checks that a hosts file does indeed exist.
  
      Parameters
      ----------
      freshen : bool
          Whether data sources should be updated. This function will return
          if it is requested that data sources not be updated.
      updateauto : bool
          Whether or not to automatically update all data sources.
  
      Returns
      -------
      updatesources : bool
          Whether or not we should update data sources for exclusion files.
      """
  
      # Create a hosts file if it doesn't exist.
      hostsfile = path_join_robust(BASEDIR_PATH, "hosts")
  
      if not os.path.isfile(hostsfile):
          try:
              open(hostsfile, "w+").close()
          except (IOError, OSError):
              # Starting in Python 3.3, IOError is aliased
              # OSError. However, we have to catch both for
              # Python 2.x failures.
              print_failure(
                  "ERROR: No 'hosts' file in the folder. Try creating one manually."
              )
  
      if not freshen:
          return False
  
      prompt = "Do you want to update all data sources?"
  
      if updateauto or query_yes_no(prompt):
          return True
      elif not updateauto:
          print("OK, we'll stick with what we've got locally.")
  
      return False
  
  
  def prompt_for_exclusions(skipprompt):
      """
      Prompt the user to exclude any custom domains from being blocked.
  
      Parameters
      ----------
      skipprompt : bool
          Whether or not to skip prompting for custom domains to be excluded.
          If true, the function returns immediately.
  
      Returns
      -------
      gatherexclusions : bool
          Whether or not we should proceed to prompt the user to exclude any
          custom domains beyond those in the whitelist.
      """
  
      prompt = (
          "Do you want to exclude any domains?\n"
          "For example, hulu.com video streaming must be able to access "
          "its tracking and ad servers in order to play video."
      )
  
      if not skipprompt:
          if query_yes_no(prompt):
              return True
          else:
              print("OK, we'll only exclude domains in the whitelist.")
  
      return False
  
  
  def prompt_for_flush_dns_cache(flushcache, promptflush):
      """
      Prompt the user to flush the DNS cache.
  
      Parameters
      ----------
      flushcache : bool
          Whether to flush the DNS cache without prompting.
      promptflush : bool
          If `flushcache` is False, whether we should prompt for flushing the
          cache. Otherwise, the function returns immediately.
      """
  
      if flushcache:
          flush_dns_cache()
      elif promptflush:
          if query_yes_no("Attempt to flush the DNS cache?"):
              flush_dns_cache()
  
  
  def prompt_for_move(finalfile, **moveparams):
      """
      Prompt the user to move the newly created hosts file to its designated
      location in the OS.
  
      Parameters
      ----------
      finalfile : file
          The file object that contains the newly created hosts data.
      moveparams : kwargs
          Dictionary providing additional parameters for moving the hosts file
          into place. Currently, those fields are:
  
          1) auto
          2) replace
          3) skipstatichosts
  
      Returns
      -------
      movefile : bool
          Whether or not the final hosts file was moved.
      """
  
      skipstatichosts = moveparams["skipstatichosts"]
  
      if moveparams["replace"] and not skipstatichosts:
          movefile = True
      elif moveparams["auto"] or skipstatichosts:
          movefile = False
      else:
          prompt = "Do you want to replace your existing hosts file with the newly generated file?"
          movefile = query_yes_no(prompt)
  
      if movefile:
          movefile = move_hosts_file_into_place(finalfile)
  
      return movefile
  
  
  # End Prompt the User
  
  
  def sort_sources(sources):
      """
      Sorts the sources.
      The idea is that all Steven Black's list, file or entries
      get on top and the rest sorted alphabetically.
  
      Parameters
      ----------
      sources: list
          The sources to sort.
      """
  
      result = sorted(
          sources.copy(),
          key=lambda x: x.lower().replace("-", "").replace("_", "").replace(" ", ""),
      )
  
      # Steven Black's repositories/files/lists should be on top!
      stevenblackpositions = [
          x for x, y in enumerate(result) if "stevenblack" in y.lower()
      ]
  
      for index in stevenblackpositions:
          result.insert(0, result.pop(index))
  
      return result
  
  
  # Exclusion logic
  def display_exclusion_options(commonexclusions, exclusionpattern, exclusionregexes):
      """
      Display the exclusion options to the user.
  
      This function checks whether a user wants to exclude particular domains,
      and if so, excludes them.
  
      Parameters
      ----------
      commonexclusions : list
          A list of common domains that are excluded from being blocked. One
          example is Hulu. This setting is set directly in the script and cannot
          be overwritten by the user.
      exclusionpattern : str
          The exclusion pattern with which to create the domain regex.
      exclusionregexes : list
          The list of regex patterns used to exclude domains.
  
      Returns
      -------
      aug_exclusionregexes : list
          The original list of regex patterns potentially with additional
          patterns from domains that the user chooses to exclude.
      """
  
      for exclusionoption in commonexclusions:
          prompt = "Do you want to exclude the domain " + exclusionoption + " ?"
  
          if query_yes_no(prompt):
              exclusionregexes = exclude_domain(
                  exclusionoption, exclusionpattern, exclusionregexes
              )
          else:
              continue
  
      if query_yes_no("Do you want to exclude any other domains?"):
          exclusionregexes = gather_custom_exclusions(
              exclusionpattern, exclusionregexes
          )
  
      return exclusionregexes
  
  
  def gather_custom_exclusions(exclusionpattern, exclusionregexes):
      """
      Gather custom exclusions from the user.
  
      Parameters
      ----------
      exclusionpattern : str
          The exclusion pattern with which to create the domain regex.
      exclusionregexes : list
          The list of regex patterns used to exclude domains.
  
      Returns
      -------
      aug_exclusionregexes : list
          The original list of regex patterns potentially with additional
          patterns from domains that the user chooses to exclude.
      """
  
      # We continue running this while-loop until the user
      # says that they have no more domains to exclude.
      while True:
          domainprompt = "Enter the domain you want to exclude (e.g. facebook.com): "
          userdomain = input(domainprompt)
  
          if is_valid_user_provided_domain_format(userdomain):
              exclusionregexes = exclude_domain(
                  userdomain, exclusionpattern, exclusionregexes
              )
  
          continueprompt = "Do you have more domains you want to enter?"
          if not query_yes_no(continueprompt):
              break
  
      return exclusionregexes
  
  
  def exclude_domain(domain, exclusionpattern, exclusionregexes):
      """
      Exclude a domain from being blocked.
  
      This creates the domain regex by which to exclude this domain and appends
      it a list of already-existing exclusion regexes.
  
      Parameters
      ----------
      domain : str
          The filename or regex pattern to exclude.
      exclusionpattern : str
          The exclusion pattern with which to create the domain regex.
      exclusionregexes : list
          The list of regex patterns used to exclude domains.
  
      Returns
      -------
      aug_exclusionregexes : list
          The original list of regex patterns with one additional pattern from
          the `domain` input.
      """
  
      exclusionregex = re.compile(exclusionpattern + domain)
      exclusionregexes.append(exclusionregex)
  
      return exclusionregexes
  
  
  def matches_exclusions(strippedrule, exclusionregexes):
      """
      Check whether a rule matches an exclusion rule we already provided.
  
      If this function returns True, that means this rule should be excluded
      from the final hosts file.
  
      Parameters
      ----------
      strippedrule : str
          The rule that we are checking.
      exclusionregexes : list
          The list of regex patterns used to exclude domains.
  
      Returns
      -------
      matches_exclusion : bool
          Whether or not the rule string matches a provided exclusion.
      """
  
      try:
          strippeddpmain = strippedrule.split()[1]
      except IndexError:
          # Example: 'example.org' instead of '0.0.0.0 example.org'
          strippeddpmain = strippedrule
  
      for exclusionRegex in exclusionregexes:
          if exclusionRegex.search(strippeddpmain):
              return True
  
      return False
  
  
  # End Exclusion Logic
  
  
  # Update Logic
  def update_sources_data(sourcesdata, **sourcesparams):
      """
      Update the sources data and information for each source.
  
      Parameters
      ----------
      sourcesdata : list
          The list of sources data that we are to update.
      sourcesparams : kwargs
          Dictionary providing additional parameters for updating the
          sources data. Currently, those fields are:
  
          1) datapath
          2) extensions
          3) extensionspath
          4) sourcedatafilename
          5) nounifiedhosts
  
      Returns
      -------
      update_sources_data : list
          The original source data list with new source data appended.
      """
  
      sourcedatafilename = sourcesparams["sourcedatafilename"]
  
      if not sourcesparams["nounifiedhosts"]:
          for source in sort_sources(
              recursive_glob(sourcesparams["datapath"], sourcedatafilename)
          ):
              updatefile = open(source, "r", encoding="UTF-8")
              try:
                  updatedata = json.load(updatefile)
                  sourcesdata.append(updatedata)
              finally:
                  updatefile.close()
  
      for source in sourcesparams["extensions"]:
          sourcedir = path_join_robust(sourcesparams["extensionspath"], source)
          for updatefile_path in sort_sources(
              recursive_glob(sourcedir, sourcedatafilename)
          ):
              updatefile = open(updatefile_path, "r")
              try:
                  updatedata = json.load(updatefile)
                  sourcesdata.append(updatedata)
              finally:
                  updatefile.close()
  
      return sourcesdata
  
  
  def jsonarray(json_array_string):
      """
      Transformer, converts a json array string hosts into one host per
      line, prefixing each line with "127.0.0.1 ".
  
      Parameters
      ----------
      json_array_string : str
          The json array string in the form
            '["example1.com", "example1.com", ...]'
      """
  
      templist = json.loads(json_array_string)
      hostlines = "127.0.0.1 " + "\n127.0.0.1 ".join(templist)
      return hostlines
  
  
  def update_all_sources(sourcedatafilename, hostfilename):
      """
      Update all host files, regardless of folder depth.
  
      Parameters
      ----------
      sourcedatafilename : str
          The name of the filename where information regarding updating
          sources for a particular URL is stored. This filename is assumed
          to be the same for all sources.
      hostfilename : str
          The name of the file in which the updated source information
          is stored for a particular URL. This filename is assumed to be
          the same for all sources.
      """
  
      # The transforms we support
      transformmethods = {"jsonarray": jsonarray}
  
      allsources = sort_sources(recursive_glob("*", sourcedatafilename))
  
      for source in allsources:
          updatefile = open(source, "r", encoding="UTF-8")
          updatedata = json.load(updatefile)
          updatefile.close()
  
          # we can pause updating any given hosts source.
          # if the update.json "pause" key is missing, don't pause.
          if updatedata.get("pause", False):
              continue
  
          updateurl = updatedata["url"]
          update_transforms = []
          if updatedata.get("transforms"):
              update_transforms = updatedata["transforms"]
  
          print("Updating source " + os.path.dirname(source) + " from " + updateurl)
  
          try:
              updatedfile = get_file_by_url(updateurl)
  
              # spin the transforms as required
              for transform in update_transforms:
                  updatedfile = transformmethods[transform](updatedfile)
  
              # get rid of carriage-return symbols
              updatedfile = updatedfile.replace("\r", "")
  
              hostsfile = open(
                  path_join_robust(BASEDIR_PATH, os.path.dirname(source), hostfilename),
                  "wb",
              )
              write_data(hostsfile, updatedfile)
              hostsfile.close()
          except Exception:
              print("Error in updating source: ", updateurl)
  
  
  # End Update Logic
  
  
  # File Logic
  def create_initial_file(**initial_file_params):
      """
      Initialize the file in which we merge all host files for later pruning.
  
      Parameters
      ----------
      headerparams : kwargs
          Dictionary providing additional parameters for populating the initial file
          information. Currently, those fields are:
  
          1) nounifiedhosts
      """
  
      mergefile = tempfile.NamedTemporaryFile()
  
      if not initial_file_params["nounifiedhosts"]:
          # spin the sources for the base file
          for source in sort_sources(
              recursive_glob(settings["datapath"], settings["hostfilename"])
          ):
              start = "# Start {}\n\n".format(os.path.basename(os.path.dirname(source)))
              end = "\n# End {}\n\n".format(os.path.basename(os.path.dirname(source)))
  
              with open(source, "r", encoding="UTF-8") as curFile:
                  write_data(mergefile, start + curFile.read() + end)
  
      # spin the sources for extensions to the base file
      for source in settings["extensions"]:
          for filename in sort_sources(
              recursive_glob(
                  path_join_robust(settings["extensionspath"], source),
                  settings["hostfilename"],
              )
          ):
              with open(filename, "r") as curFile:
                  write_data(mergefile, curFile.read())
  
      maybe_copy_example_file(settings["blacklistfile"])
  
      if os.path.isfile(settings["blacklistfile"]):
          with open(settings["blacklistfile"], "r") as curFile:
              write_data(mergefile, curFile.read())
  
      return mergefile
  
  
  def compress_file(inputfile, targetip, outputfile):
      """
      Reduce the file dimension removing non-necessary lines (empty lines and
      comments) and putting multiple domains in each line.
      Reducing the number of lines of the file, the parsing under Microsoft
      Windows is much faster.
  
      Parameters
      ----------
      inputfile : file
          The file object that contains the hostnames that we are reducing.
      targetip : str
          The target IP address.
      outputfile : file
          The file object that will contain the reduced hostnames.
      """
  
      inputfile.seek(0)  # reset file pointer
      write_data(outputfile, "\n")
  
      targetip_len = len(targetip)
      lines = [targetip]
      lines_index = 0
      for line in inputfile.readlines():
          line = line.decode("UTF-8")
  
          if line.startswith(targetip):
              if lines[lines_index].count(" ") < 9:
                  lines[lines_index] += (
                      " " + line[targetip_len : line.find("#")].strip()  # noqa: E203
                  )
              else:
                  lines[lines_index] += "\n"
                  lines.append(line[: line.find("#")].strip())
                  lines_index += 1
  
      for line in lines:
          write_data(outputfile, line)
  
      inputfile.close()
  
  
  def minimise_file(inputfile, targetip, outputfile):
      """
      Reduce the file dimension removing non-necessary lines (empty lines and
      comments).
  
      Parameters
      ----------
      inputfile : file
          The file object that contains the hostnames that we are reducing.
      targetip : str
          The target IP address.
      outputfile : file
          The file object that will contain the reduced hostnames.
      """
  
      inputfile.seek(0)  # reset file pointer
      write_data(outputfile, "\n")
  
      lines = []
      for line in inputfile.readlines():
          line = line.decode("UTF-8")
  
          if line.startswith(targetip):
              lines.append(line[: line.find("#")].strip() + "\n")
  
      for line in lines:
          write_data(outputfile, line)
  
      inputfile.close()
  
  
  def remove_dups_and_excl(mergefile, exclusionregexes, outputfile=None):
      """
      Remove duplicates and remove hosts that we are excluding.
  
      We check for duplicate hostnames as well as remove any hostnames that
      have been explicitly excluded by the user.
  
      Parameters
      ----------
      mergefile : file
          The file object that contains the hostnames that we are pruning.
      exclusionregexes : list
          The list of regex patterns used to exclude domains.
      outputfile : file
          The file object in which the result is written. If None, the file
          'settings["outputpath"]' will be created.
      """
  
      numberofrules = settings["numberofrules"]
      maybe_copy_example_file(settings["whitelistfile"])
  
      if os.path.isfile(settings["whitelistfile"]):
          with open(settings["whitelistfile"], "r") as ins:
              for line in ins:
                  line = line.strip(" \t\n\r")
                  if line and not line.startswith("#"):
                      settings["exclusions"].append(line)
  
      if not os.path.exists(settings["outputpath"]):
          os.makedirs(settings["outputpath"])
  
      if outputfile is None:
          finalfile = open(path_join_robust(settings["outputpath"], "hosts"), "w+b")
      else:
          finalfile = outputfile
  
      # analyze any post.json here
      post_json_path = os.path.join(os.path.dirname(finalfile.name), "post.json")
      filters = []
      if os.path.isfile(post_json_path):
          try:
              with open(post_json_path, "r", encoding="UTF-8") as post_file:
                  post_data = json.load(post_file)
                  filters = post_data.get("filters", [])
          except Exception as e:
              print_failure(f"Error reading post.json: {e}")
  
      mergefile.seek(0)  # reset file pointer
      hostnames = {"localhost", "localhost.localdomain", "local", "broadcasthost"}
      exclusions = settings["exclusions"]
  
      for line in mergefile.readlines():
          write_line = True
  
          # Explicit encoding
          line = line.decode("UTF-8")
  
          # Apply post.json filters
          if filters and any(f in line for f in filters):
              continue
  
          # replace tabs with space
          line = line.replace("\t+", " ")
  
          # see gh-271: trim trailing whitespace, periods
          line = line.rstrip(" .")
  
          # Testing the first character doesn't require startswith
          if line[0] == "#" or re.match(r"^\s*$", line[0]):
              write_data(finalfile, line)
              continue
          if "::1" in line:
              continue
  
          strippedrule = strip_rule(line)  # strip comments
          if not strippedrule or matches_exclusions(strippedrule, exclusionregexes):
              continue
  
          # Issue #1628
          if "@" in strippedrule:
              continue
  
          # Normalize rule
          hostname, normalized_rule = normalize_rule(
              strippedrule,
              targetip=settings["targetip"],
              keep_domain_comments=settings["keepdomaincomments"],
          )
  
          for exclude in exclusions:
              if re.search(r"(^|[\s\.])" + re.escape(exclude) + r"\s", line):
                  write_line = False
                  break
  
          if normalized_rule and (hostname not in hostnames) and write_line:
              write_data(finalfile, normalized_rule)
              hostnames.add(hostname)
              numberofrules += 1
  
      settings["numberofrules"] = numberofrules
      mergefile.close()
  
      if outputfile is None:
          return finalfile
  
  
  def normalize_rule(rule, targetip, keep_domain_comments):
      """
      Standardize and format the rule string provided.
  
      Parameters
      ----------
      rule : str
          The rule whose spelling and spacing we are standardizing.
      targetip : str
          The target IP address for the rule.
      keep_domain_comments : bool
          Whether or not to keep comments regarding these domains in
          the normalized rule.
  
      Returns
      -------
      normalized_rule : tuple
          A tuple of the hostname and the rule string with spelling
          and spacing reformatted.
      """
  
      def normalize_response(
          extracted_hostname: str, extracted_suffix: Optional[str]
      ) -> Tuple[str, str]:
          """
          Normalizes the responses after the provision of the extracted
          hostname and suffix - if exist.
  
          Parameters
          ----------
          extracted_hostname: str
              The extracted hostname to work with.
          extracted_suffix: str
              The extracted suffix to with.
  
          Returns
          -------
          normalized_response: tuple
              A tuple of the hostname and the rule string with spelling
              and spacing reformatted.
          """
  
          rule = "%s %s" % (targetip, extracted_hostname)
  
          if keep_domain_comments and extracted_suffix:
              if not extracted_suffix.strip().startswith("#"):
                  # Strings are stripped, therefore we need to add the space back.
                  rule += " # %s" % extracted_suffix
              else:
                  rule += " %s" % extracted_suffix
  
          return extracted_hostname, rule + "\n"
  
      def is_ip(dataset: str) -> bool:
          """
          Checks whether the given dataset is an IP.
  
          Parameters
          ----------
  
          dataset: str
              The dataset to work with.
  
          Returns
          -------
          is_ip: bool
              Whether the dataset is an IP.
          """
  
          try:
              _ = ipaddress.ip_address(dataset)
              return True
          except ValueError:
              return False
  
      def belch_unwanted(unwanted: str) -> Tuple[None, None]:
          """
          Belches unwanted to screen.
  
          Parameters
          ----------
          unwanted: str
              The unwanted string to belch.
  
          Returns
          -------
          belched: tuple
              A tuple of None, None.
          """
  
          """
          finally, if we get here, just belch to screen
          """
          print("==>%s<==" % unwanted)
          return None, None
  
      """
      first try: IP followed by domain
      """
  
      static_ip_regex = r"^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$"
      split_rule = rule.split(maxsplit=1)
  
      if is_ip(split_rule[0]):
          # Assume that the first item is an IP address following the rule.
  
          if " " or "\t" in split_rule[-1]:
              try:
                  # Example: 0.0.0.0 example.org # hello, world!
                  hostname, suffix = split_rule[-1].split(maxsplit=1)
              except ValueError:
                  # Example: 0.0.0.0 example.org[:space:]
                  hostname, suffix = split_rule[-1], None
          else:
              # Example: 0.0.0.0 example.org
              hostname, suffix = split_rule[-1], None
  
          hostname = hostname.lower()
  
          if (
              is_ip(hostname)
              or re.search(static_ip_regex, hostname)
              or "." not in hostname
              or ".." in hostname
              or "." in hostname[-1]
              or "/" in hostname
              or ":" in hostname
          ):
              # Example: 0.0.0.0 127.0.0.1
  
              # If the hostname is:
              #   - an IP - or looks like it,
              #   - doesn't contain dots, or
              #   - contains repeated dots,
              #   - ends in a dot, or
              #   - contains a slash, or
              #   - contains a colon,
              #   - contains an underscore,
              # we don't want to normalize it.
              return belch_unwanted(rule)
  
          return normalize_response(hostname, suffix)
  
      if (
          not re.search(static_ip_regex, split_rule[0])
          and ":" not in split_rule[0]
          and ".." not in split_rule[0]
          and "/" not in split_rule[0]
          and "." in split_rule[0]
      ):
          # Deny anything that looks like an IP; doesn't container dots or INVALID.
  
          try:
              hostname, suffix = split_rule
          except ValueError:
              hostname, suffix = split_rule[0], None
  
          hostname = hostname.lower()
  
          return normalize_response(hostname, suffix)
  
      return belch_unwanted(rule)
  
  
  def strip_rule(line):
      """
      Sanitize a rule string provided before writing it to the output hosts file.
  
      Parameters
      ----------
      line : str
          The rule provided for sanitation.
  
      Returns
      -------
      sanitized_line : str
          The sanitized rule.
      """
  
      return " ".join(line.split())
  
  
  def write_opening_header(finalfile, **headerparams):
      """
      Write the header information into the newly-created hosts file.
  
      Parameters
      ----------
      finalfile : file
          The file object that points to the newly-created hosts file.
      headerparams : kwargs
          Dictionary providing additional parameters for populating the header
          information. Currently, those fields are:
  
          1) extensions
          2) numberofrules
          3) outputsubfolder
          4) skipstatichosts
          5) nounifiedhosts
      """
  
      finalfile.seek(0)  # Reset file pointer.
      file_contents = finalfile.read()  # Save content.
  
      finalfile.seek(0)  # Write at the top.
  
      nounifiedhosts = headerparams["nounifiedhosts"]
  
      if headerparams["extensions"]:
          if nounifiedhosts:
              if len(headerparams["extensions"]) > 1:
                  write_data(
                      finalfile,
                      "# Title: StevenBlack/hosts extensions {0} and {1} \n#\n".format(
                          ", ".join(headerparams["extensions"][:-1]),
                          headerparams["extensions"][-1],
                      ),
                  )
              else:
                  write_data(
                      finalfile,
                      "# Title: StevenBlack/hosts extension {0}\n#\n".format(
                          ", ".join(headerparams["extensions"])
                      ),
                  )
          else:
              if len(headerparams["extensions"]) > 1:
                  write_data(
                      finalfile,
                      "# Title: StevenBlack/hosts with the {0} and {1} extensions\n#\n".format(
                          ", ".join(headerparams["extensions"][:-1]),
                          headerparams["extensions"][-1],
                      ),
                  )
              else:
                  write_data(
                      finalfile,
                      "# Title: StevenBlack/hosts with the {0} extension\n#\n".format(
                          ", ".join(headerparams["extensions"])
                      ),
                  )
      else:
          write_data(finalfile, "# Title: StevenBlack/hosts\n#\n")
  
      write_data(
          finalfile,
          "# This hosts file is a merged collection "
          "of hosts from reputable sources,\n",
      )
      write_data(finalfile, "# with a dash of crowd sourcing via GitHub\n#\n")
      write_data(
          finalfile,
          "# Date: " + time.strftime("%d %B %Y %H:%M:%S (%Z)", time.gmtime()) + "\n",
      )
  
      if headerparams["extensions"]:
          if headerparams["nounifiedhosts"]:
              write_data(
                  finalfile,
                  "# The unified hosts file was not used while generating this file.\n"
                  "# Extensions used to generate this file: "
                  + ", ".join(headerparams["extensions"])
                  + "\n",
              )
          else:
              write_data(
                  finalfile,
                  "# Extensions added to this file: "
                  + ", ".join(headerparams["extensions"])
                  + "\n",
              )
  
      write_data(
          finalfile,
          (
              "# Number of unique domains: {:,}\n#\n".format(
                  headerparams["numberofrules"]
              )
          ),
      )
      write_data(
          finalfile,
          "# Fetch the latest version of this file: "
          "https://raw.githubusercontent.com/StevenBlack/hosts/master/"
          + path_join_robust(headerparams["outputsubfolder"], "").replace("\\", "/")
          + "hosts\n",
      )
      write_data(
          finalfile, "# Project home page: https://github.com/StevenBlack/hosts\n"
      )
      write_data(
          finalfile,
          "# Project releases: https://github.com/StevenBlack/hosts/releases\n#\n",
      )
      write_data(
          finalfile,
          "# ===============================================================\n",
      )
      write_data(finalfile, "\n")
  
      if not headerparams["skipstatichosts"]:
          write_data(finalfile, "127.0.0.1 localhost\n")
          write_data(finalfile, "127.0.0.1 localhost.localdomain\n")
          write_data(finalfile, "127.0.0.1 local\n")
          write_data(finalfile, "255.255.255.255 broadcasthost\n")
          write_data(finalfile, "::1 localhost\n")
          write_data(finalfile, "::1 ip6-localhost\n")
          write_data(finalfile, "::1 ip6-loopback\n")
          write_data(finalfile, "fe80::1%lo0 localhost\n")
          write_data(finalfile, "ff00::0 ip6-localnet\n")
          write_data(finalfile, "ff00::0 ip6-mcastprefix\n")
          write_data(finalfile, "ff02::1 ip6-allnodes\n")
          write_data(finalfile, "ff02::2 ip6-allrouters\n")
          write_data(finalfile, "ff02::3 ip6-allhosts\n")
          write_data(finalfile, "0.0.0.0 0.0.0.0\n")
  
          if platform.system() == "Linux":
              write_data(finalfile, "127.0.1.1 " + socket.gethostname() + "\n")
              write_data(finalfile, "127.0.0.53 " + socket.gethostname() + "\n")
  
          write_data(finalfile, "\n")
  
      preamble = path_join_robust(BASEDIR_PATH, "myhosts")
      maybe_copy_example_file(preamble)
  
      if os.path.isfile(preamble):
          with open(preamble, "r") as f:
              write_data(finalfile, f.read())
  
      finalfile.write(file_contents)
  
  
  def update_readme_data(readme_file, **readme_updates):
      """
      Update the host and website information provided in the README JSON data.
  
      Parameters
      ----------
      readme_file : str
          The name of the README file to update.
      readme_updates : kwargs
          Dictionary providing additional JSON fields to update before
          saving the data. Currently, those fields are:
  
          1) extensions
          2) sourcesdata
          3) numberofrules
          4) outputsubfolder
          5) nounifiedhosts
      """
  
      extensions_key = "base"
      extensions = readme_updates["extensions"]
      nounifiedhosts = readme_updates["nounifiedhosts"]
  
      if extensions:
          extensions_key = "-".join(extensions)
          if nounifiedhosts:
              extensions_key = extensions_key + "-only"
  
      output_folder = readme_updates["outputsubfolder"]
      generation_data = {
          "location": path_join_robust(output_folder, ""),
          "nounifiedhosts": nounifiedhosts,
          "entries": readme_updates["numberofrules"],
          "sourcesdata": readme_updates["sourcesdata"],
      }
  
      with open(readme_file, "r") as f:
          readme_data = json.load(f)
          readme_data[extensions_key] = generation_data
  
      for denomination, data in readme_data.copy().items():
          if "location" in data and data["location"] and "\\" in data["location"]:
              # Windows compatibility: #1166
              readme_data[denomination]["location"] = data["location"].replace("\\", "/")
  
      with open(readme_file, "w") as f:
          json.dump(readme_data, f)
  
  
  def move_hosts_file_into_place(finalfile):
      r"""
      Move the newly-created hosts file into its correct location on the OS.
  
      For UNIX systems, the hosts file is "etc/hosts." On Windows, it's
      "C:\Windows\System32\drivers\etc\hosts."
  
      For this move to work, you must have administrator privileges to do this.
      On UNIX systems, this means having "sudo" access, and on Windows, it
      means being able to run command prompt in administrator mode.
  
      Parameters
      ----------
      finalfile : file object
          The newly-created hosts file to move.
      """  # noqa: W605
  
      filename = os.path.abspath(finalfile.name)
  
      try:
          if not Path(filename).exists():
              raise FileNotFoundError
      except Exception:
          print_failure(f"{filename} does not exist.")
          return False
  
      if platform.system() == "Windows":
          target_file = str(
              Path(os.getenv("SystemRoot")) / "system32" / "drivers" / "etc" / "hosts"
          )
      else:
          target_file = "/etc/hosts"
  
      if os.getenv("IN_CONTAINER"):
          # It's not allowed to remove/replace a mounted /etc/hosts, so we replace the content.
          # This requires running the container user as root, as is the default.
          print(f"Running in container, so we will replace the content of {target_file}.")
          try:
              with open(target_file, "w") as target_stream:
                  with open(filename, "r") as source_stream:
                      source = source_stream.read()
                      target_stream.write(source)
              return True
          except Exception:
              print_failure(f"Replacing content of {target_file} failed.")
              return False
      elif (
          platform.system() == "Linux"
          or platform.system() == "Windows"
          or platform.system() == "Darwin"
      ):
          print(
              f"Replacing {target_file} requires root privileges. You might need to enter your password."
          )
          try:
              subprocess.run(SUDO + ["cp", filename, target_file], check=True)
              return True
          except subprocess.CalledProcessError:
              print_failure(f"Replacing {target_file} failed.")
              return False
  
  
  def flush_dns_cache():
      """
      Flush the DNS cache.
      """
  
      print("Flushing the DNS cache to utilize new hosts file...")
      print(
          "Flushing the DNS cache requires administrative privileges. You might need to enter your password."
      )
  
      dns_cache_found = False
  
      if platform.system() == "Darwin":
          if subprocess.call(SUDO + ["killall", "-HUP", "mDNSResponder"]):
              print_failure("Flushing the DNS cache failed.")
      elif os.name == "nt":
          print("Automatically flushing the DNS cache is not yet supported.")
          print(
              "Please copy and paste the command 'ipconfig /flushdns' in "
              "administrator command prompt after running this script."
          )
      else:
          nscd_prefixes = ["/etc", "/etc/rc.d"]
          nscd_msg = "Flushing the DNS cache by restarting nscd {result}"
  
          for nscd_prefix in nscd_prefixes:
              nscd_cache = nscd_prefix + "/init.d/nscd"
  
              if os.path.isfile(nscd_cache):
                  dns_cache_found = True
  
                  if subprocess.call(SUDO + [nscd_cache, "restart"]):
                      print_failure(nscd_msg.format(result="failed"))
                  else:
                      print_success(nscd_msg.format(result="succeeded"))
  
          centos_file = "/etc/init.d/network"
          centos_msg = "Flushing the DNS cache by restarting network {result}"
  
          if os.path.isfile(centos_file):
              if subprocess.call(SUDO + [centos_file, "restart"]):
                  print_failure(centos_msg.format(result="failed"))
              else:
                  print_success(centos_msg.format(result="succeeded"))
  
          system_prefixes = ["/usr", ""]
          service_types = ["NetworkManager", "wicd", "dnsmasq", "networking"]
          restarted_services = []
  
          for system_prefix in system_prefixes:
              systemctl = system_prefix + "/bin/systemctl"
              system_dir = system_prefix + "/lib/systemd/system"
  
              for service_type in service_types:
                  service = service_type + ".service"
                  if service in restarted_services:
                      continue
  
                  service_file = path_join_robust(system_dir, service)
                  service_msg = (
                      "Flushing the DNS cache by restarting " + service + " {result}"
                  )
  
                  if os.path.isfile(service_file):
                      if 0 != subprocess.call(
                          [systemctl, "status", service], stdout=subprocess.DEVNULL
                      ):
                          continue
                      dns_cache_found = True
  
                      if subprocess.call(SUDO + [systemctl, "restart", service]):
                          print_failure(service_msg.format(result="failed"))
                      else:
                          print_success(service_msg.format(result="succeeded"))
                      restarted_services.append(service)
  
          dns_clean_file = "/etc/init.d/dns-clean"
          dns_clean_msg = "Flushing the DNS cache via dns-clean executable {result}"
  
          if os.path.isfile(dns_clean_file):
              dns_cache_found = True
  
              if subprocess.call(SUDO + [dns_clean_file, "start"]):
                  print_failure(dns_clean_msg.format(result="failed"))
              else:
                  print_success(dns_clean_msg.format(result="succeeded"))
  
          if not dns_cache_found:
              print_failure("Unable to determine DNS management tool.")
  
  
  def remove_old_hosts_file(path_to_file, file_name, backup):
      """
      Remove the old hosts file.
  
      This is a hotfix because merging with an already existing hosts file leads
      to artifacts and duplicates.
  
      Parameters
      ----------
      backup : boolean, default False
          Whether or not to backup the existing hosts file.
      """
  
      fullfilepath = path_join_robust(path_to_file, file_name)
  
      if os.path.exists(fullfilepath):
          if backup:
              backupfilepath = fullfilepath + "-{}".format(
                  time.strftime("%Y-%m-%d-%H-%M-%S")
              )
  
              # Make a backup copy, marking the date in which the list was updated
              shutil.copy(fullfilepath, backupfilepath)
  
          os.remove(fullfilepath)
  
      # Create directory if not exists
      if not os.path.exists(path_to_file):
          os.makedirs(path_to_file)
  
      # Create new empty hosts file
      open(fullfilepath, "a").close()
  
  
  # End File Logic
  
  
  def domain_to_idna(line):
      """
      Encode a domain that is present into a line into `idna`. This way we
      avoid most encoding issues.
  
      Parameters
      ----------
      line : str
          The line we have to encode/decode.
  
      Returns
      -------
      line : str
          The line in a converted format.
  
      Notes
      -----
      - This function encodes only the domain to `idna` format because in
          most cases, the encoding issue is due to a domain which looks like
          `b'\xc9\xa2oogle.com'.decode('idna')`.
      - About the splitting:
          We split because we only want to encode the domain and not the full
          line, which may cause some issues. Keep in mind that we split, but we
          still concatenate once we encoded the domain.
  
          - The following split the prefix `0.0.0.0` or `127.0.0.1` of a line.
          - The following also split the trailing comment of a given line.
      """
  
      if not line.startswith("#"):
          tabs = "\t"
          space = " "
  
          tabsposition, spaceposition = (line.find(tabs), line.find(space))
  
          if tabsposition > -1 and spaceposition > -1:
              if spaceposition < tabsposition:
                  separator = space
              else:
                  separator = tabs
          elif not tabsposition == -1:
              separator = tabs
          elif not spaceposition == -1:
              separator = space
          else:
              separator = ""
  
          if separator:
              splited_line = line.split(separator)
  
              try:
                  index = 1
                  while index < len(splited_line):
                      if splited_line[index]:
                          break
                      index += 1
  
                  if "#" in splited_line[index]:
                      index_comment = splited_line[index].find("#")
  
                      if index_comment > -1:
                          comment = splited_line[index][index_comment:]
  
                          splited_line[index] = (
                              splited_line[index]
                              .split(comment)[0]
                              .encode("IDNA")
                              .decode("UTF-8")
                              + comment
                          )
  
                  splited_line[index] = splited_line[index].encode("IDNA").decode("UTF-8")
              except IndexError:
                  pass
              return separator.join(splited_line)
          return line.encode("IDNA").decode("UTF-8")
      return line.encode("UTF-8").decode("UTF-8")
  
  
  # Helper Functions
  def maybe_copy_example_file(file_path):
      """
      Given a file path, copy over its ".example" if the path doesn't exist.
  
      If the path does exist, nothing happens in this function.
  
      If the path doesn't exist, and the ".example" file doesn't exist, nothing happens in this function.
  
      Parameters
      ----------
      file_path : str
          The full file path to check.
      """
  
      if not os.path.isfile(file_path):
          examplefilepath = file_path + ".example"
          if os.path.isfile(examplefilepath):
              shutil.copyfile(examplefilepath, file_path)
  
  
  def get_file_by_url(url, params=None, **kwargs):
      """
      Retrieve the contents of the hosts file at the URL, then pass it through domain_to_idna().
  
      Parameters are passed to the requests.get() function.
  
      Parameters
      ----------
      url : str or bytes
          URL for the new Request object.
      params :
          Dictionary, list of tuples or bytes to send in the query string for the Request.
      kwargs :
          Optional arguments that request takes.
  
      Returns
      -------
      url_data : str or None
          The data retrieved at that URL from the file. Returns None if the
          attempted retrieval is unsuccessful.
      """
  
      try:
          req = requests.get(url=url, params=params, **kwargs)
      except requests.exceptions.RequestException:
          print("Error retrieving data from {}".format(url))
          return None
  
      req.encoding = req.apparent_encoding
      res_text = "\n".join([domain_to_idna(line) for line in req.text.split("\n")])
      return res_text
  
  
  def write_data(f, data):
      """
      Write data to a file object.
  
      Parameters
      ----------
      f : file
          The file object at which to write the data.
      data : str
          The data to write to the file.
      """
  
      f.write(bytes(data, "UTF-8"))
  
  
  def list_dir_no_hidden(path):
      """
      List all files in a directory, except for hidden files.
  
      Parameters
      ----------
      path : str
          The path of the directory whose files we wish to list.
      """
  
      return glob(os.path.join(path, "*"))
  
  
  def query_yes_no(question, default="yes"):
      """
      Ask a yes/no question via input() and get answer from the user.
  
      Inspired by the following implementation:
  
      https://code.activestate.com/recipes/577058/
  
      Parameters
      ----------
      question : str
          The question presented to the user.
      default : str, default "yes"
          The presumed answer if the user just hits <Enter>. It must be "yes",
          "no", or None (means an answer is required of the user).
  
      Returns
      -------
      yes : Whether or not the user replied yes to the question.
      """
  
      valid = {"yes": "yes", "y": "yes", "ye": "yes", "no": "no", "n": "no"}
      prompt = {None: " [y/n] ", "yes": " [Y/n] ", "no": " [y/N] "}.get(default, None)
  
      if not prompt:
          raise ValueError("invalid default answer: '%s'" % default)
  
      reply = None
  
      while not reply:
          sys.stdout.write(colorize(question, Colors.PROMPT) + prompt)
  
          choice = input().lower()
          reply = None
  
          if default and not choice:
              reply = default
          elif choice in valid:
              reply = valid[choice]
          else:
              print_failure("Please respond with 'yes' or 'no' (or 'y' or 'n').\n")
  
      return reply == "yes"
  
  
  def is_valid_user_provided_domain_format(domain):
      """
      Check whether a provided domain is valid.
  
      Parameters
      ----------
      domain : str
          The domain against which to check.
  
      Returns
      -------
      valid_domain : bool
          Whether or not the domain provided is valid.
      """
  
      if domain == "":
          print("You didn't enter a domain. Try again.")
          return False
  
      domain_regex = re.compile(r"www\d{0,3}[.]|https?")
  
      if domain_regex.match(domain):
          print(
              "The domain " + domain + " is not valid. Do not include "
              "www.domain.com or http(s)://domain.com. Try again."
          )
          return False
      else:
          return True
  
  
  def recursive_glob(stem, file_pattern):
      """
      Recursively match files in a directory according to a pattern.
  
      Parameters
      ----------
      stem : str
          The directory in which to recurse
      file_pattern : str
          The filename regex pattern to which to match.
  
      Returns
      -------
      matches_list : list
          A list of filenames in the directory that match the file pattern.
      """
  
      if sys.version_info >= (3, 5):
          return glob(stem + "/**/" + file_pattern, recursive=True)
      else:
          # gh-316: this will avoid invalid unicode comparisons in Python 2.x
          if stem == str("*"):
              stem = "."
          matches = []
          for root, dirnames, filenames in os.walk(stem):
              for filename in fnmatch.filter(filenames, file_pattern):
                  matches.append(path_join_robust(root, filename))
      return matches
  
  
  def path_join_robust(path, *paths):
      """
      Wrapper around `os.path.join` with handling for locale issues.
  
      Parameters
      ----------
      path : str
          The first path to join.
      paths : varargs
          Subsequent path strings to join.
  
      Returns
      -------
      joined_path : str
          The joined path string of the two path inputs.
  
      Raises
      ------
      locale.Error : A locale issue was detected that prevents path joining.
      """
  
      try:
          # gh-316: joining unicode and str can be saddening in Python 2.x
          path = str(path)
          paths = [str(another_path) for another_path in paths]
  
          return os.path.join(path, *paths)
      except UnicodeDecodeError as e:
          raise locale.Error(
              "Unable to construct path. This is likely a LOCALE issue:\n\n" + str(e)
          )
  
  
  # Colors
  class Colors(object):
      PROMPT = "\033[94m"
      SUCCESS = "\033[92m"
      FAIL = "\033[91m"
      ENDC = "\033[0m"
  
  
  def supports_color():
      """
      Check whether the running terminal or command prompt supports color.
  
      Inspired by StackOverflow link (and Django implementation) here:
  
      https://stackoverflow.com/questions/7445658
  
      Returns
      -------
      colors_supported : bool
          Whether the running terminal or command prompt supports color.
      """
  
      sys_platform = sys.platform
      supported = sys_platform != "Pocket PC" and (
          sys_platform != "win32" or "ANSICON" in os.environ
      )
  
      atty_connected = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
      return supported and atty_connected
  
  
  def colorize(text, color):
      """
      Wrap a string so that it displays in a particular color.
  
      This function adds a prefix and suffix to a text string so that it is
      displayed as a particular color, either in command prompt or the terminal.
  
      If the running terminal or command prompt does not support color, the
      original text is returned without being wrapped.
  
      Parameters
      ----------
      text : str
          The message to display.
      color : str
          The color string prefix to put before the text.
  
      Returns
      -------
      wrapped_str : str
          The wrapped string to display in color, if possible.
      """
  
      if not supports_color():
          return text
  
      return color + text + Colors.ENDC
  
  
  def print_success(text):
      """
      Print a success message.
  
      Parameters
      ----------
      text : str
          The message to display.
      """
  
      print(colorize(text, Colors.SUCCESS))
  
  
  def print_failure(text):
      """
      Print a failure message.
  
      Parameters
      ----------
      text : str
          The message to display.
      """
  
      print(colorize(text, Colors.FAIL))
  
  
  # End Helper Functions
  
  
  if __name__ == "__main__":
      main()
  