pax_global_header00006660000000000000000000000064135347132310014514gustar00rootroot0000000000000052 comment=46d77a8d804129fb7369a6bcce9aaea05347204d .gitignore000066400000000000000000000000321353471323100130430ustar00rootroot00000000000000__pycache__/ *.pyc venv/ .travis.yml000066400000000000000000000004271353471323100131740ustar00rootroot00000000000000language: python # workaround to make travis work with python3.7 # https://github.com/travis-ci/travis-ci/issues/9069#issuecomment-425720905 sudo: required dist: xenial python: - '3.5' - '3.6' - '3.7' - 'nightly' install: - pip install -e .['dev'] script: - make CHANGELOG.md000066400000000000000000000010371353471323100126720ustar00rootroot00000000000000# Changelog ## [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 LICENSE000066400000000000000000000020601353471323100120630ustar00rootroot00000000000000MIT 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.in000066400000000000000000000000371353471323100126160ustar00rootroot00000000000000recursive-include completion * Makefile000066400000000000000000000004311353471323100125160ustar00rootroot00000000000000.PHONY: \ test \ lint \ docs \ release \ clean all: lint test test: pytest lint: flake8 docs: $(MAKE) -C docs html release: python3 setup.py sdist bdist_wheel upload clean: find . -type f -name *.pyc -delete find . -type d -name __pycache__ -delete README.md000066400000000000000000000044531353471323100123450ustar00rootroot00000000000000# dotenv CLI Dotenv-CLI is a simple package that 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` parametes. `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 python3-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/000077500000000000000000000000001353471323100132315ustar00rootroot00000000000000completion/bash/000077500000000000000000000000001353471323100141465ustar00rootroot00000000000000completion/bash/dotenv000066400000000000000000000015451353471323100153750ustar00rootroot00000000000000_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/000077500000000000000000000000001353471323100123025ustar00rootroot00000000000000debian/changelog000066400000000000000000000022541353471323100141570ustar00rootroot00000000000000dotenv-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/compat000066400000000000000000000000031353471323100135010ustar00rootroot0000000000000011 debian/control000066400000000000000000000015011353471323100137020ustar00rootroot00000000000000Source: dotenv-cli Section: python Priority: optional Maintainer: Bastian Venthur Build-Depends: debhelper (>= 11), dh-python, python3-all, python3-setuptools, bash-completion Standards-Version: 4.3.0 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: python3-dotenv-cli Architecture: all Depends: ${python3:Depends}, ${misc:Depends} Conflicts: ruby-dotenv 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. debian/copyright000066400000000000000000000025241353471323100142400ustar00rootroot00000000000000Format: 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/python3-dotenv-cli.bash-completion000066400000000000000000000000271353471323100207550ustar00rootroot00000000000000completion/bash/dotenv debian/rules000077500000000000000000000011421353471323100133600ustar00rootroot00000000000000#!/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_DISABLE=test export PYBUILD_NAME=dotenv-cli %: 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/000077500000000000000000000000001353471323100136025ustar00rootroot00000000000000debian/source/format000066400000000000000000000000141353471323100150100ustar00rootroot000000000000003.0 (quilt) debian/source/options000066400000000000000000000000521353471323100152150ustar00rootroot00000000000000extend-diff-ignore = "^[^/]*[.]egg-info/" debian/watch000066400000000000000000000004171353471323100133350ustar00rootroot00000000000000# 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 dotenv_cli/000077500000000000000000000000001353471323100132065ustar00rootroot00000000000000dotenv_cli/__init__.py000066400000000000000000000000631353471323100153160ustar00rootroot00000000000000from dotenv_cli.version import __VERSION__ # noqa dotenv_cli/__main__.py000066400000000000000000000012661353471323100153050ustar00rootroot00000000000000import argparse import logging from dotenv_cli.core import run_dotenv from dotenv_cli import __VERSION__ logger = logging.getLogger(__name__) # logging.basicConfig(level=logging.DEBUG) parser = argparse.ArgumentParser() parser.add_argument( '-e', '--dotenv', help='Alternative .env file', default='.env', ) parser.add_argument( 'command', help='Shell command to execute', nargs=argparse.REMAINDER, ) parser.add_argument( '--version', action='version', version=__VERSION__, ) def main(): args = parser.parse_args() if not args.command: return return run_dotenv(args.dotenv, args.command) if __name__ == '__main__': main() dotenv_cli/core.py000066400000000000000000000033041353471323100145100ustar00rootroot00000000000000import logging import os from subprocess import Popen # , PIPE, STDOUT logger = logging.getLogger(__name__) def read_dotenv(filename): """Read dotenv file. Parameters ---------- filename : str path to the filename Returns ------- dict Raises ------ FileNotFoundError """ with open(filename, 'r') as fh: data = fh.read() 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] # escape escape characters value = bytes(value, 'utf-8').decode('unicode-escape') elif len(value) >= 2 and value[0] == value[-1] == "'": value = value[1:-1] res[key] = value logger.debug(res) return res def run_dotenv(filename, command): # read dotenv dotenv = read_dotenv(filename) # update env env = os.environ.copy() env.update(dotenv) # execute proc = Popen( command, # stdin=PIPE, # stdout=PIPE, # stderr=STDOUT, universal_newlines=True, bufsize=0, shell=False, env=env, ) _, _ = proc.communicate() return proc.returncode dotenv_cli/version.py000066400000000000000000000000261353471323100152430ustar00rootroot00000000000000__VERSION__ = '2.0.1' setup.cfg000066400000000000000000000001571353471323100127040ustar00rootroot00000000000000[tool:pytest] addopts = --cov=dotenv_cli --cov-branch --cov-report=term-missing [flake8] exclude = venv,build setup.py000066400000000000000000000020211353471323100125650ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup meta = {} exec(open('./dotenv_cli/version.py').read(), meta) meta['long_description'] = open('./README.md').read() setup( name='dotenv-cli', version=meta['__VERSION__'], description='Simple dotenv CLI.', long_description=meta['long_description'], long_description_content_type='text/markdown', keywords='dotenv cli .env', author='Bastian Venthur', author_email='mail@venthur.de', url='https://github.com/venthur/dotenv-cli', python_requires='>=3', extras_require={ 'dev': [ 'pytest', 'pytest-cov', 'flake8', ] }, packages=['dotenv_cli'], entry_points={ 'console_scripts': [ 'dotenv = dotenv_cli.__main__:main' ] }, license='MIT', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', ], ) tests/000077500000000000000000000000001353471323100122225ustar00rootroot00000000000000tests/__init__.py000066400000000000000000000000001353471323100143210ustar00rootroot00000000000000tests/test_cli.py000066400000000000000000000026641353471323100144120ustar00rootroot00000000000000from subprocess import run, PIPE from pathlib import Path import tempfile import pytest DOTENV_FILE = """ # comment=foo TEST=foo TWOLINES='foo\nbar' TEST_COMMENT=foo # bar LINE_WITH_EQUAL='foo=bar' """ @pytest.fixture def dotenvfile(): _file = Path.cwd() / '.env' with _file.open('w') as fh: fh.write(DOTENV_FILE) yield _file _file.unlink() def test_stdout(dotenvfile): proc = run(['dotenv', 'echo', 'test'], stdout=PIPE) assert b'test' in proc.stdout def test_stderr(dotenvfile): proc = run(['dotenv', 'python', '-c', 'import os; os.write(2, b"test")'], stderr=PIPE) assert b'test' in proc.stderr def test_returncode(dotenvfile): proc = run(['dotenv', 'false']) assert proc.returncode == 1 proc = run(['dotenv', 'true']) assert proc.returncode == 0 def test_alternative_dotenv(): f = tempfile.NamedTemporaryFile('w') with open(f.name, 'w') as fh: fh.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_nonexisting_dotenv(): proc = run(['dotenv', '-e', '/tmp/i.dont.exist', 'true'], stderr=PIPE) assert proc.returncode != 0 assert b'FileNotFoundError' in proc.stderr def test_no_command(): proc = run(['dotenv']) assert proc.returncode == 0 tests/test_core.py000066400000000000000000000077441353471323100145770ustar00rootroot00000000000000import tempfile import pytest from dotenv_cli import core def test_full(): f = tempfile.NamedTemporaryFile('w') 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 open(f.name, 'w') as fh: fh.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(): """Basic unquoted strings""" f = tempfile.NamedTemporaryFile('w') TEST = "FOO=BAR" with open(f.name, 'w') as fh: fh.write(TEST) env = core.read_dotenv(f.name) assert env['FOO'] == 'BAR' def test_empty(): """Empty values become empty strings.""" f = tempfile.NamedTemporaryFile('w') TEST = "FOO=" with open(f.name, 'w') as fh: fh.write(TEST) env = core.read_dotenv(f.name) assert env['FOO'] == '' def test_inner_quotes(): """Inner quotes are mainained.""" f = tempfile.NamedTemporaryFile('w') TEST = "FOO=this 'is' a test" with open(f.name, 'w') as fh: fh.write(TEST) env = core.read_dotenv(f.name) assert env['FOO'] == "this 'is' a test" TEST = 'FOO=this "is" a test' with open(f.name, 'w') as fh: fh.write(TEST) env = core.read_dotenv(f.name) assert env['FOO'] == 'this "is" a test' def test_trim_whitespaces(): """Whitespaces are stripped from unquoted values.""" f = tempfile.NamedTemporaryFile('w') TEST = "FOO= test " with open(f.name, 'w') as fh: fh.write(TEST) env = core.read_dotenv(f.name) assert env['FOO'] == "test" def test_keep_whitespaces(): """Whitespaces are mainteined from quoted values.""" f = tempfile.NamedTemporaryFile('w') TEST = "FOO=' test '" with open(f.name, 'w') as fh: fh.write(TEST) env = core.read_dotenv(f.name) assert env['FOO'] == " test " def test_multiline(): """Quoted values can contain newlines.""" f = tempfile.NamedTemporaryFile('w') TEST = r'FOO="This is\nbar"' with open(f.name, 'w') as fh: fh.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_, expected): f = tempfile.NamedTemporaryFile('w') with open(f.name, 'w') as fh: fh.write(input_) env = core.read_dotenv(f.name) assert env['FOO'] == expected def test_comments(): """Lines starting with # are ignored.""" f = tempfile.NamedTemporaryFile('w') TEST = """ FOO=BAR # comment BAR=BAZ """ with open(f.name, 'w') as fh: fh.write(TEST) env = core.read_dotenv(f.name) assert len(env) == 2 assert env['FOO'] == 'BAR' assert env['BAR'] == 'BAZ' def test_emtpy_lines(): """Empty lines are skipped.""" f = tempfile.NamedTemporaryFile('w') TEST = """ FOO=BAR BAR=BAZ """ with open(f.name, 'w') as fh: fh.write(TEST) env = core.read_dotenv(f.name) assert len(env) == 2 assert env['FOO'] == 'BAR' assert env['BAR'] == 'BAZ' def test_export(): """Exports are allowed.""" f = tempfile.NamedTemporaryFile('w') TEST = "export FOO=BAR" with open(f.name, 'w') as fh: fh.write(TEST) env = core.read_dotenv(f.name) assert env['FOO'] == 'BAR' tests/test_version.py000066400000000000000000000001431353471323100153160ustar00rootroot00000000000000def test_version(): from dotenv_cli import __VERSION__ assert isinstance(__VERSION__, str)