Repository: sqlfluff/sqlfluff

Issue:
Standardise `--disable_progress_bar` naming
### Search before asking

- [X] I searched the [issues](https://github.com/sqlfluff/sqlfluff/issues) and found no similar issues.


### What Happened

As noted in https://github.com/sqlfluff/sqlfluff/pull/3610#discussion_r926014745 `--disable_progress_bar` is the only command line option using underscores instead of dashes.

Should we change this?

This would be a breaking change, so do we leave until next major release?
Or do we accept both options?

### Expected Behaviour

We should be standard in out command line option format

### Observed Behaviour

`--disable_progress_bar` is the only non-standard one

### How to reproduce

N/A

### Dialect

N/A

### Version

1.2.1

### Configuration

N/A

### Are you willing to work on and submit a PR to address the issue?

- [X] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/sqlfluff/sqlfluff/blob/main/CODE_OF_CONDUCT.md)



Discussion:
I like the idea (of at least as a transition) or accepting both, but then defaulting to the one consistent with the other options.
So this is about changing option `--disable_progress_bar` to `--disable-progress-bar`, right? I think I can take care of that, it was me who introduced it here :)

Additionally I would make an attempt to have these two options available, but to nicely inform users that one with underscores is deprecated. What do you think @tunetheweb? 

I see I cannot assign myself to that Issue.

> So this is about changing option `--disable_progress_bar` to `--disable-progress-bar`, right? I think I can take care of that, it was me who introduced it here :)

Correct and thanks for taking on

> Additionally I would make an attempt to have these two options available, but to nicely inform users that one with underscores is deprecated. What do you think @tunetheweb?

@alanmcruickshank added some functionality that might help in #3874 but not sure if that applies to command lines options too (I’m having less time to work on SQLFluff lately so not following it as closely as I used to). If not maybe it should?

> I see I cannot assign myself to that Issue.

Yeah only maintainers can assign, which is a bit of an annoying restriction of GitHub so we tend not to use that field and commenting (like you’ve done here) is sufficient to claim an issue. Please comment again if to “unassign” yourself if it turns out you won’t be able to work on it after all. Though lack of progress is a usually good indicator of that anyway 😄

