pax_global_header00006660000000000000000000000064146636354140014526gustar00rootroot0000000000000052 comment=8f118c32447255c11e32a3aafc157adaa690760d .github/000077500000000000000000000000001466363541400124325ustar00rootroot00000000000000.github/dependabot.yml000066400000000000000000000004071466363541400152630ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" groups: all: patterns: - "*" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" .github/workflows/000077500000000000000000000000001466363541400144675ustar00rootroot00000000000000.github/workflows/python-package.yaml000066400000000000000000000027541466363541400202750ustar00rootroot00000000000000name: CI/CD Pipeline on: - push - pull_request jobs: test: name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - ubuntu-latest - macos-latest - windows-latest python-version: - "3.9" - "3.10" - "3.11" - "3.12" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - run: | # ensure we're using *our* dotenv during testing and not some other one # installed on the system, e.g. gh machines apparently have sometimes # the ruby dotenv package installed pip install --upgrade -r requirements-dev.txt pip install -e .['dev'] make test lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.x" - run: | make lint mypy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.x" - run: | make mypy test-release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.x" - run: | make test-release .gitignore000066400000000000000000000001621466363541400130610ustar00rootroot00000000000000__pycache__/ *.pyc *.egg-info/ build/ dist/ .coverage htmlcov/ .mypy_cache/ .pytest_cache/ site/ venv/ .idea/ .readthedocs.yaml000066400000000000000000000002421466363541400143170ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.12" mkdocs: configuration: mkdocs.yml python: install: - requirements: requirements-dev.txt CHANGELOG.md000066400000000000000000000055751466363541400127170ustar00rootroot00000000000000# Changelog ## [3.4.1] -- 2024-08-28 * Fixed Unicode issue in double quoted values ## [3.4.0] -- 2024-08-06 * Support multiple .env files via `-e` or `--dotenv` parameters * Dropped support for Python 3.8 * Updated dependencies ## [3.3.1] -- 2024-07-13 * renamed debian package to dotenv-cli * changed section to devel ## [3.3.0] -- 2024-04-07 * added option to completely replace environment with contents of the dotenv file * added very simple docs for readthedocs ## [3.2.2] -- 2023-11-10 * replaced flake8 with ruff * when building the debian package, don't run coverage when running tests ## [3.2.1] -- 2023-08-27 * updated debian/watch * updated dev-dependencies ## [3.2.0] -- 2023-07-01 * on POSIX systems we don't fork a new child process anymore but use `exec*` to replace the `dotenv` process * Dropped Python 3.7 support * replaced setup.py/.cfg with pyproject.toml * modernized github actions: * don't run linter and mypy on all platforms, only one * run test-release * updated dev-dependencies ## [3.1.1] -- 2023-04-13 * updated dependencies: * mypy * pytest * pytest-cov * wheel * Debian: * added htmlcov and .mypy_cache to extended-diff-ignore * bump debhelper from 11 -> 13 * use debhelper-compat * use standards-version 4.6.2 ## [3.1.0] -- 2022-09-07 * added type hints and mypy --strict to test suite * updated dependencies: * flake8 * pytest * twine ## [3.0.1] - 2022-06-26 * bumped version (no changes) ## [3.0.0] - 2022-05-31 * removed python 3.6 support * added dependabot * updated makefile ## [2.2.0] - 2020-10-30 * Allow for missing .env file -- in this case the command will be executed without setting any environment variables. The previous behaviour was to fail with a FileNotFoundError * Migrated from TravisCI to github actions. We test now on Linux, Mac and Windows x all supported Python versions! * Fixed tests under windows, where NamedTemporaryFile cannot be opened twice. * refactored __main__.py into cli.py and wrapped argparsing into dedicated function * bumped minimal Python version to 3.6 * Added 3.8, 3.9 to travis tests * Cleaned up Makefile * Added twine to dev-dependencies ## [2.1.0] - 2020-10-27 * make sure child process terminates when dotenv terminates * measure coverage for tests as well * skip coverage report for files w/ complete coverage * use twine for uploading to pypi ## [2.0.1] - 2019-09-07 * Version bump for Debian source-only upload ## [2.0.0] - 2019-08-03 * Differentiate single vs double quotes ## [1.3.0] - 2019-05-11 * Support for lines starting with `export` * Support for empty values ## [1.2.0] - 2019-05-10 * Fixed newlines * Added more tests ## [1.1.0] - 2019-04-28 * Added Bash completion and provide it via sdist and Debian package ## [1.0.2] - 2019-04-14 * Debian package * Fixed Travis-CI pipeline and added tests for py37 ## [1.0.0] - 2018-10-14 * Initial Release LICENSE000066400000000000000000000020601466363541400120750ustar00rootroot00000000000000MIT License Copyright (c) 2018 Bastian Venthur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. MANIFEST.in000066400000000000000000000000371466363541400126300ustar00rootroot00000000000000recursive-include completion * Makefile000066400000000000000000000030351466363541400125330ustar00rootroot00000000000000# system python interpreter. used only to create virtual environment PY = python3 VENV = venv BIN=$(VENV)/bin DOCS_SRC = docs DOCS_OUT = site ifeq ($(OS), Windows_NT) BIN=$(VENV)/Scripts PY=python endif .PHONY: all all: lint mypy test test-docs test-release $(VENV): requirements-dev.txt pyproject.toml $(PY) -m venv $(VENV) $(BIN)/pip install --upgrade -r requirements-dev.txt $(BIN)/pip install -e .['dev'] touch $(VENV) # in this target, our tests are using Popen etc to run other scrips. Therefore # we must set the PATH to include the virtual environment's bin directory .PHONY: test test: PATH := $(BIN):$(PATH) test: $(VENV) $(BIN)/pytest .PHONY: mypy mypy: $(VENV) $(BIN)/mypy .PHONY: lint lint: $(VENV) $(BIN)/ruff check . .PHONY: build build: $(VENV) rm -rf dist $(BIN)/python3 -m build .PHONY: test-release test-release: $(VENV) build $(BIN)/twine check dist/* .PHONY: release release: $(VENV) build $(BIN)/twine upload dist/* .PHONY: test-docs test-docs: $(VENV) # we try to keep the README and the docs/index.md in sync @cmp README.md docs/index.md .PHONY: docs docs: $(VENV) $(BIN)/mkdocs build VERSION = $(shell python3 -c 'from dotenv_cli import __VERSION__; print(__VERSION__)') tarball: git archive --output=../dotenv-cli_$(VERSION).orig.tar.gz HEAD .PHONY: clean clean: rm -rf build dist *.egg-info rm -rf $(VENV) rm -rf $(DOCS_OUT) find . -type f -name *.pyc -delete find . -type d -name __pycache__ -delete # coverage rm -rf htmlcov .coverage rm -rf .mypy_cache rm -rf .pytest_cache rm -rf .ruff_cache README.md000066400000000000000000000052371466363541400123600ustar00rootroot00000000000000# dotenv CLI Dotenv-CLI provides the `dotenv` command. `dotenv` loads the `.env` file from the current directory, puts the contents in the environment by either changing existing- or adding new environment variables, and executes the given command. `dotenv` supports alternative `.env` files like `.env.development` via the `-e` or `--dotenv` parameters. This parameter can be repeated to load multiple files, the .env files will be loaded in the order they are provided. With the `--replace` flag, `dotenv` also provides an option to completely replace the environment variables with the ones from the `.env` file, allowing you to control exactly which environment variables are set. `dotenv` provides bash completion, so you can use `dotenv` like this: ```bash $ dotenv make all clean docs lint release test ``` ## Install ### Using PyPi dotenv-cli is [available on PyPi][pypi], you can install it via: [pypi]: https://pypi.org/project/dotenv-cli/ ```bash $ pip install dotenv-cli ``` ### On Debian and Ubuntu Alternatively, you can install dotenv-cli on Debian based distributions via: ```bash # apt-get install dotenv-cli ``` ## Usage Create an `.env` file in the root of your project and populate it with some values like so: ```sh SOME_SECRET=donttrythisathome SOME_CONFIG=foo ``` Just prepend the command you want to run with the extra environment variables from the `.env` file with `dotenv`: ```bash $ dotenv some-command ``` and those variables will be available in your environment variables. ## Rules The parser understands the following: * Basic unquoted values (`BASIC=basic basic`) * Lines starting with `export` (`export EXPORT=foo`), so you can `source` the file in bash * Lines starting with `#` are ignored (`# Comment`) * Empty values (`EMPTY=`) become empty strings * Inner quotes are maintained in basic values: `INNER_QUOTES=this 'is' a test` or `INNER_QUOTES2=this "is" a test` * White spaces are trimmed from unquoted values: `TRIM_WHITESPACE= foo ` and maintained in quoted values: `KEEP_WHITESPACE=" foo "` * Interpret escapes (e.g. `\n`) in double quoted values, keep them as-is in single quoted values. Example `.env` file: ```sh BASIC=basic basic export EXPORT=foo EMPTY= INNER_QUOTES=this 'is' a test INNER_QUOTES2=this "is" a test TRIM_WHITESPACE= foo KEEP_WHITESPACE=" foo " MULTILINE_DQ="multi\nline" MULTILINE_SQ='multi\nline' MULTILINE_NQ=multi\nline # # some comment ``` becomes: ```sh $ dotenv env BASIC=basic basic EXPORT=foo EMPTY= INNER_QUOTES=this 'is' a test INNER_QUOTES2=this "is" a test TRIM_WHITESPACE=foo KEEP_WHITESPACE= foo MULTILINE_DQ=multi line MULTILINE_SQ=multi\nline MULTILINE_NQ=multi\nline ``` completion/000077500000000000000000000000001466363541400132435ustar00rootroot00000000000000completion/bash/000077500000000000000000000000001466363541400141605ustar00rootroot00000000000000completion/bash/dotenv000066400000000000000000000015451466363541400154070ustar00rootroot00000000000000_dotenv() { local cur prev words cword _init_completion || return # find completion(s) for command executed with dotenv local i for (( i=1; i <= COMP_CWORD; i++ )); do if [[ ${COMP_WORDS[i]} != -* ]]; then _command_offset $i return fi # if current option requires a parameter, ignore the next one case "${COMP_WORDS[i]}" in -e|--dotenv) ((i++)) ;; esac done # completion for dotenv files case "$prev" in -e|--dotenv) COMPREPLY=( $( compgen -f -- "$cur" ) ) return ;; esac # check dotenv's options if [[ $cur == -* ]]; then COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) ) return fi } && complete -F _dotenv dotenv # vim: filetype=sh debian/000077500000000000000000000000001466363541400123145ustar00rootroot00000000000000debian/changelog000066400000000000000000000116751466363541400142000ustar00rootroot00000000000000dotenv-cli (3.4.1-1) unstable; urgency=medium * Fixed Unicode issue in double quoted values * Limit Conflicts with ruby-dotenv to pre-bookworm release as the newer package doesn't provide a binary anymore (Closes: #1079832), thanks Antonio Terceiro for the report and patch -- Bastian Venthur Wed, 28 Aug 2024 16:47:15 +0200 dotenv-cli (3.4.0-1) unstable; urgency=medium * Support multiple .env files via `-e` or `--dotenv` parameters * Dropped support for Python 3.8 * Updated dependencies -- Bastian Venthur Tue, 06 Aug 2024 18:22:57 +0200 dotenv-cli (3.3.1-1) unstable; urgency=medium * renamed package to dotenv-cli * changed section to devel -- Bastian Venthur Sat, 13 Jul 2024 09:14:27 +0200 dotenv-cli (3.3.0-1) unstable; urgency=medium * added option to completely replace environment with contents of the dotenv file * added very simple docs for readthedocs -- Bastian Venthur Sun, 07 Apr 2024 13:53:59 +0200 dotenv-cli (3.2.2-1) unstable; urgency=medium * replaced flake8 with ruff * when building the debian package, don't run coverage when executing the tests (Closes: #1040232) -- Bastian Venthur Fri, 10 Nov 2023 21:16:08 +0100 dotenv-cli (3.2.1-1) unstable; urgency=medium * fixed debian/watch * updated dev-dependencies -- Bastian Venthur Sun, 27 Aug 2023 15:16:48 +0200 dotenv-cli (3.2.0-1) unstable; urgency=medium * on POSIX systems we don't fork a new child process anymore but use `exec*` to replace the `dotenv` process * Dropped Python 3.7 support * replaced setup.py/.cfg with pyproject.toml * modernized github actions: * don't run linter and mypy on all platforms, only one * run test-release * updated dev-dependencies * re-enabled tests on building debian package -- Bastian Venthur Sat, 01 Jul 2023 13:24:59 +0200 dotenv-cli (3.1.1-1) unstable; urgency=medium * new upstream version * added htmlcov and .mypy_cache to extended-diff-ignore * bump debhelper from 11 -> 13 * use debhelper-compat * use standards-version 4.6.2 -- Bastian Venthur Thu, 13 Apr 2023 10:20:04 +0200 dotenv-cli (3.1.0-1) unstable; urgency=medium * added type hints * updated dependencies -- Bastian Venthur Wed, 07 Sep 2022 20:15:01 +0200 dotenv-cli (3.0.1-2) unstable; urgency=medium * source-only upload -- Bastian Venthur Wed, 29 Jun 2022 21:33:12 +0200 dotenv-cli (3.0.1-1) unstable; urgency=medium * removed python 3.6 support * added dependabot * updated makefile -- Bastian Venthur Tue, 31 May 2022 19:58:13 +0200 dotenv-cli (2.2.0-1) unstable; urgency=medium * Allow for missing .env file -- in this case the command will be executed without setting any environment variables. The previous behaviour was to fail with a FileNotFoundError * Migrated from TravisCI to github actions. We test now on Linux, Mac and Windows x all supported Python versions! * Fixed tests under windows, where NamedTemporaryFile cannot be opened twice. * refactored __main__.py into cli.py and wrapped argparsing into dedicated function * bumped minimal Python version to 3.6 * Added 3.8, 3.9 to travis tests * Cleaned up Makefile * Added twine to dev-dependencies -- Bastian Venthur Fri, 30 Oct 2020 16:48:44 +0100 dotenv-cli (2.1.0-1) unstable; urgency=medium * make sure child process terminates when dotenv terminates * measure coverage for tests as well * skip coverage report for files w/ complete coverage * use twine for uploading to pypi -- Bastian Venthur Tue, 27 Oct 2020 22:21:31 +0100 dotenv-cli (2.0.1-1) unstable; urgency=medium * Source only upload * Minor version bump -- Bastian Venthur Sat, 07 Sep 2019 13:30:25 +0200 dotenv-cli (2.0.0-1) unstable; urgency=medium * Interpret escapes only in double quoted values, keep them as is in single quoted -- Bastian Venthur Sat, 03 Aug 2019 14:38:36 +0200 dotenv-cli (1.3.0-1) unstable; urgency=medium * Added support for export-lines * Added support for empty values -- Bastian Venthur Sat, 11 May 2019 14:34:47 +0200 dotenv-cli (1.2.0-1) unstable; urgency=medium * Fixed newlines -- Bastian Venthur Fri, 10 May 2019 19:47:57 +0200 dotenv-cli (1.1.0-1) unstable; urgency=medium * Added bash completion -- Bastian Venthur Sun, 28 Apr 2019 12:56:49 +0200 dotenv-cli (1.0.2-1) unstable; urgency=medium * Conflict with ruby-dotenv (Closes: #926916) -- Bastian Venthur Sun, 14 Apr 2019 17:42:29 +0200 dotenv-cli (1.0.0-1) unstable; urgency=medium * Initial release (Closes: #923856) -- Bastian Venthur Wed, 06 Mar 2019 09:55:47 +0100 debian/control000066400000000000000000000027241466363541400137240ustar00rootroot00000000000000Source: dotenv-cli Section: devel Priority: optional Maintainer: Bastian Venthur Build-Depends: debhelper-compat (= 13), pybuild-plugin-pyproject, dh-python, python3-all, python3-setuptools, python3-pytest, python3-pytest-cov, bash-completion Standards-Version: 4.6.2 Homepage: https://github.com/venthur/dotenv-cli Vcs-Browser: https://github.com/venthur/dotenv-cli Vcs-Git: https://github.com/venthur/dotenv-cli.git Testsuite: autopkgtest-pkg-python Package: dotenv-cli Architecture: all Depends: ${python3:Depends}, ${misc:Depends} Conflicts: ruby-dotenv (<< 2.4.0-2) Replaces: python3-dotenv-cli (<< 3.3.1-1) Breaks: python3-dotenv-cli (<< 3.3.1-1) Description: CLI that loads .env configuration This package provides the dotenv command. It reads the .env file from the current directory puts the contents in the environment and executes the given command. . dotenv supports alternative .env files like .env.development via the -e or --dotenv parameters. Package: python3-dotenv-cli Architecture: all Depends: dotenv-cli, ${misc:Depends} Conflicts: ruby-dotenv (<< 2.4.0-2) Section: oldlibs Description: transitional package This package provides the dotenv command. It reads the .env file from the current directory puts the contents in the environment and executes the given command. . dotenv supports alternative .env files like .env.development via the -e or --dotenv parameters. . This is a transitional package It can be safely removed. debian/copyright000066400000000000000000000025241466363541400142520ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: dotenv-cli Source: https://github.com/venthur/dotenv-cli Files: * Copyright: 2018 Bastian Venthur License: MIT Files: debian/* Copyright: 2019 Bastian Venthur License: MIT License: MIT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. debian/dotenv-cli.bash-completion000066400000000000000000000000271466363541400173650ustar00rootroot00000000000000completion/bash/dotenv debian/rules000077500000000000000000000012611466363541400133740ustar00rootroot00000000000000#!/usr/bin/make -f # See debhelper(7) (uncomment to enable) # output every command that modifies files on the build system. #export DH_VERBOSE = 1 export PYBUILD_DESTDIR=debian/dotenv-cli export PYBUILD_NAME=dotenv-cli export PYBUILD_SYSTEM=pyproject export PYBUILD_TEST_ARGS=--no-cov %: dh $@ --with python3,bash-completion --buildsystem=pybuild # If you need to rebuild the Sphinx documentation # Add spinxdoc to the dh --with line #override_dh_auto_build: # dh_auto_build # PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bhtml docs/ build/html # HTML generator # PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bman docs/ build/man # Manpage generator debian/source/000077500000000000000000000000001466363541400136145ustar00rootroot00000000000000debian/source/format000066400000000000000000000000141466363541400150220ustar00rootroot000000000000003.0 (quilt) debian/source/options000066400000000000000000000005351466363541400152350ustar00rootroot00000000000000extend-diff-ignore = "^[^/]*[.]egg-info/" # below are my exceptions, probably best to try to mirror the .gitignore extend-diff-ignore = "^build/" extend-diff-ignore = "^dist/" extend-diff-ignore = "^.pytest_cache/" extend-diff-ignore = "^htmlcov/" extend-diff-ignore = "^.coverage" extend-diff-ignore = "^.mypy_cache/" extend-diff-ignore = "^venv/" debian/watch000066400000000000000000000004131466363541400133430ustar00rootroot00000000000000# You can run the "uscan" command to check for upstream updates and more. # See uscan(1) for format # Compulsory line, this is a version 4 file version=4 # Direct Git opts="mode=git" https://github.com/venthur/dotenv-cli.git \ refs/tags/v([\d\.]+) debian uupdate docs/000077500000000000000000000000001466363541400120225ustar00rootroot00000000000000docs/CHANGELOG.md000077700000000000000000000000001466363541400156522../CHANGELOG.mdustar00rootroot00000000000000docs/index.md000077700000000000000000000000001466363541400151402../README.mdustar00rootroot00000000000000dotenv_cli/000077500000000000000000000000001466363541400132205ustar00rootroot00000000000000dotenv_cli/__init__.py000066400000000000000000000001021466363541400153220ustar00rootroot00000000000000from dotenv_cli.version import __VERSION__ as __VERSION__ # noqa dotenv_cli/cli.py000066400000000000000000000036371466363541400143520ustar00rootroot00000000000000"""Command line interface for dotenv-cli.""" # remove when we don't support py38 anymore from __future__ import annotations import argparse import logging from typing import NoReturn from dotenv_cli import __VERSION__ from dotenv_cli.core import run_dotenv logger = logging.getLogger(__name__) def parse_args(args: list[str] | None = None) -> argparse.Namespace: """Parse arguments. Paramters --------- args This if for debugging only. Returns ------- argparse.Namespace """ parser = argparse.ArgumentParser( description=( "dotenv executes a given command with environment variables " "loaded from a .env file." ), ) parser.add_argument( "-e", "--dotenv", help=( "alternative .env file; this parameter can be provided multiple " "times and the .env files will be evaluated in order" ), action="append", default=[".env"], ) parser.add_argument( "command", help="shell command to execute", nargs=argparse.REMAINDER, ) parser.add_argument( "--version", action="version", version=__VERSION__, ) parser.add_argument( "-r", "--replace", action="store_true", help=( "completely replace all existing environment variables with the " "ones loaded from the .env file" ) ) return parser.parse_args(args) def main() -> NoReturn | int: """Run dotenv. This function parses sys.argv and runs dotenv. Returns ------- int the return value """ args = parse_args() # if alternative .env file is given, remove the default one if len(args.dotenv) > 1: args.dotenv = args.dotenv[1:] if not args.command: return 0 return run_dotenv(args.dotenv, args.command, args.replace) dotenv_cli/core.py000066400000000000000000000071631466363541400145310ustar00rootroot00000000000000"""Core functions.""" # remove when we don't support py38 anymore from __future__ import annotations import atexit import logging import os from subprocess import Popen from typing import NoReturn logger = logging.getLogger(__name__) def read_dotenv(filename: str) -> dict[str, str]: """Read dotenv file. Parameters ---------- filename path to the filename Returns ------- dict """ try: with open(filename) as fh: data = fh.read() except FileNotFoundError: logger.warning( f"{filename} does not exist, continuing without " "setting environment variables." ) data = "" res = {} for line in data.splitlines(): logger.debug(line) line = line.strip() # ignore comments if line.startswith("#"): continue # ignore empty lines or lines w/o '=' if "=" not in line: continue key, value = line.split("=", 1) # allow export if key.startswith("export "): key = key.split(" ", 1)[-1] key = key.strip() value = value.strip() # remove quotes (not sure if this is standard behaviour) if len(value) >= 2 and value[0] == value[-1] == '"': value = value[1:-1] # un-escape escape characters control_chars = { r'\a': '\a', r'\b': '\b', r'\f': '\f', r'\n': '\n', r'\r': '\r', r'\t': '\t', r'\v': '\v', r'\\': '\\', } for char, repl in control_chars.items(): value = value.replace(char, repl) elif len(value) >= 2 and value[0] == value[-1] == "'": value = value[1:-1] res[key] = value logger.debug(res) return res def run_dotenv( filenames: list[str], command: list[str], replace: bool = False ) -> NoReturn | int: """Run dotenv. This function executes the commands with the environment variables parsed from filename. Parameters ---------- filenames paths to the .env files command command to execute replace_env Replace the current environment instead of updating it. Returns ------- NoReturn | int The exit status code in Windows. In POSIX-compatible systems, the function does not return normally. """ # read dotenv files dotenv = {} for filename in filenames: dotenv.update(read_dotenv(filename)) if replace: # replace env env = dotenv else: # update env env = os.environ.copy() env.update(dotenv) # in POSIX, we replace the current process with the command, execvpe does # not return if os.name == "posix": os.execvpe(command[0], command, env) # in Windows, we spawn a new process # execute proc = Popen( command, # stdin=PIPE, # stdout=PIPE, # stderr=STDOUT, universal_newlines=True, bufsize=0, shell=False, env=env, ) def terminate_proc() -> None: """Kill child process. All signals should be forwarded to the child processes automatically, however child processes are also free to ignore some of them. With this we make sure the child processes get killed once dotenv exits. """ proc.kill() # register atexit.register(terminate_proc) _, _ = proc.communicate() # unregister atexit.unregister(terminate_proc) return proc.returncode dotenv_cli/version.py000066400000000000000000000001161466363541400152550ustar00rootroot00000000000000"""Version information for the dotenv-cli package.""" __VERSION__ = "3.4.1" mkdocs.yml000066400000000000000000000002521466363541400130740ustar00rootroot00000000000000site_name: dotenv-cli site_url: https://dotenv-cli.readthedocs.io/ repo_url: https://github.com/venthur/dotenv-cli repo_name: venthur/dotenv-cli theme: name: material pyproject.toml000066400000000000000000000030551466363541400140110ustar00rootroot00000000000000[build-system] requires = ["setuptools>=64.0"] build-backend = "setuptools.build_meta" [project] name = "dotenv-cli" authors = [ { name="Bastian Venthur", email="mail@venthur.de" } ] description = "Simple dotenv CLI." keywords = ["dotenv", "cli", ".env"] readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.9" dynamic = ["version"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", ] [project.scripts] dotenv = "dotenv_cli.cli:main" [project.urls] 'Documentation' = 'https://dotenv-cli.readthedocs.io/' 'Source' = 'https://github.com/venthur/dotenv-cli' 'Changelog' = 'https://github.com/venthur/dotenv-cli/blob/master/CHANGELOG.md' [project.optional-dependencies] dev = [ "build", "mkdocs", "mkdocs-material", "mypy", "pytest", "pytest-cov", "ruff", "twine", ] [tool.setuptools.dynamic] version = { attr = "dotenv_cli.__VERSION__" } [tool.setuptools] packages = ["dotenv_cli"] [tool.pytest.ini_options] addopts = [ "--cov=dotenv_cli", "--cov=tests", "--cov-report=html", "--cov-report=term-missing:skip-covered" ] [tool.ruff] line-length = 79 target-version = "py39" [tool.ruff.lint] select = [ "F", # pyflakes "E", "W", # pycodestyle "C90", # mccabe "I", # isort "D", # pydocstyle "UP" # pyupgrade ] pydocstyle.convention = "numpy" [tool.mypy] files = ["dotenv_cli", "tests"] strict = true requirements-dev.txt000066400000000000000000000001711466363541400151310ustar00rootroot00000000000000build==1.2.1 mkdocs-material==9.5.33 mkdocs==1.6.0 mypy==1.11.2 pytest-cov==5.0.0 pytest==8.3.2 ruff==0.6.2 twine==5.1.1 tests/000077500000000000000000000000001466363541400122345ustar00rootroot00000000000000tests/__init__.py000066400000000000000000000000341466363541400143420ustar00rootroot00000000000000"""Tests for dotenv-cli.""" tests/test_cli.py000066400000000000000000000070501466363541400144160ustar00rootroot00000000000000"""Test the CLI interface.""" import tempfile from collections.abc import Iterator from pathlib import Path from subprocess import PIPE, run import pytest from dotenv_cli import __VERSION__ DOTENV_FILE = """ # comment=foo TEST=foo TWOLINES='foo\nbar' TEST_COMMENT=foo # bar LINE_WITH_EQUAL='foo=bar' """ @pytest.fixture def dotenvfile() -> Iterator[Path]: """Provide temporary dotenv file.""" _file = Path.cwd() / ".env" with _file.open("w") as fh: fh.write(DOTENV_FILE) yield _file _file.unlink() def test_this_dotenv() -> None: """Simple test for CI to assert we're running *our* dotenv.""" proc = run(["dotenv", "--version"], stdout=PIPE) assert __VERSION__.encode() in proc.stdout def test_stdout(dotenvfile: Path) -> None: """Test stdout.""" proc = run(["dotenv", "echo", "test"], stdout=PIPE) assert b"test" in proc.stdout def test_stderr(dotenvfile: Path) -> None: """Test stderr.""" proc = run(["dotenv echo test 1>&2"], stderr=PIPE, shell=True) assert b"test" in proc.stderr def test_returncode(dotenvfile: Path) -> None: """Test returncode.""" proc = run(["dotenv", "false"]) assert proc.returncode == 1 proc = run(["dotenv", "true"]) assert proc.returncode == 0 def test_alternative_dotenv() -> None: """Test alternative dotenv file.""" with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write("foo=bar") proc = run(["dotenv", "-e", f.name, "env"], stdout=PIPE) assert b"foo=bar" in proc.stdout proc = run(["dotenv", "--dotenv", f.name, "env"], stdout=PIPE) assert b"foo=bar" in proc.stdout def test_multiple_dotenv() -> None: """Test multiple dotenv files.""" with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write("foo=foo") with tempfile.NamedTemporaryFile("w", delete=False) as b: b.write("bar=bar") proc = run(["dotenv", "-e", f.name, "-e", b.name, "env"], stdout=PIPE) assert b"foo=foo" in proc.stdout assert b"bar=bar" in proc.stdout def test_multiple_dotenv_order() -> None: """Test multiple dotenv files are processed in correct order.""" with tempfile.NamedTemporaryFile("w", delete=False) as f1: f1.write("foo=1") with tempfile.NamedTemporaryFile("w", delete=False) as f2: f2.write("foo=2") proc = run(["dotenv", "-e", f1.name, "-e", f2.name, "env"], stdout=PIPE) assert b"foo=2" in proc.stdout assert b"foo=1" not in proc.stdout proc = run( ["dotenv", "-e", f1.name, "-e", f2.name, "-e", f1.name, "env"], stdout=PIPE ) assert b"foo=1" in proc.stdout assert b"foo=2" not in proc.stdout def test_nonexisting_dotenv() -> None: """Test non-existing dotenv file.""" proc = run(["dotenv", "-e", "/tmp/i.dont.exist", "true"], stderr=PIPE) assert proc.returncode == 0 assert b"does not exist" in proc.stderr def test_no_command() -> None: """Test no command.""" proc = run(["dotenv"]) assert proc.returncode == 0 def test_replace_environment(dotenvfile: Path) -> None: """Test replace environment.""" proc = run(["dotenv", "-r", "env"], stdout=PIPE) # the above .env file has exactly 4 lines, on some test platforms, the CI # environment itself adds a few more environment variables into the shell, # see: # https://stackoverflow.com/questions/78226424/custom-environment-variables-with-popen-on-windows-on-github-actions assert len(proc.stdout.splitlines()) < 10 proc = run(["dotenv", "--replace", "env"], stdout=PIPE) assert len(proc.stdout.splitlines()) < 10 tests/test_core.py000066400000000000000000000107611466363541400146020ustar00rootroot00000000000000"""Test core module.""" import tempfile import pytest from dotenv_cli import core def test_full() -> None: """Test full dotenv file.""" TEST = r""" BASIC=basic basic export EXPORT=foo EMPTY= INNER_QUOTES=this 'is' a test INNER_QUOTES2=this "is" a test TRIM_WHITESPACE= foo KEEP_WHITESPACE=" foo " MULTILINE_DQ="multi\nline" MULTILINE_SQ='multi\nline' MULTILINE_NQ=multi\nline # some comment should be ignored """ with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(TEST) env = core.read_dotenv(f.name) assert env["BASIC"] == "basic basic" assert env["EXPORT"] == "foo" assert env["EMPTY"] == "" assert env["INNER_QUOTES"] == "this 'is' a test" assert env["INNER_QUOTES2"] == 'this "is" a test' assert env["TRIM_WHITESPACE"] == "foo" assert env["KEEP_WHITESPACE"] == " foo " assert env["MULTILINE_DQ"] == "multi\nline" assert env["MULTILINE_SQ"] == "multi\\nline" assert env["MULTILINE_NQ"] == "multi\\nline" assert len(env) == 10 def test_basic() -> None: """Basic unquoted strings.""" TEST = "FOO=BAR" with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(TEST) env = core.read_dotenv(f.name) assert env["FOO"] == "BAR" def test_empty() -> None: """Empty values become empty strings.""" TEST = "FOO=" with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(TEST) env = core.read_dotenv(f.name) assert env["FOO"] == "" def test_inner_quotes() -> None: """Inner quotes are mainained.""" TEST = "\n".join(["FOO1=this 'is' a test", 'FOO2=this "is" a test']) with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(TEST) env = core.read_dotenv(f.name) assert env["FOO1"] == "this 'is' a test" assert env["FOO2"] == 'this "is" a test' def test_trim_whitespaces() -> None: """Whitespaces are stripped from unquoted values.""" TEST = "FOO= test " with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(TEST) env = core.read_dotenv(f.name) assert env["FOO"] == "test" def test_keep_whitespaces() -> None: """Whitespaces are mainteined from quoted values.""" TEST = "FOO=' test '" with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(TEST) env = core.read_dotenv(f.name) assert env["FOO"] == " test " def test_multiline() -> None: """Quoted values can contain newlines.""" TEST = r'FOO="This is\nbar"' with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(TEST) env = core.read_dotenv(f.name) assert env["FOO"] == "This is\nbar" @pytest.mark.parametrize( "input_, expected", [ ('FOO="Test"', "Test"), ("FOO='Test'", "Test"), ("FOO='\"Test\"'", '"Test"'), ("FOO=\"'Test'\"", "'Test'"), ], ) def test_quotes(input_: str, expected: str) -> None: """Test different quotes.""" with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(input_) env = core.read_dotenv(f.name) assert env["FOO"] == expected def test_comments() -> None: """Test comments.""" """Lines starting with # are ignored.""" TEST = """ FOO=BAR # comment BAR=BAZ """ with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(TEST) env = core.read_dotenv(f.name) assert len(env) == 2 assert env["FOO"] == "BAR" assert env["BAR"] == "BAZ" def test_emtpy_lines() -> None: """Empty lines are skipped.""" TEST = """ FOO=BAR BAR=BAZ """ with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(TEST) env = core.read_dotenv(f.name) assert len(env) == 2 assert env["FOO"] == "BAR" assert env["BAR"] == "BAZ" def test_export() -> None: """Exports are allowed.""" TEST = "export FOO=BAR" with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(TEST) env = core.read_dotenv(f.name) assert env["FOO"] == "BAR" def test_non_existing_dotenv() -> None: """Non-existing dotenv file.""" env = core.read_dotenv("/tmp/i.dont.exist") assert len(env) == 0 def test_unicode_in_double_quoted_values() -> None: """Test unicode in double quoted values.""" TEST = """ FOO=ä BAR='ä' BAZ="ä" """ with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write(TEST) env = core.read_dotenv(f.name) assert env["FOO"] == "ä" assert env["BAR"] == "ä" assert env["BAZ"] == "ä" tests/test_version.py000066400000000000000000000002461466363541400153340ustar00rootroot00000000000000"""Tests for version module.""" def test_version() -> None: """Test version.""" from dotenv_cli import __VERSION__ assert isinstance(__VERSION__, str)