# This file is part of wajig.  The copyright file is at debian/copyright.

"""Implementation of all COMMANDs"""

# Do not include any function in here that does not correspond to a COMMAND

import os
import inspect
import tempfile
import subprocess
import urllib.request
import webbrowser
import shutil

import apt

# wajig modules
import perform
import util
import debfile

# before we do any other command make sure the right files exist
util.ensure_initialised()


def addcdrom(args):
    """Add a Debian CD/DVD to APT's list of available sources"""
    perform.execute("/usr/bin/apt-cdrom add", root=True)


def addrepo(args):
    """Add a Launchpad PPA (Personal Package Archive) repository

    An example that shows how to add the daily builds of
    Google's Chromium browser:

    $ wajig addrepo ppa:chromium-daily
    """
    util.requires_package("add-apt-repository")
    perform.execute("/usr/bin/add-apt-repository " + args.ppa, root=True)


def aptlog(args):
    """Display APT log file"""
    perform.execute("cat /var/log/apt/history.log")


def autoalts(args):
    """Mark the Alternative to be auto-set (using set priorities)"""
    perform.execute(
        "/usr/bin/update-alternatives --auto " + args.alternative, root=True
    )


def autodownload(args):
    """Do an update followed by a download of all updated packages"""
    util.do_update(args.simulate)
    command = ("/usr/bin/apt-get --download-only --show-upgraded --assume-yes "
               "--force-yes dist-upgrade")
    perform.execute(command, root=True)
    if not args.simulate:
        upgradable_packages = util.upgradable()
        if upgradable_packages:
            util.do_describe(upgradable_packages, args.verbose)
        else:
            print("no upgradable packages")
        util.show_package_versions()


def autoclean(args):
    """Remove no-longer-downloadable .deb files from the download cache"""
    perform.execute("/usr/bin/apt-get autoclean", root=True)


def autoremove(args):
    """Remove unused dependency packages"""
    perform.execute("/usr/bin/apt-get autoremove", root=True, log=True)


def build(args):
    """Get source packages, unpack them, and build binary packages from them.

    Note: This also installs the needed build-dependencies if needed
    """
    util.requires_package("sudo")
    # First make sure dependencies are met
    if not builddeps(args):
        command = "apt-get {} source --build " + " ".join(args.packages)
        command = command.format(args.noauth)
        perform.execute(command)


def builddeps(args):
    """Install build-dependencies for given packages"""
    command = "/usr/bin/apt-get {} {} build-dep " + " ".join(args.packages)
    command = command.format(args.yes, args.noauth)
    return perform.execute(command, root=True, log=True)


def changelog(args):
    """Display Debian changelog of a package

    network on:
         changelog - if there's newer entries, display them
      -v changelog - if there's newer entries, display them, and proceed to
                     display complete local changelog

    network off:
         changelog - if there's newer entries, mention failure to retrieve
      -v changelog - if there's newer entries, mention failure to retrieve, and
                     proceed to display complete local changelog
    """

    package = util.package_exists(apt.Cache(), args.package)
    changelog = "{:=^79}\n".format(" {} ".format(args.package))  # header

    try:
        changelog += package.get_changelog()
    except AttributeError:
        # This is caught so as to avoid an ugly python-apt trace; it's a bug
        # that surfaces when:
        # 1. The package is not available in the default Debian suite
        # 2. The suite the package belongs to is set to a pin of < 0
        print("If this package is not on your default Debian suite, " \
              "ensure that its APT pinning isn't less than 0.")
        return
    help_message = "\nTo display the local changelog, run:\n" \
                   "wajig changelog --verbose " + args.package
    if "Failed to download the list of changes" in changelog:
        if not args.verbose:
            changelog += help_message
        else:
            changelog += "\n"
    elif changelog.endswith("The list of changes is not available"):
        changelog += ".\nYou are likely running the latest version.\n"
        if not args.verbose:
            changelog += help_message
    if not args.verbose:
        print(changelog)
    else:
        tmp = tempfile.mkstemp()[1]
        with open(tmp, "w") as f:
            if package.is_installed:
                changelog += "{:=^79}\n".format(" local changelog ")
            f.write(changelog)
        if package.is_installed:
            command = util.local_changelog(args.package, tmp)
            if not command:
                return
            perform.execute(command)
        with open(tmp) as f:
            for line in f:
                try:
                    print(line, end="")
                except BrokenPipeError:
                    return


