diff --git a/lib/ansible/inventory/__init__.py b/lib/ansible/inventory/__init__.py index b9a69bb3d29..03ac2ee05a8 100644 --- a/lib/ansible/inventory/__init__.py +++ b/lib/ansible/inventory/__init__.py @@ -267,76 +267,81 @@ class Inventory(object): if pattern.startswith("&") or pattern.startswith("!"): pattern = pattern[1:] - if pattern in self._pattern_cache: - return self._pattern_cache[pattern] - - (name, enumeration_details) = self._enumeration_info(pattern) - hpat = self._hosts_in_unenumerated_pattern(name) - result = self._apply_ranges(pattern, hpat) - self._pattern_cache[pattern] = result - return result - - def _enumeration_info(self, pattern): + if pattern not in self._pattern_cache: + (expr, slice) = self._split_subscript(pattern) + hosts = self._enumerate_matches(expr) + try: + hosts = self._apply_subscript(hosts, slice) + except IndexError: + raise AnsibleError("No hosts matched the subscripted pattern '%s'" % pattern) + self._pattern_cache[pattern] = hosts + + return self._pattern_cache[pattern] + + def _split_subscript(self, pattern): """ - returns (pattern, limits) taking a regular pattern and finding out - which parts of it correspond to start/stop offsets. limits is - a tuple of (start, stop) or None + Takes a pattern, checks if it has a subscript, and returns the pattern + without the subscript and a (start,end) tuple representing the given + subscript (or None if there is no subscript). + + Validates that the subscript is in the right syntax, but doesn't make + sure the actual indices make sense in context. """ # Do not parse regexes for enumeration info if pattern.startswith('~'): return (pattern, None) - # The regex used to match on the range, which can be [x] or [x-y]. - pattern_re = re.compile("^(.*)\[([-]?[0-9]+)(?:(?:-)([0-9]+))?\](.*)$") - m = pattern_re.match(pattern) + # We want a pattern followed by an integer or range subscript. + # (We can't be more restrictive about the expression because the + # fnmatch semantics permit [\[:\]] to occur.) + + pattern_with_subscript = re.compile( + r'''^ + (.+) # A pattern expression ending with... + \[(?: # A [subscript] expression comprising: + (-?[0-9]+) # A single positive or negative number + | # Or a numeric range + ([0-9]+)([:-])([0-9]+) + )\] + $ + ''', re.X + ) + + subscript = None + m = pattern_with_subscript.match(pattern) if m: - (target, first, last, rest) = m.groups() - first = int(first) - if last: - if first < 0: - raise AnsibleError("invalid range: negative indices cannot be used as the first item in a range") - last = int(last) + (pattern, idx, start, sep, end) = m.groups() + if idx: + subscript = (int(idx), None) else: - last = first - return (target, (first, last)) - else: - return (pattern, None) + subscript = (int(start), int(end)) + if sep == '-': + display.deprecated("Use [x:y] inclusive subscripts instead of [x-y]", version=2.0, removed=True) - def _apply_ranges(self, pat, hosts): + return (pattern, subscript) + + def _apply_subscript(self, hosts, subscript): """ - given a pattern like foo, that matches hosts, return all of hosts - given a pattern like foo[0:5], where foo matches hosts, return the first 6 hosts + Takes a list of hosts and a (start,end) tuple and returns the subset of + hosts based on the subscript (which may be None to return all hosts). """ - # If there are no hosts to select from, just return the - # empty set. This prevents trying to do selections on an empty set. - # issue#6258 - if not hosts: - return hosts - - (loose_pattern, limits) = self._enumeration_info(pat) - if not limits: + if not hosts or not subscript: return hosts - (left, right) = limits + (start, end) = subscript - if left == '': - left = 0 - if right == '': - right = 0 - left=int(left) - right=int(right) - try: - if left != right: - return hosts[left:right] - else: - return [ hosts[left] ] - except IndexError: - raise AnsibleError("no hosts matching the pattern '%s' were found" % pat) + if end: + return hosts[start:end+1] + else: + return [ hosts[start] ] - def _hosts_in_unenumerated_pattern(self, pattern): - """ Get all host names matching the pattern """ + def _enumerate_matches(self, pattern): + """ + Returns a list of host names matching the given pattern according to the + rules explained above in _match_one_pattern. + """ results = [] hosts = []