Pattern Matching

The API of Maya has a well-defined syntax to describe DG names and DAG paths but the solution offered to match wildcard patterns, through the use of methods such as maya.OpenMaya.MGlobal.getSelectionListByName(), can sometimes lead to unexpected results.

As an example, Maya defines the pattern |* as matching only the DAG nodes of depth 1, that is the nodes directly parented under the world. Therefore, when using a similar pattern applied to the underworld, for instance node|shape->|*, one would intuitively expect that only the nodes located directly beneath the underworld are to be matched. Instead, Maya’s implementation leads to match all the nodes at any depth below the underworld, which is inconsistent.

To alleviate this lack of predictability and to add a whole new set of possibilities loosely borrowed from Python’s re module, a new specification dedicated to matching name and path patterns is being used across the Bana extensions, mostly through the bnFind*() and bnGet*() methods (see Retrieving Nodes).

This pattern matching specification introduces:

  • a new syntax built upon Maya’s DG names and DAG paths syntaxes, with support for four wildcard operators *, +, ?, and ..
  • a well-defined set of matching rules describing the expected behaviour when using these wildcards in each possible scenario.

Syntax

The standard syntax defined by Maya, and recognized by Bana, describes DG names and DAG paths as they are expected to be returned by methods like maya.OpenMaya.MFnDependencyNode.name() and maya.OpenMaya.MFnDagNode.fullPathName():

alpha     ::=  "a"..."z" | "A"..."Z" | "_"
character ::=  alpha | "0"..."9"
name      ::=  alpha character*
full_name ::=  (name ":")* name
path      ::=  ("|" full_name)+
full_path ::=  path ("->" path)* "->"?

The library Bana extends the standard syntax by adding support for the four wildcard operators:

wcard           ::=  "*" | "+" | "?" | "."
wcard_name      ::=  (alpha | wcard+) (character | wcard+)*
wcard_full_name ::=  (":" wcard+ | wcard_name) (":" wcard_name)*
wcard_path      ::=  ("|" wcard_full_name | wcard+)+
wcard_full_path ::=  wcard_path ("->" wcard_path)* "->"?

Note

The syntax groups are listed in ascending precedence order. In other words: character < name < full name < path < full path. This is useful for determining the context.

In English

Names can identify DG nodes, excluding the ones carrying any namespace or hierarchy information. They are made of character elements, that is alphanumeric characters, underscores, and wildcards.

Full names can fully identify any DG node. They are composed by one or more name elements, each separated by the namespace delimiter :.

Paths can identify DAG nodes, excluding the ones carrying any underworld information. They are composed by one or more full name elements, each starting with the hierarchy delimiter |.

Full paths can fully identify any DAG node. They are composed by one or more path elements, each separated by the underworld delimiter ->.

Patterns can be checked against any of these syntax groups using the corresponding bana.OpenMaya.MGlobal.bnIsValid*() method:

>>> import bana
>>> bana.initialize()
>>> from maya import OpenMaya
>>> OpenMaya.MGlobal.bnIsValidName('node')
True
>>> OpenMaya.MGlobal.bnIsValidName('node_*', allowWildcards=True)
True
>>> OpenMaya.MGlobal.bnIsValidName('ns:node')
False
>>> OpenMaya.MGlobal.bnIsValidFullName('ns:node')
True
>>> OpenMaya.MGlobal.bnIsValidFullName('*:node', allowWildcards=True)
True
>>> OpenMaya.MGlobal.bnIsValidFullName('|node')
False
>>> OpenMaya.MGlobal.bnIsValidPath('|node')
True
>>> OpenMaya.MGlobal.bnIsValidPath('*|node', allowWildcards=True)
True
>>> OpenMaya.MGlobal.bnIsValidPath('|root->|node')
False
>>> OpenMaya.MGlobal.bnIsValidFullPath('|root->|node')
True
>>> OpenMaya.MGlobal.bnIsValidFullPath('*->|node', allowWildcards=True)
True

TL;DR