def clean(args):
    """Remove all deb files from the download cache"""
    perform.execute("/usr/bin/apt-get clean", root=True)


def contents(args):
    """List the contents of a package file (.deb)"""
    perform.execute("dpkg --contents " + args.debfile)


def dailyupgrade(args):
    """Perform an update then a dist-upgrade"""
    util.do_update(args.simulate)
    perform.execute("/usr/bin/apt-get --show-upgraded dist-upgrade",
                    root=True, log=True)


def dependents(args):
    """Display packages which have some form of dependency on the given package

    Types of dependencies:
    * Depends
    * Recommends
    * Suggests
    * Replaces
    * Enhances
    """

    DEPENDENCY_TYPES = [
        "Depends",
        "Recommends",
        "Suggests",
        "Replaces",
        "Enhances",
    ]

    cache = apt.cache.Cache()
    package = util.package_exists(cache, args.package)
    dependents = {name : [] for name in DEPENDENCY_TYPES}

    for key in cache.keys():
        other_package = cache[key]
        for dependency_type, specific_dependents in dependents.items():
            if package.shortname in \
            util.extract_dependencies(other_package, dependency_type):
                specific_dependents.append(other_package.shortname)

    for dependency_type, specific_dependents in dependents.items():
        if specific_dependents:
            print("{}: {}".format(
                dependency_type.upper(), " ".join(specific_dependents)
            ))


def describe(args):
    """Display one-line descriptions for the given packages"""
    util.do_describe(args.packages, args.verbose)



def describenew(args):
    """Display one-line descriptions of newly-available packages

    This produces the same output as 'wajig new'
    """
    util.newly_available()


def distupgrade(args):
    """Comprehensive system upgrade

    This may remove some packages in order to ensure no package is
    left stale. Use the more conservative 'upgrade' command to avoid
    that.
    """
    packages = util.upgradable(distupgrade=True)
    if not packages and not args.dist:
        print('No upgrades. Did you run "wajig update" beforehand?')
        return
    if args.backup:
        util.requires_package("dpkg-repack")
        util.requires_package("fakeroot")
        util.backup_before_upgrade(packages, distupgrade=True)
    cmd = "/usr/bin/apt-get --show-upgraded {} {} {} ".format(
        args.local, args.yes, args.noauth
    )
    if args.dist:
        cmd += "--target-release " + args.dist + " "
    cmd += "dist-upgrade"
    perform.execute(cmd, root=True, log=True)


def download(args):
    """Download one or more packages without installing them"""
    print("Packages being downloaded to /var/cache/apt/archives/")
    command = "/usr/bin/apt-get --reinstall --download-only install "
    packages = util.consolidate_package_names(args)
    command = command + " ".join(packages)
    perform.execute(command, root=True)


def editsources(args):
    """Edit list of Debian repository locations for packages"""
    # stolen from http://selenic.com/hg/file/889789a2ca9f/mercurial/ui.py#l836
    editor = (
        os.environ.get('VISUAL') or
        os.environ.get('EDITOR') or
        '/usr/bin/sensible-editor'
    )
    perform.execute(editor + ' /etc/apt/sources.list', root=True)


def extract(args):
    """Extract the files from a package file to a directory"""
    command = "dpkg --extract {} {}"
    command = command.format(args.debfile, args.destination_directory)
    perform.execute(command)


def fixconfigure(args):
    """Fix an interrupted install"""
    perform.execute("/usr/bin/dpkg --configure --pending", root=True)


def fixinstall(args):
    """Fix an install interrupted by broken dependencies"""
    command = "/usr/bin/apt-get --fix-broken {} install".format(args.noauth)
    perform.execute(command, root=True, log=True)


def fixmissing(args):
    """Fix and install even though there are missing dependencies"""
    command = "/usr/bin/apt-get --ignore-missing {} upgrade".format(args.noauth)
    perform.execute(command, root=True, log=True)


