290 lines
7.4 KiB
Python
Executable file
290 lines
7.4 KiB
Python
Executable file
#!/usr/bin/python
|
|
import glob, os, sys, string, xml.sax, getopt
|
|
|
|
|
|
__doc__ = """\
|
|
List properties defined in FlightGear's <PropertyList> XML files.
|
|
|
|
Usage:
|
|
lsprop [-v] [-p] [-i|-I] [-f <format>] [<list-of-xml-files>]
|
|
lsprop -h
|
|
|
|
Options:
|
|
-h, --help print this help screen
|
|
-v, --verbose increase verbosity
|
|
-i, --all-indices also show null indices in properties
|
|
-I, --no-indices don't show any indices in properties
|
|
-p, --raw-paths don't use symbols "$FG_ROOT" and "$FG_HOME" as path prefix
|
|
-f, --format set output format (default: --format="%f +%l: %p = '%v'")
|
|
|
|
Format:
|
|
%f file path
|
|
%l line number
|
|
%c column number
|
|
%p property path
|
|
%t property type
|
|
%V raw value (unescaped)
|
|
%v cooked value (carriage return, non printable chars etc. escaped)
|
|
%q like %v, but single quotes escaped to \\'
|
|
%Q like %v, but double quotes escaped to \\"
|
|
%% percent sign
|
|
|
|
Environment:
|
|
FG_ROOT
|
|
FG_HOME
|
|
LSPROP_FORMAT overrides default format
|
|
|
|
Arguments:
|
|
If no file arguments are specified, then the following files are assumed:
|
|
$FG_ROOT/preferences.xml
|
|
$FG_ROOT/Aircraft/*/*-set.xml
|
|
|
|
Current settings:\
|
|
"""
|
|
|
|
|
|
class config:
|
|
root = "/usr/local/share/FlightGear"
|
|
home = os.environ["HOME"] + "/.fgfs"
|
|
raw_paths = 0
|
|
format = "%f +%l: %p = '%v'"
|
|
verbose = 1
|
|
indices = 1 # 0: no indices; 1: only indices != [0]; 2: all indices
|
|
|
|
|
|
def errmsg(msg, color = "31;1"):
|
|
if os.isatty(2):
|
|
print >>sys.stderr, "\033[%sm%s\033[m" % (color, msg)
|
|
else:
|
|
print >>sys.stderr, msg
|
|
|
|
|
|
def cook_path(path, force = 0):
|
|
path = os.path.normpath(os.path.abspath(path))
|
|
if config.raw_paths and not force:
|
|
return path
|
|
if path.startswith(config.root):
|
|
path = path.replace(config.root, "$FG_ROOT", 1)
|
|
elif path.startswith(config.home):
|
|
path = path.replace(config.home, "$FG_HOME", 1)
|
|
return path
|
|
|
|
|
|
class Error(Exception):
|
|
pass
|
|
|
|
|
|
class Abort(Exception):
|
|
pass
|
|
|
|
|
|
class XMLError(Exception):
|
|
def __init__(self, locator, msg):
|
|
msg = "%s in %s +%d:%d" \
|
|
% (msg.replace("\n", "\\n"), cook_path(locator.getSystemId()), \
|
|
locator.getLineNumber(), locator.getColumnNumber())
|
|
raise Error(msg)
|
|
|
|
|
|
class parse_xml_file(xml.sax.handler.ContentHandler):
|
|
def __init__(self, path, nesting = 0, stack = None):
|
|
self.level = 0
|
|
self.path = path
|
|
self.nesting = nesting
|
|
self.type = None
|
|
if stack:
|
|
self.stack = stack
|
|
else:
|
|
self.stack = [[None, None, {}, []]] # name, index, indices, data
|
|
|
|
self.pretty_path = cook_path(path)
|
|
|
|
if config.verbose > 1:
|
|
errmsg("%s (%d)" % (path, nesting), "35")
|
|
if not os.path.exists(path):
|
|
raise Error("file doesn't exist: " + self.pretty_path)
|
|
|
|
try:
|
|
xml.sax.parse(path, self, self)
|
|
except ValueError:
|
|
pass # FIXME hack arount DTD error
|
|
|
|
def startElement(self, name, attrs):
|
|
self.level += 1
|
|
if self.level == 1:
|
|
if name != "PropertyList":
|
|
raise XMLError(self.locator, "XML file isn't a <PropertyList>")
|
|
else:
|
|
index = 0
|
|
if attrs.has_key("n"):
|
|
index = int(attrs["n"])
|
|
elif name in self.stack[-1][2]:
|
|
index = self.stack[-1][2][name] + 1
|
|
self.stack[-1][2][name] = index
|
|
|
|
self.type = "unspecified"
|
|
if attrs.has_key("type"):
|
|
self.type = attrs["type"]
|
|
|
|
if attrs.has_key("include"):
|
|
path = os.path.dirname(os.path.abspath(self.path)) + "/" + attrs["include"]
|
|
if attrs.has_key("omit-node") and attrs["omit-node"] == "y" or self.level == 1:
|
|
self.stack.append([None, None, self.stack[-1][2], []])
|
|
else:
|
|
self.stack.append([name, index, {}, []])
|
|
parse_xml_file(path, self.nesting + 1, self.stack)
|
|
elif self.level > 1:
|
|
self.stack.append([name, index, {}, []])
|
|
|
|
def endElement(self, name):
|
|
value = string.join(self.stack[-1][3], '')
|
|
if not len(self.stack[-1][2]) and self.level > 1:
|
|
path = self.pathname()
|
|
if path:
|
|
cooked_value = self.escape(value.encode("iso-8859-15", "backslashreplace"))
|
|
print config.cooked_format % {
|
|
"f": self.pretty_path,
|
|
"l": self.locator.getLineNumber(),
|
|
"c": self.locator.getColumnNumber(),
|
|
"p": path,
|
|
"t": self.type,
|
|
"V": value,
|
|
"v": cooked_value,
|
|
"q": cooked_value.replace("'", "\\'"),
|
|
'Q': cooked_value.replace('"', '\\"'),
|
|
}
|
|
|
|
elif len(string.strip(value)):
|
|
raise XMLError(self.locator, "garbage found '%s'" % string.strip(value))
|
|
|
|
self.level -= 1
|
|
if self.level:
|
|
self.stack.pop()
|
|
|
|
def characters(self, data):
|
|
self.stack[-1][3].append(data)
|
|
|
|
def setDocumentLocator(self, locator):
|
|
self.locator = locator
|
|
|
|
def pathname(self):
|
|
path = ""
|
|
for e in self.stack[1:]:
|
|
if e[0] == None: # omit-node
|
|
continue
|
|
path += "/" + e[0]
|
|
if e[1] and config.indices == 1 or config.indices == 2:
|
|
path += "[%d]" % e[1]
|
|
return path
|
|
|
|
def escape(self, string):
|
|
s = ""
|
|
for c in string:
|
|
if c == '\n':
|
|
s += '\\n'
|
|
elif c == '\r':
|
|
s += '\\r'
|
|
elif c == '\v':
|
|
s += '\\v'
|
|
elif c == '\\':
|
|
s += '\\\\'
|
|
elif not c.isalnum() and " \t!@#$%^&*()_+|~-=\`[]{};':\",./<>?".find(c) < 0:
|
|
s += "\\x%02x" % ord(c)
|
|
else:
|
|
s += c
|
|
return s
|
|
|
|
def warning(self, exception):
|
|
raise XMLError(self.locator, "WARNING: " + str(exception))
|
|
|
|
def error(self, exception):
|
|
raise XMLError(self.locator, "ERROR: " + str(exception))
|
|
|
|
def fatalError(self, exception):
|
|
raise XMLError(self.locator, "FATAL: " + str(exception))
|
|
|
|
|
|
def main():
|
|
if 'FG_ROOT' in os.environ:
|
|
config.root = os.environ['FG_ROOT'].lstrip().rstrip("/\\\t ")
|
|
if 'FG_HOME' in os.environ:
|
|
config.home = os.environ['FG_HOME'].lstrip().rstrip("/\\\t ")
|
|
if 'LSPROP_FORMAT' in os.environ:
|
|
config.format = os.environ['LSPROP_FORMAT']
|
|
|
|
# options
|
|
try:
|
|
opts, args = getopt.getopt(sys.argv[1:], \
|
|
"hviIpf:", \
|
|
["help", "verbose", "all-indices", "no-indices", "raw-paths", "format="])
|
|
except getopt.GetoptError, msg:
|
|
errmsg("Error: %s" % msg)
|
|
return -1
|
|
|
|
for o, a in opts:
|
|
if o in ("-h", "--help"):
|
|
print __doc__
|
|
print '\t--format="%s"' % config.format.replace('"', '\\"')
|
|
return 0
|
|
if o in ("-v", "--verbose"):
|
|
config.verbose += 1
|
|
if o in ("-i", "--all-indices"):
|
|
config.indices = 2
|
|
if o in ("-I", "--no-indices"):
|
|
config.indices = 0
|
|
if o in ("-p", "--raw-paths"):
|
|
config.raw_paths = 1
|
|
if o in ("-f", "--format"):
|
|
config.format = a
|
|
|
|
# format
|
|
f = config.format
|
|
f = f.replace("\\e", "\x1b")
|
|
f = f.replace("\\033", "\x1b")
|
|
f = f.replace("\\x1b", "\x1b")
|
|
f = f.replace("%%", "\x01\x01")
|
|
f = f.replace("%(", "\x01\x01(")
|
|
f = f.replace("%f", "\x01(f)s")
|
|
f = f.replace("%l", "\x01(l)d")
|
|
f = f.replace("%c", "\x01(c)d")
|
|
f = f.replace("%p", "\x01(p)s")
|
|
f = f.replace("%t", "\x01(t)s")
|
|
f = f.replace("%V", "\x01(V)s")
|
|
f = f.replace("%v", "\x01(v)s")
|
|
f = f.replace("%q", "\x01(q)s")
|
|
f = f.replace('%Q', '\x01(Q)s')
|
|
f = f.replace("%", "%%")
|
|
f = f.replace("\x01", "%")
|
|
config.cooked_format = f
|
|
|
|
if config.verbose > 2:
|
|
print >>sys.stderr, "internal format = [%s]" % config.cooked_format
|
|
|
|
# arguments
|
|
if not len(args):
|
|
args = [config.root + "/preferences.xml"]
|
|
if not os.path.exists(args[0]):
|
|
errmsg("Error: environment variable FG_ROOT not set or set wrongly?")
|
|
return -1
|
|
for f in glob.glob(config.root + '/Aircraft/*/*-set.xml'):
|
|
args.append(f)
|
|
|
|
for arg in args:
|
|
try:
|
|
parse_xml_file(arg)
|
|
except Abort, e:
|
|
errmsg("Abort: " + e.args[0])
|
|
return -1
|
|
except Error, e:
|
|
errmsg("Error: " + e.args[0])
|
|
except IOError, (errno, msg):
|
|
errmsg("Error: " + msg)
|
|
return errno
|
|
except KeyboardInterrupt:
|
|
print >>sys.stderr, "\033[m"
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|
|
|