diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2020-12-22 18:48:08 -0500 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2020-12-22 21:04:24 -0500 |
| commit | 9f7d1dd547930efeb80a46cc2abe1f0b5c32a889 (patch) | |
| tree | 38de9b6d3d5ee0a3eb20633989dbdb36ae56c0fe | |
| parent | 7f06bbc3769ce67fe9d3ab5160e0135dc634a842 (diff) | |
Rust rewrite.
The Python version was taking hundreds of millis for a small number of
env vars on my machine - enough that starting a new shell was a bit
itchy. The compiled version, even in debug mode, is ~10x faster.
| -rw-r--r-- | .envrc | 5 | ||||
| -rw-r--r-- | .gitignore | 23 | ||||
| -rw-r--r-- | .pylintrc | 16 | ||||
| -rw-r--r-- | .python-version | 1 | ||||
| -rw-r--r-- | Cargo.lock | 282 | ||||
| -rw-r--r-- | Cargo.toml | 11 | ||||
| -rw-r--r-- | README.md | 21 | ||||
| -rw-r--r-- | dev-requirements.in | 3 | ||||
| -rw-r--r-- | dev-requirements.txt | 26 | ||||
| -rw-r--r-- | envdir/__init__.py | 7 | ||||
| -rw-r--r-- | envdir/cli.py | 145 | ||||
| -rw-r--r-- | setup.py | 25 | ||||
| -rw-r--r-- | src/main.rs | 195 | ||||
| -rwxr-xr-x | tools/check | 10 | ||||
| -rwxr-xr-x | tools/check-lint | 10 | ||||
| -rwxr-xr-x | tools/check-style | 10 |
16 files changed, 495 insertions, 295 deletions
@@ -1,5 +0,0 @@ -if has pyenv; then - pyenv install --skip-existing -fi - -layout python3 @@ -1,21 +1,2 @@ -# If you're using direnv, the .envrc file will generate .direnv automatically -# when loaded. -/.direnv/ - -# Generated metadata created by `pip install`, `python setup.py sdist`, and -# friends. Will be reconstituted from setup.py as needed. -*.egg-info - -# Compiled Python bytecode. Will be regenerated on import, for the current -# Python version. -*.pyc - -# Dependencies installed via setup_requires; will be recreated from setuptools -# as needed. -/.eggs/ - -# Intermediate build artifacts generated by `python setup.py sdist`. -/build/ - -# Final outputs generated by `python setup.py bdist`. -/dist/ +# Cargo build output, recreated by any Cargo command. +/target/ diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index c590644..0000000 --- a/.pylintrc +++ /dev/null @@ -1,16 +0,0 @@ -[MASTER] -# No lint failures allowed. -fail-under=10.0 - -# Prevent litter in ~/.pylint.d -persistent=no - -[MESSAGES CONTROL] -disable= - # None of the modules in this are designed for import. This is a tool, - # with no library attached. - missing-module-docstring, - # I fundamentally disagree with pylint's assertion that stdlib imports go - # before library imports. All libraries are created more or less equal; - # the stdlib merely happens to come with the language. - wrong-import-order, diff --git a/.python-version b/.python-version deleted file mode 100644 index a5c4c76..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.9.0 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..26dcf56 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,282 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "clap" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "envdir-helper" +version = "0.2.0" +dependencies = [ + "clap", + "libc", + "shlex", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" + +[[package]] +name = "os_str_bytes" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a571a711dddd09019ccc628e1b17fe87c59b09d513c06c026877aa708334f37a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e266d8c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "envdir-helper" +version = "0.2.0" +authors = ["Owen Jacobson <owen@grimoire.ca>"] +edition = "2018" + +[dependencies] +clap = "~3.0.0-beta.2" +libc = "~0.2.40" +shlex = "~0.1.1" +thiserror = "~1.0" @@ -13,8 +13,9 @@ I've replaced all of that with a single line: eval "$(envdir-helper)" This program also supports setting non-exported shell variables, using the -`--no-export` flag. This is useful for prompts and other shell configuration -that should not be propagated through to subshells and other programs. This behaviour is the default if the env directory's name ends in `rc`: +`--export false` flag. This is useful for prompts and other shell configuration +that should not be propagated through to subshells and other programs. This +behaviour is the default if the env directory's name ends in `rc`: eval "$(envdir-helper .envdir.rc)" @@ -37,18 +38,6 @@ This program does relatively little to manage this directly. One approach that h ## Installation -Some familiarity with Python is assumed, here: +Some familiarity with Rust is assumed, here: -* Make a virtual environment; -* `$VIRTUALENV/bin/pip install git+https://github.com/ojacobson/envdir-helper/#egg=envdir-helper`; and -* Add its `bin` directory to `PATH` by other means, or invoke it by full path. - -## Development - -I use [pyenv] and [`direnv`] to manage development. The configuration in -`.envrc` will automatically create a virtual Python environment using Pyenv (if -possible) or your current Python version (otherwise), and load it, once the -configuration is allowed. See the `direnv` documentation and the included -`.envrc` script for details. - -[pyenv]: https://github.com/pyenv/pyenv +* `cargo install --git https://github.com/ojacobson/envdir-helper` diff --git a/dev-requirements.in b/dev-requirements.in deleted file mode 100644 index 60ced2c..0000000 --- a/dev-requirements.in +++ /dev/null @@ -1,3 +0,0 @@ -black -pylint -pip-tools diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index a8f9e72..0000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,26 +0,0 @@ -# -# This file is autogenerated by pip-compile -# To update, run: -# -# pip-compile dev-requirements.in -# -appdirs==1.4.4 # via black -astroid==2.4.2 # via pylint -black==20.8b1 # via -r dev-requirements.in -click==7.1.2 # via black, pip-tools -isort==5.6.4 # via pylint -lazy-object-proxy==1.4.3 # via astroid -mccabe==0.6.1 # via pylint -mypy-extensions==0.4.3 # via black -pathspec==0.8.1 # via black -pip-tools==5.4.0 # via -r dev-requirements.in -pylint==2.6.0 # via -r dev-requirements.in -regex==2020.11.13 # via black -six==1.15.0 # via astroid, pip-tools -toml==0.10.2 # via black, pylint -typed-ast==1.4.1 # via black -typing-extensions==3.7.4.3 # via black -wrapt==1.12.1 # via astroid - -# The following packages are considered to be unsafe in a requirements file: -# pip diff --git a/envdir/__init__.py b/envdir/__init__.py deleted file mode 100644 index a7b85bd..0000000 --- a/envdir/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from importlib.metadata import version, PackageNotFoundError - -try: - __version__ = version("envdir-helper") -except PackageNotFoundError: - # package is not installed - pass diff --git a/envdir/cli.py b/envdir/cli.py deleted file mode 100644 index 9a06baf..0000000 --- a/envdir/cli.py +++ /dev/null @@ -1,145 +0,0 @@ -import click -import os -import os.path as op -import pathlib -import shlex -import stat -import subprocess as sp - - -@click.command() -@click.pass_context -@click.version_option() -@click.option( - "--export/--no-export", - default=None, - help="Export generated environment variables [default: --export]", -) -@click.argument( - "envdir", - default=str(pathlib.Path.home() / ".envdir"), - metavar="DIR", -) -def main(context, export, envdir): - r"""Load environment variables from DIR (or ~/.envdir). - - For each non-directory entry in DIR, this will output a brief shell script - to set an environment with the same name. If the entry is an executable - program, it will be run (with the current environment) and its output will - be used as the value of the environment variable. Otherwise, it will be - read, and its content will be used. In either case, a single trailing - newline will be removed, if present. - - This process skips any file which can't be run or read, as appropriate, and - outputs a warning on stderr. - - The intended use case for this is in shell profiles, in the form: - - eval "$(envdir-helper)" - - The generated output is compatible with sh, and thus with bash and zsh. - """ - - def warn_on_skipped(path, reason): - """Log any skipped paths to stderr.""" - stderr = click.get_text_stream("stderr") - click.echo(f"{context.info_name}: skipping {path}: {reason}", file=stderr) - - if export is None: - env_script = detect_env_script( - envdir, rc=no_export_env_script, default=export_env_script - ) - elif export: - env_script = export_env_script - else: - env_script = no_export_env_script - - for name, content in walk_entries(envdir, on_skipped=warn_on_skipped): - script = env_script(name, content) - click.echo(script) - - -def detect_env_script(path, rc, default): # pylint: disable=invalid-name - """Detect which of two values to use based on whether `path` ends with - `"rc"`. If it does, returns `rc`; otherwise, returns `default`. - """ - if path.endswith("rc"): - return rc - return default - - -def export_env_script(name, content): - """Given a name and contents, generate a shell script that will set and - export the corresponding environment variable, with that content.""" - # use sh-friendly syntax here: don't assume `export` can have assignment - # side effects, so don't use `export FOO=BAR`. `FOO=BAR; export FOO` is - # portable to all posix shells. - qname = shlex.quote(name) - qcontent = shlex.quote(content) - return f"{qname}={qcontent}; export {qname}" - - -def no_export_env_script(name, content): - """Given a name and contents, generate a shell script that will set and NOT - export the corresponding environment variable, with that content.""" - qname = shlex.quote(name) - qcontent = shlex.quote(content) - return f"{qname}={qcontent}" - - -def walk_entries(envdir, on_skipped): - """Yields a name, value pair for each environment file in envdir. - - The path for any skipped items (generally, failing or unrunnable programs, - or unreadable files) will be passed to the `on_skipped` callback, along with - the exception that caused it to be skipped. - """ - for name in sorted(os.listdir(envdir)): - path = op.join(envdir, name) - try: - path_stat = os.stat(path) - if directory(path_stat): - continue - - if executable(path_stat): - content = from_program(path) - else: - content = from_file(path) - except Exception as reason: # pylint: disable=broad-except - on_skipped(path, reason) - continue - if content.endswith("\n"): - content = content[:-1] - yield name, content - - -def directory(item): - """True iff item is a stat_result representing a directory.""" - return stat.S_ISDIR(item.st_mode) - - -def executable(item): - """True iff item is a stat_result representing something executable. - - This doesn't distinguish between dirs and files; both are considered - executable if any +x bit is set. - """ - mode = stat.S_IMODE(item.st_mode) - mask = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH - return mode & mask != 0 - - -def from_program(path): - """Reads a program's complete stdout into a string.""" - result = sp.run( - [path], - check=True, - stdout=sp.PIPE, - ) - return result.stdout.decode("UTF-8") - - -def from_file(path): - """Reads a file's complete content into a string.""" - with open(path, "r") as file: - return file.read() diff --git a/setup.py b/setup.py deleted file mode 100644 index 8b2b5af..0000000 --- a/setup.py +++ /dev/null @@ -1,25 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="envdir-helper", - use_scm_version=True, - - author="Owen Jacobson", - author_email="owen@grimoire.ca", - - packages=find_packages(), - - setup_requires=[ - "setuptools_scm ~= 4.1", - ], - - install_requires=[ - "click ~= 7.1.0", - ], - - entry_points={ - "console_scripts": [ - "envdir-helper=envdir.cli:main", - ], - }, -) diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..df34235 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,195 @@ +use std::env; +use std::ffi::OsString; +use std::fmt::Debug; +use std::fs::{DirEntry, metadata, read_dir, read_to_string}; +use std::io; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio, ExitStatus}; +use std::string::FromUtf8Error; + +use clap::Clap; +use thiserror::Error; + +/// Load environment variables from DIR (or ~/.envdir). +/// +/// For each non-directory entry in DIR, this will output a brief shell script +/// to set an environment variable with the same name. If the entry is an +/// executable program, it will be run (with the current environment) and its +/// output will be used as the value of the environment variable. Otherwise, it +/// will be read, and its content will be used. In either case, a single +/// trailing newline will be removed, if present. +/// +/// This process skips any file which can't be run or read, as appropriate, and +/// outputs a warning on stderr. +/// +/// The intended use case for this is in shell profiles, in the form: +/// +/// eval "$(envdir-helper)" +/// +/// The generated output is compatible with sh, and thus with bash and zsh. +#[derive(Clap)] +#[clap(version=env!("CARGO_PKG_VERSION"))] +struct Opts { + /// Directory to read environment variables from [default: ~/.envdir] + envdir: Option<PathBuf>, + /// Export generated environment variables [default: true] + #[clap(long)] + export: Option<bool>, +} + +#[derive(Error, Debug)] +enum EnvdirError { + #[error("failed to locate default envdir")] + NoDefaultEnvdir(#[from] DefaultDirError), + #[error("failed to read envdir directory")] + EnvdirListFailed(#[from] io::Error), + #[error("failed to decode a filename")] + PathStringError(#[from] PathStringError), +} + +const SELF: &str = env!("CARGO_BIN_NAME"); + +fn main() -> Result<(), EnvdirError> { + let opts: Opts = Opts::parse(); + + let envdir = match opts.envdir { + None => default_envdir()?, + Some(envdir) => envdir, + }; + + let output_fn = match opts.export { + None => detect_env_script(&envdir)?, + Some(true) => export_env_script, + Some(false) => no_export_env_script, + }; + + for path in read_dir(envdir)? + .filter_map(skip_failing_direntry) + .map(|entry| entry.path()) + .filter(|path| !path.is_dir()) + { + let name = path_to_string(&path)?; + match env_content(&path) { + Ok(content) => println!("{}", output_fn(name, &content)), + Err(e) => eprintln!("{}: error reading env value for {:?}: {:?}", SELF, name, e), + }; + } + + Ok(()) +} + +fn skip_failing_direntry<E: Debug>(result: Result<DirEntry, E>) -> Option<DirEntry> { + match result { + Ok(direntry) => Some(direntry), + Err(e) => { + eprintln!("{}: error reading envdir: {:?}", SELF, e); + None + } + } +} + +#[derive(Error, Debug)] +#[error("a required environment variable was not set")] +struct DefaultDirError(#[from] env::VarError); + +fn default_envdir() -> Result<PathBuf, DefaultDirError> { + let mut envdir = PathBuf::from(env::var("HOME")?); + envdir.push(".envdir"); + + Ok(envdir) +} + +type ExportScript = fn(&str, &str) -> String; + +fn detect_env_script(path: &Path) -> Result<ExportScript, PathStringError> { + let file_name = path_to_string(path)?; + Ok(if file_name.ends_with("rc") { + no_export_env_script + } else { + export_env_script + }) +} + +fn no_export_env_script(name: &str, content: &str) -> String { + let name = shlex::quote(name); + let content= shlex::quote(content); + format!("{}={}", name, content) +} + +fn export_env_script(name: &str, content: &str) -> String { + let name = shlex::quote(name); + let content= shlex::quote(content); + format!("{}={}; export {}", name, content, name) +} + +#[derive(Error, Debug)] +enum PathStringError { + #[error("path has no name: {0}")] + NamelessPath(PathBuf), + #[error("path has a non-unicode name: {0:?}")] + NonUnicodePath(OsString), +} + +fn path_to_string(path: &Path) -> Result<&str, PathStringError> { + use PathStringError::*; + let file_name = path.file_name() + .ok_or_else(|| NamelessPath(path.into()))?; + file_name.to_str() + .ok_or_else(|| NonUnicodePath(file_name.into())) +} + +#[derive(Error, Debug)] +enum EnvContentError { + #[error("io error: {0}")] + IoError(#[from] io::Error), + #[error("program produced non-UTF-8 output: {0}")] + NonUnicodeOutput(#[from] FromUtf8Error), + #[error("program {0:?} exited with status: {1}")] + ProgramFailed(PathBuf, ExitStatus) +} + +fn env_content(path: &Path) -> Result<String, EnvContentError> { + let mut content = if is_program(path)? { + env_program_content(path)? + } else { + env_file_content(path)? + }; + + if content.ends_with("\n") { + content.pop(); + } + + Ok(content) +} + +fn is_program(path: &Path) -> io::Result<bool> { + const EXEC_MASK: u32 = (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) as u32; + + use std::os::unix::fs::PermissionsExt; + + let metadata = metadata(path)?; + let permissions = metadata.permissions(); + + Ok(permissions.mode() & EXEC_MASK != 0) +} + +fn env_program_content(path: &Path) -> Result<String, EnvContentError> { + use EnvContentError::*; + + let output = Command::new(path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .output()?; + + if output.status.success() { + let output = String::from_utf8(output.stdout)?; + Ok(output) + } else { + Err(ProgramFailed(path.to_path_buf(), output.status)) + } +} + +fn env_file_content(path: &Path) -> Result<String, EnvContentError> { + Ok(read_to_string(path)?) +} diff --git a/tools/check b/tools/check deleted file mode 100755 index 269a23d..0000000 --- a/tools/check +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -e - -cd "$(dirname "$0")/.." - -## tools/check -## -## Run checks on the project, exiting with a zero status if all checks pass. - -tools/check-lint -tools/check-style diff --git a/tools/check-lint b/tools/check-lint deleted file mode 100755 index eaebdf0..0000000 --- a/tools/check-lint +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -e - -cd "$(dirname "$0")/.." - -## tools/check-lint -## -## Run lint checks on the project, exiting with a zero status if there are no lint -## issues. - -exec pylint envdir diff --git a/tools/check-style b/tools/check-style deleted file mode 100755 index adb2499..0000000 --- a/tools/check-style +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -e - -cd "$(dirname "$0")/.." - -## tools/check-style -## -## Run style checks on the project, exiting with a zero status if there are no style -## issues. - -exec black --check --diff envdir |