def force(args):
    """Install packages and ignore file overwrites and depends

    Note: This is useful when there is a conflict of the same file from
          multiple packages or when a dependency is not installed for
          whatever reason
    """

    command = "/usr/bin/dpkg --install --force overwrite --force depends "
    archives = "/var/cache/apt/archives/"

    # For a .deb file we simply force install it.
    if args.packages[0].endswith(".deb"):
        for package in args.packages:
            if os.path.exists(package):
                command += "'" + package + "' "
            elif os.path.exists(archives + package):
                command += "'" + archives + package + "' "
            else:
                message = ("File {} not found. "
                           "Searched current directory and {}."
                           "Please confirm the location and try again.")
                print(message.format(package, archives))
                return()
    else:
        # Package names rather than a specific deb package archive
        # is expected.
        for package in args.packages:
            # Identify the latest version of the package available in
            # the download archive, if there is any there.
            lscmd = "ls {} | grep -E '^{}_' | sort -k 1b,1 | tail -n -1"
            lscmd = lscmd.format(archives, package)
            matches = perform.execute(lscmd, pipe=True)
            debpkg = matches.readline().strip()

            if not debpkg:
                dlcmd = (
                    "apt-get --quiet=2 --reinstall --download-only "
                    "install '{}'"
                ).format(package)
                perform.execute(dlcmd, root=True)
                matches = perform.execute(lscmd, pipe=True)
                debpkg = matches.readline().strip()

            # Force install the package from the download archive.
            command += "'" + archives + debpkg + "' "

    perform.execute(command, root=True, log=True)


def hold(args):
    """Place packages on hold (so they will not be upgraded)"""
    for package in args.packages:
        # The dpkg needs sudo but not the echo.
        # Do all of it as root then!
        command = '/bin/echo "{} hold" | /usr/bin/dpkg --set-selections'
        perform.execute(command.format(package), root=True)
    print("The following packages are on hold:")
    perform.execute("dpkg --get-selections | grep -E 'hold$' | cut -f1")


def info(args):
    """List the information contained in a package file"""
    perform.execute("dpkg --info " + args.package)


def init(args):
    """Initialise or reset wajig archive files"""
    util.reset_files()


def install(args):
    """Package installer

    notes:
    * specifying a .deb file will also try to satisfy that deb's dependencies;
    * one can specify multiple files with --fileinput option
    * specifying a url will try fetch the file from the internet, and keep it
      in "~/.wajig/$HOSTNAME"

    example:
    $ wajig install a b_1.0_all.deb http://example.com/c_1.0_all.deb

    Assuming there's no errors, the command will install 3 packages named
    'a', 'b', and 'c''
    """

    packages = util.consolidate_package_names(args)

    online_files = [
        package for package in packages if
        package.startswith(("http://", "ftp://"))
    ]
    deb_files = list()
    for package in online_files:
        if not package.endswith(".deb"):
            print("A valied .deb file should have a '.deb' extension")
            continue
        filename = os.path.join(util.init_dir, package.split("/")[-1])
        try:
            response = urllib.request.urlopen(package)
        except urllib.error.HTTPError as error:
            print("{}; is '{}' the correct url?".format(error.reason, package))
        else:
            with open(filename, "wb") as f:
                f.write(response.read())
            deb_files.append(filename)

    deb_files.extend([
        package for package in packages if package.endswith(".deb")
        and os.path.exists(package)
    ])
    if deb_files:
        debfile.install(deb_files)

    packages = packages.difference(online_files, deb_files)
    if packages:
        if args.dist:
            args.dist = "--target-release " + args.dist
        command = "/usr/bin/apt-get {} {} {} {} --auto-remove install "
        command += " ".join(packages)
        command = command.format(args.yes, args.noauth, args.recommends,
                                 args.dist)
        perform.execute(command, root=True, log=True)


