Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
executable file 784 lines (663 sloc) 22.929 kb
#!/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.searchpaths:
for p in args.searchpaths[0].split(os.pathsep):
path = os.path.expanduser(p)
path = os.path.expandvars(path)
autostart_directories += [path]
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-paths", nargs=1, dest="searchpaths", help="colon separated list of paths to search for desktop files, 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))
Jump to Line
Something went wrong with that request. Please try again.