Repository: alanmcruickshank/sqlfluff

Issue:
Consistent yaml and json output on `parse` and `lint`
@nolanbconaway this would be a great one to follow up from #195 .

The `parse` command has a yaml output option.
The `lint` command now has a json output option.

I think that both should have both yaml and json outputs.

Discussion:

Patch:
diff --git a/src/sqlfluff/cli/commands.py b/src/sqlfluff/cli/commands.py
index 16672d55..ac4b9a00 100644
--- a/src/sqlfluff/cli/commands.py
+++ b/src/sqlfluff/cli/commands.py
@@ -3,6 +3,8 @@
 import sys
 import json
 
+import oyaml as yaml
+
 import click
 # For the profiler
 import pstats
@@ -113,7 +115,7 @@ def rules(**kwargs):
 @cli.command()
 @common_options
 @click.option('-f', '--format', 'format', default='human',
-              type=click.Choice(['human', 'json'], case_sensitive=False),
+              type=click.Choice(['human', 'json', 'yaml'], case_sensitive=False),
               help='What format to return the lint result in.')
 @click.argument('paths', nargs=-1)
 def lint(paths, format, **kwargs):
@@ -136,7 +138,7 @@ def lint(paths, format, **kwargs):
 
     """
     c = get_config(**kwargs)
-    lnt = get_linter(c, silent=format == 'json')
+    lnt = get_linter(c, silent=format in ('json', 'yaml'))
     verbose = c.get('verbose')
 
     config_string = format_config(lnt, verbose=verbose)
@@ -159,6 +161,8 @@ def lint(paths, format, **kwargs):
 
     if format == 'json':
         click.echo(json.dumps(result.as_records()))
+    elif format == 'yaml':
+        click.echo(yaml.dump(result.as_records()))
 
     sys.exit(result.stats()['exit code'])
 
@@ -257,7 +261,7 @@ def fix(force, paths, **kwargs):
 @click.option('-c', '--code-only', is_flag=True,
               help='Output only the code elements of the parse tree.')
 @click.option('-f', '--format', default='human',
-              type=click.Choice(['human', 'yaml'], case_sensitive=False),
+              type=click.Choice(['human', 'json', 'yaml'], case_sensitive=False),
               help='What format to return the parse result in.')
 @click.option('--profiler', is_flag=True,
               help='Set this flag to engage the python profiler.')
@@ -271,7 +275,7 @@ def parse(path, code_only, format, profiler, **kwargs):
     """
     c = get_config(**kwargs)
     # We don't want anything else to be logged if we want a yaml output
-    lnt = get_linter(c, silent=format == 'yaml')
+    lnt = get_linter(c, silent=format in ('json', 'yaml'))
     verbose = c.get('verbose')
     recurse = c.get('recurse')
 
@@ -290,23 +294,54 @@ def parse(path, code_only, format, profiler, **kwargs):
             sys.exit(1)
         pr = cProfile.Profile()
         pr.enable()
+
     try:
-        # A single path must be specified for this command
-        for parsed, violations, time_dict in lnt.parse_path(path, verbosity=verbose, recurse=recurse):
-            if parsed:
-                if format == 'human':
+        # handle stdin if specified via lone '-'
+        if '-' == path:
+            # put the parser result in a list to iterate later
+            config = lnt.config.make_child_from_path('stdin')
+            result = [lnt.parse_string(
+                sys.stdin.read(),
+                'stdin',
+                verbosity=verbose,
+                recurse=recurse,
+                config=config
+            )]
+        else:
+            # A single path must be specified for this command
+            result = lnt.parse_path(path, verbosity=verbose, recurse=recurse)
+
+        # iterative print for human readout
+        if format == 'human':
+            for parsed, violations, time_dict in result:
+                if parsed:
                     lnt.log(parsed.stringify(code_only=code_only))
