2 """XML Virtual FileSystem for Midnight Commander
4 The script requires Midnight Commander 3.1+
5 (http://www.midnight-commander.org/), Python 2.4+ (http://www.python.org/).
7 For mc 4.7+ 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.
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.
16 The VFS represents tags as directories; the directories are numbered to
17 distinguish tags with the same name; also numbering helps to sort tags by their
18 order in XML instead of sorting them by name. Attributes, text nodes and
19 comments are represented as text files; attributes are shown in a file named
20 "attributes", attributes are listed in the file as name=value lines (I
21 deliberately ignore a small chance of newline characters in values); names and
22 values are reencoded to the console encoding. Text nodes and comments are
23 collected in a file named "text", stripped and reencoded. The filesystem is
26 It is useful to have a top-down view on an XML structure but it's especially
27 convenient to extract text values from tags. One can get, for example, a
28 base64-encoded image - just walk down the VFS to the tag's directory and copy
29 its text file to a real file.
31 The VFS was inspired by a FUSE xmlfs: https://github.com/halhen/xmlfs
36 __author__ = "Oleg Broytman <phd@phdru.name>"
37 __copyright__ = "Copyright (C) 2013 PhiloSoft Design"
40 default_implementation = None # Can be elementtree or minidom
43 use_elementtree = False
47 import xml.dom.minidom
50 import xml.etree.ElementTree as ET
54 use_elementtree = True
63 # Get the default charset.
65 lcAll = locale.getdefaultlocale()
66 except locale.Error, err:
67 print >>sys.stderr, "WARNING:", err
71 default_encoding = lcAll[1]
74 default_encoding = locale.getpreferredencoding()
75 except locale.Error, err:
76 print >>sys.stderr, "WARNING:", err
77 default_encoding = sys.getdefaultencoding()
79 default_encoding = sys.getdefaultencoding()
82 logger = logging.getLogger('xml-mcextfs')
83 log_err_handler = logging.StreamHandler(sys.stderr)
84 logger.addHandler(log_err_handler)
85 logger.setLevel(logging.INFO)
89 XML Virtual FileSystem for Midnight Commander version %s
93 This is not a program. Put the script in $HOME/[.local/share/].mc/extfs.d or
94 /usr/[local/][lib|share]/mc/extfs. For more information read the source!""",
95 __version__, __author__, __copyright__
100 locale.setlocale(locale.LC_ALL, '')
103 class XmlVfs(object):
108 self._list(self.getroot())
110 def get_child_node(self, node, i):
112 for element in self.getchildren(node):
113 if self.istag(element):
117 xml_error('There are less than %d nodes' % i)
120 class MiniDOMXmlVfs(XmlVfs):
122 self.document = xml.dom.minidom.parse(sys.argv[2])
124 def hasattrs(self, node):
125 return bool(node.attributes)
127 def attrs2text(self, node):
128 attrs = node.attributes
129 attrs = [attrs.item(i) for i in range (attrs.length)]
130 return '\n'.join(["%s=%s" %
131 (a.name.encode(default_encoding, "replace"),
132 a.value.encode(default_encoding, "replace"))
135 def collect_text(self, node):
136 text_accumulator = []
137 for element in node.childNodes:
138 if element.localName:
140 elif element.nodeType == element.COMMENT_NODE:
141 text = u"<!--%s-->" % element.nodeValue
142 elif element.nodeType == element.TEXT_NODE:
143 text = element.nodeValue.strip()
145 xml_error("Unknown node type %d" % element.nodeType)
146 if text: text_accumulator.append(text)
147 return '\n'.join(text_accumulator).encode(default_encoding, "replace")
149 def _list(self, node, path=''):
150 childNodes = node.childNodes
152 for element in childNodes:
153 if element.localName:
156 width = int(math.log10(n))+1
157 template = "%%0%dd" % width
161 for element in childNodes:
162 if element.localName:
165 subpath = '%s/%s %s' % (path, template % n, element.localName)
167 subpath = '%s %s' % (template % n, element.localName)
168 subpath_encoded = subpath.encode(default_encoding, "replace")
169 print "dr-xr-xr-x 1 user group 0 Jan 1 00:00 %s" % subpath_encoded
170 if self.hasattrs(element):
171 attr_text = self.attrs2text(element)
172 print "-r--r--r-- 1 user group %d Jan 1 00:00 %s/attributes" % (
173 len(attr_text), subpath_encoded)
174 text = self.collect_text(element)
176 print "-r--r--r-- 1 user group %d Jan 1 00:00 %s/text" % (
177 len(text), subpath_encoded)
178 self._list(element, subpath)
183 def getchildren(self, node):
184 return node.childNodes
186 def istag(self, node):
187 return bool(node.localName)
191 class ElementTreeXmlVfs(XmlVfs):
193 # Copied from http://effbot.org/zone/element-pi.ht
195 class PIParser(ET.XMLTreeBuilder):
198 ET.XMLTreeBuilder.__init__(self)
199 # assumes ElementTree 1.2.X
200 self._parser.CommentHandler = self.handle_comment
201 self._parser.ProcessingInstructionHandler = self.handle_pi
202 self._target.start("document", {})
205 self._target.end("document")
206 return ET.XMLTreeBuilder.close(self)
208 def handle_comment(self, data):
209 self._target.start(ET.Comment, {})
210 self._target.data(data)
211 self._target.end(ET.Comment)
213 def handle_pi(self, target, data):
214 self._target.start(ET.PI, {})
215 self._target.data(target + " " + data)
216 self._target.end(ET.PI)
218 self.document = ET.parse(sys.argv[2], PIParser())
220 def hasattrs(self, node):
221 return bool(node.attrib)
223 def attrs2text(self, node):
224 attr_accumulator = []
225 for name, value in node.attrib.items():
226 name = name.encode(default_encoding, "replace")
227 value = value.encode(default_encoding, "replace")
228 if name.startswith('{'):
229 name = name.split('}', 1)[1] # Remove XML namespace
230 attr_accumulator.append("%s=%s" % (name, value))
231 return '\n'.join(attr_accumulator)
233 def collect_text(self, node):
234 text_accumulator = []
236 text = node.text.strip()
237 if text: text_accumulator.append(text)
239 if element.tag is ET.Comment:
240 text = u"<!--%s-->" % text
241 text_accumulator.append(text)
243 text = node.tail.strip()
244 if text: text_accumulator.append(text)
245 return '\n'.join(text_accumulator).encode(default_encoding, "replace")
247 def _list(self, node, path=''):
250 width = int(math.log10(n))+1
251 template = "%%0%dd" % width
256 if not isinstance(element.tag, basestring):
260 if tag.startswith('{'):
261 tag = tag.split('}', 1)[1] # Remove XML namespace
263 subpath = '%s/%s %s' % (path, template % n, tag)
265 subpath = '%s %s' % (template % n, tag)
266 subpath_encoded = subpath.encode(default_encoding, "replace")
267 print "dr-xr-xr-x 1 user group 0 Jan 1 00:00 %s" % subpath_encoded
268 if self.hasattrs(element):
269 attr_text = self.attrs2text(element)
270 print "-r--r--r-- 1 user group %d Jan 1 00:00 %s/attributes" % (
271 len(attr_text), subpath_encoded)
272 text = self.collect_text(element)
274 print "-r--r--r-- 1 user group %d Jan 1 00:00 %s/text" % (
275 len(text), subpath_encoded)
276 self._list(element, subpath)
279 return self.document.getroot()
281 def getchildren(self, node):
284 def istag(self, node):
285 return isinstance(node.tag, basestring)
289 if default_implementation is None:
291 return ElementTreeXmlVfs()
293 return MiniDOMXmlVfs()
294 elif default_implementation == 'minidom':
295 return MiniDOMXmlVfs()
296 elif default_implementation == 'elementtree':
297 return ElementTreeXmlVfs()
301 """List the entire VFS"""
303 xmlvfs = build_xmlvfs()
308 """Extract a file from the VFS"""
310 xmlvfs = build_xmlvfs()
311 xml_filename = sys.argv[3]
312 real_filename = sys.argv[4]
314 node = xmlvfs.getroot()
315 for path_comp in xml_filename.split('/'):
317 i = int(path_comp.split(' ', 1)[0])
318 node = xmlvfs.get_child_node(node, i)
319 elif path_comp in ('attributes', 'text'):
322 xml_error('Unknown file')
324 if path_comp == 'attributes':
325 if xmlvfs.hasattrs(node):
326 text = xmlvfs.attrs2text(node)
328 xml_error('There are no attributes')
330 if path_comp == 'text':
331 text = xmlvfs.collect_text(node)
333 outfile = open(real_filename, 'w')
339 """Put a file to the VFS"""
340 sys.exit("XML VFS doesn't support adding files (read-only filesystem)")
343 """Remove a file from the VFS"""
344 sys.exit("XML VFS doesn't support removing files/directories (read-only filesystem)")
346 mcxml_rmdir = mcxml_rm
349 """Create a directory in the VFS"""
350 sys.exit("XML VFS doesn't support creating directories (read-only filesystem)")
353 def xml_error(error_str):
354 logger.critical("Error walking XML file: %s", error_str)
357 command = sys.argv[1]
358 procname = "mcxml_" + command
361 if not g.has_key(procname):
362 logger.critical("Unknown command %s", command)
370 logger.exception("Error during run")