Switching the working directory between parallel hierarchies, in particular code and tests, remains a standard operation. My simple substitution script still serves its purpose well for this task. However, sooner or later one has to run into the situation that those hierarchies are not quite as parallel. (Looking back, I'm actually surprised that it took me over 5 years to run into that situation.) E.g., one could have src versus src/test, so that the parallel test path to /home/aehlig/Projects/foo/src/bar/baz would be /home/aehlig/Projects/foo/src/test/bar/baz. In this case, the number of path segments changes. Of course, I could cheat by substituting in src/test or the empty string, but this is not the way I think of this operation.
So I decided to extend my script by adding first-class addition and removal. To keep the command-line interface simple, it still takes a single argument; if that starts with +, addition of the rest is attempted, if it starts with -, removal of a path component called like the rest is attempted. In this way, the switching operations become cd `spwd +test` and cd `spwd -test`. Of course, this interface overlaps with the standard substitution semantics, as substituting the whole argument (including the leading plus or minus sign) for a path segment, might result in a valid directory as well. But it turns out that the heuristics work sufficiently well, as I don't have too many directories that have a name starting with + or -. And, the whole substitution approach is a heuristics anyway, as there can be more than one place where the substitution yields a valid directory.
While touching the script anyway, also
#!/usr/bin/env python3 ## Print the current directory with argv substituted in ## at an appropriate place in the path; here appropriate means ## the uppermost place, such that the resulting directory exists. ## If argv starts with +, first search for a place to add the ## rest of argv; similarly, if argv starts with -, first ## try to drop a component named as the rest of argv # Copyright (C) 2015--2021 Klaus Aehlig. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import os import sys def substpath(path, newpiece): """Substitute the new directory at a fitting place. """ components = path.split(os.path.sep) for i in range(len(components)): newcomponents = components[:] newcomponents[i] = newpiece newpath = os.path.sep.join(newcomponents) if os.path.isdir(newpath): return newpath return path def droppath(path, todrop): components = path.split(os.path.sep) for i in range(len(components)): if components[i] == todrop: newpath = os.path.sep.join(components[:i] + components[i+1:]) if os.path.isdir(newpath): return newpath def addpath(path, toadd): components = path.split(os.path.sep) for i in range(len(components) + 1): newpath = os.path.sep.join(components[:i] + [toadd] + components[i:]) if os.path.isdir(newpath): return newpath def main(): path = os.getcwd() if len(sys.argv) < 2: print(path) else: newpiece = sys.argv if newpiece and newpiece == '-': dropped = droppath(path, newpiece[1:]) if dropped: print(dropped) return if newpiece and newpiece == '+': added = addpath(path, newpiece[1:]) if added: print(added) return print(substpath(path, newpiece)) if __name__ == "__main__": main()