Change to using a list of mappings to maintain order

Signed-off-by: martin f. krafft <madduck@madduck.net>
diff --git a/doc/source/operations.rst b/doc/source/operations.rst
index 0bbf599..a157403 100644
--- a/doc/source/operations.rst
+++ b/doc/source/operations.rst
@@ -92,6 +92,11 @@
 can be assigned to each mapping by providing a space-separated list (class
 names cannot contain spaces anyway).
 
+Note that mappings are not designed to replace node definitions. Mappings can
+be used to pre-populate the classes of existing nodes, but you still need to
+define all nodes (and if only to allow them to be enumerated for the
+inventory).
+
 Parameter interpolation
 ------------------------
 Parameters may reference each other, including deep references, e.g.::
diff --git a/reclass/errors.py b/reclass/errors.py
index ac06098..d3a0239 100644
--- a/reclass/errors.py
+++ b/reclass/errors.py
@@ -129,6 +129,18 @@
         super(InfiniteRecursionError, self).__init__(msg)
 
 
+class MappingError(ReclassException):
+
+    def __init__(self, msg, rc=posix.EX_DATAERR):
+        super(MappingError, self).__init__(msg, rc)
+
+
+class MappingFormatError(MappingError):
+
+    def __init__(self, msg):
+        super(MappingFormatError, self).__init__(msg)
+
+
 class NameError(ReclassException):
 
     def __init__(self, msg, rc=posix.EX_DATAERR):
diff --git a/reclass/storage/__init__.py b/reclass/storage/__init__.py
index d4d4d35..96f3624 100644
--- a/reclass/storage/__init__.py
+++ b/reclass/storage/__init__.py
@@ -12,7 +12,9 @@
 import re
 import sys
 import fnmatch
+import shlex
 from reclass.datatypes import Entity, Classes
+from reclass.errors import MappingFormatError
 
 def _get_timestamp():
     return time.strftime('%c')
@@ -39,20 +41,37 @@
     def _match_glob(self, key, nodename):
         return fnmatch.fnmatchcase(nodename, key)
 
+    def _shlex_split(self, instr):
+        lexer = shlex.shlex(instr, posix=True)
+        lexer.whitespace_split = True
+        lexer.commenters = ''
+        regexp = False
+        if instr[0] == '/':
+            lexer.quotes += '/'
+            regexp = True
+        try:
+            key = lexer.get_token()
+        except ValueError, e:
+            raise MappingFormatError('Error in mapping "{0}": missing closing '
+                                     'quote (or slash)'.format(instr))
+        if regexp:
+            key = '/{0}/'.format(key)
+        return key, list(lexer)
+
     def _populate_with_class_mappings(self, nodename):
+        if not self.class_mappings:
+            return Entity(name='empty')
         c = Classes()
-        for key, value in self.class_mappings.iteritems():
-            match = False
-            if key.startswith('/') and key.endswith('/'):
-                match = self._match_regexp(key[1:-1], nodename)
+        for mapping in self.class_mappings:
+            matched = False
+            key, klasses = self._shlex_split(mapping)
+            if key[0] == ('/'):
+                matched = self._match_regexp(key[1:-1], nodename)
             else:
-                match = self._match_glob(key, nodename)
-            if match:
-                if isinstance(value, (types.ListType, types.TupleType)):
-                    for v in value:
-                        c.append_if_new(v)
-                else:
-                    c.append_if_new(value)
+                matched = self._match_glob(key, nodename)
+            if matched:
+                for klass in klasses:
+                    c.append_if_new(klass)
 
         return Entity(classes=c,
                       name='class mappings for node {0}'.format(nodename))