Patch:
diff --git a/src/sqlfluff/cli/click_deprecated_option.py b/src/sqlfluff/cli/click_deprecated_option.py
new file mode 100644
--- /dev/null
+++ b/src/sqlfluff/cli/click_deprecated_option.py
@@ -0,0 +1,104 @@
+"""Allows to provide deprecated options for click's command."""
+
+from typing import Any, Callable
+
+import click
+from click import Context, OptionParser, echo, style
+from click.parser import Option, ParsingState
+
+
+class DeprecatedOption(click.Option):
+    """Allows to provide deprecated options for click's command.
+
+    Works with `DeprecatedOptionsCommand` (see below).
+    Expects to be provided into standard `@click.option` with:
+      * two parameter declarations arguments - old one (deprecated)
+        and new one (preferred);
+      * `cls` parameter (standard click Option) as `cls=DeprecatedOption`;
+      * `deprecated` parameter - which says which ones are deprecated,
+        like`deprecated=["--disable_progress_bar"]1.
+
+    This is based on
+      * https://stackoverflow.com/a/50402799/5172513
+
+    It's somewhat hackish and may broke when click internals are changed, it is even
+    mentioned in SO:
+    > This code reaches into some private structures in the parser, but this is
+    unlikely to be an issue. This parser code was last changed 4 years ago.
+    The parser code is unlikely to undergo significant revisions.
+
+    Hopefully will be removed when
+      * https://github.com/pallets/click/issues/2263
+    is finished.
+    """
+
+    def __init__(self, *args, **kwargs):
+        self.deprecated = kwargs.pop("deprecated", ())
+        self.preferred = args[0][-1]
+
+        super().__init__(*args, **kwargs)
+
+
+class DeprecatedOptionsCommand(click.Command):
+    """Allows to provide deprecated options for click's command.
+
+    Works with `DeprecatedOption` (see above).
+    Expects to be provided into standard `@click.command` as:
+      * `@cli.command(cls=DeprecatedOptionsCommand)`
+    """
+
+    def make_parser(self, ctx: Context) -> OptionParser:
+        """Hook 'make_parser' and during processing check the name.
+
+        Used to invoke the option to see if it is preferred.
+        """
+        parser: OptionParser = super().make_parser(ctx)
+
+        # get the parser options
+        options = set(parser._short_opt.values())
+        options |= set(parser._long_opt.values())
+
+        for option in options:
+            if not isinstance(option.obj, DeprecatedOption):
+                continue
+
+            option.process = self._make_process(option)  # type: ignore
+
+        return parser
+
+    def _make_process(self, an_option: Option) -> Callable:
+        """Construct a closure to the parser option processor."""
+        orig_process: Callable = an_option.process
+        deprecated = getattr(an_option.obj, "deprecated", None)
+        preferred = getattr(an_option.obj, "preferred", None)
+
+        if not deprecated:
+            raise ValueError(
+                f"Expected `deprecated` value for `{an_option.obj.name!r}`"
+            )
+
+        def process(value: Any, state: ParsingState) -> None:
+            """Custom process method.
+
+            The function above us on the stack used 'opt' to
+            pick option from a dict, see if it is deprecated.
+            """
+            # reach up the stack and get 'opt'
+            import inspect
+
+            frame = inspect.currentframe()
+            try:
+                opt = frame.f_back.f_locals.get("opt")  # type: ignore
+            finally:
+                del frame
+
+            if opt in deprecated:  # type: ignore
+                msg = (
+                    f"DeprecationWarning: The option {opt!r} is deprecated, "
+                    f"use {preferred!r}."
+                )
+                echo(style(msg, fg="red"), err=True)
+
+            return orig_process(value, state)
+
+        return process
diff --git a/src/sqlfluff/cli/commands.py b/src/sqlfluff/cli/commands.py
--- a/src/sqlfluff/cli/commands.py
+++ b/src/sqlfluff/cli/commands.py
@@ -23,6 +23,10 @@
 from sqlfluff.cli.autocomplete import shell_completion_enabled, dialect_shell_complete
 
 from sqlfluff.cli import EXIT_SUCCESS, EXIT_ERROR, EXIT_FAIL
+from sqlfluff.cli.click_deprecated_option import (
+    DeprecatedOption,
+    DeprecatedOptionsCommand,
+)
 from sqlfluff.cli.formatters import (
     format_linting_result_header,
     OutputStreamFormatter,
@@ -455,7 +459,7 @@ def dump_file_payload(filename: Optional[str], payload: str):
         click.echo(payload)
 
 
-@cli.command()
+@cli.command(cls=DeprecatedOptionsCommand)
 @common_options
 @core_options
 @click.option(
@@ -509,8 +513,11 @@ def dump_file_payload(filename: Optional[str], payload: str):
 )
 @click.option(
     "--disable_progress_bar",
+    "--disable-progress-bar",
     is_flag=True,
     help="Disables progress bars.",
+    cls=DeprecatedOption,
+    deprecated=["--disable_progress_bar"],
 )
 @click.argument("paths", nargs=-1, type=click.Path(allow_dash=True))
 def lint(
@@ -704,7 +711,7 @@ def do_fixes(lnt, result, formatter=None, **kwargs):
     ),
 )
 @click.option(
-    "--disable_progress_bar",
+    "--disable-progress-bar",
     is_flag=True,
     help="Disables progress bars.",
 )


Test patch:
diff --git a/test/cli/commands_test.py b/test/cli/commands_test.py
--- a/test/cli/commands_test.py
+++ b/test/cli/commands_test.py
@@ -112,7 +112,7 @@ def test__cli__command_directed():
         args=[
             lint,
             [
-                "--disable_progress_bar",
+                "--disable-progress-bar",
                 "test/fixtures/linter/indentation_error_simple.sql",
             ],
         ],