-                elif format == 'yaml':
-                    click.echo(parsed.to_yaml(code_only=code_only, show_raw=True))
-            else:
-                # TODO: Make this prettier
-                lnt.log('...Failed to Parse...')
-            nv += len(violations)
-            for v in violations:
-                lnt.log(format_violation(v, verbose=verbose))
-            if verbose >= 2:
-                lnt.log("==== timings ====")
-                lnt.log(cli_table(time_dict.items()))
+                else:
+                    # TODO: Make this prettier
+                    lnt.log('...Failed to Parse...')
+                nv += len(violations)
+                for v in violations:
+                    lnt.log(format_violation(v, verbose=verbose))
+                if verbose >= 2:
+                    lnt.log("==== timings ====")
+                    lnt.log(cli_table(time_dict.items()))
+        else:
+            # collect result and print as single payload
+            # will need to zip in the file paths
+            filepaths = ['stdin'] if '-' == path else lnt.paths_from_path(path)
+            result = [
+                dict(
+                    filepath=filepath,
+                    segments=parsed.as_record(code_only=code_only, show_raw=True)
+                )
+                for filepath, (parsed, _, _) in zip(filepaths, result)
+            ]
+
+            if format == 'yaml':
+                click.echo(yaml.dump(result))
+            elif format == 'json':
+                click.echo(json.dumps(result))
+
     except IOError:
         click.echo(colorize('The path {0!r} could not be accessed. Check it exists.'.format(path), 'red'))
         sys.exit(1)
diff --git a/src/sqlfluff/parser/segments_base.py b/src/sqlfluff/parser/segments_base.py
index e2ef6690..55381a92 100644
--- a/src/sqlfluff/parser/segments_base.py
+++ b/src/sqlfluff/parser/segments_base.py
@@ -13,7 +13,7 @@ These are the fundamental building blocks of the rest of the parser.
 """
 
 import logging
-import oyaml as yaml
+
 from io import StringIO
 
 from .match import MatchResult, curtail_string, join_segments_raw
@@ -522,31 +522,33 @@ class BaseSegment:
             result = (self.type, tuple(seg.to_tuple(**kwargs) for seg in self.segments if not seg.is_meta))
         return result
 
-    def to_yaml(self, **kwargs):
-        """Return a yaml structure from this segment."""
-        tpl = self.to_tuple(**kwargs)
-
-        def _structural_simplify(elem):
-            """Simplify the structure recursively so it outputs nicely in yaml."""
-            if isinstance(elem, tuple):
-                # Does this look like an element?
-                if len(elem) == 2 and isinstance(elem[0], str):
-                    # This looks like a single element, make a dict
-                    elem = {elem[0]: _structural_simplify(elem[1])}
-                elif isinstance(elem[0], tuple):
-                    # This looks like a list of elements.
-                    keys = [e[0] for e in elem]
-                    # Any duplicate elements?
-                    if len(set(keys)) == len(keys):
-                        # No, we can use a mapping typle
-                        elem = {e[0]: _structural_simplify(e[1]) for e in elem}
-                    else:
-                        # Yes, this has to be a list :(
-                        elem = [_structural_simplify(e) for e in elem]
-            return elem
+    @classmethod
+    def structural_simplify(cls, elem):
+        """Simplify the structure recursively so it serializes nicely in json/yaml."""
+        if isinstance(elem, tuple):
+            # Does this look like an element?
+            if len(elem) == 2 and isinstance(elem[0], str):
+                # This looks like a single element, make a dict
+                elem = {elem[0]: cls.structural_simplify(elem[1])}
+            elif isinstance(elem[0], tuple):
+                # This looks like a list of elements.
+                keys = [e[0] for e in elem]
+                # Any duplicate elements?
+                if len(set(keys)) == len(keys):
+                    # No, we can use a mapping typle
+                    elem = {e[0]: cls.structural_simplify(e[1]) for e in elem}
+                else:
+                    # Yes, this has to be a list :(
+                    elem = [cls.structural_simplify(e) for e in elem]
+        return elem
+
+    def as_record(self, **kwargs):
+        """Return the segment as a structurally simplified record.
 