def installsuggested(args):
    """Install a package and its Suggests dependencies"""
    cache = apt.cache.Cache()
    package = util.package_exists(cache, args.package,
                                  ignore_virtual_packages=True)
    dependencies = list(util.extract_dependencies(package, "Suggests"))
    for n, dependency in enumerate(dependencies):
        dependencies[n] = util.package_exists(cache, dependency).shortname
    dependencies = " ".join(dependencies)
    command = "/usr/bin/apt-get {} {} {} --auto-remove install {} {}"
    command = command.format(args.recommends, args.yes, args.noauth,
                             dependencies, args.package)
    perform.execute(command, root=True, log=True)


def integrity(args):
    """Check the integrity of installed packages (through checksums)"""
    util.requires_package("debsums")
    perform.execute("debsums --all --silent")


def large(args):
    """List size of all large (>10MB) installed packages"""
    util.sizes(size=10000)


def lastupdate(args):
    """Identify when an update was last performed"""
    command = ("ls -l --full-time " + util.available_file + " 2> "
               "/dev/null | awk '{printf \"Last update was %s %s %s\\n\""
               ", $6, $7, $8}' | sed 's|\.000000000||'")
    perform.execute(command)


def listall(args):
    """List one line descriptions for all packages"""
    command = ("apt-cache dumpavail |"
               "grep -E \"^(Package|Description): \" |"
               "awk '/^Package: /{pkg=$2} /^Description: /"
               "{printf(\"%-24s %s\\n\", pkg,"
               "substr($0,13))}' | sort -u -k 1b,1")
    if args.pattern:
        command = "{} | grep -E '{}'".format(command, args.pattern)
    perform.execute(command)



def listcache(args):
    """List the contents of the download cache"""
    command = "printf 'Found %d files %s in the cache.\n\n'\
           $(ls /var/cache/apt/archives/ | wc -l) \
           $(ls -sh /var/cache/apt/archives/ | head -1 | awk '{print $2}')"
    perform.execute(command)
    command = "ls /var/cache/apt/archives/"
    if args.pattern:
        command = "{} | grep -E '{}'".format(command, args.pattern)
    perform.execute(command)


def listalternatives(args):
    """List the objects that can have alternatives configured"""
    command = ("ls /etc/alternatives/ | "
               "grep -E -v '(\.1|\.1\.gz|\.8|\.8\.gz|README)$'")
    perform.execute(command)


def listcommands(args):
    """Display all wajig commands"""
    for name, value in sorted(globals().items()):
        if inspect.isfunction(value):
            summary = value.__doc__.split('\n')[0]
            if args.pattern:
                if args.pattern not in summary and \
                   args.pattern not in name:
                    continue
            print("{:<18} {}".format(name, summary))


def listdaemons(args):
    """List the daemons that wajig can start, stop, restart, or reload"""
    util.requires_package("chkconfig")
    perform.execute("chkconfig")


def listfiles(args):
    """List the files that are supplied by the named package"""
    if args.package.endswith(".deb"):
        perform.execute("dpkg --contents " + args.package)
        return
    try:
        output = perform.execute(
            "dpkg --listfiles " + args.package, getoutput=True
        )
    except subprocess.CalledProcessError:
        if shutil.which("apt-file"):
            perform.execute("apt-file list --regexp ^{}$".format(args.package))
    else:
        for line in output.decode().strip().split('\n'):
            print(line)


def listhold(args):
    """List packages that are on hold (i.e. those that won't be upgraded)"""
    perform.execute("dpkg --get-selections | grep -E 'hold$' | cut -f1")


def listinstalled(args):
    """List installed packages"""
    command = "dpkg --get-selections | cut -f1"
    if args.pattern:
        command += " | grep -E '{}' | sort -k 1b,1".format(args.pattern)
    perform.execute(command)


def listlog(args):
    """Display wajig log file"""
    perform.execute("cat " + util.log_file)


def listnames(args):
    """List all known packages; optionally filter the list with a pattern"""
    util.do_listnames(args.pattern)


def listpackages(args):
    """List the status, version, and description of installed packages"""
    command = "dpkg --list '*' | grep -E -v 'no description avail'"
    if args.pattern:
        command += " | grep -E '{}' | sort -k 1b,1".format(args.pattern)
    perform.execute(command)


