Repository: sqlfluff/sqlfluff

Issue:
On macos: Respect XDG base dirs.
Apropos to #405, a large number of developer tools (especially those with a cli) respect XDG base dirs in both macos and linux. Those that insist on using `~/Library` on macos are outliers, as it creates unnecessary headaches for anyone that (a) maintains their own user configurations and / or (b) frequently switches between macos and linux. 

Can `sqlfluff` at least first look for configuration in `${XDG_CONFIG_HOME:-~/.config}` first on macos? Obviously `~/Library` can be used as a fallback if desired for some reason, though I think it becomes irrelevant for configuration / data if the XDG base dir spec is followed. (As indicated in https://stackoverflow.com/questions/3373948/equivalents-of-xdg-config-home-and-xdg-data-home-on-mac-os-x caching is a little more subtle). 


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/sqlfluff/sqlfluff/pull/955?src=pr&el=h1) Report
> Merging [#955](https://codecov.io/gh/sqlfluff/sqlfluff/pull/955?src=pr&el=desc) (4b221cf) into [master](https://codecov.io/gh/sqlfluff/sqlfluff/commit/82ebb4388352a94ef78d3d8ddc6902f5d30180fb?el=desc) (82ebb43) will **increase** coverage by `0.01%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/sqlfluff/sqlfluff/pull/955/graphs/tree.svg?width=650&height=150&src=pr&token=maM8kLgCmn)](https://codecov.io/gh/sqlfluff/sqlfluff/pull/955?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #955      +/-   ##
==========================================
+ Coverage   96.19%   96.20%   +0.01%     
==========================================
  Files         115      115              
  Lines        7748     7754       +6     
==========================================
+ Hits         7453     7460       +7     
+ Misses        295      294       -1     
```

| Flag | Coverage Δ | |
|---|---|---|
| dbt017-py38 | `60.70% <66.66%> (-0.01%)` | :arrow_down: |
| dbt018-py38 | `60.72% <66.66%> (-0.01%)` | :arrow_down: |
| dbt019-py38 | `60.72% <66.66%> (-0.01%)` | :arrow_down: |
| py36 | `95.41% <100.00%> (+0.01%)` | :arrow_up: |
| py37 | `95.41% <100.00%> (+0.01%)` | :arrow_up: |
| py38 | `95.37% <100.00%> (+0.01%)` | :arrow_up: |
| py39 | `95.37% <100.00%> (+0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/sqlfluff/sqlfluff/pull/955?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/sqlfluff/core/config.py](https://codecov.io/gh/sqlfluff/sqlfluff/pull/955/diff?src=pr&el=tree#diff-c3JjL3NxbGZsdWZmL2NvcmUvY29uZmlnLnB5) | `96.77% <100.00%> (+0.49%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sqlfluff/sqlfluff/pull/955?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sqlfluff/sqlfluff/pull/955?src=pr&el=footer). Last update [82ebb43...4b221cf](https://codecov.io/gh/sqlfluff/sqlfluff/pull/955?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

dmateusp: Thanks for the suggestion @barrywhart! So I tested it manually like this:

``` bash
❯ git status
On branch dmateusp/#889/use_linux_style_xdg_on_mac
❯ export XDG_CONFIG_HOME=~/.config/linters
❯ echo "select 1 FROM table_a" > test.sql
❯ echo "[sqlfluff:rules:L010]\ncapitalisation_policy = upper" > ~/Library/Application\ Support/sqlfluff/.sqlfluff
❯ echo "[sqlfluff:rules:L010]\ncapitalisation_policy = lower" > ${XDG_CONFIG_HOME}/sqlfluff/.sqlfluff
❯ sqlfluff fix test.sql
❯ cat test.sql
select 1 from table_a
❯ git checkout master
Switched to branch 'master'
❯ sqlfluff fix test.sql
❯ cat test.sql
SELECT 1 FROM table_a
```

So I think it's working as expected
dmateusp: @barrywhart this made me realize that the error message in L010 isn't very helpful so I opened https://github.com/sqlfluff/sqlfluff/issues/1128

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2ea7b323..f25ad70e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,10 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
-
 ### Changed
 - L034 now ignores select statements which contain macros.
 - L034 now ignores select statements part of a set expression, most commonly a union.
+- Respect XDG base dirs on Mac OS ([#889](https://github.com/sqlfluff/sqlfluff/issues/889)).
 - Fix bug [#1082](https://github.com/sqlfluff/sqlfluff/issues/1082), adding
   support for BigQuery `select as struct '1' as bb, 2 as aa` syntax
 
diff --git a/src/sqlfluff/core/config.py b/src/sqlfluff/core/config.py
index dc4769632..851644f55 100644
--- a/src/sqlfluff/core/config.py
+++ b/src/sqlfluff/core/config.py
@@ -285,7 +285,7 @@ class ConfigLoader:
         else:
             p = os.path.dirname(path)
 
-        d = os.listdir(p)
+        d = os.listdir(os.path.expanduser(p))
         # iterate this way round to make sure things overwrite is the right direction
         for fname in filename_options:
             if fname in d:
@@ -299,14 +299,31 @@ class ConfigLoader:
         self._config_cache[str(path)] = configs
         return configs
 
-    def load_user_appdir_config(self) -> dict:
-        """Load the config from the user's OS specific appdir config directory."""
+    @staticmethod
+    def _get_user_config_dir_path() -> str:
         appname = "sqlfluff"
         appauthor = "sqlfluff"
-        user_config_dir_path = appdirs.user_config_dir(appname, appauthor)
+
+        # On Mac OSX follow Linux XDG base dirs
+        # https://github.com/sqlfluff/sqlfluff/issues/889
+        user_config_dir_path = os.path.expanduser("~/.config/sqlfluff")
+        if appdirs.system == "darwin":
+            appdirs.system = "linux2"
+            user_config_dir_path = appdirs.user_config_dir(appname, appauthor)
+            appdirs.system = "darwin"
+
+        if not os.path.exists(user_config_dir_path):
+            user_config_dir_path = appdirs.user_config_dir(appname, appauthor)
+
+        return user_config_dir_path
+
+    def load_user_appdir_config(self) -> dict:
+        """Load the config from the user's OS specific appdir config directory."""
+        user_config_dir_path = self._get_user_config_dir_path()
         if os.path.exists(user_config_dir_path):
             return self.load_config_at_path(user_config_dir_path)
-        return {}
+        else:
+            return {}
 
     def load_user_config(self) -> dict:
         """Load the config from the user's home directory."""


Test patch:
diff --git a/test/core/config_test.py b/test/core/config_test.py
index d8340c2da..e2b201738 100644
--- a/test/core/config_test.py
+++ b/test/core/config_test.py
@@ -6,6 +6,9 @@ from sqlfluff.core.config import ConfigLoader, nested_combine, dict_diff
 from sqlfluff.core import Linter, FluffConfig
 
 from pathlib import Path
+from unittest.mock import patch, call
+import appdirs
+import pytest
 
 
 config_a = {
@@ -14,6 +17,12 @@ config_a = {
 }
 
 
+@pytest.fixture
+def mock_xdg_home(monkeypatch):
+    """Sets the XDG_CONFIG_HOME variable."""
+    monkeypatch.setenv("XDG_CONFIG_HOME", "~/.config/my/special/path")
+
+
 def test__config__nested_combine():
     """Test combination of two config dicts."""
     a = {"a": {"b": {"c": 123, "d": 456}}}
@@ -143,3 +152,36 @@ def test__config__nested_config_tests():
             assert ("L003", 1, 4) in violations[k]
             assert "L002" not in [c[0] for c in violations[k]]
             assert "L009" not in [c[0] for c in violations[k]]
+
+
+@patch("os.path.exists")
+@patch("os.listdir")
+def test__config__load_user_appdir_config(
+    mock_listdir, mock_path_exists, mock_xdg_home
+):
+    """Test loading config from user appdir."""
+    xdg_config_path = os.environ.get("XDG_CONFIG_HOME") + "/sqlfluff"
+
+    def path_exists(x):
+        if x == os.path.expanduser("~/.config/sqlfluff"):
+            return False
+        if x == xdg_config_path:
+            return False
+        else:
+            return True
+
+    mock_path_exists.side_effect = path_exists
+
+    c = ConfigLoader()
+
+    with patch.object(appdirs, attribute="system", new="darwin"):
+        resolved_path = c._get_user_config_dir_path()
+        c.load_user_appdir_config()
+    assert resolved_path == os.path.expanduser("~/Library/Application Support/sqlfluff")
+
+    mock_path_exists.assert_has_calls(
+        [
+            call(xdg_config_path),
+            call(os.path.expanduser("~/Library/Application Support/sqlfluff")),
+        ]
+    )