|
2 | 2 | import logging |
3 | 3 | import os |
4 | 4 | import pathlib |
| 5 | +import re |
5 | 6 | import stat |
6 | 7 | import sys |
7 | 8 | import tempfile |
@@ -480,3 +481,91 @@ def _is_file_or_fifo(path: StrPath) -> bool: |
480 | 481 | return False |
481 | 482 |
|
482 | 483 | return stat.S_ISFIFO(st.st_mode) |
| 484 | + |
| 485 | + |
| 486 | +_DIRECTIVE_PRESERVE = "::dotenv-template-preserve" |
| 487 | +_DIRECTIVE_EXCLUDE = "::dotenv-template-exclude" |
| 488 | + |
| 489 | +# Matches `= <optional whitespace> <value>` where value is single-quoted, |
| 490 | +# double-quoted, or unquoted (everything up to a comment or end of line). |
| 491 | +_VALUE_RE = re.compile( |
| 492 | + r"(=[ \t]*)" |
| 493 | + r"(?:'(?:\\'|[^'])*'|\"(?:\\\"|[^\"])*\"|[^\s#\r\n]*)" |
| 494 | +) |
| 495 | + |
| 496 | +# Matches a directive token and any surrounding horizontal whitespace. |
| 497 | +_DIRECTIVE_RE = re.compile( |
| 498 | + r"[ \t]*(?:::dotenv-template-preserve|::dotenv-template-exclude)[ \t]*" |
| 499 | +) |
| 500 | + |
| 501 | + |
| 502 | +def _strip_directives(line: str) -> str: |
| 503 | + """Remove directive tokens from a line and trim any resulting trailing whitespace.""" |
| 504 | + result = _DIRECTIVE_RE.sub("", line) |
| 505 | + # Preserve the original line ending |
| 506 | + stripped = result.rstrip("\r\n") |
| 507 | + ending = result[len(stripped) :] |
| 508 | + return stripped.rstrip() + ending |
| 509 | + |
| 510 | + |
| 511 | +def generate_template( |
| 512 | + dotenv_path: Optional[StrPath] = None, |
| 513 | + stream: Optional[IO[str]] = None, |
| 514 | + encoding: Optional[str] = "utf-8", |
| 515 | + keep_directives: bool = False, |
| 516 | +) -> str: |
| 517 | + """ |
| 518 | + Generate a template from a .env file. |
| 519 | +
|
| 520 | + For each key-value binding, the value is replaced with the key name, unless |
| 521 | + an inline directive overrides this behavior: |
| 522 | +
|
| 523 | + - ``::dotenv-template-preserve`` keeps the line as-is (value included). |
| 524 | + - ``::dotenv-template-exclude`` removes the line from the template. |
| 525 | +
|
| 526 | + By default, directive tokens are stripped from the output. Set |
| 527 | + *keep_directives* to ``True`` to retain them. |
| 528 | +
|
| 529 | + Comments and blank lines are preserved. |
| 530 | +
|
| 531 | + Parameters: |
| 532 | + dotenv_path: Absolute or relative path to the .env file. |
| 533 | + stream: ``StringIO`` with .env content, used if *dotenv_path* is ``None``. |
| 534 | + encoding: Encoding used to read the file. |
| 535 | + keep_directives: If ``True``, directive comments are kept in the output. |
| 536 | +
|
| 537 | + Returns: |
| 538 | + The generated template as a string. |
| 539 | + """ |
| 540 | + if dotenv_path is None and stream is None: |
| 541 | + dotenv_path = find_dotenv() |
| 542 | + |
| 543 | + dotenv = DotEnv( |
| 544 | + dotenv_path=dotenv_path, |
| 545 | + stream=stream, |
| 546 | + interpolate=False, |
| 547 | + encoding=encoding, |
| 548 | + ) |
| 549 | + |
| 550 | + lines: list[str] = [] |
| 551 | + with dotenv._get_stream() as s: |
| 552 | + for binding in with_warn_for_invalid_lines(parse_stream(s)): |
| 553 | + original = binding.original.string |
| 554 | + |
| 555 | + # Comments, blank lines, and parse errors: preserve as-is |
| 556 | + if binding.key is None: |
| 557 | + lines.append(original) |
| 558 | + continue |
| 559 | + |
| 560 | + if _DIRECTIVE_EXCLUDE in original: |
| 561 | + continue |
| 562 | + |
| 563 | + if _DIRECTIVE_PRESERVE in original: |
| 564 | + line = original if keep_directives else _strip_directives(original) |
| 565 | + lines.append(line) |
| 566 | + continue |
| 567 | + |
| 568 | + # Replace the value with the key name |
| 569 | + lines.append(_VALUE_RE.sub(r"\g<1>" + binding.key, original, count=1)) |
| 570 | + |
| 571 | + return "".join(lines) |
0 commit comments