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