def listscripts(args):
    """List the control scripts of the package of deb file"""
    package = args.debfile
    scripts = ["preinst", "postinst", "prerm", "postrm"]
    if package.endswith(".deb"):
        command = "ar p " + package + " control.tar.gz | tar ztvf -"
        pkgScripts = perform.execute(command, pipe=True).readlines()
        for script in scripts:
            if "./" + script in "".join(pkgScripts):
                nlen = int((72 - len(script)) / 2)
                print(">"*nlen, script, "<"*nlen)
                command = "ar p " + package + " control.tar.gz |" +\
                          "tar zxvf - -O ./" + script +\
                          " 2>/dev/null"
                perform.execute(command)
    else:
        root = "/var/lib/dpkg/info/"
        for script in scripts:
            fname = root + package + "." + script
            if os.path.exists(fname):
                nlen = int((72 - len(script))/2)
                print(">"*nlen, script, "<"*nlen)
                perform.execute("cat " + fname)


def listsection(args):
    """List packages that belong to a specific section

    Note: Use the LISTSECTIONS command for a list of Debian Sections
    """
    cache = apt.cache.Cache()
    for package in cache.keys():
        package = cache[package]
        if package.section == args.section:
            print(package.name)


def listsections(args):
    """List all available sections"""
    cache = apt.cache.Cache()
    sections = list()
    for package in cache.keys():
        package = cache[package]
        sections.append(package.section)
    sections = set(sections)
    for section in sections:
        print(section)


def liststatus(args):
    """Same as list but only prints first two columns, not truncated"""
    command = "COLUMNS=400 "
    command += "dpkg --list '*' | grep -E -v 'no description avail'"
    command += " | awk '{print $1,$2}'"
    if args.pattern:
        command += " | grep -E '{}' | sort -k 1b,1".format(args.pattern)
    perform.execute(command)


def localupgrade(args):
    """Upgrade using only packages that are already downloaded"""
    command = (
        "/usr/bin/apt-get --no-download --ignore-missing "
        "--show-upgraded upgrade"
    )
    perform.execute(command, root=True, log=True)


def madison(args):
    """Runs the madison command of apt-cache"""
    command = "apt-cache madison " + " ".join(set(args.packages))
    perform.execute(command)


def move(args):
    """Move packages in the download cache to a local Debian mirror"""
    util.requires_package("apt-move")
    perform.execute("/usr/bin/apt-move update", root=True)


def new(args):
    """Display newly-available packages"""
    util.newly_available(args.verbose)


def newdetail(args):
    """Display detailed descriptions of newly-available packages

    This produces the same output as 'wajig new --verbose'
    """
    util.newly_available(verbose=True)


def news(args):
    """Display the NEWS file of a given package"""
    util.display_sys_docs(args.package, "NEWS.Debian NEWS".split())


def nonfree(args):
    """List packages that don't meet the Debian Free Software Guidelines"""
    util.requires_package("vrms")
    perform.execute("vrms")


def orphans(args):
    """List libraries not required by any installed package """
    util.requires_package("deborphan")
    perform.execute("deborphan")


def policy(args):
    """From preferences file show priorities/policy (available)"""
    perform.execute("apt-cache policy " + " ".join(args.packages))


def purge(args):
    """Remove one or more packages and their configuration files"""
    packages = util.consolidate_package_names(args)
    command = "/usr/bin/apt-get {} {} --auto-remove purge "
    command = command.format(args.yes, args.noauth)
    command = command + " ".join(packages)
    perform.execute(command, root=True, log=True)


def purgeorphans(args):
    """Purge orphaned libraries (not required by installed packages)"""
    # Deborphans does not require root, but dpkg does,
    # so build up the orphans list first, then pass that to dpkg.
    util.requires_package("deborphan")
    packages = ""
    for package in perform.execute("deborphan", pipe=True):
        packages += " " + package.strip()
    if packages:
        command = "/usr/bin/apt-get --auto-remove purge {} {}"
        command = command.format(args.yes, packages)
        perform.execute(command, root=True, log=True)