The composition of names, full names, paths, and full paths, can approximately be summed up as follows:

  • a name is composed of one or more character elements.
  • a full name is composed of one or more name elements separated by the : symbol.
  • a path is composed of one or more full name elements separated by the | symbol.
  • a full path is composed of one or more path elements separated by the -> symbol.

Matching Rules

Depending on where a wildcard operator is located within a pattern, it might end up matching a certain number of occurrences of either one of the character, name, full name, or path syntax groups. For example the wildcard in the pattern |node_* matches a name formed by any number of characters, but the same wildcard in the pattern *|node matches a path composed by any number of full names (e.g.: |root|parent|node).

In order to understand what a wildcard, or a combination of wildcards, will precisely match, there are two aspects to take into consideration:

Context

The context represents the syntax group to be matched. It can be determined by looking at the delimiters surrounding the wildcards, picking the one with the highest precedence, and retrieving the syntax group associated with it as defined in this table sorted in descending precedence order:

delimiter syntax group
character name
: full name
| path
-> full path

For example, the wildcard in the pattern |ns:*|leaf is surrounded by the delimiters : and |, respectively representing the full name and path syntax groups, hence the context is full name since it has a higher precedence than path.

When the wildcards are located at the beginning or the end of a string, then the only delimiter found is used to define the context. For example, the context for the wildcard in the pattern *->|leaf is full path, as per the -> delimiter.

If one of the delimiters is a character, then the context is bound to be name. The pattern |node*->leaf is an example of such a case.

Finally, if a pattern is only composed of wildcards, then the global context defined by the matching method called is used. For example the method MGlobal.bnMatchFullPath() defines the global context full path.

Number of Occurrences

Remember how, according to the rules of syntax composition, a syntax group might be made of one or more elements of another syntax group. With this in mind, the number of occurences specifies how many elements of a context needs to be matched.

The special characters *, +, ?, and . all carry the same purpose of matching a context element but a different number of times. The quantity being described by these wildcards is the same as their regular expression language counterparts, meaning that:

  • * matches 0 or more occurrences of a context element.
  • + matches 1 or more occurrences of a context element.
  • ? matches 0 or 1 occurrences of a context element.
  • . matches 1 occurrence of a context element.

As an example, if the context is full name, then the quantifier defines how many name elements needs to be matched: the wildcard in the pattern |ns:+|leaf will match 1 or more names separated by the : delimiter, thus forming in the end a full name.

Matching Nothing

It sometimes makes sense to allow a wildcard to match zero occurrences. This is especially useful when performing recursive searches where the pattern *|leaf can match any node named leaf, including the one directly parented under the world, and where the pattern |ns:*:leaf can match nodes such as |ns:ns2:ns3:leaf and |ns:leaf.

In some other cases, this doesn’t make too much sense. For example the pattern |ns:* cannot match any node named |ns: because this isn’t a valid pattern.

To check if a wildcard is allowed to match zero occurrences or not, see the TL;DR table.

TL;DR

The table below regroups all the possible valid uses of wildcard operators located between two adjacent delimiters.

Reminder

If the occurrence of wildcard is not listed in this table, it is bound to belong to the name context.

pattern example context can match nothing
^@$ @ same as the global context yes
^@: @:leaf full name yes
^@| @|leaf path yes
^@-> @->|leaf full path yes
:@$ |ns:@ full name no
:@: |ns:@:leaf full name yes
:@| |ns:@|leaf full name no
:@-> |ns:@->|leaf full name no
|@$ |root|@ path no
|@: |root|@:leaf full name yes
|@| |root|@|leaf path yes
|@-> |root|@->|leaf path no
->@$ |root->@ full path yes
->@| |root->@|leaf path yes
->@-> |root->@->|leaf full path yes

Note

The characters ^ and $ used in the table respectively refer to the start and the end of a string. As for the character @, it is to be replaced by one or more wildcards.

Combining Wildcards

If needed, it is possible to come up with some fancy patterns by successively writing multiple wildcard operators that will combine to define a specific number of occurrences. For example, the pattern ... matches 3 occurrences of the context element, while .+ matches at least 2 occurrences, and ..?? matches from 2 to 4 occurrences.

