""" To be used with a companion fish function like this: function refish set -l _x (python /tmp/bass.py source ~/.nvm/nvim.sh ';' nvm use iojs); source $_x; and rm -f $_x end """ import json import os import signal import subprocess import sys BASH = "bash" FISH_READONLY = [ "PWD", "SHLVL", "history", "pipestatus", "status", "version", "FISH_VERSION", "fish_pid", "hostname", "_", "fish_private_mode", ] IGNORED = ["PS1", "XPC_SERVICE_NAME"] def ignored(name): if name == "PWD": # this is read only, but has special handling return False # ignore other read only variables if name in FISH_READONLY: return True if name in IGNORED or name.startswith("BASH_FUNC"): return True return name.startswith("%") def escape(string): # use json.dumps to reliably escape quotes and backslashes return json.dumps(string).replace(r"$", r"\$") def escape_identifier(word): return escape(word.replace("?", "\\?")) def comment(string): return "\n".join(["# " + line for line in string.split("\n")]) def gen_script(): # Use the following instead of /usr/bin/env to read environment so we can # deal with multi-line environment variables (and other odd cases). env_reader = f"{sys.executable} -c 'import os,json; print(json.dumps({{k:v for k,v in os.environ.items()}}))'" args = [BASH, "-c", env_reader] output = subprocess.check_output(args, universal_newlines=True) old_env = output.strip() pipe_r, pipe_w = os.pipe() os.set_inheritable(pipe_w, True) command = f"eval $1 && ({env_reader}; alias) >&{pipe_w}" args = [BASH, "-c", command, "bass", " ".join(sys.argv[1:])] p = subprocess.Popen(args, universal_newlines=True, close_fds=False) os.close(pipe_w) with os.fdopen(pipe_r) as f: new_env = f.readline() alias_str = f.read() if p.wait() != 0: raise subprocess.CalledProcessError( returncode=p.returncode, cmd=" ".join(sys.argv[1:]), output=new_env + alias_str ) new_env = new_env.strip() old_env = json.loads(old_env) new_env = json.loads(new_env) script_lines = [] for k, v in new_env.items(): if ignored(k): continue v1 = old_env.get(k) if not v1: script_lines.append(comment(f"adding {k}={v}")) elif v1 != v: script_lines.append(comment(f"updating {k}={v1} -> {v}")) # process special variables if k == "PWD": script_lines.append(f"cd {escape(v)}") continue else: continue if k == "PATH": # noqa: SIM108 value = " ".join([escape(directory) for directory in v.split(":")]) else: value = escape(v) script_lines.append(f"set -g -x {k} {value}") for var in set(old_env.keys()) - set(new_env.keys()): script_lines.append(comment(f"removing {var}")) script_lines.append(f"set -e {var}") script = "\n".join(script_lines) alias_lines = [] for line in alias_str.splitlines(): _, rest = line.split(None, 1) k, v = rest.split("=", 1) alias_lines.append("alias " + escape_identifier(k) + "=" + v) alias = "\n".join(alias_lines) return script + "\n" + alias script_file = os.fdopen(3, "w") if not sys.argv[1:]: print("__bass_usage", file=script_file, end="") sys.exit(0) try: script = gen_script() except subprocess.CalledProcessError as e: sys.exit(e.returncode) except Exception: print("Bass internal error!", file=sys.stderr) raise # traceback will output to stderr except KeyboardInterrupt: signal.signal(signal.SIGINT, signal.SIG_DFL) os.kill(os.getpid(), signal.SIGINT) else: script_file.write(script)