def purgeremoved(args):
    """Purge all packages marked as deinstall"""
    packages = ""
    cmd = (
        "dpkg-query --show "
        "--showformat='${Package}:${Architecture}\t${Status}\n' | "
        "grep -E \"deinstall ok config-files\" | cut -f 1 "
    )
    packages = perform.execute(cmd, pipe=True)
    if packages:
        packages = " ".join(packages)
        packages = " ".join(packages.split())
        perform.execute("/usr/bin/apt-get purge " + packages,
                        root=True, log=True)


def rbuilddeps(args):
    """Display the packages which build-depend on the given package"""
    util.requires_package("grep-dctrl")
    command = "grep-available -sPackage -FBuild-Depends,Build-Depends-Indep "
    command = command + args.package + " /var/lib/apt/lists/*Sources"
    perform.execute(command)


def readme(args):
    """Display the README file(s) of a given package

    This will display README, README.Debian, README.rst, and USAGE
    files of a package. It will also decompress them if they are
    postfixed with .gz.
    """
    matches = 'README README.Debian README.rst USAGE'
    util.display_sys_docs(args.package, matches.split())


def recdownload(args):
    """Download a package and all its dependencies"""

    package_names = list()

    cache = apt.cache.Cache()
    for package in args.packages:
        util.package_exists(cache, package)

    print("Calculating all dependencies...")
    for package in args.packages:
        package_names.extend(util.get_deps_recursively(cache, package, []))
    print("Packages to download to /var/cache/apt/archives:")
    for package in package_names:
        # We do this because apt-get install dont list the packages to
        # reinstall if they don't need to be upgraded
        print(package, end=' ')
    print()

    command = "/usr/bin/apt-get --download-only --reinstall -u install {} {}"
    command = command.format(args.noauth, " ".join(package_names))
    perform.execute(command, root=True)


def reconfigure(args):
    """Reconfigure package"""
    command = "/usr/sbin/dpkg-reconfigure " + " ".join(args.packages)
    perform.execute(command, root=True)


def recommended(args):
    """Display packages installed as Recommends and have no dependents"""
    perform.execute(
        "aptitude search "
        "'?and( ?automatic(?reverse-recommends(?installed)), "
        "?not(?automatic(?reverse-depends(?installed))) )'"
    )


def reinstall(args):
    """Reinstall the given packages"""
    command = "/usr/bin/apt-get install --reinstall {} {} {}"
    command = command.format(args.noauth, args.yes, " ".join(args.packages))
    perform.execute(command, root=True, log=True)


def reload(args):
    """Reload system daemons (see LIST-DAEMONS for available daemons)"""
    command = "/usr/sbin/service {} reload".format(args.daemon)
    if perform.execute(command, root=True):
        print("attempt FORCE-RELOAD instead")
        command = "/usr/sbin/service {} force-reload ".format(args.daemon)
        perform.execute(command, root=True)


def remove(args):
    """Remove packages (see also PURGE command)"""
    packages = util.consolidate_package_names(args)
    command = "/usr/bin/apt-get {} {}--auto-remove remove " + " ".join(packages)
    command = command.format(args.yes, args.noauth)
    perform.execute(command, root=True, log=True)


def removeorphans(args):
    """Remove orphaned libraries"""
    util.requires_package("deborphan")
    packages = ""
    for package in perform.execute("deborphan", pipe=True):
        packages += " " + package.strip()
    if packages:
        command = "/usr/bin/apt-get --auto-remove remove {} {}"
        command = command.format(args.yes, packages)
        perform.execute(command, root=True, log=True)


def repackage(args):
    """Generate a .deb file from an installed package"""
    util.requires_package("dpkg-repack")
    util.requires_package("fakeroot")
    command = "fakeroot --unknown-is-real dpkg-repack " + args.package
    perform.execute(command)


def reportbug(args):
    """Report a bug in a package using Debian BTS (Bug Tracking System)"""
    util.requires_package("reportbug", "/usr/bin/reportbug")
    perform.execute("reportbug " + args.package)


def restart(args):
    """Restart system daemons (see LIST-DAEMONS for available daemons)"""
    command = "/usr/sbin/service {} restart".format(args.daemon)
    perform.execute(command, root=True)


