#!/usr/bin/env python
'''
    %prog - A simpler interactive interface to Debian APT commands.
    (C) 2012-2018 Mike Miller, License: GPLv3+

    %prog [options] COMMAND ARGS
    %prog           # with no arguments to list available commands
'''
from __future__ import print_function

import sys, os, signal
from subprocess import call
from optparse import OptionParser, BadOptionError, AmbiguousOptionError
try:
    from urlparse import urlparse
except ImportError:
    from urllib.parse import urlparse

# initialization
__version__ = '1.18'
use_sudo = ('apt-get', 'apt-get remove', 'aptitude', 'apt-key',
            'add-apt-repository ', 'add-apt-repository --remove',
            'dpkg --install', 'apt-get install')

# map commands to their appropriate binaries:
cmd_map = {
    # apt-get
    'autoclean': 'apt-get',
    'autoremove': 'apt-get',
    'build-dep': 'apt-get',
    'changelog': 'apt-get',
    'check': 'apt-get',
    'clean': 'apt-get',
    'dist-upgrade': 'apt-get',
    'download': 'apt-get',
    'dselect-upgrade': 'apt-get',
    'install': 'apt-get',
    'in': 'apt-get install',        # alias
    'markauto': 'apt-get',
    'purge': 'apt-get',
    'remove': 'apt-get',
    'rm': 'apt-get remove',         # alias
    'source': 'apt-get',
    'unmarkauto': 'apt-get',
    'update': 'apt-get',
    'upgrade': 'apt-get',

    # cache
    'depends': 'apt-cache',
    'dotty': 'apt-cache',
    'dump': 'apt-cache',
    'dumpavail': 'apt-cache',
    'gencaches': 'apt-cache',
    'pkgnames': 'apt-cache',
    'policy': 'apt-cache',
    'rdepends': 'apt-cache',
    'search': 'apt-cache',
    'se': 'apt-cache search',       # alias
    'show': 'apt-cache',
    'showauto': 'apt-cache',
    'showpkg': 'apt-cache',
    'showsrc': 'apt-cache',
    'stats': 'apt-cache',
    'unmet': 'apt-cache',
    'xvcg': 'apt-cache',

    # config
    'shell': 'apt-config',
    'dumpcon': 'apt-config',

    # aptitude
    'hold': 'aptitude',
    'unhold': 'aptitude',
    'reinstall': 'aptitude',
    'why': 'aptitude',
    'why-not': 'aptitude',

    # more
    'fingerprint': 'apt-key',
    'dir': 'dpkg-query --list',
    'ls': 'dpkg --get-selections',          # alias
    'status': 'dpkg --status',
    'listfiles': 'dpkg-query --listfiles',
    'searchfiles': 'dpkg-query --search',
    'who-owns': 'dpkg-query --search',      # alias
    'instdeb': 'dpkg --install',
    'addrepo': 'add-apt-repository ',  # trailing space hack to override cmd
    'rmrepo': 'add-apt-repository --remove',
}
commands = sorted(set(cmd_map.keys()))  # make a list for the user


class PassThroughParser(OptionParser):
    ''' An unknown-option pass-through implementation of OptionParser.
        http://stackoverflow.com/a/9307174/450917

        When unknown options are encountered, bundle with argument list and
        try again, until arguments are depleted.
        parser.error() will still be called if a known argument is passed
        incorrectly (e.g. missing arguments or bad argument types, etc.)
    '''
    def _process_args(self, largs, rargs, values):
        while rargs:
            try:
                OptionParser._process_args(self, largs, rargs, values)
            except (BadOptionError, AmbiguousOptionError) as err:
                largs.append(err.opt_str)


