Format: text/x-python
⬇ Download File (5.6 KB)
   1  import json
   2  import os
   3  import shlex
   4  import sys
   5  from contextlib import contextmanager
   6  from typing import Any, Dict, IO, Iterator, List, Optional
   7  
   8  try:
   9      import click
  10  except ImportError:
  11      sys.stderr.write('It seems python-dotenv is not installed with cli option. \n'
  12                       'Run pip install "python-dotenv[cli]" to fix this.')
  13      sys.exit(1)
  14  
  15  from .main import dotenv_values, set_key, unset_key
  16  from .version import __version__
  17  
  18  
  19  def enumerate_env() -> Optional[str]:
  20      """
  21      Return a path for the ${pwd}/.env file.
  22  
  23      If pwd does not exist, return None.
  24      """
  25      try:
  26          cwd = os.getcwd()
  27      except FileNotFoundError:
  28          return None
  29      path = os.path.join(cwd, '.env')
  30      return path
  31  
  32  
  33  @click.group()
  34  @click.option('-f', '--file', default=enumerate_env(),
  35                type=click.Path(file_okay=True),
  36                help="Location of the .env file, defaults to .env file in current working directory.")
  37  @click.option('-q', '--quote', default='always',
  38                type=click.Choice(['always', 'never', 'auto']),
  39                help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.")
  40  @click.option('-e', '--export', default=False,
  41                type=click.BOOL,
  42                help="Whether to write the dot file as an executable bash script.")
  43  @click.version_option(version=__version__)
  44  @click.pass_context
  45  def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None:
  46      """This script is used to set, get or unset values from a .env file."""
  47      ctx.obj = {'QUOTE': quote, 'EXPORT': export, 'FILE': file}
  48  
  49  
  50  @contextmanager
  51  def stream_file(path: os.PathLike) -> Iterator[IO[str]]:
  52      """
  53      Open a file and yield the corresponding (decoded) stream.
  54  
  55      Exits with error code 2 if the file cannot be opened.
  56      """
  57  
  58      try:
  59          with open(path) as stream:
  60              yield stream
  61      except OSError as exc:
  62          print(f"Error opening env file: {exc}", file=sys.stderr)
  63          exit(2)
  64  
  65  
  66  @cli.command()
  67  @click.pass_context
  68  @click.option('--format', default='simple',
  69                type=click.Choice(['simple', 'json', 'shell', 'export']),
  70                help="The format in which to display the list. Default format is simple, "
  71                     "which displays name=value without quotes.")
  72  def list(ctx: click.Context, format: bool) -> None:
  73      """Display all the stored key/value."""
  74      file = ctx.obj['FILE']
  75  
  76      with stream_file(file) as stream:
  77          values = dotenv_values(stream=stream)
  78  
  79      if format == 'json':
  80          click.echo(json.dumps(values, indent=2, sort_keys=True))
  81      else:
  82          prefix = 'export ' if format == 'export' else ''
  83          for k in sorted(values):
  84              v = values[k]
  85              if v is not None:
  86                  if format in ('export', 'shell'):
  87                      v = shlex.quote(v)
  88                  click.echo(f'{prefix}{k}={v}')
  89  
  90  
  91  @cli.command()
  92  @click.pass_context
  93  @click.argument('key', required=True)
  94  @click.argument('value', required=True)
  95  def set(ctx: click.Context, key: Any, value: Any) -> None:
  96      """Store the given key/value."""
  97      file = ctx.obj['FILE']
  98      quote = ctx.obj['QUOTE']
  99      export = ctx.obj['EXPORT']
 100      success, key, value = set_key(file, key, value, quote, export)
 101      if success:
 102          click.echo(f'{key}={value}')
 103      else:
 104          exit(1)
 105  
 106  
 107  @cli.command()
 108  @click.pass_context
 109  @click.argument('key', required=True)
 110  def get(ctx: click.Context, key: Any) -> None:
 111      """Retrieve the value for the given key."""
 112      file = ctx.obj['FILE']
 113  
 114      with stream_file(file) as stream:
 115          values = dotenv_values(stream=stream)
 116  
 117      stored_value = values.get(key)
 118      if stored_value:
 119          click.echo(stored_value)
 120      else:
 121          exit(1)
 122  
 123  
 124  @cli.command()
 125  @click.pass_context
 126  @click.argument('key', required=True)
 127  def unset(ctx: click.Context, key: Any) -> None:
 128      """Removes the given key."""
 129      file = ctx.obj['FILE']
 130      quote = ctx.obj['QUOTE']
 131      success, key = unset_key(file, key, quote)
 132      if success:
 133          click.echo(f"Successfully removed {key}")
 134      else:
 135          exit(1)
 136  
 137  
 138  @cli.command(context_settings={'ignore_unknown_options': True})
 139  @click.pass_context
 140  @click.option(
 141      "--override/--no-override",
 142      default=True,
 143      help="Override variables from the environment file with those from the .env file.",
 144  )
 145  @click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
 146  def run(ctx: click.Context, override: bool, commandline: List[str]) -> None:
 147      """Run command with environment variables present."""
 148      file = ctx.obj['FILE']
 149      if not os.path.isfile(file):
 150          raise click.BadParameter(
 151              f'Invalid value for \'-f\' "{file}" does not exist.',
 152              ctx=ctx
 153          )
 154      dotenv_as_dict = {
 155          k: v
 156          for (k, v) in dotenv_values(file).items()
 157          if v is not None and (override or k not in os.environ)
 158      }
 159  
 160      if not commandline:
 161          click.echo('No command given.')
 162          exit(1)
 163      run_command(commandline, dotenv_as_dict)
 164  
 165  
 166  def run_command(command: List[str], env: Dict[str, str]) -> None:
 167      """Replace the current process with the specified command.
 168  
 169      Replaces the current process with the specified command and the variables from `env`
 170      added in the current environment variables.
 171  
 172      Parameters
 173      ----------
 174      command: List[str]
 175          The command and it's parameters
 176      env: Dict
 177          The additional environment variables
 178  
 179      Returns
 180      -------
 181      None
 182          This function does not return any value. It replaces the current process with the new one.
 183  
 184      """
 185      # copy the current environment variables and add the vales from
 186      # `env`
 187      cmd_env = os.environ.copy()
 188      cmd_env.update(env)
 189  
 190      os.execvpe(command[0], args=command, env=cmd_env)
 191