From 52d54f461b0a7f6cd7a06b4288d64e807368d4dd Mon Sep 17 00:00:00 2001 From: Jan Pokorný Date: Mon, 22 Jun 2015 23:04:02 +0200 Subject: filters/cmd-wrap: new filter (promoting command format) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan Pokorný --- ext-plugins/lib-general/formats/command.py | 95 ------------------------------ filters/cmd_wrap.py | 60 +++++++++++++++++++ formats/command.py | 95 ++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 95 deletions(-) delete mode 100644 ext-plugins/lib-general/formats/command.py create mode 100644 filters/cmd_wrap.py create mode 100644 formats/command.py diff --git a/ext-plugins/lib-general/formats/command.py b/ext-plugins/lib-general/formats/command.py deleted file mode 100644 index 04a49ad..0000000 --- a/ext-plugins/lib-general/formats/command.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright 2015 Red Hat, Inc. -# Part of clufter project -# Licensed under GPLv2+ (a copy included | http://gnu.org/licenses/gpl-2.0.txt) -"""Format representing merged/isolated (1/2 levels) of single command to exec""" -__author__ = "Jan Pokorný " - -try: - from collections import OrderedDict -except ImportError: - from ordereddict import OrderedDict -from logging import getLogger - -log = getLogger(__name__) - -from ..format import SimpleFormat -from ..protocol import Protocol -from ..utils import head_tail -from ..utils_func import apply_intercalate - - -class command(SimpleFormat): - native_protocol = SEPARATED = Protocol('separated') - BYTESTRING = SimpleFormat.BYTESTRING - DICT = Protocol('dict') - MERGED = Protocol('merged') - - @SimpleFormat.producing(BYTESTRING, chained=True) - def get_bytestring(self, *protodecl): - """Return command as canonical single string""" - # chained fallback - return ' '.join(self.MERGED(protect_safe=True)) - - @SimpleFormat.producing(SEPARATED, protect=True) - def get_separated(self, *protodecl): - merged = self.MERGED() - merged.reverse() - ret, acc = [], [] - while merged: - i = merged.pop() - if acc == ['--'] or i is None or i.startswith('-') and i != '-': - if acc: - ret.append(tuple(acc)) - acc = [] if i is None else [i] - elif self._dict.get('magic_split', False): - acc.extend(i.split('::')) # magic "::"-split - merged.append(None) - else: - acc.append(i) - # expect that, by convention, option takes at most a single argument - ret.extend(filter(bool, (tuple(acc[:2]), tuple(acc[2:])))) - return ret - - @SimpleFormat.producing(MERGED, protect=True) - def get_merged(self, *protodecl): - # try to look (indirectly) if we have "separated" at hand first - if self.BYTESTRING in self._representations: # break the possible loop - from shlex import split - ret = split(self.BYTESTRING()) - for i, lexeme in enumerate(ret[:]): - # heuristic(!) method to normalize: '-a=b' -> '-a', 'b' - if (lexeme.count('=') == 1 and - ('"' not in lexeme or lexeme.count('"') % 2) and - ("'" not in lexeme or lexeme.count("'") % 2)): - ret[i:i + 1] = lexeme.split('=') - elif self.DICT in self._representations: # break the possible loop (2) - d = self.DICT(protect_safe=True) - if not isinstance(d, OrderedDict): - log.warning("'{0}' format: not backed by OrderedDict".format( - self.__class__.name - )) - ret = list(d.get('__cmd__', ())) - ret.extend((k, v) for k, vs in d.iteritems() for v in (vs or ((), )) - if k not in ('__cmd__', '__args__')) - ret.extend(d.get('__args__', ())) - else: - ret = self.SEPARATED(protect_safe=True) - return apply_intercalate(ret) - - @SimpleFormat.producing(DICT, protect=True) - # not a perfectly bijective mapping, this is a bit lossy representation, - # on the other hand it canonicalizes the notation when turned to other forms - def get_dict(self, *protodecl): - separated = self.SEPARATED() - separated.reverse() - ret = OrderedDict() - arg_bucket = '__cmd__' - while separated: - head, tail = head_tail(separated.pop()) - if head.startswith('-') and head != '-': - arg_bucket = '__args__' - else: - head, tail = arg_bucket, head - ret.setdefault(head, []).append(tail) - return ret diff --git a/filters/cmd_wrap.py b/filters/cmd_wrap.py new file mode 100644 index 0000000..4ac5d21 --- /dev/null +++ b/filters/cmd_wrap.py @@ -0,0 +1,60 @@ +# -*- coding: UTF-8 -*- +# Copyright 2015 Red Hat, Inc. +# Part of clufter project +# Licensed under GPLv2+ (a copy included | http://gnu.org/licenses/gpl-2.0.txt) +"""cmd-wrap filter""" +__author__ = "Jan Pokorný " + +from ..filter import Filter +from ..formats.command import command + +from os import getenv +from textwrap import TextWrapper + + +@Filter.deco('string-iter', 'string-iter') +def cmd_wrap(flt_ctxt, in_obj): + """Try to apply a bit smarter wrapping on lines carrying shell commands + + Smarter means: + - do not delimit option from its argument + - when line is broken vertically, append backslash for the continuation + and indent subsequent lines for visual clarity + - and as a side-effect: normalize whitespace occurrences + """ + try: + tw_system = int(getenv('COLUMNS')) + except TypeError: + tw_system = 0 + try: + tw = int(flt_ctxt.get('text_width')) + if not tw: + raise TypeError + elif tw < 0: + tw = -tw + tw = tw if not tw_system or tw < tw_system else tw_system + except TypeError: + tw = tw_system + if not tw: + tw = 72 + cw = TextWrapper(width=tw, subsequent_indent='# ') # wrapper for comments + + ret = [] + for line in in_obj('stringiter', protect_safe=True): + if line.lstrip().startswith('#'): + ret.append(cw.wrap(line)) + continue + linecnt, rline, remains = 0, [], tw - 2 # ' \' + for itemgroup in command('bytestring', line)('separated'): + item = ' '.join(itemgroup) + fills = len(item) + (1 if rline else 0) + # also deal with text width overflow on the first line + if (remains - fills >= 0 or not rline): + rline.append(item) + remains -= fills + else: + ret.append((' ' if linecnt else '') + ' '.join(rline) + ' \\') + linecnt += 1 + rline, remains = [item], tw - len(item) - 4 # ' \' & ident + ret.append((' ' if linecnt else '') + ' '.join(rline)) + return ('stringiter', ret) diff --git a/formats/command.py b/formats/command.py new file mode 100644 index 0000000..04a49ad --- /dev/null +++ b/formats/command.py @@ -0,0 +1,95 @@ +# -*- coding: UTF-8 -*- +# Copyright 2015 Red Hat, Inc. +# Part of clufter project +# Licensed under GPLv2+ (a copy included | http://gnu.org/licenses/gpl-2.0.txt) +"""Format representing merged/isolated (1/2 levels) of single command to exec""" +__author__ = "Jan Pokorný " + +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict +from logging import getLogger + +log = getLogger(__name__) + +from ..format import SimpleFormat +from ..protocol import Protocol +from ..utils import head_tail +from ..utils_func import apply_intercalate + + +class command(SimpleFormat): + native_protocol = SEPARATED = Protocol('separated') + BYTESTRING = SimpleFormat.BYTESTRING + DICT = Protocol('dict') + MERGED = Protocol('merged') + + @SimpleFormat.producing(BYTESTRING, chained=True) + def get_bytestring(self, *protodecl): + """Return command as canonical single string""" + # chained fallback + return ' '.join(self.MERGED(protect_safe=True)) + + @SimpleFormat.producing(SEPARATED, protect=True) + def get_separated(self, *protodecl): + merged = self.MERGED() + merged.reverse() + ret, acc = [], [] + while merged: + i = merged.pop() + if acc == ['--'] or i is None or i.startswith('-') and i != '-': + if acc: + ret.append(tuple(acc)) + acc = [] if i is None else [i] + elif self._dict.get('magic_split', False): + acc.extend(i.split('::')) # magic "::"-split + merged.append(None) + else: + acc.append(i) + # expect that, by convention, option takes at most a single argument + ret.extend(filter(bool, (tuple(acc[:2]), tuple(acc[2:])))) + return ret + + @SimpleFormat.producing(MERGED, protect=True) + def get_merged(self, *protodecl): + # try to look (indirectly) if we have "separated" at hand first + if self.BYTESTRING in self._representations: # break the possible loop + from shlex import split + ret = split(self.BYTESTRING()) + for i, lexeme in enumerate(ret[:]): + # heuristic(!) method to normalize: '-a=b' -> '-a', 'b' + if (lexeme.count('=') == 1 and + ('"' not in lexeme or lexeme.count('"') % 2) and + ("'" not in lexeme or lexeme.count("'") % 2)): + ret[i:i + 1] = lexeme.split('=') + elif self.DICT in self._representations: # break the possible loop (2) + d = self.DICT(protect_safe=True) + if not isinstance(d, OrderedDict): + log.warning("'{0}' format: not backed by OrderedDict".format( + self.__class__.name + )) + ret = list(d.get('__cmd__', ())) + ret.extend((k, v) for k, vs in d.iteritems() for v in (vs or ((), )) + if k not in ('__cmd__', '__args__')) + ret.extend(d.get('__args__', ())) + else: + ret = self.SEPARATED(protect_safe=True) + return apply_intercalate(ret) + + @SimpleFormat.producing(DICT, protect=True) + # not a perfectly bijective mapping, this is a bit lossy representation, + # on the other hand it canonicalizes the notation when turned to other forms + def get_dict(self, *protodecl): + separated = self.SEPARATED() + separated.reverse() + ret = OrderedDict() + arg_bucket = '__cmd__' + while separated: + head, tail = head_tail(separated.pop()) + if head.startswith('-') and head != '-': + arg_bucket = '__args__' + else: + head, tail = arg_bucket, head + ret.setdefault(head, []).append(tail) + return ret -- cgit