From afee15f04b060a557897fa876f6e5d37f6d898cb Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 6 Nov 2020 16:09:42 -0800 Subject: [PATCH] --tsv output option, closes #193 --- docs/cli.rst | 20 +++++++++++----- sqlite_utils/cli.py | 17 ++++++++++---- tests/test_cli.py | 56 ++++++++++++++++++++++++++++++--------------- 3 files changed, 65 insertions(+), 28 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 03676d4..166eea1 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -122,14 +122,14 @@ You can use the ``--json-cols`` option to automatically detect these JSON column } ] } - ] + ]- .. _cli_query_csv: Running queries and returning CSV ================================= -You can use the ``--csv`` option (or ``-c`` shortcut) to return results as CSV:: +You can use the ``--csv`` option to return results as CSV:: $ sqlite-utils dogs.db "select * from dogs" --csv id,age,name @@ -142,6 +142,13 @@ This will default to including the column names as a header row. To exclude the 1,4,Cleo 2,2,Pancakes +Use ``--tsv`` instead of ``--csv`` to get back tab-separated values:: + + $ sqlite-utils dogs.db "select * from dogs" --tsv + id age name + 1 4 Cleo + 2 2 Pancakes + .. _cli_query_table: Running queries and outputting a table @@ -189,7 +196,7 @@ You can return every row in a specified table using the ``rows`` command:: [{"id": 1, "age": 4, "name": "Cleo"}, {"id": 2, "age": 2, "name": "Pancakes"}] -This command accepts the same output options as ``query`` - so you can pass ``--nl``, ``--csv``, ``--no-headers``, ``--table`` and ``--fmt``. +This command accepts the same output options as ``query`` - so you can pass ``--nl``, ``--csv``, ``--tsv``, ``--no-headers``, ``--table`` and ``--fmt``. .. _cli_tables: @@ -203,7 +210,7 @@ You can list the names of tables in a database using the ``tables`` command:: {"table": "cats"}, {"table": "chickens"}] -You can output this list in CSV using the ``--csv`` option:: +You can output this list in CSV using the ``--csv`` or ``--tsv`` options:: $ sqlite-utils tables mydb.db --csv --no-headers dogs @@ -241,7 +248,7 @@ Use ``--schema`` to include the schema of each table:: [age] INTEGER, [name] TEXT) -The ``--nl``, ``--csv`` and ``--table`` options are all available. +The ``--nl``, ``--csv``, ``--tsv`` and ``--table`` options are all available. .. _cli_views: @@ -263,6 +270,7 @@ It takes the same options as the ``tables`` command: * ``--counts`` * ``--nl`` * ``--csv`` +* ``--tsv`` * ``--table`` .. _cli_inserting_data: @@ -894,7 +902,7 @@ Once you have configured full-text search for a table, you can search it using ` $ sqlite-utils search mydb.db documents searchterm -This command accepts the same output options as ``sqlite-utils query``: ``--table``, ``--csv``, ``--nl`` etc. +This command accepts the same output options as ``sqlite-utils query``: ``--table``, ``--csv``, ``--tsv``, ``--nl`` etc. By default it shows the most relevant matches first. You can specify a different sort order using the ``-o`` option, which can take a column or a column followed by ``desc``:: diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 37c44f7..18247eb 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -46,6 +46,7 @@ def output_options(fn): default=False, ), click.option("--csv", is_flag=True, help="Output CSV"), + click.option("--tsv", is_flag=True, help="Output TSV"), click.option("--no-headers", is_flag=True, help="Omit CSV headers"), click.option("-t", "--table", is_flag=True, help="Output as a table"), click.option( @@ -120,6 +121,7 @@ def tables( nl, arrays, csv, + tsv, no_headers, table, fmt, @@ -161,8 +163,8 @@ def tables( if table: print(tabulate.tabulate(_iter(), headers=headers, tablefmt=fmt)) - elif csv: - writer = csv_std.writer(sys.stdout) + elif csv or tsv: + writer = csv_std.writer(sys.stdout, dialect="excel-tab" if tsv else "excel") if not no_headers: writer.writerow(headers) for row in _iter(): @@ -201,6 +203,7 @@ def views( nl, arrays, csv, + tsv, no_headers, table, fmt, @@ -218,6 +221,7 @@ def views( nl=nl, arrays=arrays, csv=csv, + tsv=tsv, no_headers=no_headers, table=table, fmt=fmt, @@ -930,6 +934,7 @@ def query( nl, arrays, csv, + tsv, no_headers, table, fmt, @@ -958,8 +963,8 @@ def query( sys.stdout.write(str(data)) elif table: print(tabulate.tabulate(list(cursor), headers=headers, tablefmt=fmt)) - elif csv: - writer = csv_std.writer(sys.stdout) + elif csv or tsv: + writer = csv_std.writer(sys.stdout, dialect="excel-tab" if tsv else "excel") if not no_headers: writer.writerow(headers) for row in cursor: @@ -1003,6 +1008,7 @@ def search( nl, arrays, csv, + tsv, no_headers, table, fmt, @@ -1039,6 +1045,7 @@ def search( nl=nl, arrays=arrays, csv=csv, + tsv=tsv, no_headers=no_headers, table=table, fmt=fmt, @@ -1065,6 +1072,7 @@ def rows( nl, arrays, csv, + tsv, no_headers, table, fmt, @@ -1079,6 +1087,7 @@ def rows( nl=nl, arrays=arrays, csv=csv, + tsv=tsv, no_headers=no_headers, table=table, fmt=fmt, diff --git a/tests/test_cli.py b/tests/test_cli.py index 69aef64..db31121 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -62,24 +62,37 @@ def test_tables_counts_and_columns(db_path): ) == result.output.strip() -def test_tables_counts_and_columns_csv(db_path): +@pytest.mark.parametrize( + "format,expected", + [ + ( + "--csv", + ( + "table,count,columns\n" + 'Gosh,0,"c1\n' + "c2\n" + 'c3"\n' + 'Gosh2,0,"c1\n' + "c2\n" + 'c3"\n' + 'lots,30,"id\n' + 'age"' + ), + ), + ( + "--tsv", + "table\tcount\tcolumns\nGosh\t0\t['c1', 'c2', 'c3']\nGosh2\t0\t['c1', 'c2', 'c3']\nlots\t30\t['id', 'age']", + ), + ], +) +def test_tables_counts_and_columns_csv(db_path, format, expected): db = Database(db_path) with db.conn: db["lots"].insert_all([{"id": i, "age": i + 1} for i in range(30)]) result = CliRunner().invoke( - cli.cli, ["tables", "--counts", "--columns", "--csv", db_path] + cli.cli, ["tables", "--counts", "--columns", format, db_path] ) - assert ( - "table,count,columns\n" - 'Gosh,0,"c1\n' - "c2\n" - 'c3"\n' - 'Gosh2,0,"c1\n' - "c2\n" - 'c3"\n' - 'lots,30,"id\n' - 'age"' - ) == result.output.strip() + assert result.output.strip() == expected def test_tables_schema(db_path): @@ -809,7 +822,14 @@ def test_insert_alter(db_path, tmpdir): ] == db.execute_returning_dicts("select foo, n, baz from from_json_nl") -def test_query_csv(db_path): +@pytest.mark.parametrize( + "format,expected", + [ + ("--csv", "id,name,age\n1,Cleo,4\n2,Pancakes,2\n"), + ("--tsv", "id\tname\tage\n1\tCleo\t4\n2\tPancakes\t2\n"), + ], +) +def test_query_csv(db_path, format, expected): db = Database(db_path) with db.conn: db["dogs"].insert_all( @@ -819,15 +839,15 @@ def test_query_csv(db_path): ] ) result = CliRunner().invoke( - cli.cli, [db_path, "select id, name, age from dogs", "--csv"] + cli.cli, [db_path, "select id, name, age from dogs", format] ) assert 0 == result.exit_code - assert "id,name,age\n1,Cleo,4\n2,Pancakes,2\n" == result.output + assert result.output == expected # Test the no-headers option: result = CliRunner().invoke( - cli.cli, [db_path, "select id, name, age from dogs", "--no-headers", "--csv"] + cli.cli, [db_path, "select id, name, age from dogs", "--no-headers", format] ) - assert "1,Cleo,4\n2,Pancakes,2\n" == result.output + assert result.output.strip() == "\n".join(expected.split("\n")[1:]).strip() _all_query = "select id, name, age from dogs"