#!/usr/bin/python3 # # Generate documentation for how this machine works by # parsing our bash scripts! import cgi, re import markdown from modgrammar import * def generate_documentation(): print("""
Here’s how you can build your own mail server from scratch.
This document is generated automatically from Mail-in-a-Box’s setup script source code.
%s
\n" return "" class EditConf(Grammar): grammar = ( L('tools/editconf.py '), FILENAME, SPACE, OPTIONAL((LIST_OF( L("-w") | L("-s") | L("-c ;"), sep=SPACE, ), SPACE)), REST_OF_LINE, OPTIONAL(SPACE), EOL ) def value(self): conffile = self[1] options = [] eq = "=" if self[3] and "-s" in self[3].string: eq = " " for opt in re.split("\s+", self[4].string): k, v = opt.split("=", 1) v = re.sub(r"\n+", "", fixup_tokens(v)) # not sure why newlines are getting doubled options.append("%s%s%s" % (k, eq, v)) return "" + recode_bash(self.string.strip()) + "
" + "\n".join(cgi.escape(s) for s in options) + "
" + cgi.escape(cmd) + "
replace
" + cgi.escape(self[3].string.replace(".*", ". . .")) + "
with
" + cgi.escape(self[5].string.replace("\\n", "\n").replace("\\t", "\t")) + "
\n" def shell_line(bash): return "echo " + recode_bash(text) + " \
| " + recode_bash(self[4].string) + "
\n" class AptGet(Grammar): grammar = (ZERO_OR_MORE(SPACE), L("apt_install "), REST_OF_LINE, EOL) def value(self): return shell_line("apt-get install -y " + re.sub(r"\s+", " ", self[2].string)) class UfwAllow(Grammar): grammar = (ZERO_OR_MORE(SPACE), L("ufw_allow "), REST_OF_LINE, EOL) def value(self): return shell_line("ufw allow " + self[2].string) class UfwLimit(Grammar): grammar = (ZERO_OR_MORE(SPACE), L("ufw_limit "), REST_OF_LINE, EOL) def value(self): return shell_line("ufw limit " + self[2].string) class RestartService(Grammar): grammar = (ZERO_OR_MORE(SPACE), L("restart_service "), REST_OF_LINE, EOL) def value(self): return shell_line("service " + self[2].string + " restart") class OtherLine(Grammar): grammar = (REST_OF_LINE, EOL) def value(self): if self.string.strip() == "": return "" if "source setup/functions.sh" in self.string: return "" if "source /etc/mailinabox.conf" in self.string: return "" return "" + recode_bash(bash.strip()) + "
\n" class BashElement(Grammar): grammar = Comment | CatEOF | EchoPipe | EchoLine | HideOutput | EditConf | SedReplace | AptGet | UfwAllow | UfwLimit | RestartService | OtherLine def value(self): return self[0].value() # Make some special characters to private use Unicode code points. bash_special_characters1 = { "\n": "\uE000", " ": "\uE001", } bash_special_characters2 = { "$": "\uE010", } bash_escapes = { "n": "\uE020", "t": "\uE021", } def quasitokenize(bashscript): # Make a parse of bash easier by making the tokenization easy. newscript = "" quote_mode = None escape_next = False line_comment = False subshell = 0 for c in bashscript: if line_comment: # We're in a comment until the end of the line. newscript += c if c == '\n': line_comment = False elif escape_next: # Previous character was a \. Normally the next character # comes through literally, but escaped newlines are line # continuations and some escapes are for special characters # which we'll recode and then turn back into escapes later. if c == "\n": c = " " elif c in bash_escapes: c = bash_escapes[c] newscript += c escape_next = False elif c == "\\": # Escaping next character. escape_next = True elif quote_mode is None and c in ('"', "'"): # Starting a quoted word. quote_mode = c elif c == quote_mode: # Ending a quoted word. quote_mode = None elif quote_mode is not None and quote_mode != "EOF" and c in bash_special_characters1: # Replace special tokens within quoted words so that they # don't interfere with tokenization later. newscript += bash_special_characters1[c] elif quote_mode is None and c == '#': # Start of a line comment. newscript += c line_comment = True elif quote_mode is None and c == ';' and subshell == 0: # End of a statement. newscript += "\n" elif quote_mode is None and c == '(': # Start of a subshell. newscript += c subshell += 1 elif quote_mode is None and c == ')': # End of a subshell. newscript += c subshell -= 1 elif quote_mode is None and c == '\t': # Make these just spaces. if newscript[-1] != " ": newscript += " " elif quote_mode is None and c == ' ': # Collapse consecutive spaces. if newscript[-1] != " ": newscript += " " elif c in bash_special_characters2: newscript += bash_special_characters2[c] else: # All other characters. newscript += c # "<< EOF" escaping. if quote_mode is None and re.search("<<\s*EOF\n$", newscript): quote_mode = "EOF" elif quote_mode == "EOF" and re.search("\nEOF\n$", newscript): quote_mode = None return newscript def recode_bash(s): def requote(tok): tok = tok.replace("\\", "\\\\") for c in bash_special_characters2: tok = tok.replace(c, "\\" + c) tok = fixup_tokens(tok) if " " in tok or '"' in tok: tok = tok.replace("\"", "\\\"") tok = '"' + tok +'"' else: tok = tok.replace("'", "\\'") return tok return cgi.escape(" ".join(requote(tok) for tok in s.split(" "))) def fixup_tokens(s): for c, enc in bash_special_characters1.items(): s = s.replace(enc, c) for c, enc in bash_special_characters2.items(): s = s.replace(enc, c) for esc, c in bash_escapes.items(): s = s.replace(c, "\\" + esc) return s class BashScript(Grammar): grammar = (OPTIONAL(HashBang), REPEAT(BashElement)) def value(self): return [line.value() for line in self[1]] @staticmethod def parse(fn): if fn in ("setup/functions.sh", "/etc/mailinabox.conf"): return "" with open(fn, "r") as f: string = f.read() # tokenize string = re.sub(".* #NODOC\n", "", string) string = re.sub("\n\s*if .*then.*|\n\s*fi|\n\s*else|\n\s*elif .*", "", string) string = quasitokenize(string) string = re.sub("hide_output ", "", string) parser = BashScript.parser() result = parser.parse_string(string) v = "" + recode_bash(self.string.strip()) + "
\n" # col v += "
", "") v = re.sub("([\w\W]*?)", lambda m : "" + strip_indent(m.group(1)) + "", v) v = re.sub(r"(\$?)PRIMARY_HOSTNAME", r"box.yourdomain.com", v) v = re.sub(r"\$STORAGE_ROOT", r"$STORE", v) v = v.replace("`pwd`", "/path/to/mailinabox
") return v def wrap_lines(text, cols=60): ret = "" words = re.split("(\s+)", text) linelen = 0 for w in words: if linelen + len(w) > cols-1: ret += " \\\n" ret += " " linelen = 0 if linelen == 0 and w.strip() == "": continue ret += w linelen += len(w) return ret if __name__ == '__main__': generate_documentation()