def download(url):
    ''' Download a file to $TEMP and return its path, with progress.
        http://stackoverflow.com/a/22776/450917
    '''
    import tempfile
    from os.path import join
    try:
        from urllib2 import urlopen
    except ImportError:
        from urllib.request import urlopen

    file_size, downloaded = 0, 0
    block_sz = 8192
    try:
        filename = url.split('/')[-1]
        tempname = join(tempfile.gettempdir(), filename)
        infile = urlopen(url)
        outfile = open(tempname, 'wb')
        meta = infile.info()
        try:
            file_size = int(meta.getheaders('Content-Length')[0])
        except AttributeError:
            file_size = int(meta.get_all('Content-Length')[0])
        except IndexError:
            pass
        print('Downloading: "%s"  Bytes: %s' % (filename, file_size))
        while True:
            buffer = infile.read(block_sz)
            if not buffer:
                break
            outfile.write(buffer)
            if file_size:
                downloaded += len(buffer)
                print('\r  %10d  [%3.2f%%]' % (downloaded,
                                               downloaded*100.0 / file_size)),
        outfile.close()
        print()
    except Exception as err:
        print('Error:', err.__class__.__name__, str(err))
        tempname = None
    return tempname


def get_version():
    'Read distro/codename from the lsb-release file.'
    release, codename = None, None
    try:
        with open('/etc/lsb-release') as f:
            for line in f:
                if line.startswith('DISTRIB_CODENAME'):
                    _, _, codename = line.rstrip().partition('=')
                elif line.startswith('DISTRIB_RELEASE'):
                    _, _, release = line.rstrip().partition('=')
    except IOError as err:
        print(err.__class__.__name__, err)
    return release, codename


def is_uniq(fragment):
    ''' Check whether a given command fragment uniquely identifies a command.
        Returns:  boolean, first command or None, all matches.
    '''
    results = [ c  for c in commands  if c.startswith(fragment) ]
    uniq = (len(results) == 1)
    return uniq, (results or [None])[0], results


def main(argv):
    # check & prepare args
    parser = PassThroughParser(usage=__doc__.rstrip(), version=__version__)
    parser.add_option('-d', '--debug', action='store_true', default=True,
                      help='print command-line before execution, deprecated.')
    opts, args = parser.parse_args(argv[1:])

    if args:
        command = args[0]
    else:
        print('Error: no command given.  Must be one of: \n\n(%s).\n' % (
              ', '.join(commands)))
        return os.EX_USAGE

    # find applicable binary, command
    if command in cmd_map:
        binary = cmd_map[command]
    else:
        uniq, command, matches = is_uniq(command)
        if not uniq:
            print('Error: %s command, %s --> %s' % (
                        ('ambiguous' if matches else 'unknown'),
                        args[0], matches)
            )
            return os.EX_USAGE
        if opts.debug:
            print('* Unique:', args[0], '-->', command)
        binary = cmd_map[command]

    # shift, as arg 0 no longer needed
    args = args[1:]

    # download .deb urls
    if command == 'instdeb':
        if args and urlparse(args[0]).scheme:
            path = download(args[0])
            if not path:
                return os.EX_UNAVAILABLE
            args[0] = path
    elif command == 'addrepo':
        if args:
            if args[0].startswith('ppa:'):  pass
            else:   # 12.10 add-apt-repository can enable std repos by name.
                release, codename = get_version()
                if not codename:
                    print('Exiting.');  return os.EX_OSFILE
                if release < '12.10':
                    args = ['"deb http://archive.ubuntu.com/ubuntu %s %s"' %
                            (codename, ' '.join(args)) ]
        else:
            print('Error: addrepo requires ppa or repo name as argument.')
            return os.EX_USAGE

    # should this be elevated?
    prefix = ''
    if binary in use_sudo and os.geteuid() != 0:
        prefix = 'sudo '

    # check if binary uses a flag instead of command
    if ' ' in binary:
        binary, command = binary.split(' ', 1)

    # build command line
    cmdline = (prefix + ' '.join([binary, command] + args)).replace('  ', ' ')
    if opts.debug:
        print('* Running:', cmdline)

    return call(cmdline, shell=True)


# http://youtu.be/0hiUuL5uTKc?t=8s
try:
    sys.exit(main(sys.argv))
except KeyboardInterrupt:
    print('\nWarning: Ctrl+C entered, exiting.', file=sys.stderr)
    sys.exit(signal.SIGINT)