@@ -1002,7 +1002,7 @@ def test__cli__command_fix_stdin(stdin, rules, stdout):
     result = invoke_assert_code(
         args=[
             fix,
-            ("-", "--rules", rules, "--disable_progress_bar", "--dialect=ansi"),
+            ("-", "--rules", rules, "--disable-progress-bar", "--dialect=ansi"),
         ],
         cli_input=stdin,
     )
@@ -1036,7 +1036,7 @@ def test__cli__command_fix_stdin_safety():
 
     # just prints the very same thing
     result = invoke_assert_code(
-        args=[fix, ("-", "--disable_progress_bar", "--dialect=ansi")],
+        args=[fix, ("-", "--disable-progress-bar", "--dialect=ansi")],
         cli_input=perfect_sql,
     )
     assert result.output.strip() == perfect_sql
@@ -1177,7 +1177,7 @@ def test__cli__command_lint_serialize_from_stdin(serialize, sql, expected, exit_
                 "L010",
                 "--format",
                 serialize,
-                "--disable_progress_bar",
+                "--disable-progress-bar",
                 "--dialect=ansi",
             ),
         ],
@@ -1222,7 +1222,7 @@ def test__cli__command_lint_nocolor(isatty, should_strip_ansi, capsys, tmpdir):
         "--nocolor",
         "--dialect",
         "ansi",
-        "--disable_progress_bar",
+        "--disable-progress-bar",
         fpath,
         "--write-output",
         output_file,
@@ -1253,7 +1253,7 @@ def test__cli__command_lint_serialize_multiple_files(serialize, write_file, tmp_
         fpath,
         "--format",
         serialize,
-        "--disable_progress_bar",
+        "--disable-progress-bar",
     )
 
     if write_file:
@@ -1310,7 +1310,7 @@ def test__cli__command_lint_serialize_github_annotation():
                 "github-annotation",
                 "--annotation-level",
                 "warning",
-                "--disable_progress_bar",
+                "--disable-progress-bar",
             ),
         ],
         ret_code=1,
@@ -1421,7 +1421,7 @@ def test__cli__command_lint_serialize_github_annotation_native():
                 "github-annotation-native",
                 "--annotation-level",
                 "error",
-                "--disable_progress_bar",
+                "--disable-progress-bar",
             ),
         ],
         ret_code=1,
@@ -1465,7 +1465,7 @@ def test__cli__command_lint_serialize_annotation_level_error_failure_equivalent(
                 serialize,
                 "--annotation-level",
                 "error",
-                "--disable_progress_bar",
+                "--disable-progress-bar",
             ),
         ],
         ret_code=1,
@@ -1480,7 +1480,7 @@ def test__cli__command_lint_serialize_annotation_level_error_failure_equivalent(
                 serialize,
                 "--annotation-level",
                 "failure",
-                "--disable_progress_bar",
+                "--disable-progress-bar",
             ),
         ],
         ret_code=1,
@@ -1613,6 +1613,25 @@ def test_cli_lint_disabled_progress_bar(
         self, mock_disable_progress_bar: MagicMock
     ) -> None:
         """When progress bar is disabled, nothing should be printed into output."""