def rpm2deb(args):
    """Convert an .rpm file to a Debian .deb file"""
    command = "alien " + args.rpm
    perform.execute(command)


def rpminstall(args):
    """Install an .rpm package file"""
    command = "/usr/bin/alien --install " + args.rpm
    perform.execute(command, root=True, log=True)


def search(args):
    """Search for package names containing the given pattern

    If '::' is found in the search term, use a debtags search; example:

    $ wajig search implemented-in::python
    balazar - adventure/action game Balazar -- Arkanae II, reforged scepters
    compizconfig-settings-manager - Compizconfig Settings Manager
    ...
    """
    if len(args.patterns) == 1 and '::' in args.patterns[0]:
        util.requires_package('debtags')
        command = 'debtags search ' + args.patterns[0]
        if args.verbose:
            command += ' --full'
    elif not args.verbose:
        command = "apt-cache --names-only search {}"
        command = command.format(" ".join(args.patterns))
    elif args.verbose == 1:
        import shlex
        args.patterns = [shlex.quote(pattern) for pattern in args.patterns]
        command = "apt-cache search {} | grep -E --ignore-case '{}'"
        command = command.format(" ".join(args.patterns),
                                 "\|".join(args.patterns))
    else:
        command = "apt-cache search --full " + " ".join(args.patterns)
    perform.execute(command)


def searchapt(args):
    """Find nearby Debian package repositories"""
    util.requires_package("netselect-apt")
    command = "netselect-apt " + args.dist
    perform.execute(command)


def show(args):
    """Provide a detailed description of package"""
    package_names = " ".join(set(args.packages))
    tool = "apt-cache" if args.fast else "aptitude"
    command = "{} show {}".format(tool, package_names)
    perform.execute(command)


def start(args):
    """Start system daemons (see LIST-DAEMONS for available daemons)"""
    command = "/usr/sbin/service {} start".format(args.daemon)
    perform.execute(command, root=True)


def stop(args):
    """Stop system daemons (see LISTDAEMONS for available daemons)"""
    command = "/usr/sbin/service {} stop".format(args.daemon)
    perform.execute(command, root=True)


def sizes(args):
    """Display installed sizes of given packages

    To display sizes of all packages, do not use any argument:
    $ wajig sizes
    """
    util.sizes(args.packages)


def snapshot(args):
    """Generates a list of package=version for all installed packages"""
    util.do_status([], snapshot=True)


def source(args):
    """Retrieve and unpack sources for the named packages"""
    util.requires_package("dpkg-source")
    perform.execute("apt-get source " + " ".join(args.packages))


def status(args):
    """Show the version and available versions of packages"""
    util.do_status(args.packages)


def statusmatch(args):
    """Show the version and available versions of matching packages"""
    try:
        packages = [s.strip() for s in
                    util.do_listnames(args.pattern, pipe=True).readlines()]
    except AttributeError:
        print("No packages found matching '{}'".format(args.pattern))
    else:
        util.do_status(packages)


def tasksel(args):
    """Run the task selector to install groups of packages"""
    util.requires_package("tasksel")
    perform.execute("/usr/bin/tasksel", root=True, log=True)


def todo(args):
    """Display the TODO file of a given package"""
    util.display_sys_docs(args.package, ["TODO"])


def toupgrade(args):
    """List versions of upgradable packages"""
    if not util.show_package_versions():
        print("No upgradeable packages")


def tutorial(args):
    """Display wajig tutorial"""
    perform.execute('zcat /usr/share/doc/wajig/TUTORIAL')


def unhold(args):
    """Remove listed packages from hold so they are again upgradeable"""
    for package in args.packages:
        # The dpkg needs sudo but not the echo.
        # Do all of it as root then.
        command = "echo \"" + package + " install\" | dpkg --set-selections"
        perform.execute(command, root=True)
    print("The following packages are still on hold:")
    perform.execute("dpkg --get-selections | grep -E 'hold$' | cut -f1")


def unofficial(args):
    """Search for an unofficial Debian package at apt-get.org"""
    aptget_org = "http://www.apt-get.org"
    try:
        urllib.request.urlopen(aptget_org)
    except urllib.error.URLError as error:
        print("'{}' is unreachable".format(aptget_org))
    else:
        url = aptget_org + "/search/?query=" + args.package
        webbrowser.open(url)


