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