• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Serializes an Environment into a shell file."""
15
16import inspect
17
18# Disable super() warnings since this file must be Python 2 compatible.
19# pylint: disable=super-with-arguments
20
21
22class _BaseShellVisitor(object):  # pylint: disable=useless-object-inheritance
23    def __init__(self, *args, **kwargs):
24        pathsep = kwargs.pop('pathsep', ':')
25        super(_BaseShellVisitor, self).__init__(*args, **kwargs)
26        self._pathsep = pathsep
27        self._outs = None
28
29    def _remove_value_from_path(self, variable, value):
30        return (
31            '{variable}="$(echo "${variable}"'
32            ' | sed "s|{pathsep}{value}{pathsep}|{pathsep}|g;"'
33            ' | sed "s|^{value}{pathsep}||g;"'
34            ' | sed "s|{pathsep}{value}$||g;"'
35            ')"\nexport {variable}\n'.format(
36                variable=variable, value=value, pathsep=self._pathsep
37            )
38        )
39
40    def visit_hash(self, hash):  # pylint: disable=redefined-builtin
41        del hash
42        self._outs.write(
43            inspect.cleandoc(
44                '''
45        # This should detect bash and zsh, which have a hash command that must
46        # be called to get it to forget past commands. Without forgetting past
47        # commands the $PATH changes we made may not be respected.
48        if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
49            hash -r\n
50        fi
51        '''
52            )
53        )
54
55
56class ShellVisitor(_BaseShellVisitor):
57    """Serializes an Environment into a bash-like shell file."""
58
59    def __init__(self, *args, **kwargs):
60        super(ShellVisitor, self).__init__(*args, **kwargs)
61        self._replacements = ()
62
63    def serialize(self, env, outs):
64        """Write a shell file based on the given environment.
65
66        Args:
67            env (environment.Environment): Environment variables to use.
68            outs (file): Shell file to write.
69        """
70        try:
71            self._replacements = tuple(
72                (key, env.get(key) if value is None else value)
73                for key, value in env.replacements
74            )
75            self._outs = outs
76
77            env.accept(self)
78
79        finally:
80            self._replacements = ()
81            self._outs = None
82
83    def _apply_replacements(self, action):
84        value = action.value
85        for var, replacement in self._replacements:
86            if var != action.name:
87                value = value.replace(replacement, '${}'.format(var))
88        return value
89
90    def visit_set(self, set):  # pylint: disable=redefined-builtin
91        value = self._apply_replacements(set)
92        self._outs.write(
93            '{name}="{value}"\nexport {name}\n'.format(
94                name=set.name, value=value
95            )
96        )
97
98    def visit_clear(self, clear):
99        self._outs.write('unset {name}\n'.format(**vars(clear)))
100
101    def visit_remove(self, remove):
102        value = self._apply_replacements(remove)
103        self._outs.write(
104            '# Remove \n#   {value}\n# from {name} before adding it '
105            'back.\n'.format(value=remove.value, name=remove.name)
106        )
107        self._outs.write(self._remove_value_from_path(remove.name, value))
108
109    def _join(self, *args):
110        if len(args) == 1 and isinstance(args[0], (list, tuple)):
111            args = args[0]
112        return self._pathsep.join(args)
113
114    def visit_prepend(self, prepend):
115        value = self._apply_replacements(prepend)
116        value = self._join(value, '${}'.format(prepend.name))
117        self._outs.write(
118            '{name}="{value}"\nexport {name}\n'.format(
119                name=prepend.name, value=value
120            )
121        )
122
123    def visit_append(self, append):
124        value = self._apply_replacements(append)
125        value = self._join('${}'.format(append.name), value)
126        self._outs.write(
127            '{name}="{value}"\nexport {name}\n'.format(
128                name=append.name, value=value
129            )
130        )
131
132    def visit_echo(self, echo):
133        # TODO(mohrr) use shlex.quote().
134        self._outs.write('if [ -z "${PW_ENVSETUP_QUIET:-}" ]; then\n')
135        if echo.newline:
136            self._outs.write('  echo "{}"\n'.format(echo.value))
137        else:
138            self._outs.write('  echo -n "{}"\n'.format(echo.value))
139        self._outs.write('fi\n')
140
141    def visit_comment(self, comment):
142        for line in comment.value.splitlines():
143            self._outs.write('# {}\n'.format(line))
144
145    def visit_command(self, command):
146        # TODO(mohrr) use shlex.quote here?
147        self._outs.write('{}\n'.format(' '.join(command.command)))
148        if not command.exit_on_error:
149            return
150
151        # Assume failing command produced relevant output.
152        self._outs.write('if [ "$?" -ne 0 ]; then\n  return 1\nfi\n')
153
154    def visit_doctor(self, doctor):
155        self._outs.write('if [ -z "$PW_ACTIVATE_SKIP_CHECKS" ]; then\n')
156        self.visit_command(doctor)
157        self._outs.write('else\n')
158        self._outs.write(
159            'echo Skipping environment check because '
160            'PW_ACTIVATE_SKIP_CHECKS is set\n'
161        )
162        self._outs.write('fi\n')
163
164    def visit_blank_line(self, blank_line):
165        del blank_line
166        self._outs.write('\n')
167
168    def visit_function(self, function):
169        self._outs.write(
170            '{name}() {{\n{body}\n}}\n'.format(
171                name=function.name, body=function.body
172            )
173        )
174
175
176class DeactivateShellVisitor(_BaseShellVisitor):
177    """Removes values from a bash-like shell environment."""
178
179    def __init__(self, *args, **kwargs):
180        pathsep = kwargs.pop('pathsep', ':')
181        super(DeactivateShellVisitor, self).__init__(*args, **kwargs)
182        self._pathsep = pathsep
183
184    def serialize(self, env, outs):
185        try:
186            self._outs = outs
187
188            env.accept(self)
189
190        finally:
191            self._outs = None
192
193    def visit_set(self, set):  # pylint: disable=redefined-builtin
194        if set.deactivate:
195            self._outs.write('unset {name}\n'.format(name=set.name))
196
197    def visit_clear(self, clear):
198        pass  # Not relevant.
199
200    def visit_remove(self, remove):
201        pass  # Not relevant.
202
203    def visit_prepend(self, prepend):
204        self._outs.write(
205            self._remove_value_from_path(prepend.name, prepend.value)
206        )
207
208    def visit_append(self, append):
209        self._outs.write(
210            self._remove_value_from_path(append.name, append.value)
211        )
212
213    def visit_echo(self, echo):
214        pass  # Not relevant.
215
216    def visit_comment(self, comment):
217        pass  # Not relevant.
218
219    def visit_command(self, command):
220        pass  # Not relevant.
221
222    def visit_doctor(self, doctor):
223        pass  # Not relevant.
224
225    def visit_blank_line(self, blank_line):
226        pass  # Not relevant.
227
228    def visit_function(self, function):
229        pass  # Not relevant.
230
231
232class FishShellVisitor(ShellVisitor):
233    """Serializes an Environment into a fish shell file."""
234
235    def __init__(self, *args, **kwargs):
236        super(FishShellVisitor, self).__init__(*args, **kwargs)
237        self._pathsep = ' '
238
239    def _remove_value_from_path(self, variable, value):
240        return 'set PATH (string match -v {value} ${variable})\n'.format(
241            variable=variable, value=value
242        )
243
244    def visit_set(self, set):  # pylint: disable=redefined-builtin
245        value = self._apply_replacements(set)
246        self._outs.write(
247            'set -x {name} {value}\n'.format(name=set.name, value=value)
248        )
249
250    def visit_clear(self, clear):
251        self._outs.write('set -e {name}\n'.format(**vars(clear)))
252
253    def visit_remove(self, remove):
254        value = self._apply_replacements(remove)
255        self._remove_value_from_path(remove.name, value)
256
257    def visit_prepend(self, prepend):
258        value = self._apply_replacements(prepend)
259        self._outs.write(
260            'set -x --prepend {name} {value}\n'.format(
261                name=prepend.name, value=value
262            )
263        )
264
265    def visit_append(self, append):
266        value = self._apply_replacements(append)
267        self._outs.write(
268            'set -x --append {name} {value}\n'.format(
269                name=append.name, value=value
270            )
271        )
272
273    def visit_echo(self, echo):
274        self._outs.write('if not set -q PW_ENVSETUP_QUIET\n')
275        if echo.newline:
276            self._outs.write('  echo "{}"\n'.format(echo.value))
277        else:
278            self._outs.write('  echo -n "{}"\n'.format(echo.value))
279        self._outs.write('end\n')
280
281    def visit_hash(self, hash):  # pylint: disable=redefined-builtin
282        del hash
283
284    def visit_function(self, function):
285        self._outs.write(
286            'function {name}\n{body}\nend\n'.format(
287                name=function.name, body=function.body
288            )
289        )
290
291    def visit_command(self, command):
292        self._outs.write('{}\n'.format(' '.join(command.command)))
293        if not command.exit_on_error:
294            return
295
296        # Assume failing command produced relevant output.
297        self._outs.write('if test $status -ne 0\n  return 1\nend\n')
298
299    def visit_doctor(self, doctor):
300        self._outs.write('if not set -q PW_ACTIVATE_SKIP_CHECKS\n')
301        self.visit_command(doctor)
302        self._outs.write('else\n')
303        self._outs.write(
304            'echo Skipping environment check because '
305            'PW_ACTIVATE_SKIP_CHECKS is set\n'
306        )
307        self._outs.write('end\n')
308
309
310class DeactivateFishShellVisitor(FishShellVisitor):
311    """Removes values from a fish shell environment."""
312
313    def serialize(self, env, outs):
314        try:
315            self._outs = outs
316
317            env.accept(self)
318
319        finally:
320            self._outs = None
321
322    def visit_set(self, set):  # pylint: disable=redefined-builtin
323        if set.deactivate:
324            self._outs.write('set -e {name}\n'.format(name=set.name))
325
326    def visit_clear(self, clear):
327        pass  # Not relevant.
328
329    def visit_remove(self, remove):
330        pass  # Not relevant.
331
332    def visit_prepend(self, prepend):
333        self._outs.write(
334            self._remove_value_from_path(prepend.name, prepend.value)
335        )
336
337    def visit_append(self, append):
338        self._outs.write(
339            self._remove_value_from_path(append.name, append.value)
340        )
341
342    def visit_echo(self, echo):
343        pass  # Not relevant.
344
345    def visit_comment(self, comment):
346        pass  # Not relevant.
347
348    def visit_command(self, command):
349        pass  # Not relevant.
350
351    def visit_doctor(self, doctor):
352        pass  # Not relevant.
353
354    def visit_blank_line(self, blank_line):
355        pass  # Not relevant.
356
357    def visit_function(self, function):
358        pass  # Not relevant.
359