summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.envrc5
-rw-r--r--.gitignore23
-rw-r--r--.pylintrc16
-rw-r--r--.python-version1
-rw-r--r--Cargo.lock282
-rw-r--r--Cargo.toml11
-rw-r--r--README.md21
-rw-r--r--dev-requirements.in3
-rw-r--r--dev-requirements.txt26
-rw-r--r--envdir/__init__.py7
-rw-r--r--envdir/cli.py145
-rw-r--r--setup.py25
-rw-r--r--src/main.rs195
-rwxr-xr-xtools/check10
-rwxr-xr-xtools/check-lint10
-rwxr-xr-xtools/check-style10
16 files changed, 495 insertions, 295 deletions
diff --git a/.envrc b/.envrc
deleted file mode 100644
index 1caca09..0000000
--- a/.envrc
+++ /dev/null
@@ -1,5 +0,0 @@
-if has pyenv; then
- pyenv install --skip-existing
-fi
-
-layout python3
diff --git a/.gitignore b/.gitignore
index a1b52c5..162885f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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"
diff --git a/README.md b/README.md
index f5f4439..63f46b5 100644
--- a/README.md
+++ b/README.md
@@ -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