mirror of
https://github.com/pi-hole/FTL.git
synced 2025-12-19 23:08:23 +00:00
301 lines
11 KiB
Python
301 lines
11 KiB
Python
import re
|
|
from pathlib import Path
|
|
|
|
|
|
def parse_toml_with_comments(filepath):
|
|
"""
|
|
Parse a TOML file with comments and generate a Markdown documentation string.
|
|
|
|
Args:
|
|
filepath (str or Path): Path to the TOML file.
|
|
|
|
Returns:
|
|
str: Markdown-formatted documentation.
|
|
"""
|
|
with open(filepath, encoding="utf-8") as f:
|
|
lines = f.readlines()
|
|
|
|
# Start with a Markdown header and introductory documentation block
|
|
documentation = [
|
|
"""<!--!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
! DO NOT EDIT THIS FILE MANUALLY! !
|
|
! THIS FILE IS AUTO-GENERATED FROM THE FTL REPO !
|
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
-->
|
|
<!-- markdownlint-disable MD033 -->
|
|
# Pi-hole FTL Configuration Reference
|
|
|
|
This page documents the available options of `pihole-FTL`. They are typically managed via the [TOML](https://toml.io/)-formatted configuration file `/etc/pihole/pihole.toml`. This file may be edited directly or you can use the command line (CLI) option, the web interface, the application programming interface (API) or environment variables.
|
|
|
|
Using the web interface, the API or the CLI is preferred as they can do error checking for you, trying to prevent any incompatible options which could prevent FTL from starting on a severely broken configuration.
|
|
|
|
To edit with the command line, use the format `key.name=value`, e.g:
|
|
|
|
```text
|
|
sudo pihole-FTL --config dns.dnssec=true
|
|
```
|
|
|
|
!!! note "Environment Variables"
|
|
**⚙️ Configuration Precedence**
|
|
Every Pi-hole setting in this file can be overridden using an environment variable.
|
|
This is especially common in Docker deployments.
|
|
Environment variable names follow the format:
|
|
```text
|
|
FTLCONF_<section>_<key>
|
|
```
|
|
For example:
|
|
```text
|
|
FTLCONF_dns_upstreams
|
|
FTLCONF_database_DBimport
|
|
```
|
|
⚠️ **If a setting is defined via an environment variable, it becomes read-only.**
|
|
You will not be able to override it through the TOML file, the command line, or the web interface until the variable is removed from the environment.
|
|
|
|
---
|
|
<!-- markdownlint-disable-file MD034 -->
|
|
"""
|
|
]
|
|
section_stack = []
|
|
comment_buffer = []
|
|
in_config = False # <-- New flag to skip file header comments
|
|
|
|
lines_iter = iter(lines)
|
|
for line in lines_iter:
|
|
stripped = line.strip()
|
|
|
|
# Handle section headers
|
|
if re.match(r"^\[.*\]$", stripped):
|
|
in_config = True # <-- Start processing comments now
|
|
section_stack = [stripped.strip("[]")]
|
|
documentation.append(f"\n## `[{'.'.join(section_stack)}]`\n")
|
|
continue
|
|
|
|
# If we are in a config section, start buffering comments
|
|
elif stripped.startswith("#"):
|
|
if in_config:
|
|
comment_buffer.append(stripped.lstrip("#").strip())
|
|
continue
|
|
|
|
# Handle key-value pairs
|
|
elif "=" in stripped and in_config:
|
|
key, value = map(str.strip, stripped.split("=", 1))
|
|
value_lines = [value]
|
|
|
|
# Check if value is a multi-line array
|
|
if value.startswith("[") and not value.endswith("]"):
|
|
# Multi-line array
|
|
while not value_lines[-1].strip().endswith("]"):
|
|
next_line = next(lines_iter).rstrip("\n")
|
|
value_lines.append(next_line)
|
|
value = "\n".join(value_lines)
|
|
|
|
else:
|
|
value = value_lines[0]
|
|
|
|
documentation.append(f"### `{key}`\n")
|
|
|
|
# Process the comments collected for this key
|
|
if comment_buffer:
|
|
adjusted_comments = []
|
|
in_allowed_values = False
|
|
|
|
i = 0
|
|
while i < len(comment_buffer):
|
|
line = comment_buffer[i]
|
|
is_bullet = line.lstrip().startswith("-")
|
|
prev_is_bullet = i > 0 and comment_buffer[i - 1].lstrip().startswith("-")
|
|
prev_is_blank = i > 0 and comment_buffer[i - 1].strip() == ""
|
|
|
|
# Fix malformed emphasis due to underscores or asterisks
|
|
line = re.sub(r'\b(_[a-zA-Z0-9.-]+)', r'`\1`', line)
|
|
line = re.sub(r'\*\.[a-zA-Z0-9]+', lambda m: f"`{m.group(0)}`", line)
|
|
|
|
# Mark if we are in an "Allowed values are:" section
|
|
if line.find('Allowed values are:') > -1:
|
|
in_allowed_values = True
|
|
|
|
# Bold "Allowed values are:"
|
|
line = re.sub(
|
|
r'(^|\s)(Allowed values are:)',
|
|
r'\1**Allowed values are:**',
|
|
line
|
|
)
|
|
|
|
# Bold "Example:"
|
|
line = re.sub(
|
|
r'(^|\s)(Example:)',
|
|
r'\1**Example:**',
|
|
line
|
|
)
|
|
|
|
# Insert 2 spaces to force a markdown line break after the value
|
|
# only if the bullet is in "Allowed values are:" section
|
|
if is_bullet and in_allowed_values:
|
|
line = re.sub(r'(-\s*\S+)$', r'\1 ', line)
|
|
|
|
# Insert blank line before bullet if needed
|
|
if is_bullet and not prev_is_bullet and not prev_is_blank:
|
|
adjusted_comments.append("")
|
|
|
|
# Default: just append the line
|
|
adjusted_comments.append(line)
|
|
i += 1
|
|
|
|
documentation.append(wrap_examples_and_allowed_values("\n".join(adjusted_comments)))
|
|
documentation.append("") # spacer after comment block
|
|
# Format default value for Markdown
|
|
if "\n" in value:
|
|
documentation.append("**Default value:**")
|
|
documentation.append("")
|
|
documentation.append("```toml")
|
|
documentation.append(f" {value}")
|
|
documentation.append("```")
|
|
documentation.append("")
|
|
else:
|
|
documentation.append(f"**Default value:** `{value}`\n")
|
|
|
|
# Compose full key for CLI/env var examples
|
|
full_key = ".".join(section_stack + [key])
|
|
env_var = "FTLCONF_" + full_key.replace(".", "_")
|
|
|
|
# TOML example tab
|
|
documentation.append(f'=== "TOML"')
|
|
documentation.append(" ```toml")
|
|
documentation.append(f" [{'.'.join(section_stack)}]")
|
|
# Indent multi-line values for TOML block
|
|
if "\n" in value:
|
|
indented_value = "\n".join(" " + v for v in value.splitlines()).strip()
|
|
documentation.append(f" {key} = {indented_value}")
|
|
else:
|
|
documentation.append(f" {key} = {value}")
|
|
documentation.append(" ```")
|
|
|
|
# CLI example tab
|
|
documentation.append(f'=== "CLI"')
|
|
documentation.append(" ```shell")
|
|
if "\n" in value and value.strip().startswith("["):
|
|
# Flatten multi-line array to single line for CLI
|
|
array_str = "".join(value.split())
|
|
documentation.append(f" sudo pihole-FTL --config {full_key}='{array_str}'")
|
|
else:
|
|
documentation.append(f" sudo pihole-FTL --config {full_key}={value}")
|
|
documentation.append(" ```")
|
|
|
|
# Environment variable example tab (for Docker Compose)
|
|
documentation.append(f'=== "Environment (Docker Compose)"')
|
|
documentation.append(" ```yaml")
|
|
documentation.append(" environment:")
|
|
yaml_value = value.replace('"',"'")
|
|
if "\n" in yaml_value:
|
|
yaml_value = f"|\n " + "\n ".join(yaml_value.splitlines())
|
|
documentation.append(f" {env_var}: {yaml_value}")
|
|
documentation.append(" ```\n")
|
|
comment_buffer = []
|
|
|
|
return "\n".join(documentation)
|
|
|
|
def wrap_examples_and_allowed_values(line):
|
|
"""
|
|
Wrap specific patterns in backticks:
|
|
- Complete arrays: [ "example" ] -> `[ "example" ]`
|
|
- Quoted strings: "example" -> `"example"`
|
|
- Angle brackets: <example> -> `<example>`
|
|
|
|
Ensures no nested backticks appear within wrapped content.
|
|
"""
|
|
|
|
# Process other patterns
|
|
result = ''
|
|
i = 0
|
|
in_backticks = False
|
|
|
|
while i < len(line):
|
|
# Skip content already in backticks
|
|
if in_backticks:
|
|
if line[i:i+1] == '`':
|
|
in_backticks = False
|
|
result += line[i]
|
|
i += 1
|
|
continue
|
|
|
|
# Look for patterns to wrap
|
|
if line[i:i+1] == '"':
|
|
# Find the matching closing quote
|
|
j = i + 1
|
|
while j < len(line) and line[j] != '"':
|
|
j += 1
|
|
if j < len(line): # Found closing quote
|
|
quoted_content = line[i:j+1]
|
|
result += f'`{quoted_content}`'
|
|
i = j + 1
|
|
in_backticks = False
|
|
|
|
elif line[i:i+1] == '<':
|
|
# Find the matching closing angle bracket
|
|
j = i + 1
|
|
while j < len(line) and line[j] != '>':
|
|
j += 1
|
|
if j < len(line): # Found closing bracket
|
|
angle_content = line[i:j+1]
|
|
result += f'`{angle_content}`'
|
|
i = j + 1
|
|
in_backticks = False
|
|
else: # No closing bracket found
|
|
result += line[i]
|
|
i += 1
|
|
|
|
elif line[i:i+1] == '[':
|
|
# Track nested square brackets
|
|
j = i + 1
|
|
bracket_count = 1
|
|
while j < len(line):
|
|
if line[j] == '[':
|
|
bracket_count += 1
|
|
elif line[j] == ']':
|
|
bracket_count -= 1
|
|
if bracket_count == 0:
|
|
break
|
|
j += 1
|
|
if bracket_count == 0 and j < len(line): # Found matching closing bracket
|
|
square_content = line[i:j+1]
|
|
result += f'`{square_content}`'
|
|
i = j + 1
|
|
in_backticks = False
|
|
|
|
else: # No matching closing bracket found
|
|
result += line[i]
|
|
i += 1
|
|
|
|
else:
|
|
result += line[i]
|
|
i += 1
|
|
|
|
return result
|
|
|
|
def write_markdown_doc(input_toml_path, output_md_path):
|
|
"""
|
|
Generate Markdown documentation from a TOML file and write it to a file.
|
|
|
|
Args:
|
|
input_toml_path (str or Path): Path to the input TOML file.
|
|
output_md_path (str or Path): Path to the output Markdown file.
|
|
"""
|
|
markdown = parse_toml_with_comments(input_toml_path)
|
|
Path(output_md_path).write_text(markdown, encoding="utf-8")
|
|
print(f"Documentation written to {output_md_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
# Expect exactly two arguments: input TOML and output Markdown
|
|
if len(sys.argv) != 3:
|
|
print("Usage: python pihole_toml_to_markdown.py <input.toml> <output.md>")
|
|
sys.exit(1)
|
|
|
|
input_path = sys.argv[1]
|
|
output_path = sys.argv[2]
|
|
write_markdown_doc(input_path, output_path)
|
|
|
|
|