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 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 try: 65 self._replacements = tuple( 66 (key, env.get(key) if value is None else value) 67 for key, value in env.replacements 68 ) 69 self._outs = outs 70 71 env.accept(self) 72 73 finally: 74 self._replacements = () 75 self._outs = None 76 77 def _apply_replacements(self, action): 78 value = action.value 79 for var, replacement in self._replacements: 80 if var != action.name: 81 value = value.replace(replacement, '${}'.format(var)) 82 return value 83 84 def visit_set(self, set): # pylint: disable=redefined-builtin 85 value = self._apply_replacements(set) 86 self._outs.write( 87 '{name}="{value}"\nexport {name}\n'.format( 88 name=set.name, value=value 89 ) 90 ) 91 92 def visit_clear(self, clear): 93 self._outs.write('unset {name}\n'.format(**vars(clear))) 94 95 def visit_remove(self, remove): 96 value = self._apply_replacements(remove) 97 self._outs.write( 98 '# Remove \n# {value}\n# from {name} before adding it ' 99 'back.\n'.format(value=remove.value, name=remove.name) 100 ) 101 self._outs.write(self._remove_value_from_path(remove.name, value)) 102 103 def _join(self, *args): 104 if len(args) == 1 and isinstance(args[0], (list, tuple)): 105 args = args[0] 106 return self._pathsep.join(args) 107 108 def visit_prepend(self, prepend): 109 value = self._apply_replacements(prepend) 110 value = self._join(value, '${}'.format(prepend.name)) 111 self._outs.write( 112 '{name}="{value}"\nexport {name}\n'.format( 113 name=prepend.name, value=value 114 ) 115 ) 116 117 def visit_append(self, append): 118 value = self._apply_replacements(append) 119 value = self._join('${}'.format(append.name), value) 120 self._outs.write( 121 '{name}="{value}"\nexport {name}\n'.format( 122 name=append.name, value=value 123 ) 124 ) 125 126 def visit_echo(self, echo): 127 # TODO(mohrr) use shlex.quote(). 128 self._outs.write('if [ -z "${PW_ENVSETUP_QUIET:-}" ]; then\n') 129 if echo.newline: 130 self._outs.write(' echo "{}"\n'.format(echo.value)) 131 else: 132 self._outs.write(' echo -n "{}"\n'.format(echo.value)) 133 self._outs.write('fi\n') 134 135 def visit_comment(self, comment): 136 for line in comment.value.splitlines(): 137 self._outs.write('# {}\n'.format(line)) 138 139 def visit_command(self, command): 140 # TODO(mohrr) use shlex.quote here? 141 self._outs.write('{}\n'.format(' '.join(command.command))) 142 if not command.exit_on_error: 143 return 144 145 # Assume failing command produced relevant output. 146 self._outs.write('if [ "$?" -ne 0 ]; then\n return 1\nfi\n') 147 148 def visit_doctor(self, doctor): 149 self._outs.write('if [ -z "$PW_ACTIVATE_SKIP_CHECKS" ]; then\n') 150 self.visit_command(doctor) 151 self._outs.write('else\n') 152 self._outs.write( 153 'echo Skipping environment check because ' 154 'PW_ACTIVATE_SKIP_CHECKS is set\n' 155 ) 156 self._outs.write('fi\n') 157 158 def visit_blank_line(self, blank_line): 159 del blank_line 160 self._outs.write('\n') 161 162 def visit_function(self, function): 163 self._outs.write( 164 '{name}() {{\n{body}\n}}\n'.format( 165 name=function.name, body=function.body 166 ) 167 ) 168 169 170class DeactivateShellVisitor(_BaseShellVisitor): 171 """Removes values from an Environment.""" 172 173 def __init__(self, *args, **kwargs): 174 pathsep = kwargs.pop('pathsep', ':') 175 super(DeactivateShellVisitor, self).__init__(*args, **kwargs) 176 self._pathsep = pathsep 177 178 def serialize(self, env, outs): 179 try: 180 self._outs = outs 181 182 env.accept(self) 183 184 finally: 185 self._outs = None 186 187 def visit_set(self, set): # pylint: disable=redefined-builtin 188 if set.deactivate: 189 self._outs.write('unset {name}\n'.format(name=set.name)) 190 191 def visit_clear(self, clear): 192 pass # Not relevant. 193 194 def visit_remove(self, remove): 195 pass # Not relevant. 196 197 def visit_prepend(self, prepend): 198 self._outs.write( 199 self._remove_value_from_path(prepend.name, prepend.value) 200 ) 201 202 def visit_append(self, append): 203 self._outs.write( 204 self._remove_value_from_path(append.name, append.value) 205 ) 206 207 def visit_echo(self, echo): 208 pass # Not relevant. 209 210 def visit_comment(self, comment): 211 pass # Not relevant. 212 213 def visit_command(self, command): 214 pass # Not relevant. 215 216 def visit_doctor(self, doctor): 217 pass # Not relevant. 218 219 def visit_blank_line(self, blank_line): 220 pass # Not relevant. 221 222 def visit_function(self, function): 223 pass # Not relevant. 224