The number of occurrences to match resulting from such a combination is easy to figure out. Let’s consider the regular expression notation {m,n} describing from m to n occurrences, and {m,} that specifies at least m occurrences. Rewriting the four wildcard operators following this notation gives:

  • * -> {0,}
  • + -> {1,}
  • ? -> {0,1}
  • . -> {1,1}

Combining wildcard operators is equivalent to adding their range of occurrences. From the previous example, the pattern .+ equals to {1,1} + {1,}, that is {2,}, and the pattern ..?? equals to {1,1} + {1,1} + {0,1} + {0,1}, that is {2,4}.

Namespace Construct

The pattern |root|. allows matching any node which has root as direct parent but it is not enough if filtering namespaces is also required. This is why, as per the syntax rules, a special construct has been added to allow a full name to start with the : delimiter if it is followed by one or more wildcards. With this addition, the pattern |root|:. makes it possible to match any node directly parented under root that does not belong to any namespce.

Examples

Matching DG Nodes

>>> import bana
>>> bana.initialize()
>>> from maya import OpenMaya
>>> # Match the nodes named 'leaf' belonging to any namespace.
>>> OpenMaya.MGlobal.bnMatchFullName('*:leaf', 'leaf')
True
>>> OpenMaya.MGlobal.bnMatchFullName('*:leaf', 'ns:leaf')
True
>>> OpenMaya.MGlobal.bnMatchFullName('*:leaf', 'nsa:nsb:leaf')
True
>>> # Match the nodes directly nested under a namespace 'ns'.
>>> OpenMaya.MGlobal.bnMatchFullName('ns:.', 'ns:leaf')
True
>>> OpenMaya.MGlobal.bnMatchFullName('ns:.', 'ns:nsa:leaf')
False
>>> # Match the nodes recursively nested under a namespace 'ns'.
>>> OpenMaya.MGlobal.bnMatchFullName('ns:+', 'ns:leaf')
True
>>> OpenMaya.MGlobal.bnMatchFullName('ns:+', 'ns:nsa:leaf')
True
>>> OpenMaya.MGlobal.bnMatchFullName('ns:+', 'ns:nsa:nsb:leaf')
True

Matching DAG Nodes

>>> import bana
>>> bana.initialize()
>>> from maya import OpenMaya
>>> # Match the nodes directly parented under the world.
>>> OpenMaya.MGlobal.bnMatchPath('|.', '|leaf')
True
>>> OpenMaya.MGlobal.bnMatchPath('|.', '|ns:leaf')
True
>>> OpenMaya.MGlobal.bnMatchPath('|.', '|root|leaf')
False
>>> # Match the nodes directly parented under the world but not belonging to
... # any namespace.
>>> OpenMaya.MGlobal.bnMatchPath('|:.', '|leaf')
True
>>> OpenMaya.MGlobal.bnMatchPath('|:.', '|ns:leaf')
False
>>> OpenMaya.MGlobal.bnMatchPath('|:.', '|root|leaf')
False
>>> # Match the nodes containing 'Shape' anywhere in the hierarchy but not
... # belonging to any namespace.
>>> OpenMaya.MGlobal.bnMatchPath('+|*Shape*', '|cube|cubeShape')
True
>>> OpenMaya.MGlobal.bnMatchPath('+|*Shape*', '|root|sphere|sphereShape1')
True
>>> OpenMaya.MGlobal.bnMatchPath('+|*Shape*', '|cube|ns:cubeShape')
False
>>> # Match the nodes containing 'Shape' anywhere in the hierarchy.
>>> OpenMaya.MGlobal.bnMatchPath('+|*:*Shape*', '|cube|cubeShape')
True
>>> OpenMaya.MGlobal.bnMatchPath('+|*:*Shape*', '|root|sphere|sphereShape1')
True
>>> OpenMaya.MGlobal.bnMatchPath('+|*:*Shape*', '|cube|ns:cubeShape')
True