Files
dotfiles/config/fish/functions/__bass.py

141 lines
3.8 KiB
Python

"""
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)