-        obj = _structural_simplify(tpl)
-        return yaml.dump(obj)
+        This is useful for serialization to yaml or json.
+        kwargs passed to to_tuple
+        """
+        return self.structural_simplify(self.to_tuple(**kwargs))
 
     @classmethod
     def match(cls, segments, parse_context):


Test patch:
diff --git a/test/cli_commands_test.py b/test/cli_commands_test.py
index 602d8085..44ff31e8 100644
--- a/test/cli_commands_test.py
+++ b/test/cli_commands_test.py
@@ -1,261 +1,296 @@
-"""The Test file for CLI (General)."""
-
-import configparser
-import tempfile
-import os
-import shutil
-import json
-import subprocess
-
-# Testing libraries
-import pytest
-from click.testing import CliRunner
-
-# We import the library directly here to get the version
-import sqlfluff
-from sqlfluff.cli.commands import lint, version, rules, fix, parse
-
-
-def invoke_assert_code(ret_code=0, args=None, kwargs=None, cli_input=None):
-    """Invoke a command and check return code."""
-    args = args or []
-    kwargs = kwargs or {}
-    if cli_input:
-        kwargs['input'] = cli_input
-    runner = CliRunner()
-    result = runner.invoke(*args, **kwargs)
-    # Output the CLI code for debugging
-    print(result.output)
-    # Check return codes
-    if ret_code == 0:
-        if result.exception:
-            raise result.exception
-    assert ret_code == result.exit_code
-    return result
-
-
-def test__cli__command_directed():
-    """Basic checking of lint functionality."""
-    result = invoke_assert_code(
-        ret_code=65,
-        args=[lint, ['-n', 'test/fixtures/linter/indentation_error_simple.sql']]
-    )
-    # We should get a readout of what the error was
-    check_a = "L:   2 | P:   4 | L003"
-    # NB: Skip the number at the end because it's configurable
-    check_b = "Indentation"
-    assert check_a in result.output
-    assert check_b in result.output
-
-
-def test__cli__command_dialect():
-    """Check the script raises the right exception on an unknown dialect."""
-    # The dialect is unknown should be a non-zero exit code
-    invoke_assert_code(
-        ret_code=66,
-        args=[lint, ['-n', '--dialect', 'faslkjh', 'test/fixtures/linter/indentation_error_simple.sql']]
-    )
-
-
-@pytest.mark.parametrize('command', [
-    ('-', '-n', ), ('-', '-n', '-v',), ('-', '-n', '-vv',), ('-', '-vv',),
-])
-def test__cli__command_lint_stdin(command):
-    """Check basic commands on a simple script using stdin.
-
-    The subprocess command should exit without errors, as no issues should be found.
-    """
-    with open('test/fixtures/cli/passing_a.sql', 'r') as test_file:
-        sql = test_file.read()
-    invoke_assert_code(args=[lint, command], cli_input=sql)
-
-
-@pytest.mark.parametrize('command', [
-    # Test basic linting
-    (lint, ['-n', 'test/fixtures/cli/passing_b.sql']),
-    # Original tests from test__cli__command_lint
-    (lint, ['-n', 'test/fixtures/cli/passing_a.sql']),
-    (lint, ['-n', '-v', 'test/fixtures/cli/passing_a.sql']),
-    (lint, ['-n', '-vvvv', 'test/fixtures/cli/passing_a.sql']),
-    (lint, ['-vvvv', 'test/fixtures/cli/passing_a.sql']),
-    # Test basic linting with very high verbosity
-    (lint, ['-n', 'test/fixtures/cli/passing_b.sql', '-vvvvvvvvvvv']),
-    # Check basic parsing
-    (parse, ['-n', 'test/fixtures/cli/passing_b.sql']),
-    # Test basic parsing with very high verbosity
-    (parse, ['-n', 'test/fixtures/cli/passing_b.sql', '-vvvvvvvvvvv']),
-    # Check basic parsing, with the code only option
-    (parse, ['-n', 'test/fixtures/cli/passing_b.sql', '-c']),
-    # Check basic parsing, with the yaml output
-    (parse, ['-n', 'test/fixtures/cli/passing_b.sql', '-c', '-f', 'yaml']),
-    (parse, ['-n', 'test/fixtures/cli/passing_b.sql', '--format', 'yaml']),
-    # Check linting works in specifying rules
-    (lint, ['-n', '--rules', 'L001', 'test/fixtures/linter/operator_errors.sql']),
-    # Check linting works in specifying multiple rules
-    (lint, ['-n', '--rules', 'L001,L002', 'test/fixtures/linter/operator_errors.sql']),
-    # Check linting works with both included and excluded rules
-    (lint, ['-n', '--rules', 'L001,L006', '--exclude-rules', 'L006', 'test/fixtures/linter/operator_errors.sql']),
-    # Check linting works with just excluded rules
-    (lint, ['-n', '--exclude-rules', 'L006,L007', 'test/fixtures/linter/operator_errors.sql']),
-    # Check the script doesn't raise an unexpected exception with badly formed files.
-    (fix, ['--rules', 'L001', 'test/fixtures/cli/fail_many.sql', '-vvvvvvv'], 'y'),
-    # Check that ignoring works (also checks that unicode files parse).
-    (lint, ['-n', '--exclude-rules', 'L003,L009', '--ignore', 'parsing,lexing', 'test/fixtures/linter/parse_lex_error.sql']),
-])
-def test__cli__command_lint_parse(command):
-    """Check basic commands on a more complicated script."""
-    invoke_assert_code(args=command)
-
-
-def test__cli__command_versioning():
-    """Check version command."""
-    # Get the package version info
-    pkg_version = sqlfluff.__version__
-    # Get the version info from the config file
-    config = configparser.ConfigParser()
-    config.read_file(open('src/sqlfluff/config.ini'))
-    config_version = config['sqlfluff']['version']
-    assert pkg_version == config_version
-    # Get the version from the cli
-    runner = CliRunner()
-    result = runner.invoke(version)
-    assert result.exit_code == 0
-    # We need to strip to remove the newline characters
-    assert result.output.strip() == pkg_version
-
-
-def test__cli__command_version():
-    """Just check version command for exceptions."""
-    # Get the package version info
-    pkg_version = sqlfluff.__version__
-    runner = CliRunner()
-    result = runner.invoke(version)
-    assert result.exit_code == 0
-    assert pkg_version in result.output
-    # Check a verbose version
-    result = runner.invoke(version, ['-v'])
-    assert result.exit_code == 0
-    assert pkg_version in result.output
-
-
-def test__cli__command_rules():
-    """Just check rules command for exceptions."""
-    invoke_assert_code(args=[rules])
-
-
-def generic_roundtrip_test(source_file, rulestring, final_exit_code=0, force=True, fix_input=None, fix_exit_code=0):
-    """A test for roundtrip testing, take a file buffer, lint, fix and lint.
-
-    This is explicitly different from the linter version of this, in that
-    it uses the command line rather than the direct api.
-    """
-    filename = 'testing.sql'
-    # Lets get the path of a file to use
-    tempdir_path = tempfile.mkdtemp()
-    filepath = os.path.join(tempdir_path, filename)
-    # Open the example file and write the content to it
-    with open(filepath, mode='w') as dest_file:
-        for line in source_file:
-            dest_file.write(line)
-    # Check that we first detect the issue
-    invoke_assert_code(ret_code=65, args=[lint, ['--rules', rulestring, filepath]])
-    # Fix the file (in force mode)
-    if force:
-        fix_args = ['--rules', rulestring, '-f', filepath]
-    else:
-        fix_args = ['--rules', rulestring, filepath]
-    invoke_assert_code(ret_code=fix_exit_code, args=[fix, fix_args], cli_input=fix_input)
-    # Now lint the file and check for exceptions
-    invoke_assert_code(ret_code=final_exit_code, args=[lint, ['--rules', rulestring, filepath]])
-    shutil.rmtree(tempdir_path)
-
-
-@pytest.mark.parametrize('rule,fname', [
-    ('L001', 'test/fixtures/linter/indentation_errors.sql'),
-    ('L008', 'test/fixtures/linter/whitespace_errors.sql'),
-    ('L008', 'test/fixtures/linter/indentation_errors.sql'),
-    # Really stretching the ability of the fixer to re-indent a file
-    ('L003', 'test/fixtures/linter/indentation_error_hard.sql')
-])
-def test__cli__command__fix(rule, fname):
-    """Test the round trip of detecting, fixing and then not detecting the rule."""
-    with open(fname, mode='r') as test_file:
-        generic_roundtrip_test(test_file, rule)
-
-
-@pytest.mark.parametrize('rule,fname', [
-    # NB: L004 currently has no fix routine.
-    ('L004', 'test/fixtures/linter/indentation_errors.sql')
-])
-def test__cli__command__fix_fail(rule, fname):
-    """Test the round trip of detecting, fixing and then still detecting the rule."""
-    with open(fname, mode='r') as test_file:
-        generic_roundtrip_test(test_file, rule, fix_exit_code=1, final_exit_code=65)
-
-
-def test__cli__command_fix_stdin(monkeypatch):
-    """Check stdin input for fix works."""
-    sql = 'select * from tbl'
-    expected = 'fixed sql!'
-    monkeypatch.setattr("sqlfluff.linter.LintedFile.fix_string", lambda x: expected)
-    result = invoke_assert_code(args=[fix, ('-', '--rules', 'L001')], cli_input=sql)
-    assert result.output == expected
-
-
-@pytest.mark.parametrize('rule,fname,prompt,exit_code', [
-    ('L001', 'test/fixtures/linter/indentation_errors.sql', 'y', 0),
-    ('L001', 'test/fixtures/linter/indentation_errors.sql', 'n', 65)
-])
-def test__cli__command__fix_no_force(rule, fname, prompt, exit_code):
-    """Round trip test, using the prompts."""
-    with open(fname, mode='r') as test_file:
-        generic_roundtrip_test(
-            test_file, rule, force=False, final_exit_code=exit_code,
-            fix_input=prompt)
-
-
-@pytest.mark.parametrize('sql,expected,exit_code', [
-    ('select * from tbl', [], 0),  # empty list if no violations
-    (
-        'SElect * from tbl',
-        [{
-            "filepath": "stdin",
-            "violations": [
-                {
-                    "code": "L010",
-                    "line_no": 1,
-                    "line_pos": 1,
-                    "description": "Inconsistent capitalisation of keywords."
-                }
-            ]
-        }],
-        65
-    )
-])
-def test__cli__command_lint_json_from_stdin(sql, expected, exit_code):
-    """Check an explicit JSON return value for a single error."""
-    result = invoke_assert_code(
-        args=[lint, ('-', '--rules', 'L010', '--format', 'json')],
-        cli_input=sql,
-        ret_code=exit_code
-    )
-    assert json.loads(result.output) == expected
-
-
-def test__cli__command_lint_json_multiple_files():
-    """Check the general format of JSON output for multiple files."""
-    fpath = 'test/fixtures/linter/indentation_errors.sql'
-
-    # note the file is in here twice. two files = two payloads.
-    result = invoke_assert_code(
-        args=[lint, (fpath, fpath, '--format', 'json')],
-        ret_code=65,
-    )
-    result = json.loads(result.output)
-    assert len(result) == 2
-
-
-def test___main___help():
-    """Test that the CLI can be access via __main__."""
-    # nonzero exit is good enough
-    subprocess.check_output(['python', '-m', 'sqlfluff', '--help'])
+"""The Test file for CLI (General)."""
+
+import configparser
+import tempfile
+import os
+import shutil
+import json
+import oyaml as yaml
+import subprocess
+
+# Testing libraries
+import pytest
+from click.testing import CliRunner
+
+# We import the library directly here to get the version
+import sqlfluff
+from sqlfluff.cli.commands import lint, version, rules, fix, parse
+
+
+def invoke_assert_code(ret_code=0, args=None, kwargs=None, cli_input=None):
+    """Invoke a command and check return code."""
+    args = args or []
+    kwargs = kwargs or {}
+    if cli_input:
+        kwargs['input'] = cli_input
+    runner = CliRunner()
+    result = runner.invoke(*args, **kwargs)
+    # Output the CLI code for debugging
+    print(result.output)
+    # Check return codes
+    if ret_code == 0:
+        if result.exception:
+            raise result.exception
+    assert ret_code == result.exit_code
+    return result
+
+
+def test__cli__command_directed():
+    """Basic checking of lint functionality."""
+    result = invoke_assert_code(
+        ret_code=65,
+        args=[lint, ['-n', 'test/fixtures/linter/indentation_error_simple.sql']]
+    )
+    # We should get a readout of what the error was
+    check_a = "L:   2 | P:   4 | L003"
+    # NB: Skip the number at the end because it's configurable
+    check_b = "Indentation"
+    assert check_a in result.output
+    assert check_b in result.output
+
+
+def test__cli__command_dialect():
+    """Check the script raises the right exception on an unknown dialect."""
+    # The dialect is unknown should be a non-zero exit code
+    invoke_assert_code(
+        ret_code=66,
+        args=[lint, ['-n', '--dialect', 'faslkjh', 'test/fixtures/linter/indentation_error_simple.sql']]
+    )
+
+
+@pytest.mark.parametrize('command', [
+    ('-', '-n', ), ('-', '-n', '-v',), ('-', '-n', '-vv',), ('-', '-vv',),
+])
+def test__cli__command_lint_stdin(command):
+    """Check basic commands on a simple script using stdin.
+
+    The subprocess command should exit without errors, as no issues should be found.
+    """
+    with open('test/fixtures/cli/passing_a.sql', 'r') as test_file:
+        sql = test_file.read()
+    invoke_assert_code(args=[lint, command], cli_input=sql)
+
+
+@pytest.mark.parametrize('command', [
+    # Test basic linting
+    (lint, ['-n', 'test/fixtures/cli/passing_b.sql']),
+    # Original tests from test__cli__command_lint
+    (lint, ['-n', 'test/fixtures/cli/passing_a.sql']),
+    (lint, ['-n', '-v', 'test/fixtures/cli/passing_a.sql']),
+    (lint, ['-n', '-vvvv', 'test/fixtures/cli/passing_a.sql']),
+    (lint, ['-vvvv', 'test/fixtures/cli/passing_a.sql']),
+    # Test basic linting with very high verbosity
+    (lint, ['-n', 'test/fixtures/cli/passing_b.sql', '-vvvvvvvvvvv']),
+    # Check basic parsing
+    (parse, ['-n', 'test/fixtures/cli/passing_b.sql']),
+    # Test basic parsing with very high verbosity
+    (parse, ['-n', 'test/fixtures/cli/passing_b.sql', '-vvvvvvvvvvv']),
+    # Check basic parsing, with the code only option
+    (parse, ['-n', 'test/fixtures/cli/passing_b.sql', '-c']),
+    # Check basic parsing, with the yaml output
+    (parse, ['-n', 'test/fixtures/cli/passing_b.sql', '-c', '-f', 'yaml']),
+    (parse, ['-n', 'test/fixtures/cli/passing_b.sql', '--format', 'yaml']),
+    # Check linting works in specifying rules
+    (lint, ['-n', '--rules', 'L001', 'test/fixtures/linter/operator_errors.sql']),
+    # Check linting works in specifying multiple rules
+    (lint, ['-n', '--rules', 'L001,L002', 'test/fixtures/linter/operator_errors.sql']),
+    # Check linting works with both included and excluded rules
+    (lint, ['-n', '--rules', 'L001,L006', '--exclude-rules', 'L006', 'test/fixtures/linter/operator_errors.sql']),
+    # Check linting works with just excluded rules
+    (lint, ['-n', '--exclude-rules', 'L006,L007', 'test/fixtures/linter/operator_errors.sql']),
+    # Check the script doesn't raise an unexpected exception with badly formed files.
+    (fix, ['--rules', 'L001', 'test/fixtures/cli/fail_many.sql', '-vvvvvvv'], 'y'),
+    # Check that ignoring works (also checks that unicode files parse).
+    (lint, ['-n', '--exclude-rules', 'L003,L009', '--ignore', 'parsing,lexing', 'test/fixtures/linter/parse_lex_error.sql']),
+])
+def test__cli__command_lint_parse(command):
+    """Check basic commands on a more complicated script."""
+    invoke_assert_code(args=command)
+
+
+def test__cli__command_versioning():
+    """Check version command."""
+    # Get the package version info
+    pkg_version = sqlfluff.__version__
+    # Get the version info from the config file
+    config = configparser.ConfigParser()
+    config.read_file(open('src/sqlfluff/config.ini'))
+    config_version = config['sqlfluff']['version']
+    assert pkg_version == config_version
+    # Get the version from the cli
+    runner = CliRunner()
+    result = runner.invoke(version)
+    assert result.exit_code == 0
+    # We need to strip to remove the newline characters
+    assert result.output.strip() == pkg_version
+
+
+def test__cli__command_version():
+    """Just check version command for exceptions."""
+    # Get the package version info
+    pkg_version = sqlfluff.__version__
+    runner = CliRunner()
+    result = runner.invoke(version)
+    assert result.exit_code == 0
+    assert pkg_version in result.output
+    # Check a verbose version
+    result = runner.invoke(version, ['-v'])
+    assert result.exit_code == 0
+    assert pkg_version in result.output
+
+
+def test__cli__command_rules():
+    """Just check rules command for exceptions."""
+    invoke_assert_code(args=[rules])
+
+
+def generic_roundtrip_test(source_file, rulestring, final_exit_code=0, force=True, fix_input=None, fix_exit_code=0):
+    """A test for roundtrip testing, take a file buffer, lint, fix and lint.
+
+    This is explicitly different from the linter version of this, in that
+    it uses the command line rather than the direct api.
+    """
+    filename = 'testing.sql'
+    # Lets get the path of a file to use
+    tempdir_path = tempfile.mkdtemp()
+    filepath = os.path.join(tempdir_path, filename)
+    # Open the example file and write the content to it
+    with open(filepath, mode='w') as dest_file:
+        for line in source_file:
+            dest_file.write(line)
+    # Check that we first detect the issue
+    invoke_assert_code(ret_code=65, args=[lint, ['--rules', rulestring, filepath]])
+    # Fix the file (in force mode)
+    if force:
+        fix_args = ['--rules', rulestring, '-f', filepath]
+    else:
+        fix_args = ['--rules', rulestring, filepath]
+    invoke_assert_code(ret_code=fix_exit_code, args=[fix, fix_args], cli_input=fix_input)
+    # Now lint the file and check for exceptions
+    invoke_assert_code(ret_code=final_exit_code, args=[lint, ['--rules', rulestring, filepath]])
+    shutil.rmtree(tempdir_path)
+
+
+@pytest.mark.parametrize('rule,fname', [
+    ('L001', 'test/fixtures/linter/indentation_errors.sql'),
+    ('L008', 'test/fixtures/linter/whitespace_errors.sql'),
+    ('L008', 'test/fixtures/linter/indentation_errors.sql'),
+    # Really stretching the ability of the fixer to re-indent a file
+    ('L003', 'test/fixtures/linter/indentation_error_hard.sql')
+])
+def test__cli__command__fix(rule, fname):
+    """Test the round trip of detecting, fixing and then not detecting the rule."""
+    with open(fname, mode='r') as test_file:
+        generic_roundtrip_test(test_file, rule)
+
+
+@pytest.mark.parametrize('rule,fname', [
+    # NB: L004 currently has no fix routine.
+    ('L004', 'test/fixtures/linter/indentation_errors.sql')
+])
+def test__cli__command__fix_fail(rule, fname):
+    """Test the round trip of detecting, fixing and then still detecting the rule."""
+    with open(fname, mode='r') as test_file:
+        generic_roundtrip_test(test_file, rule, fix_exit_code=1, final_exit_code=65)
+
+
+def test__cli__command_fix_stdin(monkeypatch):
+    """Check stdin input for fix works."""
+    sql = 'select * from tbl'
+    expected = 'fixed sql!'
+    monkeypatch.setattr("sqlfluff.linter.LintedFile.fix_string", lambda x: expected)
+    result = invoke_assert_code(args=[fix, ('-', '--rules', 'L001')], cli_input=sql)
+    assert result.output == expected
+
+
+@pytest.mark.parametrize('rule,fname,prompt,exit_code', [
+    ('L001', 'test/fixtures/linter/indentation_errors.sql', 'y', 0),
+    ('L001', 'test/fixtures/linter/indentation_errors.sql', 'n', 65)
+])
+def test__cli__command__fix_no_force(rule, fname, prompt, exit_code):
+    """Round trip test, using the prompts."""
+    with open(fname, mode='r') as test_file:
+        generic_roundtrip_test(
+            test_file, rule, force=False, final_exit_code=exit_code,
+            fix_input=prompt)
+
+
+@pytest.mark.parametrize('serialize', ['yaml', 'json'])
+def test__cli__command_parse_serialize_from_stdin(serialize):
+    """Check that the parser serialized output option is working.
+
+    Not going to test for the content of the output as that is subject to change.
+    """
+    result = invoke_assert_code(
+        args=[parse, ('-', '--format', serialize)],
+        cli_input='select * from tbl',
+    )
+    if serialize == 'json':
+        result = json.loads(result.output)
+    elif serialize == 'yaml':
+        result = yaml.load(result.output)
+    else:
+        raise Exception
+    result = result[0]  # only one file
+    assert result['filepath'] == 'stdin'
+
+
+@pytest.mark.parametrize('serialize', ['yaml', 'json'])
+@pytest.mark.parametrize('sql,expected,exit_code', [
+    ('select * from tbl', [], 0),  # empty list if no violations
+    (
+        'SElect * from tbl',
+        [{
+            "filepath": "stdin",
+            "violations": [
+                {
+                    "code": "L010",
+                    "line_no": 1,
+                    "line_pos": 1,
+                    "description": "Inconsistent capitalisation of keywords."
+                }
+            ]
+        }],
+        65
+    )
+])
+def test__cli__command_lint_serialize_from_stdin(serialize, sql, expected, exit_code):
+    """Check an explicit serialized return value for a single error."""
+    result = invoke_assert_code(
+        args=[lint, ('-', '--rules', 'L010', '--format', serialize)],
+        cli_input=sql,
+        ret_code=exit_code
+    )
+
+    if serialize == 'json':
+        assert json.loads(result.output) == expected
+    elif serialize == 'yaml':
+        assert yaml.load(result.output) == expected
+    else:
+        raise Exception
+
+
+@pytest.mark.parametrize('serialize', ['yaml', 'json'])
+def test__cli__command_lint_serialize_multiple_files(serialize):
+    """Check the general format of JSON output for multiple files."""
+    fpath = 'test/fixtures/linter/indentation_errors.sql'
+
+    # note the file is in here twice. two files = two payloads.
+    result = invoke_assert_code(
+        args=[lint, (fpath, fpath, '--format', serialize)],
+        ret_code=65,
+    )
+
+    if serialize == 'json':
+        result = json.loads(result.output)
+    elif serialize == 'yaml':
+        result = yaml.load(result.output)
+    else:
+        raise Exception
+    assert len(result) == 2
+
+
+def test___main___help():
+    """Test that the CLI can be access via __main__."""
+    # nonzero exit is good enough
+    subprocess.check_output(['python', '-m', 'sqlfluff', '--help'])