def update(args):
    """Update the list of new and updated packages"""
    util.do_update(args.simulate)


def updatealternatives(args):
    """Update default alternative for things like x-window-manager"""
    command = "/usr/bin/update-alternatives --config " + args.alternative
    perform.execute(command, root=True)


def updatepciids(args):
    """Updates the local list of PCI ids from the internet master list"""
    util.requires_package("pciutils", path="/usr/bin/update-pciids")
    perform.execute("/usr/bin/update-pciids", root=True)


def updateusbids(args):
    """Updates the local list of USB ids from the internet master list"""
    util.requires_package("usbutils", path="/usr/sbin/update-usbids")
    perform.execute("/usr/sbin/update-usbids", root=True)


def upgrade(args):
    """Conservative system upgrade

    This will not go as far remove packages in order to fulfill the
    upgrade, so may leave stale packages around. Use 'dist-upgrade' to
    avoid that.
    """
    packages = util.upgradable()
    if packages:
        if args.backup:
            util.requires_package("dpkg-repack")
            util.requires_package("fakeroot")
            util.backup_before_upgrade(packages)
        command = (
            "/usr/bin/apt-get {} {} {} --show-upgraded --with-new-pkgs upgrade"
        )
        command = command.format(args.local, args.yes, args.noauth)
        perform.execute(command, root=True, log=True)
    else:
        print('No upgradeable packages. Did you run "wajig update" first?')


def upgradesecurity(args):
    """Do a security upgrade"""
    sources_list = tempfile.mkstemp(".security", "wajig.", "/tmp")[1]
    sources_file = open(sources_list, "w")
    # check dist
    sources_file.write("deb http://security.debian.org/ " +\
                       "testing/updates main contrib non-free\n")
    sources_file.close()
    command = (
        "/usr/bin/apt-get --no-list-cleanup --option Dir::Etc::SourceList="
        "{} update"
    )
    command = command.format(sources_list)
    perform.execute(command, root=True)
    command = "/usr/bin/apt-get --option Dir::Etc::SourceList={} upgrade"
    command = command.format(sources_list)
    perform.execute(command, root=True, log=True)
    if os.path.exists(sources_list):
        os.remove(sources_list)


def verify(args):
    """Check package's md5sum"""
    util.requires_package("debsums")
    perform.execute("debsums " + args.package)


def versions(args):
    """List version and distribution of given packages"""
    util.requires_package("apt-show-versions")
    if args.packages:
        for package in args.packages:
            perform.execute("apt-show-versions " + package)
    else:
        perform.execute("apt-show-versions")


def whichpackage(args):
    """Search for files matching a given pattern within packages

    Note: also searches files for uninstalled packages if apt-file is
    installed
    """
    try:
        output = perform.execute(
            "dpkg --search " + args.pattern, getoutput=True
        ).decode()
    # 'dpkg --search' returns a failing error code if it does not find matches
    except subprocess.CalledProcessError:
        installed_matches = []
    else:
        installed_matches = output.strip().split('\n')
        header = "INSTALLED MATCHES (x{})".format(len(installed_matches))
        print(header)
        print('-' * len(header))
        for line in installed_matches:
            print(line)
        print()
    if shutil.which("apt-file"):
        try:
            output = perform.execute(
                "apt-file search " + args.pattern, getoutput=True
            ).decode()
            all_matches = output.strip()
            if all_matches:
                all_matches = all_matches.split('\n')
                uninstalled_matches = set(all_matches) - set(installed_matches)
                header = "UNINSTALLED MATCHES (x{})".format(
                    len(uninstalled_matches)
                )
                print(header)
                print('-' * len(header))
                for line in uninstalled_matches:
                    print(line)
        except subprocess.CalledProcessError as error:
            if error.returncode == 3:
                print("Cache found empty... be sure to run 'apt-file update'")
            else:
                print("No results found matching '{}'".format(args.pattern))
    else:
        print("NOTE: install apt-file in order to display uninstalled matches")
