codekingpro/portable-devtools
115k
1import json2import os3import shlex4import sys5from contextlib import contextmanager6from typing import IO, Any, Dict, Iterator, List, Optional7 8if sys.platform == "win32":9 from subprocess import Popen10 11try:12 import click13except ImportError:14 sys.stderr.write(15 "It seems python-dotenv is not installed with cli option. \n"16 'Run pip install "python-dotenv[cli]" to fix this.'17 )18 sys.exit(1)19 20from .main import dotenv_values, set_key, unset_key21from .version import __version__22 23 24def enumerate_env() -> Optional[str]:25 """26 Return a path for the ${pwd}/.env file.27 28 If pwd does not exist, return None.29 """30 try:31 cwd = os.getcwd()32 except FileNotFoundError:33 return None34 path = os.path.join(cwd, ".env")35 return path36 37 38@click.group()39@click.option(40 "-f",41 "--file",42 default=enumerate_env(),43 type=click.Path(file_okay=True),44 help="Location of the .env file, defaults to .env file in current working directory.",45)46@click.option(47 "-q",48 "--quote",49 default="always",50 type=click.Choice(["always", "never", "auto"]),51 help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.",52)53@click.option(54 "-e",55 "--export",56 default=False,57 type=click.BOOL,58 help="Whether to write the dot file as an executable bash script.",59)60@click.version_option(version=__version__)61@click.pass_context62def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None:63 """This script is used to set, get or unset values from a .env file."""64 ctx.obj = {"QUOTE": quote, "EXPORT": export, "FILE": file}65 66 67@contextmanager68def stream_file(path: os.PathLike) -> Iterator[IO[str]]:69 """70 Open a file and yield the corresponding (decoded) stream.71 72 Exits with error code 2 if the file cannot be opened.73 """74 75 try:76 with open(path) as stream:77 yield stream78 except OSError as exc:79 print(f"Error opening env file: {exc}", file=sys.stderr)80 sys.exit(2)81 82 83@cli.command(name="list")84@click.pass_context85@click.option(86 "--format",87 "output_format",88 default="simple",89 type=click.Choice(["simple", "json", "shell", "export"]),90 help="The format in which to display the list. Default format is simple, "91 "which displays name=value without quotes.",92)93def list_values(ctx: click.Context, output_format: str) -> None:94 """Display all the stored key/value."""95 file = ctx.obj["FILE"]96 97 with stream_file(file) as stream:98 values = dotenv_values(stream=stream)99 100 if output_format == "json":101 click.echo(json.dumps(values, indent=2, sort_keys=True))102 else:103 prefix = "export " if output_format == "export" else ""104 for k in sorted(values):105 v = values[k]106 if v is not None:107 if output_format in ("export", "shell"):108 v = shlex.quote(v)109 click.echo(f"{prefix}{k}={v}")110 111 112@cli.command(name="set")113@click.pass_context114@click.argument("key", required=True)115@click.argument("value", required=True)116def set_value(ctx: click.Context, key: Any, value: Any) -> None:117 """118 Store the given key/value.119 120 This doesn't follow symlinks, to avoid accidentally modifying a file at a121 potentially untrusted path.122 """123 124 file = ctx.obj["FILE"]125 quote = ctx.obj["QUOTE"]126 export = ctx.obj["EXPORT"]127 success, key, value = set_key(file, key, value, quote, export)128 if success:129 click.echo(f"{key}={value}")130 else:131 sys.exit(1)132 133 134@cli.command()135@click.pass_context136@click.argument("key", required=True)137def get(ctx: click.Context, key: Any) -> None:138 """Retrieve the value for the given key."""139 file = ctx.obj["FILE"]140 141 with stream_file(file) as stream:142 values = dotenv_values(stream=stream)143 144 stored_value = values.get(key)145 if stored_value:146 click.echo(stored_value)147 else:148 sys.exit(1)149 150 151@cli.command()152@click.pass_context153@click.argument("key", required=True)154def unset(ctx: click.Context, key: Any) -> None:155 """156 Removes the given key.157 158 This doesn't follow symlinks, to avoid accidentally modifying a file at a159 potentially untrusted path.160 """161 file = ctx.obj["FILE"]162 quote = ctx.obj["QUOTE"]163 success, key = unset_key(file, key, quote)164 if success:165 click.echo(f"Successfully removed {key}")166 else:167 sys.exit(1)168 169 170@cli.command(171 context_settings={172 "allow_extra_args": True,173 "allow_interspersed_args": False,174 "ignore_unknown_options": True,175 }176)177@click.pass_context178@click.option(179 "--override/--no-override",180 default=True,181 help="Override variables from the environment file with those from the .env file.",182)183@click.argument("commandline", nargs=-1, type=click.UNPROCESSED)184def run(ctx: click.Context, override: bool, commandline: tuple[str, ...]) -> None:185 """Run command with environment variables present."""186 file = ctx.obj["FILE"]187 if not os.path.isfile(file):188 raise click.BadParameter(189 f"Invalid value for '-f' \"{file}\" does not exist.", ctx=ctx190 )191 dotenv_as_dict = {192 k: v193 for (k, v) in dotenv_values(file).items()194 if v is not None and (override or k not in os.environ)195 }196 197 if not commandline:198 click.echo("No command given.")199 sys.exit(1)200 201 run_command([*commandline, *ctx.args], dotenv_as_dict)202 203 204def run_command(command: List[str], env: Dict[str, str]) -> None:205 """Replace the current process with the specified command.206 207 Replaces the current process with the specified command and the variables from `env`208 added in the current environment variables.209 210 Parameters211 ----------212 command: List[str]213 The command and it's parameters214 env: Dict215 The additional environment variables216 217 Returns218 -------219 None220 This function does not return any value. It replaces the current process with the new one.221 222 """223 # copy the current environment variables and add the vales from224 # `env`225 cmd_env = os.environ.copy()226 cmd_env.update(env)227 228 if sys.platform == "win32":229 # execvpe on Windows returns control immediately230 # rather than once the command has finished.231 p = Popen(command, universal_newlines=True, bufsize=0, shell=False, env=cmd_env)232 _, _ = p.communicate()233 234 sys.exit(p.returncode)235 else:236 os.execvpe(command[0], args=command, env=cmd_env)237 