]> git.phdru.name Git - extfs.d.git/blob - xml
Version 1.1.4: minor documentation update
[extfs.d.git] / xml
1 #! /usr/bin/env python
2 """XML Virtual FileSystem for Midnight Commander
3
4 The script requires Midnight Commander 3.1+
5 (http://www.midnight-commander.org/), Python 2.4+ (http://www.python.org/).
6
7 For mc 4.7+ just put the script in $HOME/[.local/share/].mc/extfs.d.
8 For older versions put it in /usr/[local/][lib|share]/mc/extfs
9 and add a line "xml" to the /usr/[local/][lib|share]/mc/extfs/extfs.ini.
10 Make the script executable.
11
12 For mc 4.7+ run this "cd" command in the Midnight Commander (in the "bindings"
13 file the command is "%cd"): cd file/xml://; in older versions it is
14 cd file#xml, where "file" is the name of your XML file.
15
16 See detailed installation instructions at
17 http://phdru.name/Software/mc/xml_INSTALL.html.
18
19 The VFS represents tags as directories; the directories are numbered to
20 distinguish tags with the same name; numbering also helps to sort tags by their
21 order in XML instead of sorting them by name and prevents name clash when tag
22 names coincide with the names of special files used by XML VFS. Attributes,
23 text nodes and comments are represented as text files; attributes are shown in
24 a file named "attributes", attributes are listed in the file as name=value
25 lines (I deliberately ignore a small chance of newline characters in values);
26 names and values are reencoded to the console encoding. Text nodes and comments
27 are collected in a file named "text", stripped and reencoded. The filesystem is
28 read-only.
29
30 Implementation based on minidom doesn't understand namespaces, it just shows
31 them among other attributes. ElementTree-based implementation doesn't show
32 namespaces at all. Implementation based on lxml.etree shows namespaces in a
33 separate file "namespaces".
34
35 It is useful to have a top-down view on an XML structure but it's especially
36 convenient to extract text values from tags. One can get, for example, a
37 base64-encoded image - just walk down the VFS to the tag's directory and copy
38 its text file to a real file.
39
40 The VFS was inspired by a FUSE xmlfs: https://github.com/halhen/xmlfs
41
42 """
43
44 __version__ = "1.1.4"
45 __author__ = "Oleg Broytman <phd@phdru.name>"
46 __copyright__ = "Copyright (C) 2013-2015 PhiloSoft Design"
47 __license__ = "GPL"
48
49 force_implementation = None  # Can be None for default choice,
50                              # 'lxml', 'elementtree' or 'minidom'
51
52 use_minidom = True
53 use_elementtree = False
54 use_lxml = False
55
56 import math
57 import sys
58 import xml.dom.minidom
59
60 try:
61     import xml.etree.ElementTree as ET
62 except ImportError:
63     pass
64 else:
65     use_elementtree = True
66
67 try:
68     import lxml.etree as etree
69 except ImportError:
70     pass
71 else:
72     use_lxml = True
73
74 try:
75    import locale
76    use_locale = True
77 except ImportError:
78    use_locale = False
79
80 if use_locale:
81    # Get the default charset.
82    try:
83       lcAll = locale.getdefaultlocale()
84    except locale.Error, err:
85       print >>sys.stderr, "WARNING:", err
86       lcAll = []
87
88    if len(lcAll) == 2:
89       default_encoding = lcAll[1]
90    else:
91       try:
92          default_encoding = locale.getpreferredencoding()
93       except locale.Error, err:
94          print >>sys.stderr, "WARNING:", err
95          default_encoding = sys.getdefaultencoding()
96 else:
97    default_encoding = sys.getdefaultencoding()
98
99 import logging
100 logger = logging.getLogger('xml-mcextfs')
101 log_err_handler = logging.StreamHandler(sys.stderr)
102 logger.addHandler(log_err_handler)
103 logger.setLevel(logging.INFO)
104
105 if len(sys.argv) < 3:
106     logger.critical("""\
107 XML Virtual FileSystem for Midnight Commander version %s
108 Author: %s
109 %s
110
111 This is not a program. Put the script in $HOME/[.local/share/].mc/extfs.d or
112 /usr/[local/][lib|share]/mc/extfs. For more information read the source!""",
113    __version__, __author__, __copyright__
114 )
115     sys.exit(1)
116
117
118 locale.setlocale(locale.LC_ALL, '')
119
120
121 class XmlVfs(object):
122     """Abstract base class"""
123
124     supports_namespaces = False
125
126     def __init__(self):
127         self.parse()
128
129     def list(self):
130         root_comments = self.get_root_comments()
131         if root_comments:
132             print "-r--r--r-- 1 user group %d Jan 1 00:00 text" % (len(root_comments))
133         self._list(self.getroot())
134
135     def _list(self, node, path=''):
136         n = len(self.getchildren(node))
137         if n:
138             width = int(math.log10(n)) + 1
139             template = "%%0%dd" % width
140         else:
141             template = "%d"
142         n = 0
143         for element in self.getchildren(node):
144             if not self.istag(element):
145                 continue
146             n += 1
147             tag = self.getlocalname(self.gettag(element))
148             if path:
149                 subpath = '%s/%s %s' % (path, template % n, tag)
150             else:
151                 subpath = '%s %s' % (template % n, tag)
152             subpath_encoded = subpath.encode(default_encoding, "replace")
153             print "dr-xr-xr-x 1 user group 0 Jan 1 00:00 %s" % subpath_encoded
154             if self.getattrs(element):
155                 attr_text = self.attrs2text(element)
156                 print "-r--r--r-- 1 user group %d Jan 1 00:00 %s/attributes" % (
157                     len(attr_text), subpath_encoded)
158             if self.supports_namespaces and self.has_ns(element):
159                 ns_text = self.ns2text(element)
160                 print "-r--r--r-- 1 user group %d Jan 1 00:00 %s/namespaces" % (
161                     len(ns_text), subpath_encoded)
162             text = self.collect_text(element)
163             if text:
164                 print "-r--r--r-- 1 user group %d Jan 1 00:00 %s/text" % (
165                     len(text), subpath_encoded)
166             self._list(element, subpath)
167
168     def get_tag_node(self, node, i):
169         n = 0
170         for element in self.getchildren(node):
171             if self.istag(element):
172                 n += 1
173                 if n == i:
174                     return element
175         xml_error('There are less than %d nodes' % i)
176
177     def attrs2text(self, node):
178         attr_accumulator = []
179         for name, value in self.getattrs(node):
180             name = self.getlocalname(name).encode(default_encoding, "replace")
181             value = value.encode(default_encoding, "replace")
182             attr_accumulator.append("%s=%s" % (name, value))
183         return '\n'.join(attr_accumulator)
184
185     def has_ns(self, node):
186         return False
187
188
189 class MiniDOMXmlVfs(XmlVfs):
190     def parse(self):
191         self.document = xml.dom.minidom.parse(sys.argv[2])
192
193     def getattrs(self, node):
194         attrs = node.attributes
195         attrs = [attrs.item(i) for i in range(attrs.length)]
196         return [(a.name, a.value) for a in attrs]
197
198     def collect_text(self, node):
199         text_accumulator = []
200         for element in node.childNodes:
201             if element.localName:
202                 continue
203             elif element.nodeType == element.COMMENT_NODE:
204                 text = u"<!--%s-->" % element.nodeValue
205             elif element.nodeType == element.TEXT_NODE:
206                 text = element.nodeValue.strip()
207             else:
208                 xml_error("Unknown node type %d" % element.nodeType)
209             if text: text_accumulator.append(text)
210         return '\n'.join(text_accumulator).encode(default_encoding, "replace")
211
212     def getroot(self):
213         return self.document
214
215     def get_root_comments(self):
216         return self.collect_text(self.document)
217
218     def getchildren(self, node):
219         return node.childNodes
220
221     def gettag(self, node):
222         return node.localName
223
224     def istag(self, node):
225         return bool(node.localName)
226
227     def getlocalname(self, name):
228         return name
229
230
231 if use_elementtree or use_lxml:
232     class CommonEtreeXmlVfs(XmlVfs):
233         def getattrs(self, node):
234             return node.attrib.items()
235
236         def collect_text(self, node):
237             text_accumulator = []
238             if node.text:
239                 text = node.text.strip()
240                 if text: text_accumulator.append(text)
241             for element in node:
242                 if not self.istag(element):
243                     text = u"<!--%s-->" % element.text
244                     text_accumulator.append(text)
245             if node.tail:
246                 text = node.tail.strip()
247                 if text: text_accumulator.append(text)
248             return '\n'.join(text_accumulator).encode(default_encoding, "replace")
249
250         def getchildren(self, node):
251             return list(node)
252
253         def gettag(self, node):
254             return node.tag
255
256         def istag(self, node):
257             return isinstance(node.tag, basestring)
258
259
260 if use_elementtree:
261     class ElementTreeXmlVfs(CommonEtreeXmlVfs):
262         def parse(self):
263             # Copied from http://effbot.org/zone/element-pi.htm
264
265             class PIParser(ET.XMLTreeBuilder):
266
267                 def __init__(self):
268                     ET.XMLTreeBuilder.__init__(self)
269                     # assumes ElementTree 1.2.X
270                     self._parser.CommentHandler = self.handle_comment
271                     self._parser.ProcessingInstructionHandler = self.handle_pi
272                     self._target.start("document", {})
273
274                 def close(self):
275                     self._target.end("document")
276                     return ET.XMLTreeBuilder.close(self)
277
278                 def handle_comment(self, data):
279                     self._target.start(ET.Comment, {})
280                     self._target.data(data)
281                     self._target.end(ET.Comment)
282
283                 def handle_pi(self, target, data):
284                     self._target.start(ET.PI, {})
285                     self._target.data(target + " " + data)
286                     self._target.end(ET.PI)
287
288             self.document = ET.parse(sys.argv[2], PIParser())
289
290         def getroot(self):
291             return self.document.getroot()
292
293         def get_root_comments(self):
294             text_accumulator = []
295             for element in self.getroot():
296                 if not self.istag(element):
297                     text = u"<!--%s-->" % element.text
298                     text_accumulator.append(text)
299             return '\n'.join(text_accumulator).encode(default_encoding, "replace")
300
301         def getlocalname(self, name):
302             if name.startswith('{'):
303                 name = name.split('}', 1)[1]  # Remove XML namespace
304             return name
305
306
307 if use_lxml:
308     class LxmlEtreeXmlVfs(CommonEtreeXmlVfs):
309         supports_namespaces = True
310
311         def parse(self):
312             self.document = etree.parse(sys.argv[2])
313
314         def getroot(self):
315             return [self.document.getroot()]
316
317         def get_root_comments(self):
318             text_accumulator = []
319             for element in self.document.getroot().itersiblings(tag=etree.Comment, preceding=True):
320                 text = u"<!--%s-->" % element.text
321                 text_accumulator.append(text)
322             return '\n'.join(text_accumulator).encode(default_encoding, "replace")
323
324         def getlocalname(self, name):
325             return etree.QName(name).localname
326
327         def _get_local_ns(self, node):
328             this_nsmap = node.nsmap
329             parent = node.getparent()
330             if parent is not None:
331                 parent_nsmap = parent.nsmap
332                 for key in parent_nsmap:
333                     if this_nsmap[key] == parent_nsmap[key]:
334                         del this_nsmap[key]
335             return this_nsmap
336
337         def has_ns(self, node):
338             return bool(self._get_local_ns(node))
339
340         def ns2text(self, node):
341             ns_accumulator = []
342             for name, value in self._get_local_ns(node).items():
343                 if name:
344                     name = name.encode(default_encoding, "replace")
345                 else:
346                     name = 'xmlns'
347                 value = value.encode(default_encoding, "replace")
348                 ns_accumulator.append("%s=%s" % (name, value))
349             return '\n'.join(ns_accumulator)
350
351
352 def build_xmlvfs():
353     if force_implementation is None:
354         if use_lxml:
355             return LxmlEtreeXmlVfs()
356         elif use_elementtree:
357             return ElementTreeXmlVfs()
358         else:
359             return MiniDOMXmlVfs()
360     elif force_implementation == 'minidom':
361         return MiniDOMXmlVfs()
362     elif force_implementation == 'elementtree':
363         return ElementTreeXmlVfs()
364     elif force_implementation == 'lxml':
365         return LxmlEtreeXmlVfs()
366     else:
367         raise ValueError('Unknown implementation "%s", expected "minidom", "elementtree" or "lxml"' % force_implementation)
368
369
370 def mcxml_list():
371     """List the entire VFS"""
372
373     xmlvfs = build_xmlvfs()
374     xmlvfs.list()
375
376
377 def mcxml_copyout():
378     """Extract a file from the VFS"""
379
380     xmlvfs = build_xmlvfs()
381     xml_filename = sys.argv[3]
382     real_filename = sys.argv[4]
383
384     node = xmlvfs.getroot()
385     for path_comp in xml_filename.split('/'):
386         if ' ' in path_comp:
387             i = int(path_comp.split(' ', 1)[0])
388             node = xmlvfs.get_tag_node(node, i)
389         elif path_comp in ('attributes', 'namespaces', 'text'):
390             break
391         else:
392             xml_error('Unknown file')
393
394     if path_comp == 'attributes':
395         if xmlvfs.getattrs(node):
396             text = xmlvfs.attrs2text(node)
397         else:
398             xml_error('There are no attributes')
399
400     elif path_comp == 'namespaces':
401         if xmlvfs.supports_namespaces and xmlvfs.has_ns(node):
402             text = xmlvfs.ns2text(node)
403         else:
404             xml_error('There are no namespaces')
405
406     elif path_comp == 'text':
407         if '/' in xml_filename:
408             text = xmlvfs.collect_text(node)
409         else:
410             text = xmlvfs.get_root_comments()
411
412     else:
413         xml_error('Unknown file')
414
415     outfile = open(real_filename, 'w')
416     outfile.write(text)
417     outfile.close()
418
419
420 def mcxml_copyin():
421     """Put a file to the VFS"""
422     sys.exit("XML VFS doesn't support adding files (read-only filesystem)")
423
424 def mcxml_rm():
425     """Remove a file from the VFS"""
426     sys.exit("XML VFS doesn't support removing files/directories (read-only filesystem)")
427
428 mcxml_rmdir = mcxml_rm
429
430 def mcxml_mkdir():
431     """Create a directory in the VFS"""
432     sys.exit("XML VFS doesn't support creating directories (read-only filesystem)")
433
434
435 def xml_error(error_str):
436     logger.critical("Error walking XML file: %s", error_str)
437     sys.exit(1)
438
439 command = sys.argv[1]
440 procname = "mcxml_" + command
441
442 g = globals()
443 if not g.has_key(procname):
444     logger.critical("Unknown command %s", command)
445     sys.exit(1)
446
447 try:
448     g[procname]()
449 except SystemExit:
450     raise
451 except:
452     logger.exception("Error during run")