+        result = invoke_assert_code(
+            args=[
+                lint,
+                [
+                    "--disable-progress-bar",
+                    "test/fixtures/linter/passing.sql",
+                ],
+            ],
+        )
+        raw_output = repr(result.output)
+
+        assert "\rpath test/fixtures/linter/passing.sql:" not in raw_output
+        assert "\rparsing: 0it" not in raw_output
+        assert "\r\rlint by rules:" not in raw_output
+
+    def test_cli_lint_disabled_progress_bar_deprecated_option(
+        self, mock_disable_progress_bar: MagicMock
+    ) -> None:
+        """Same as above but checks additionally if deprecation warning is printed."""
         result = invoke_assert_code(
             args=[
                 lint,
@@ -1627,6 +1646,10 @@ def test_cli_lint_disabled_progress_bar(
         assert "\rpath test/fixtures/linter/passing.sql:" not in raw_output
         assert "\rparsing: 0it" not in raw_output
         assert "\r\rlint by rules:" not in raw_output
+        assert (
+            "DeprecationWarning: The option '--disable_progress_bar' is deprecated, "
+            "use '--disable-progress-bar'"
+        ) in raw_output
 
     def test_cli_lint_enabled_progress_bar(
         self, mock_disable_progress_bar: MagicMock
@@ -1709,7 +1732,7 @@ def test__cli__fix_multiple_errors_no_show_errors():
         args=[
             fix,
             [
-                "--disable_progress_bar",
+                "--disable-progress-bar",
                 "test/fixtures/linter/multiple_sql_errors.sql",
             ],
         ],
@@ -1729,7 +1752,7 @@ def test__cli__fix_multiple_errors_show_errors():
         args=[
             fix,
             [
-                "--disable_progress_bar",
+                "--disable-progress-bar",
                 "--show-lint-violations",
                 "test/fixtures/linter/multiple_sql_errors.sql",
             ],
@@ -1771,7 +1794,7 @@ def test__cli__multiple_files__fix_multiple_errors_show_errors():
         args=[
             fix,
             [
-                "--disable_progress_bar",
+                "--disable-progress-bar",
                 "--show-lint-violations",
                 sql_path,
                 indent_path,
diff --git a/test/cli/test_click_deprecated_option.py b/test/cli/test_click_deprecated_option.py
new file mode 100644
--- /dev/null
+++ b/test/cli/test_click_deprecated_option.py
@@ -0,0 +1,68 @@
+"""The Test suite for `DeprecatedOption` - extension for click options."""
+from typing import List
+
+import click
+import pytest
+
+from sqlfluff.cli.click_deprecated_option import (
+    DeprecatedOption,
+    DeprecatedOptionsCommand,
+)
+from test.cli.commands_test import invoke_assert_code
+
+
+class TestClickDeprecatedOption:
+    """Tests for custom click's option `DeprecatedOption`."""
+
+    @pytest.mark.parametrize(
+        "option, expected_output",
+        [
+            ([], "{'old_option': False}\n"),
+            (
+                ["--old_option"],
+                "DeprecationWarning: The option '--old_option' is deprecated, "
+                "use '--new_option'.\n{'old_option': True}\n",
+            ),
+            (["--new_option"], "{'old_option': True}\n"),
+        ],
+    )
+    def test_cli_deprecated_option(
+        self, option: List[str], expected_output: str
+    ) -> None:
+        """Prepares command with option which has deprecated version and checks it."""
+
+        @click.command(cls=DeprecatedOptionsCommand)
+        @click.option(
+            "--old_option",
+            "--new_option",
+            is_flag=True,
+            cls=DeprecatedOption,
+            deprecated=["--old_option"],
+        )
+        def some_command(**kwargs):
+            click.echo("{}".format(kwargs))
+
+        result = invoke_assert_code(args=[some_command, option])
+        raw_output = result.output
+
+        assert raw_output == expected_output
+
+    def test_cli_deprecated_option_should_fail_when_missing_attr(
+        self,
+    ) -> None:
+        """The DeprecatedOption needs to have specified deprecated attr."""
+
+        @click.command(cls=DeprecatedOptionsCommand)
+        @click.option(
+            "--old_option",
+            "--new_option",
+            is_flag=True,
+            cls=DeprecatedOption,
+        )
+        def some_command(**kwargs):
+            click.echo("{}".format(kwargs))
+
+        with pytest.raises(ValueError) as exc:
+            invoke_assert_code(args=[some_command, ["--old_option"]])
+
+        assert str(exc.value) == "Expected `deprecated` value for `'old_option'`"