I've tried similar approach with handcrafted config files and comparison, and crafted a Python script to compare actual config to one I'd want:
# RouterOS config file parser/sorter/comparer ver.0
# by 611
import sys
import re
# My preferred order of parameter sorting
# Firtsparams will go first in the listed order, then unlisted params in alphabetical order, then lastparams in the listed order
firstparams = "name", "peer", "list", "time-zone-name", \
"chain", "action", "address-list", "address-list-timeout", "connection-state", "protocol", \
"src-address", "src-address-list", "src-port", "dst-address", "dst-address-list", "dst-port", \
"local-address", "remote-address", \
"master-interface", "mode", "ssid", "security-profile", "wireless-protocol", "band", "country", "frequency", "channel-width", \
"server", "address", "mac-address", "network", "target", \
"leds", "gateway", "distance", "type", \
"bridge", "interface", "user", "password", \
"servers", "authentication-types", "wpa2-pre-shared-key"
lastparams = "disabled", "log", "log-prefix", "comment", "source"
# Sorting switch
dosort = False
doimport = True
includenoncommands = False
# Sort parameters with first list and last list
# Args: dict params; Returns: list of (arg, val) tuples sortedparams
def SortParams(params):
sortedparams = []
# First, add all present params on the "first" list in their sort order.
for p in firstparams:
if p in params:
sortedparams.append((p, params.pop(p)))
# Second, add all params NOT on the "last" list in alphabetical order.
for p in sorted(params):
if p not in lastparams:
sortedparams.append((p, params.pop(p)))
# Third, add all params on the "last" list in their sort order.
for p in lastparams:
if p in params:
sortedparams.append((p, params.pop(p)))
# Here we may have checked if there's anything left in params, but it's impossible :)
# Return sorted list
return sortedparams
# Make a set out of config list
# Args: list of tuples (cmd, params); Returns: same as a set
def MakeSet(config):
configset = set()
for line in config:
(cmd, params) = line
if (len(params) != 0) | includenoncommands:
configset.add((cmd, tuple(params)))
return configset
# Parse a Mikrotik-formatted config line
# Args: string line; Returns: tuple (cmd, params)
def ParseMTrscLine(line):
# Split the line into command (starting with "/", with optional condition in "[]")
# and parameters (starting with "arg=" or "!arg").
# Suggestions are welcome: this regex that may misbehave on cases like `/cmd [cond="]"] arg=...` -
# in-string closing square bracket would be caught.
# Command starts with "/", lazy captures anything to the next group, optionally includes "[" lazy capture of condition "]"
# Parameters are starting with some space and either non-space ending with "=" or non-space starting with "!"
parsedline = re.match(r"(?P<cmd>/.+?(?:\[.+?\])?)" + r"(?P<params>\s+(?:\S+?(?==)|\!\S+).*)", line)
# Return None if the line failed to parse (doesn't look like command)
if parsedline == None:
return None
# Split parameters into arument-value pairs (no value for inverted argument starting with "!"),
# strip extra spaces between parameters.
# Suggestions are welcome: this regex that may misbehave on cases like `arg="val\\"` -
# quotes that _look_ escaped are skipped when looking for the end of quoted value,
# even though they might be not actually escaped.
# Parameters are separated (started) with some space,
# and are either (non-space lazy) arg ending with "=" or just a (greedy!) arg starting with "!".
# Values are optional and starting with with "=", either quoted with any symbols inside (lazy),
# or with end-of-line instead of closing quote, or not quoted non-space.
# Quotes that _look_ escaped are skipped using negative lookbeihing group.
# Note that we have to stick to two capruring groups (or do extra processing outside regxp),
# so the first group must catch both "arg=" and "!arg" cases.
params = re.findall(r"\s*" + r"(?P<arg>\S+?(?==)|!\S+)" + r"(?:=(?P<val>\".*?(?:(?<!\\)\"|\\$)|\S+))?", parsedline.group('params'))
# Return tuple of command and list of arument-value pairs
return (parsedline.group('cmd'), params)
# Read a Mikrotik-formatted config line
# Args: string filename; Returns: list of tuples (cmd, params)
def ReadMTrscFile(filename):
# Init an empty list
config = []
# Open the file and iterate over it
infile = open(filename, "r")
for line in infile:
# Parse the line
parsedline = ParseMTrscLine(line)
# If the line failed to parse, just add it to the list as-is with empty params list
if parsedline == None:
config.append((line.rstrip("\n"), []))
continue
# Extract command and parameters form parsed line
(cmd, params) = parsedline
# Chech if we've got an include and if we'll honor it
if (cmd == "/import") & doimport:
config += ReadMTrscFile(dict(params)["file"])
# Add command with sorted parameters if required
elif dosort:
config.append((cmd, SortParams(dict(params))))
# Add tuple as-is if we don't sort
else:
config.append(parsedline)
# Finished with file
infile.close()
# Return the list containing the configuration
return config
# Write a Mikrotik-formatted config line
# Args: string filename, list of tuples (cmd, params)
def WriteMTrscFile(filename, config):
# Open the file and iterate over config
outfile = open(filename, "w")
for line in config:
# Extract parts from tuple and compose new line (staring with command and continuing with space separated argument[=value] blocks)
(cmd, params) = line
newline = cmd
for p in params:
newline += " " + p[0]
if p[1] != '':
newline += "=" + p[1]
# Parse the line
outfile.write(newline + "\n")
# Finished with file
outfile.close()
# Return nothing
return
command = sys.argv[1]
if command == "parse":
dosort = False
infilename = sys.argv[2]
outfilename = sys.argv[3]
WriteMTrscFile(outfilename, ReadMTrscFile(infilename))
elif command == "sort":
dosort = True
infilename = sys.argv[2]
outfilename = sys.argv[3]
WriteMTrscFile(outfilename, ReadMTrscFile(infilename))
elif command == "compare":
dosort = True
in1filename = sys.argv[2]
in2filename = sys.argv[3]
diff12filename = sys.argv[4]
diff21filename = sys.argv[5]
config1 = ReadMTrscFile(in1filename)
configset1 = MakeSet(config1)
config2 = ReadMTrscFile(in2filename)
configset2 = MakeSet(config2)
configset12 = configset1.difference(configset2)
configset21 = configset2.difference(configset1)
WriteMTrscFile(diff12filename, configset12)
WriteMTrscFile(diff21filename, configset21)
else:
print("Usage: MTrscTools <parse|sort|compare>")
exit
#WriteMTrscFile(outfilename, config)
# That's all folks!
The thing is unfinished as I never had a time to do it. Use at your discretion.