Repository: bids-standard/pybids

Issue:
0.8 broken on Windows, at least partly due to layout/config/bids.json
From what I can see, Windows is not officially supported. Unfortunately, my hospital is switching over to OneDrive/SharePoint, and I haven't figured out a way to navigate SharePoint folders in Linux.

At least one of the Windows incompatibilities is the` [/\\\\]` regex pattern used throughout (e.g. [here](https://github.com/bids-standard/pybids/blob/master/bids/layout/config/bids.json#L6)). This translates to "match a forward slash or a backslash or a backslash". But on windows, os.path.sep is `\\`, i.e. double backslash. I think the regexp pattern ` (/|\\\\)` will do what you want: "match a forward slash or double backslash".

Similarly, `[_/\\\\]` becomes `(_|/|\\\\)`.

I don't know what to do about `[^/\\\\]`. Maybe it's OK as is, but the final 4 backslashes might as well be 2.

The next problem is that the windows path has double backslashes in it, but when this gets converted to a pattern, the double backslashes are interpreted as a single backslash because the first backslash is an escape character. One place that I'm encountering this problem begins [here](https://github.com/bids-standard/pybids/blob/master/bids/layout/core.py#L407). There needs to be an additional step to escape the backslashes. i.e.,
`template = template.replace('{{root}}', self.root_path)`, becomes
`template = template.replace('{{root}}', self.root_path.replace('\\', '\\\\'))`

This will cause a problem if self.root_path already has escape characters in it.


Discussion:
Thanks for the report, @cboulay.

> At least one of the Windows incompatibilities is the` [/\\\\]` regex pattern used throughout (e.g. [here](https://github.com/bids-standard/pybids/blob/master/bids/layout/config/bids.json#L6)). This translates to "match a forward slash or a backslash or a backslash".

I think it's actually just forward slash or backslash. After reading in the JSON, the 4 backslashes will have been escaped into 2 backslashes, which the regex then treats as a single (escaped) backslash. If we were expressing the patterns in Python, we could use raw strings (i.e., `r"[/\\]"`), but we have to allow for the JSON layer. (This has no implications for the behavior you're reporting, just clarifying what's going on because it's come up before and is confusing.)

If I understand you correctly, we should be able to solve the problem by specifying "one or more" instead of just one, right? I.e., `[/\\\\]+` should work, no? Or maybe `[/\\\\]{1,2}`, if we want to be conservative? I don't think this can introduce any new problems, as I believe multiple separators are technically legal in POSIX paths too (though they'll usually have been removed by the time we're matching against our patterns). This solution seems preferable, as otherwise I think we would actually need 8 (!!) backslashes in the pattern in `bids.json` to get a double backslash in the regex.

> The next problem is that the windows path has double backslashes in it, but when this gets converted to a pattern, the double backslashes are interpreted as a single backslash because the first backslash is an escape character.

I'm actually not sure we need that `{{root}}` convention any longer; it's a holdover from < 0.8, where we were using a domain-general package (grabbit) as the indexing substrate. Since we're now entirely BIDS-specific, I'm pretty sure we can just drop `{{root}}` and assume that directory paths are always relative to the BIDS root. So this might solve the problem.

Let me know if you think the above solutions will work, or if you have any other thoughts.

Resolving patch:
diff --git a/bids/layout/core.py b/bids/layout/core.py
--- a/bids/layout/core.py
+++ b/bids/layout/core.py
@@ -403,8 +403,7 @@ def _get_child_class(self, path):
             template = self.available_entities[child_ent].directory
             if template is None:
                 return BIDSNode
-            # Replace {{root}} placeholder with root node's path
-            template = template.replace('{{root}}', self.root_path)
+            template = self.root_path + template
             # Construct regex search pattern from target directory template
             to_rep = re.findall(r'\{(.*?)\}', template)
             for ent in to_rep:
diff --git a/bids/layout/layout.py b/bids/layout/layout.py
--- a/bids/layout/layout.py
+++ b/bids/layout/layout.py
@@ -546,7 +546,7 @@ def get(self, return_type='object', target=None, extensions=None,
                                      'directory template is defined for the '
                                      'target entity (\"%s\").' % target)
                 # Construct regex search pattern from target directory template
-                template = template.replace('{{root}}', self.root)
+                template = self.root + template
                 to_rep = re.findall(r'\{(.*?)\}', template)
                 for ent in to_rep:
                     patt = entities[ent].pattern


Test patch:
diff --git a/bids/layout/tests/test_core.py b/bids/layout/tests/test_core.py
--- a/bids/layout/tests/test_core.py
+++ b/bids/layout/tests/test_core.py
@@ -22,7 +22,7 @@ def sample_bidsfile(tmpdir):
 @pytest.fixture(scope='module')
 def subject_entity():
     return Entity('subject', "[/\\\\]sub-([a-zA-Z0-9]+)", False,
-               "{{root}}{subject}", None, bleargh=True)
+               "{subject}", None, bleargh=True)
 
 
 def test_config_init_bare():
@@ -83,7 +83,7 @@ def test_entity_init_all_args(subject_entity):
     assert ent.name == 'subject'
     assert ent.pattern == "[/\\\\]sub-([a-zA-Z0-9]+)"
     assert ent.mandatory == False
-    assert ent.directory == "{{root}}{subject}"
+    assert ent.directory == "{subject}"
     assert ent.map_func is None
     assert ent.kwargs == {'bleargh': True}