diff options
Diffstat (limited to 'dex')
-rwxr-xr-x | dex | 781 |
1 files changed, 781 insertions, 0 deletions
@@ -0,0 +1,781 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# vi: ft=python:tw=0:sw=4:ts=4:noet +# Author: Jan Christoph Ebersbach <jceb@e-jc.de> +# Last Modified: Thu 12. Jul 2012 20:16:13 +0200 CEST + +# dex +# DesktopEntry Execution, is a program to generate and execute DesktopEntry +# files of the type Application +# +# Depends: None +# +# Copyright (C) 2010, 2011, 2012, 2013 Jan Christoph Ebersbach +# +# http://www.e-jc.de/ +# +# All rights reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see <http://www.gnu.org/licenses/>. + + +import glob +import os +import subprocess +import sys + +__version__ = "0.7" + + +# DesktopEntry exceptions +class DesktopEntryTypeException(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class ApplicationExecException(Exception): + def __init__(self, value): + self.value = value + Exception.__init__(self, value) + + def __str__(self): + return repr(self.value) + + +# DesktopEntry class definitions +class DesktopEntry(object): + """ + Implements some parts of Desktop Entry specification: + http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.1.html + """ + def __init__(self, filename=None): + """ + @param filename Desktop Entry File + """ + if filename is not None and os.path.islink(filename) and \ + os.readlink(filename) == os.path.sep + os.path.join('dev', 'null'): + # ignore links to /dev/null + pass + elif filename is None or not os.path.isfile(filename): + raise IOError('File does not exist: %s' % filename) + self._filename = filename + self.groups = {} + + def __str__(self): + if self.Name: + return self.Name + elif self.filename: + return self.filename + return repr(self) + + def __lt__(self, y): + return self.filename < y.filename + + @property + def filename(self): + """ + The absolute filename + """ + return self._filename + + @classmethod + def fromfile(cls, filename): + """Create DesktopEntry for file + + @params filename Create a DesktopEntry object for file and determine + the type automatically + """ + + de = cls(filename=filename) + + # determine filetype + de_type = 'Link' + if os.path.exists(filename): + if os.path.isdir(filename): + de_type = 'Directory' + # TODO fix the value for directories + de.set_value('??', filename) + else: + de_type = 'Application' + de.set_value('Exec', filename) + de.set_value('Name', os.path.basename(filename)) + if os.name == 'posix': + whatis = subprocess.Popen(['whatis', filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = whatis.communicate() + res = stdout.decode(sys.stdin.encoding).split('- ', 1) + if len(res) == 2: + de.set_value('Comment', res[1].split(os.linesep, 1)[0]) + else: + # type Link + de.set_value('URL', filename) + + de.set_value('Type', de_type) + + return de + + def load(self): + """Load or reload contents of desktop entry file""" + self.groups = {} # clear settings + grp_desktopentry = 'Desktop Entry' + + _f = open(self.filename, 'r') + current_group = None + try: + for l in _f.readlines(): + l = l.strip('\n') + # handle comments and empty lines + if l.startswith('#') or l.strip() == '': + continue + + # handle groups + if l.startswith('['): + if not l.endswith(']'): + raise DesktopEntryTypeException("'%s' is not a valid Desktop Entry because of line '%s'." % (self.filename, l)) + group = l[1:-1] + if self.groups.get(group, None): + raise DesktopEntryTypeException("'%s' is not a valid Desktop Entry because group '%s' is specified multiple times." % (self.filename, group)) + current_group = group + continue + + # handle all the other lines + if not current_group: + raise DesktopEntryTypeException("'%s' is not a valid Desktop Entry because line '%s' does not belong to a group." % (self.filename, l)) + kv = l.split('=', 1) + if len(kv) != 2 or kv[0] == '': + raise DesktopEntryTypeException("'%s' is not a valid Desktop Entry because line '%s' is not a valid key=value pair." % (self.filename, l)) + k = kv[0] + v = kv[1] + # TODO: parse k for locale specific settings + # TODO: parse v for multivalue fields + self.set_value(k, v, current_group) + except Exception as ex: + _f.close() + raise ex + finally: + _f.close() + + if grp_desktopentry not in self.groups: + raise DesktopEntryTypeException("'%s' is not a valid Desktop Entry group is missing." % (self.filename, )) + if not (self.Type and self.Name): + raise DesktopEntryTypeException("'%s' is not a valid Desktop Entry because Type or Name keys are missing." % (self.filename, )) + _type = self.Type + if _type == 'Application': + if not self.Exec: + raise DesktopEntryTypeException("'%s' is not a valid Desktop Entry of type '%s' because Exec is missing." % (self.filename, _type)) + elif _type == 'Link': + if not self.URL: + raise DesktopEntryTypeException("'%s' is not a valid Desktop Entry of type '%s' because URL is missing." % (self.filename, _type)) + elif _type == 'Directory': + pass + else: + raise DesktopEntryTypeException("'%s' is not a valid Desktop Entry because Type '%s' is unkown." % (self.filename, self.Type)) + + # another name for load + reload = load + + def write(self, fileobject): + """Write DesktopEntry to a file + + @param fileobject DesktopEntry is written to file + """ + for group in self.groups: + fileobject.write('[%s]\n' % (group, )) + for key in self.groups[group]: + fileobject.write('%s=%s\n' % (key, self.groups[group][key])) + + def set_value(self, key, value, group='Desktop Entry'): + """ Set a key, value pair in group + + @param key Key + @param value Value + @param group The group key and value are set in. Default: Desktop Entry + """ + if group not in self.groups: + self.groups[group] = {} + self.groups[group][key] = str(value) + return value + + def _get_value(self, key, group='Desktop Entry', default=None): + if not self.groups: + self.load() + if group not in self.groups: + raise KeyError("Group '%s' not found." % group) + grp = self.groups[group] + if key not in grp: + return default + return grp[key] + + def get_boolean(self, key, group='Desktop Entry', default=False): + val = self._get_value(key, group=group, default=default) + if type(val) == bool: + return val + if val in ['true', 'True']: + return True + if val in ['false', 'False']: + return False + raise ValueError("'%s's value '%s' in group '%s' is not a boolean value." % (key, val, group)) + + def get_list(self, key, group='Desktop Entry', default=None): + list_of_strings = [] + res = self.get_string(key, group=group, default=default) + if type(res) == str: + list_of_strings = [x for x in res.split(';') if x] + return list_of_strings + + def get_string(self, key, group='Desktop Entry', default=''): + return self._get_value(key, group=group, default=default) + + def get_strings(self, key, group='Desktop Entry', default=''): + raise Exception("Not implemented yet.") + + def get_localestring(self, key, group='Desktop Entry', default=''): + raise Exception("Not implemented yet.") + + def get_numeric(self, key, group='Desktop Entry', default=0.0): + val = self._get_value(key, group=group, default=default) + if type(val) == float: + return val + return float(val) + + @property + def Type(self): + return self.get_string('Type') + + @property + def Version(self): + return self.get_string('Version') + + @property + def Name(self): + # SHOULD be localestring! + return self.get_string('Name') + + @property + def GenericName(self): + return self.get_localestring('GenericName') + + @property + def NoDisplay(self): + return self.get_boolean('NoDisplay') + + @property + def Comment(self): + return self.get_localestring('Comment') + + @property + def Icon(self): + return self.get_localestring('Icon') + + @property + def Hidden(self): + return self.get_boolean('Hidden') + + @property + def OnlyShowIn(self): + return self.get_list('OnlyShowIn') + + @property + def NotShowIn(self): + return self.get_list('NotShowIn') + + @property + def TryExec(self): + return self.get_string('TryExec') + + @property + def Exec(self): + return self.get_string('Exec') + + @property + def Path(self): + return self.get_string('Path') + + @property + def Terminal(self): + return self.get_boolean('Terminal') + + @property + def MimeType(self): + return self.get_strings('MimeType') + + @property + def Categories(self): + return self.get_strings('Categories') + + @property + def StartupNotify(self): + return self.get_boolean('StartupNotify') + + @property + def StartupWMClass(self): + return self.get_string('StartupWMClass') + + @property + def URL(self): + return self.get_string('URL') + + +class Application(DesktopEntry): + """ + Implements application files + """ + + def __init__(self, filename): + """ + @param filename Absolute path to a Desktop Entry File + """ + if not os.path.isabs(filename): + filename = os.path.join(os.getcwd(), filename) + super(Application, self).__init__(filename) + self._basename = os.path.basename(filename) + if self.Type != 'Application': + raise DesktopEntryTypeException("'%s' is not of type 'Application'." % self.filename) + + def __cmp__(self, y): + """ + @param y The object to compare the current object with - comparison is made on the property of basename + """ + if isinstance(y, Application): + return cmp(y.basename, self.basename) + return -1 + + def __eq__(self, y): + """ + @param y The object to compare the current object with - comparison is made on the property of basename + """ + if isinstance(y, Application): + return y.basename == self.basename + return False + + @property + def basename(self): + """ + The basename of file + """ + return self._basename + + @classmethod + def _build_cmd(cls, exec_string, needs_terminal=False): + """ + # test single and multi argument commands + >>> Application._build_cmd('gvim') + ['gvim'] + >>> Application._build_cmd('gvim test') + ['gvim', 'test'] + + # test quotes + >>> Application._build_cmd('"gvim" test') + ['gvim', 'test'] + >>> Application._build_cmd('"gvim test"') + ['gvim test'] + + # test escape sequences + >>> Application._build_cmd('"gvim test" test2 "test \\\\" 3"') + ['gvim test', 'test2', 'test " 3'] + >>> Application._build_cmd(r'"test \\\\\\\\ \\" moin" test') + ['test \\\\ " moin', 'test'] + >>> Application._build_cmd(r'"gvim \\\\\\\\ \\`test\\$"') + ['gvim \\\\ `test$'] + >>> Application._build_cmd(r'vim ~/.vimrc', True) + ['x-terminal-emulator', '-e', 'vim', '~/.vimrc'] + >>> Application._build_cmd('vim ~/.vimrc', False) + ['vim', '~/.vimrc'] + >>> Application._build_cmd("vim '~/.vimrc test'", False) + ['vim', '~/.vimrc test'] + >>> Application._build_cmd('vim \\'~/.vimrc " test\\'', False) + ['vim', '~/.vimrc " test'] + >>> Application._build_cmd('sh -c \\'vim ~/.vimrc " test\\'', False) + ['sh', '-c', 'vim ~/.vimrc " test'] + >>> Application._build_cmd("sh -c 'vim ~/.vimrc \\" test\\"'", False) + ['sh', '-c', 'vim ~/.vimrc " test"'] + + # expand field codes by removing them + >>> Application._build_cmd("vim %u", False) + ['vim'] + >>> Application._build_cmd("vim ~/.vimrc %u", False) + ['vim', '~/.vimrc'] + >>> Application._build_cmd("vim '%u' ~/.vimrc", False) + ['vim', '%u', '~/.vimrc'] + >>> Application._build_cmd("vim %u ~/.vimrc", False) + ['vim', '~/.vimrc'] + >>> Application._build_cmd("vim /%u/.vimrc", False) + ['vim', '//.vimrc'] + >>> Application._build_cmd("vim %u/.vimrc", False) + ['vim', '/.vimrc'] + >>> Application._build_cmd("vim %U/.vimrc", False) + ['vim', '/.vimrc'] + >>> Application._build_cmd("vim /%U/.vimrc", False) + ['vim', '//.vimrc'] + >>> Application._build_cmd("vim %U .vimrc", False) + ['vim', '.vimrc'] + + # preserved escaped field codes + >>> Application._build_cmd("vim \\\\%u ~/.vimrc", False) + ['vim', '%u', '~/.vimrc'] + + # test for non-valid field codes, they should be preserved + >>> Application._build_cmd("vim %x .vimrc", False) + ['vim', '%x', '.vimrc'] + >>> Application._build_cmd("vim %x/.vimrc", False) + ['vim', '%x/.vimrc'] + """ + cmd = [] + if needs_terminal: + cmd += ['x-terminal-emulator', '-e'] + _tmp = exec_string.replace('\\\\', '\\') + _arg = '' + in_esc = False + in_quote = False + in_singlequote = False + in_fieldcode = False + + for c in _tmp: + if in_esc: + in_esc = False + else: + if in_fieldcode: + in_fieldcode = False + if c in ('u', 'U', 'f', 'F'): + # TODO ignore field codes for the moment; at some point + # field codes should be supported + # strip %-char at the end of the argument + _arg = _arg[:-1] + continue + + if c == '"': + if in_quote: + in_quote = False + cmd.append(_arg) + _arg = '' + continue + if not in_singlequote: + in_quote = True + continue + + elif c == "'": + if in_singlequote: + in_singlequote = False + cmd.append(_arg) + _arg = '' + continue + if not in_quote: + in_singlequote = True + continue + + elif c == '\\': + in_esc = True + continue + + elif c == '%' and not (in_quote or in_singlequote): + in_fieldcode = True + + elif c == ' ' and not (in_quote or in_singlequote): + if not _arg: + continue + cmd.append(_arg) + _arg = '' + continue + + _arg += c + + if _arg and not (in_esc or in_quote or in_singlequote): + cmd.append(_arg) + elif _arg: + raise ApplicationExecException('Exec value contains an unbalanced number of quote characters.') + + return cmd + + def execute(self, dryrun=False, verbose=False): + """ + Execute application + @return Return subprocess.Popen object + """ + _exec = True + _try = self.TryExec + if _try and not (os.path.isabs(_try) and os.path.isfile(_try)) and not which(_try): + _exec = False + + if _exec: + path = self.Path + cmd = self._build_cmd(self.Exec) + if not cmd: + raise ApplicationExecException('Failed to build command string.') + if dryrun or verbose: + if verbose: + print('Autostart file: %s' % self.filename) + if path: + print('Changing directory to: ' + path) + print('Executing command: ' + ' '.join(cmd)) + if dryrun: + return None + if path: + return subprocess.Popen(cmd, cwd=path) + return subprocess.Popen(cmd) + + +class AutostartFile(Application): + """ + Implements autostart files + """ + + def __init__(self, filename): + """ + @param filename Absolute path to a Desktop Entry File + """ + super(AutostartFile, self).__init__(filename) + + +class EmptyAutostartFile(Application): + """ + Workaround for empty autostart files that don't contain the necessary data + """ + + def __init__(self, filename): + """ + @param filename Absolute path to a Desktop Entry File + """ + try: + super(EmptyAutostartFile, self).__init__(filename) + except DesktopEntryTypeException: + # ignore the missing type information + pass + + +# local methods +def which(filename): + path = os.environ.get('PATH', None) + if path: + for _p in path.split(os.pathsep): + _f = os.path.join(_p, filename) + if os.path.isfile(_f): + return _f + + +def get_autostart_directories(): + """ + Generate the list of autostart directories + """ + autostart_directories = [] # autostart directories, ordered by preference + + if args.searchdir: + autostart_directories.append(args.searchdir[0]) + else: + # generate list of autostart directories + if os.environ.get('XDG_CONFIG_HOME', None): + autostart_directories.append(os.path.join(os.environ.get('XDG_CONFIG_HOME'), 'autostart')) + else: + autostart_directories.append(os.path.join(os.environ['HOME'], '.config', 'autostart')) + + if os.environ.get('XDG_CONFIG_DIRS', None): + for d in os.environ['XDG_CONFIG_DIRS'].split(os.pathsep): + if not d: + continue + autostart_dir = os.path.join(d, 'autostart') + if autostart_dir not in autostart_directories: + autostart_directories.append(autostart_dir) + else: + autostart_directories.append(os.path.sep + os.path.join('etc', 'xdg', 'autostart')) + + return autostart_directories + + +def get_autostart_files(args, verbose=False): + """ + Generate a list of autostart files according to autostart-spec 0.5 + + TODO: do filetype recognition according to spec + """ + environment = args.environment[0].lower() if args.environment else '' + + autostart_files = [] # autostart files, excluding files marked as hidden + non_autostart_files = [] + + for d in get_autostart_directories(): + for _f in glob.glob1(d, '*.desktop'): + _f = os.path.join(d, _f) + af = None + + if os.path.isfile(_f) or os.path.islink(_f): + try: + af = AutostartFile(_f) + except DesktopEntryTypeException as ex: + af = EmptyAutostartFile(_f) + if af not in autostart_files and not af in non_autostart_files: + non_autostart_files.append(af) + continue + except ValueError as ex: + if verbose: + print(ex, file=sys.stderr) + continue + except IOError as ex: + if verbose: + print(ex, file=sys.stderr) + continue + else: + if verbose: + print('Ignoring unknown file: %s' % _f, file=sys.stderr) + continue + + if verbose: + if af.NotShowIn: + print('Not show in environments %s: %s' % (', '.join(af.NotShowIn), af.filename), file=sys.stderr) + if af.OnlyShowIn: + print('Only show in environments %s: %s' % (', '.join(af.OnlyShowIn), af.filename), file=sys.stderr) + if af in autostart_files or af in non_autostart_files: + if verbose: + print('Ignoring file, overridden by other autostart file: %s' % af.filename, file=sys.stderr) + continue + elif af.Hidden: + if verbose: + print('Ignoring file, hidden attribute is set: %s' % af.filename, file=sys.stderr) + non_autostart_files.append(af) + continue + elif environment: + if environment in [x.lower() for x in af.NotShowIn]: + if verbose: + print('Ignoring file, it must not start in specific environments (%s): %s' % (', '.join(af.NotShowIn), af.filename), file=sys.stderr) + non_autostart_files.append(af) + continue + elif af.OnlyShowIn and environment not in [x.lower() for x in af.OnlyShowIn]: + if verbose: + print('Ignoring file, it must only start in specific environments (%s): %s' % (', '.join(af.OnlyShowIn), af.filename), file=sys.stderr) + non_autostart_files.append(af) + continue + autostart_files.append(af) + if verbose: + for i in non_autostart_files: + print('Ignoring empty file: %s' % i.filename, file=sys.stderr) + return sorted(autostart_files) + + +def _test(args): + """ + run tests + """ + import doctest + doctest.testmod() + + +def _autostart(args): + """ + perform autostart + """ + if args.dryrun and args.verbose: + print('Dry run, nothing is executed.', file=sys.stderr) + + exit_value = 0 + for app in get_autostart_files(args, verbose=args.verbose): + try: + app.execute(dryrun=args.dryrun, verbose=args.verbose) + except Exception as ex: + exit_value = 1 + print("Execution faild: %s%s%s" % (app.filename, os.linesep, ex), file=sys.stderr) + + +def _run(args): + """ + execute specified DesktopEntry files + """ + if args.dryrun and args.verbose: + print('Dry run, nothing is executed.', file=sys.stderr) + + exit_value = 0 + if not args.files: + print("Nothing to execute, no DesktopEntry files specified!", file=sys.stderr) + parser.print_help() + exit_value = 1 + else: + for f in args.files: + try: + app = Application(f) + app.execute(dryrun=args.dryrun, verbose=args.verbose) + except ValueError as ex: + print(ex, file=sys.stderr) + except IOError as ex: + print(ex, file=sys.stderr) + except Exception as ex: + exit_value = 1 + print("Execution faild: %s%s%s" % (f, os.linesep, ex), file=sys.stderr) + return exit_value + + +def _create(args): + """ + create a new DesktopEntry file from the given argument + """ + target = args.create[0] + if args.verbose: + print('Creating DesktopEntry for file %s.' % target) + + de = DesktopEntry.fromfile(target) + if args.verbose: + print('Type: %s' % de.Type) + + # determine output file + output = '.'.join((os.path.basename(target), 'directory' if + de.Type == 'Directory' else 'desktop')) + if args.targetdir: + output = os.path.join(args.targetdir[0], output) + elif len(args.create) > 1: + output = args.create[1] + + if args.verbose: + print('Output: %s' % output) + targetfile = sys.stdout if output == '-' else open(output, 'w') + de.write(targetfile) + + if args.targetdir and len(args.create) > 1: + args.create = args.create[1:] + return _create(args) + return 0 + + +# start execution +if __name__ == '__main__': + from argparse import ArgumentParser + parser = ArgumentParser(usage='%(prog)s [options] [DesktopEntryFile [DesktopEntryFile ...]]', + description='dex, DesktopEntry Execution, is a program to generate and execute DesktopEntry files of the type Application', epilog='Example usage: list autostart programs: dex -ad') + parser.add_argument("--test", action="store_true", dest="test", help="perform a self-test") + parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", help="verbose output") + parser.add_argument("-V", "--version", action="store_true", dest="version", help="display version information") + parser.add_argument('files', nargs='*', help="DesktopEntry files") + + run = parser.add_argument_group('run') + run.add_argument("-a", "--autostart", action="store_true", dest="autostart", help="autostart programs") + run.add_argument("-d", "--dry-run", action="store_true", dest="dryrun", help="dry run, don't execute any command") + run.add_argument("-e", "--environment", nargs=1, dest="environment", help="specify the Desktop Environment an autostart should be performed for; works only in combination with --autostart") + run.add_argument("-s", "--search-directory", nargs=1, dest="searchdir", help="specify the directory to search for desktop files in, overriding the default search list") + + create = parser.add_argument_group('create') + create.add_argument("-c", "--create", nargs='+', dest="create", help="create a DesktopEntry file for the given program. If a second argument is provided it's taken as output filename or written to stdout (filname: -). By default a new file with the postfix .desktop is created") + create.add_argument("-t", "--target-directory", nargs=1, dest="targetdir", help="create files in target directory") + + parser.set_defaults(func=_run, dryrun=False, test=False, autostart=False, verbose=False) + + args = parser.parse_args() + if args.autostart: + args.func = _autostart + elif args.create: + args.func = _create + elif args.test: + args.func = _test + + # display version information + if args.version: + print("dex %s" % __version__) + else: + sys.exit(args.func(args)) |