summaryrefslogtreecommitdiffstats
path: root/filters/cmd_wrap.py
blob: 37f0819853913f92db03aa641ac53f5509803583 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# -*- 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ý <jpokorny @at@ Red Hat .dot. com>"

from ..filter import Filter
from ..formats.command import command

from logging import getLogger
from os import getenv
from sys import maxint
from textwrap import TextWrapper

log = getLogger(__name__)


def cmd_args_cutter(itemgroups):
    if not itemgroups:
        return itemgroups
    ret, acc = [], []
    cmd = itemgroups[0][0] if itemgroups[0] else ""
    for i in itemgroups:
        if len(i) > 1 and (not(i[0].startswith('-')) or i[0] == '-'):
            if cmd.endswith('pcs'):
                pos = -1
                end = len(i)
                while pos + 1 < end:
                    pos += 1
                    # try to cut into "firm groups"
                    if pos <= end - 4:
                        if i[pos:pos + 2] in (("resource", "create"),
                                              ("stonith", "create")):
                            # "resource/stonith create X Y" firm group
                            ret.extend(
                                filter(bool, (tuple(acc), tuple(i[pos:pos + 4])))
                            )
                            pos += 4
                            acc = list(i[pos:pos])
                            pos += len(acc) - 1
                            continue
                    if pos <= end - 3:
                        if i[pos:pos + 2] in (('property', 'set'),
                                              ('property', 'unset')):
                            # "property set/unset non-option [non-option...]"
                            ret.extend(filter(bool, (tuple(acc), )))
                            acc = list(i[pos:pos + 2])
                            pos += len(acc) - 1
                            continue
                    if pos <= end - 2:
                        if i[pos] in ("op", "meta"):
                            # "op/meta non-option [non-option...]"
                            ret.extend(filter(bool, (tuple(acc), )))
                            acc = list(i[pos:pos + 1])
                            pos += len(acc) - 1
                            continue
                    # TBD
                    acc.append(i[pos])
                ret.append(tuple(acc))
                acc = []
            else:
                ret.extend((ii, ) for ii in i)
        else:
            ret.append(i)
    return ret


@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:
    - standard textwrap module logic applied on comments splitting; otherwise:
    - 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

    Width used for rewrapping is based on three factors in precedence order:
    - text_width value inside the filter context (`flt_ctxt`)
      - 0 ~ fall-through to the value per the next item
      - -1 ~ apply no wrapping at all (mimicked by implying huge text_width)
      - positive ~ hard-limit the width (no smartness involved)
      - negative ~ high-limit the width, apply the inverse value only when not
                   exceeding the value per the next item
    - COLUMNS environmental variable, if defined and possesses integer value
    - hard-coded default of 72
    If absolute value at this point is lower than 20, fallback to 20.
    """
    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 < -1:
            tw = -tw
            tw = tw if not tw_system or tw < tw_system else tw_system
        elif tw == -1:
            tw = maxint >> 1  # one order of magnitude less to avoid overflows
    except TypeError:
        tw = tw_system
    if tw < 20:  # watch out for deliberate lower limit
        tw = 20 if tw else 72
        log.info('Text width fallback: {0}'.format(tw))
    cw = TextWrapper(width=tw, subsequent_indent='# ')  # wrapper for comments

    ret, continuation = [], []
    for line in in_obj('stringiter', protect_safe=True):
        if line.lstrip().startswith('#'):
            ret.extend(cw.wrap(line))
            continue
        # rough overapproximation of what is indeed a line continuation
        if line.endswith('\\') and not line.endswith('\\\\'):
            if '#' not in line:
                continuation.append(line[:-1])
                continue
            line += '\\'  # XXX
        line = ' '.join(continuation) + line
        continuation = []
        linecnt, rline, remains = -1, [], tw - 2  # ' \'
        itemgroups = cmd_args_cutter(command('bytestring', line)('separated'))
        itemgroups.reverse()
        while itemgroups:
            itemgroup = list(itemgroups.pop())
            itemgroup.reverse()
            while itemgroup:
                curlen = 0
                line = [itemgroup.pop()]
                curlen += len(line[-1])
                # best match fill
                while itemgroup \
                        and remains - (curlen + 1 + len(itemgroup[-1])) >= 0:
                    line.append(itemgroup.pop())
                    curlen += 1 + len(line[-1])
                # compensate for ' \' tail not necessary if very last item fits
                if not itemgroups and len(itemgroup) == 1 \
                        and len(itemgroup[-1]) == 1:
                    line.append(itemgroup.pop())
                    curlen += 1 + len(line[-1])
                # merge previous group to the current one if it fits the length
                if rline and not itemgroup \
                        and remains - (curlen + 1 + len(' '.join(rline))) >= 0:
                    line = rline + line
                    rline = []
                    linecnt -= 1
                # second pass optionally handles the terminal propagation
                for i in xrange(2):
                    if rline:
                        tail = ' \\' if rline is not line else ''
                        rline = ' '.join(rline)
                        if not linecnt:
                            ret.append(rline + tail)
                            remains -= 2  # initial indent
                        else:
                            ret.append('  ' + rline + tail)
                    linecnt += 1
                    rline = line
                    if itemgroups or itemgroup:
                        break
    return ('stringiter', ret)