mirror of
https://github.com/containers/podman.git
synced 2026-03-13 08:01:19 +08:00
Merge pull request #26929 from jankaluza/quadlet-docs
Rewrite the Quadlet documentation.
This commit is contained in:
@@ -23,6 +23,100 @@ class Preprocessor():
|
||||
self.pod_or_container = ''
|
||||
self.used_by = {}
|
||||
|
||||
def render(self, text: str, context: dict) -> str:
|
||||
"""
|
||||
Renders the `text` handling the following extra formatting features:
|
||||
|
||||
```
|
||||
<< if variable >>
|
||||
...
|
||||
<< endif >>
|
||||
|
||||
<< if not variable >>
|
||||
...
|
||||
<< else >>
|
||||
...
|
||||
<< endif >>
|
||||
|
||||
<< "foo" if variable else "bar" >>
|
||||
```
|
||||
|
||||
Returns the rendered text.
|
||||
"""
|
||||
# Match << ... >>
|
||||
TOK = re.compile(r"<<(.*?)>>", re.DOTALL)
|
||||
out = []
|
||||
pos = 0
|
||||
stack = [] # each frame: {"active": bool, "seen_else": bool}
|
||||
|
||||
def is_active():
|
||||
return all(f["active"] for f in stack)
|
||||
|
||||
def get_variable(name: str):
|
||||
v = context.get(name, None)
|
||||
if v is None:
|
||||
raise ValueError(f"undefined variable: {name}")
|
||||
return v
|
||||
|
||||
def truthy(name: str) -> bool:
|
||||
name = name.strip()
|
||||
if name.startswith("not "):
|
||||
v = get_variable(name[4:].strip())
|
||||
return not bool(v)
|
||||
return bool(get_variable(name))
|
||||
|
||||
for m in TOK.finditer(text):
|
||||
# write literal up to token
|
||||
literal = text[pos:m.start()]
|
||||
if is_active():
|
||||
out.append(literal)
|
||||
pos = m.end()
|
||||
|
||||
inner = m.group(1).strip()
|
||||
|
||||
# control blocks
|
||||
if inner.startswith("if ") and len(inner[3:].strip().split(" ")) in [1, 2]:
|
||||
cond = inner[3:].strip()
|
||||
stack.append({"active": is_active() and truthy(cond), "seen_else": False})
|
||||
continue
|
||||
if inner == "else":
|
||||
if not stack:
|
||||
raise ValueError("`else` without `if`")
|
||||
frame = stack[-1]
|
||||
if frame["seen_else"]:
|
||||
raise ValueError("multiple `else` in the same `if`")
|
||||
frame["seen_else"] = True
|
||||
parent_active = all(f["active"] for f in stack[:-1])
|
||||
frame["active"] = parent_active and not frame["active"]
|
||||
continue
|
||||
if inner == "endif":
|
||||
if not stack:
|
||||
raise ValueError("`end` without `if`")
|
||||
stack.pop()
|
||||
continue
|
||||
|
||||
# inline "X if cond else Y" ---
|
||||
if " if " in inner and " else " in inner:
|
||||
try:
|
||||
# split by " if " then " else "
|
||||
then_part, rest = inner.split(" if ", 1)
|
||||
cond, else_part = rest.split(" else ", 1)
|
||||
cond = cond.strip()
|
||||
chosen = then_part if truthy(cond) else else_part
|
||||
if is_active():
|
||||
out.append(chosen.strip().strip("'\""))
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid inline if/else syntax: {inner}") from e
|
||||
continue
|
||||
|
||||
# trailing literal
|
||||
if is_active():
|
||||
out.append(text[pos:])
|
||||
|
||||
if stack:
|
||||
raise ValueError("unclosed `if` block(s)")
|
||||
return "".join(out)
|
||||
|
||||
def process(self, infile:str):
|
||||
"""
|
||||
Main calling point: preprocesses one file
|
||||
@@ -40,18 +134,24 @@ class Preprocessor():
|
||||
|
||||
with open(infile, 'r', encoding='utf-8') as fh_in, open(outfile_tmp, 'w', encoding='utf-8', newline='\n') as fh_out:
|
||||
for line in fh_in:
|
||||
# '@@option foo' -> include file options/foo.md
|
||||
if line.startswith('@@option '):
|
||||
_, optionname = line.strip().split(" ")
|
||||
optionfile = os.path.join("options", optionname + '.md')
|
||||
self.track_optionfile(optionfile)
|
||||
self.insert_file(fh_out, optionfile)
|
||||
# '@@include relative-path/must-exist.md'
|
||||
elif line.startswith('@@include '):
|
||||
_, path = line.strip().split(" ")
|
||||
self.insert_file(fh_out, path)
|
||||
else:
|
||||
fh_out.write(line)
|
||||
try:
|
||||
# '@@option foo' -> include file options/foo.md
|
||||
if line.startswith('@@option '):
|
||||
_, optionname = line.strip().split(" ")
|
||||
is_quadlet = optionname.startswith("quadlet:")
|
||||
if is_quadlet:
|
||||
optionname = optionname[len("quadlet:"):]
|
||||
optionfile = os.path.join("options", optionname + '.md')
|
||||
self.track_optionfile(optionfile)
|
||||
self.insert_file(fh_out, optionfile, is_quadlet)
|
||||
# '@@include relative-path/must-exist.md'
|
||||
elif line.startswith('@@include '):
|
||||
_, path = line.strip().split(" ")
|
||||
self.insert_file(fh_out, path)
|
||||
else:
|
||||
fh_out.write(line)
|
||||
except Exception as ex:
|
||||
raise Exception(f"Error while processing {infile} line '{line[:-1]}'") from ex
|
||||
|
||||
os.chmod(outfile_tmp, 0o444)
|
||||
os.rename(outfile_tmp, outfile)
|
||||
@@ -87,7 +187,7 @@ class Preprocessor():
|
||||
else:
|
||||
os.unlink(tmpfile)
|
||||
|
||||
def insert_file(self, fh_out, path: str):
|
||||
def insert_file(self, fh_out, path: str, is_quadlet=False):
|
||||
"""
|
||||
Reads one option file, writes it out to the given output filehandle
|
||||
"""
|
||||
@@ -98,13 +198,15 @@ class Preprocessor():
|
||||
# comment in its output.
|
||||
fh_out.write("\n[//]: # (BEGIN included file " + path + ")\n")
|
||||
with open(path, 'r', encoding='utf-8') as fh_included:
|
||||
for opt_line in fh_included:
|
||||
rendered = self.render(fh_included.read(), {"is_quadlet": is_quadlet})
|
||||
for opt_line in rendered.splitlines():
|
||||
if opt_line.startswith('####>'):
|
||||
continue
|
||||
|
||||
opt_line = self.replace_type(opt_line)
|
||||
opt_line = opt_line.replace('<<subcommand>>', self.podman_subcommand())
|
||||
opt_line = opt_line.replace('<<fullsubcommand>>', self.podman_subcommand('full'))
|
||||
fh_out.write(opt_line)
|
||||
fh_out.write(opt_line + '\n')
|
||||
fh_out.write("\n[//]: # (END included file " + path + ")\n")
|
||||
|
||||
def podman_subcommand(self, full=None) -> str:
|
||||
@@ -117,6 +219,9 @@ class Preprocessor():
|
||||
if not full:
|
||||
if subcommand.startswith("podman-pod-"):
|
||||
subcommand = subcommand[len("podman-pod-"):]
|
||||
# For quadlet man-pages, the subcommand is simply the full manpage name.
|
||||
if subcommand.endswith(".unit.5.md.in"):
|
||||
return subcommand
|
||||
if subcommand.startswith("podman-"):
|
||||
subcommand = subcommand[len("podman-"):]
|
||||
if subcommand.endswith(".1.md.in"):
|
||||
|
||||
@@ -19,7 +19,16 @@ our $VERSION = '0.1';
|
||||
# BEGIN user-customizable section
|
||||
|
||||
our $Go = 'pkg/systemd/quadlet/quadlet.go';
|
||||
our $Doc = 'docs/source/markdown/podman-systemd.unit.5.md';
|
||||
our @Docs = (
|
||||
"docs/source/markdown/podman-build.unit.5.md",
|
||||
"docs/source/markdown/podman-container.unit.5.md",
|
||||
"docs/source/markdown/podman-kube.unit.5.md",
|
||||
"docs/source/markdown/podman-network.unit.5.md",
|
||||
"docs/source/markdown/podman-pod.unit.5.md",
|
||||
"docs/source/markdown/podman-volume.unit.5.md",
|
||||
"docs/source/markdown/podman-image.unit.5.md",
|
||||
"docs/source/markdown/podman-quadlet.7.md",
|
||||
);
|
||||
|
||||
# END user-customizable section
|
||||
###############################################################################
|
||||
@@ -35,7 +44,7 @@ $ME cross-checks quadlet documentation between the Go source[Go]
|
||||
and the man page[MD].
|
||||
|
||||
[Go]: $Go
|
||||
[MD]: $Doc
|
||||
[MD]: @Docs
|
||||
|
||||
We check that:
|
||||
|
||||
@@ -95,7 +104,7 @@ sub main {
|
||||
my $true_keys = read_go($Go);
|
||||
|
||||
# Read md file, compare against Truth
|
||||
crossref_doc($Doc, $true_keys);
|
||||
crossref_docs(\@Docs, $true_keys);
|
||||
|
||||
exit $errs;
|
||||
}
|
||||
@@ -141,19 +150,17 @@ sub read_go {
|
||||
}
|
||||
|
||||
##################
|
||||
# crossref_doc # Read the markdown page, cross-check against Truth
|
||||
# crossref_docs # Read the markdown pages, cross-check against Truth
|
||||
##################
|
||||
sub crossref_doc {
|
||||
my $path = shift; # in: path to .md file
|
||||
sub crossref_docs {
|
||||
my $paths_ref = shift; # in: array with paths to .md file
|
||||
my $true_keys = shift; # in: AREF, list of keys from .go
|
||||
|
||||
open my $fh, '<', $path
|
||||
or die "$ME: Cannot read $path: $!\n";;
|
||||
|
||||
my $unit = '';
|
||||
my %documented;
|
||||
my @found_in_table;
|
||||
my @described;
|
||||
my $read_first_table;
|
||||
|
||||
# Helper function: when done reading description blocks,
|
||||
# make sure that there's one block for each key listed
|
||||
@@ -166,85 +173,86 @@ sub crossref_doc {
|
||||
}
|
||||
};
|
||||
|
||||
# Main loop: read the docs line by line
|
||||
while (my $line = <$fh>) {
|
||||
chomp $line;
|
||||
# foreach loop
|
||||
foreach my $path (@$paths_ref) {
|
||||
open my $fh, '<', $path
|
||||
or die "$ME: Cannot read $path: $!\n";;
|
||||
|
||||
# New section, with its own '| table |' and '### Keyword blocks'
|
||||
if ($line =~ /^##\s+(\S+)\s+(?:units|section)\s+\[(\S+)\]/) {
|
||||
my $new_unit = $1;
|
||||
$new_unit eq $2
|
||||
or warn "$ME: $path:$.: inconsistent block names in '$line'\n";
|
||||
my $new_unit = $path;
|
||||
$crossref_against_table->();
|
||||
$unit = $new_unit;
|
||||
|
||||
$crossref_against_table->();
|
||||
# Reset, because each section has its own table & blocks
|
||||
@found_in_table = ();
|
||||
@described = ();
|
||||
$read_first_table = 0;
|
||||
|
||||
$unit = $new_unit;
|
||||
|
||||
# Reset, because each section has its own table & blocks
|
||||
@found_in_table = ();
|
||||
@described = ();
|
||||
next;
|
||||
}
|
||||
# Main loop: read the docs line by line
|
||||
while (my $line = <$fh>) {
|
||||
chomp $line;
|
||||
|
||||
# Table line
|
||||
if ($line =~ s/^\|\s+//) {
|
||||
next if $line =~ /^\*\*/; # title
|
||||
next if $line =~ /^-----/; # divider
|
||||
# Table line
|
||||
if ($read_first_table == 0 && $line =~ s/^\|\s+//) {
|
||||
next if $line =~ /^\*\*/; # title
|
||||
next if $line =~ /^-----/; # divider
|
||||
|
||||
if ($line =~ /^([A-Z][A-Za-z6]+)=/) {
|
||||
if ($line =~ /^([A-Z][A-Za-z6]+)=/) {
|
||||
my $key = $1;
|
||||
|
||||
grep { $_ eq $key } @$true_keys
|
||||
or warn "$ME: $path:$.: unknown key '$key' (not present in $Go)\n";
|
||||
|
||||
# Sorting check
|
||||
if (@found_in_table) {
|
||||
if (lc($key) lt lc($found_in_table[-1])) {
|
||||
warn "$ME: $path:$.: out-of-order key '$key' in table\n";
|
||||
}
|
||||
}
|
||||
|
||||
push @found_in_table, $key;
|
||||
$documented{$key}++;
|
||||
}
|
||||
else {
|
||||
warn "$ME: $path:$.: cannot grok table line '$line'\n";
|
||||
}
|
||||
}
|
||||
|
||||
# Description block
|
||||
elsif ($line =~ /^###\s+`([A-Z][A-Za-z6]+)=.*`/) {
|
||||
my $key = $1;
|
||||
|
||||
grep { $_ eq $key } @$true_keys
|
||||
or warn "$ME: $path:$.: unknown key '$key' (not present in $Go)\n";
|
||||
$read_first_table = 1;
|
||||
|
||||
# Sorting check
|
||||
if (@found_in_table) {
|
||||
if (lc($key) lt lc($found_in_table[-1])) {
|
||||
warn "$ME: $path:$.: out-of-order key '$key' in table\n";
|
||||
# Check for dups and for out-of-order
|
||||
if (@described) {
|
||||
if (lc($key) lt lc($described[-1])) {
|
||||
warn "$ME: $path:$.: out-of-order key '$key'\n";
|
||||
}
|
||||
if (grep { lc($_) eq lc($key) } @described) {
|
||||
warn "$ME: $path:$.: duplicate key '$key'\n";
|
||||
}
|
||||
}
|
||||
|
||||
push @found_in_table, $key;
|
||||
grep { $_ eq $key } @found_in_table
|
||||
or warn "$ME: $path:$.: key '$key' is not listed in table for unit/section '$unit'\n";
|
||||
|
||||
push @described, $key;
|
||||
$documented{$key}++;
|
||||
}
|
||||
else {
|
||||
warn "$ME: $path:$.: cannot grok table line '$line'\n";
|
||||
}
|
||||
}
|
||||
|
||||
# Description block
|
||||
elsif ($line =~ /^###\s+`(\S+)=`/) {
|
||||
my $key = $1;
|
||||
|
||||
# Check for dups and for out-of-order
|
||||
if (@described) {
|
||||
if (lc($key) lt lc($described[-1])) {
|
||||
warn "$ME: $path:$.: out-of-order key '$key'\n";
|
||||
}
|
||||
if (grep { lc($_) eq lc($key) } @described) {
|
||||
warn "$ME: $path:$.: duplicate key '$key'\n";
|
||||
}
|
||||
}
|
||||
|
||||
grep { $_ eq $key } @found_in_table
|
||||
or warn "$ME: $path:$.: key '$key' is not listed in table for unit/section '$unit'\n";
|
||||
|
||||
push @described, $key;
|
||||
$documented{$key}++;
|
||||
}
|
||||
close $fh;
|
||||
}
|
||||
|
||||
close $fh;
|
||||
|
||||
# Final cross-check between table and description blocks
|
||||
$crossref_against_table->();
|
||||
|
||||
# Check that no Go keys are missing
|
||||
|
||||
(my $md_basename = $path) =~ s|^.*/||;
|
||||
for my $k (@$true_keys) {
|
||||
$documented{$k}
|
||||
or warn "$ME: undocumented key: '$k' not found anywhere in $md_basename\n";
|
||||
or warn "$ME: undocumented key: '$k' not found anywhere in @$paths_ref\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user