Skip to content

Commit

Permalink
Add Markdown output format to the CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
nineinchnick committed Jul 21, 2023
1 parent c381c97 commit 109762e
Show file tree
Hide file tree
Showing 5 changed files with 332 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ public enum OutputFormat
CSV_UNQUOTED,
CSV_HEADER_UNQUOTED,
JSON,
MARKDOWN,
NULL
}

Expand Down
144 changes: 144 additions & 0 deletions client/trino-cli/src/main/java/io/trino/cli/MarkdownTablePrinter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.cli;

import com.google.common.collect.ImmutableSet;
import io.trino.client.Column;

import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Set;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.repeat;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static io.trino.client.ClientStandardTypes.BIGINT;
import static io.trino.client.ClientStandardTypes.DECIMAL;
import static io.trino.client.ClientStandardTypes.DOUBLE;
import static io.trino.client.ClientStandardTypes.INTEGER;
import static io.trino.client.ClientStandardTypes.REAL;
import static io.trino.client.ClientStandardTypes.SMALLINT;
import static io.trino.client.ClientStandardTypes.TINYINT;
import static java.lang.Math.max;
import static java.util.Objects.requireNonNull;
import static org.jline.utils.AttributedString.stripAnsi;
import static org.jline.utils.WCWidth.wcwidth;

public class MarkdownTablePrinter
implements OutputPrinter
{
private static final Set<String> NUMERIC_TYPES = ImmutableSet.of(TINYINT, SMALLINT, INTEGER, BIGINT, REAL, DOUBLE, DECIMAL);
private final List<String> fieldNames;
private final List<Align> alignments;
private final Writer writer;

private boolean headerOutput;

public MarkdownTablePrinter(List<Column> columns, Writer writer)
{
requireNonNull(columns, "columns is null");
this.fieldNames = columns.stream()
.map(Column::getName)
.collect(toImmutableList());
this.alignments = columns.stream()
.map(Column::getTypeSignature)
.map(signature -> NUMERIC_TYPES.contains(signature.getRawType()) ? Align.RIGHT : Align.LEFT)
.collect(toImmutableList());
this.writer = requireNonNull(writer, "writer is null");
}

private enum Align
{
LEFT,
RIGHT;
}

@Override
public void printRows(List<List<?>> rows, boolean complete)
throws IOException
{
int columns = fieldNames.size();

int[] maxWidth = new int[columns];
for (int i = 0; i < columns; i++) {
maxWidth[i] = max(1, consoleWidth(fieldNames.get(i)));
}
for (List<?> row : rows) {
for (int i = 0; i < row.size(); i++) {
String s = formatValue(row.get(i));
maxWidth[i] = max(maxWidth[i], consoleWidth(s));
}
}

if (!headerOutput) {
headerOutput = true;

for (int i = 0; i < columns; i++) {
writer.append('|');
writer.append(align(fieldNames.get(i), maxWidth[i], alignments.get(i)));
}
writer.append("|\n");

for (int i = 0; i < columns; i++) {
writer.append("| ");
writer.append(repeat("-", maxWidth[i]));
writer.write(alignments.get(i) == Align.RIGHT ? ':' : ' ');
}
writer.append("|\n");
}

for (List<?> row : rows) {
for (int column = 0; column < columns; column++) {
writer.append('|');
writer.append(align(formatValue(row.get(column)), maxWidth[column], alignments.get(column)));
}
writer.append("|\n");
}
writer.flush();
}

static String formatValue(Object o)
{
return FormatUtils.formatValue(o)
.replaceAll("([\\\\`*_{}\\[\\]<>()#+!|])", "\\\\$1")
.replace("\n", "<br>");
}

@Override
public void finish()
throws IOException
{
writer.flush();
}

private static String align(String value, int maxWidth, Align align)
{
int width = consoleWidth(value);
checkState(width <= maxWidth, "Variable width %s is greater than column width %s", width, maxWidth);
String large = repeat(" ", (maxWidth - width) + 1);
String small = repeat(" ", 1);
return align == Align.RIGHT ? (large + value + small) : (small + value + large);
}

static int consoleWidth(String value)
{
CharSequence plain = stripAnsi(value);
int n = 0;
for (int i = 0; i < plain.length(); i++) {
n += max(wcwidth(plain.charAt(i)), 0);
}
return n;
}
}
2 changes: 2 additions & 0 deletions client/trino-cli/src/main/java/io/trino/cli/Query.java
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,8 @@ private static OutputPrinter createOutputPrinter(OutputFormat format, int maxWid
return new TsvPrinter(fieldNames, writer, true);
case JSON:
return new JsonPrinter(fieldNames, writer);
case MARKDOWN:
return new MarkdownTablePrinter(columns, writer);
case NULL:
return new NullPrinter();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.cli;

import com.google.common.collect.ImmutableList;
import io.trino.client.ClientTypeSignature;
import io.trino.client.Column;
import org.testng.annotations.Test;

import java.io.StringWriter;
import java.util.List;

import static io.trino.client.ClientStandardTypes.BIGINT;
import static io.trino.client.ClientStandardTypes.VARBINARY;
import static io.trino.client.ClientStandardTypes.VARCHAR;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static org.testng.Assert.assertEquals;

public class TestMarkdownTablePrinter
{
@Test
public void testMarkdownPrinting()
throws Exception
{
List<Column> columns = ImmutableList.<Column>builder()
.add(column("first", VARCHAR))
.add(column("last", VARCHAR))
.add(column("quantity", BIGINT))
.build();
StringWriter writer = new StringWriter();
OutputPrinter printer = new MarkdownTablePrinter(columns, writer);

printer.printRows(rows(
row("hello", "world", 123),
row("a", null, 4.5),
row("b", null, null),
row("some long\ntext that\ndoes not\nfit on\none line", "more\ntext", 4567),
row("bye | not **& <a>**", "done", -15)),
true);
printer.finish();

String expected = "" +
"| first | last | quantity |\n" +
"| -------------------------------------------------------- | ------------ | --------:|\n" +
"| hello | world | 123 |\n" +
"| a | NULL | 4.5 |\n" +
"| b | NULL | NULL |\n" +
"| some long<br>text that<br>does not<br>fit on<br>one line | more<br>text | 4567 |\n" +
"| bye \\| not \\*\\*& \\<a\\>\\*\\* | done | -15 |\n";

assertEquals(writer.getBuffer().toString(), expected);
}

@Test
public void testMarkdownPrintingOneRow()
throws Exception
{
List<Column> columns = ImmutableList.<Column>builder()
.add(column("first", VARCHAR))
.add(column("last", VARCHAR))
.build();
StringWriter writer = new StringWriter();
OutputPrinter printer = new MarkdownTablePrinter(columns, writer);

printer.printRows(rows(row("a long line\nwithout wrapping", "text")), true);
printer.finish();

String expected = "" +
"| first | last |\n" +
"| ------------------------------- | ---- |\n" +
"| a long line<br>without wrapping | text |\n";

assertEquals(writer.getBuffer().toString(), expected);
}

@Test
public void testMarkdownPrintingNoRows()
throws Exception
{
List<Column> columns = ImmutableList.<Column>builder()
.add(column("first", VARCHAR))
.add(column("last", VARCHAR))
.build();
StringWriter writer = new StringWriter();
OutputPrinter printer = new MarkdownTablePrinter(columns, writer);

printer.finish();

String expected = "";

assertEquals(writer.getBuffer().toString(), expected);
}

@Test
public void testMarkdownPrintingHex()
throws Exception
{
List<Column> columns = ImmutableList.<Column>builder()
.add(column("first", VARCHAR))
.add(column("binary", VARBINARY))
.add(column("last", VARCHAR))
.build();
StringWriter writer = new StringWriter();
OutputPrinter printer = new MarkdownTablePrinter(columns, writer);

printer.printRows(rows(
row("hello", bytes("hello"), "world"),
row("a", bytes("some long text that is more than 16 bytes"), "b"),
row("cat", bytes(""), "dog")),
true);
printer.finish();

String expected = "" +
"| first | binary | last |\n" +
"| ----- | -------------------------------------------------------------------------------------------------------------------------------- | ----- |\n" +
"| hello | 68 65 6c 6c 6f | world |\n" +
"| a | 73 6f 6d 65 20 6c 6f 6e 67 20 74 65 78 74 20 74<br>68 61 74 20 69 73 20 6d 6f 72 65 20 74 68 61 6e<br>20 31 36 20 62 79 74 65 73 | b |\n" +
"| cat | | dog |\n";

assertEquals(writer.getBuffer().toString(), expected);
}

@Test
public void testMarkdownPrintingWideCharacters()
throws Exception
{
List<Column> columns = ImmutableList.<Column>builder()
.add(column("go\u7f51", VARCHAR))
.add(column("last", VARCHAR))
.add(column("quantity\u7f51", BIGINT))
.build();
StringWriter writer = new StringWriter();
OutputPrinter printer = new MarkdownTablePrinter(columns, writer);

printer.printRows(rows(
row("hello", "wide\u7f51", 123),
row("some long\ntext \u7f51\ndoes not\u7f51\nfit", "more\ntext", 4567),
row("bye", "done", -15)),
true);
printer.finish();

String expected = "" +
"| go\u7f51 | last | quantity\u7f51 |\n" +
"| ----------------------------------------- | ------------ | ----------:|\n" +
"| hello | wide\u7f51 | 123 |\n" +
"| some long<br>text \u7f51<br>does not\u7f51<br>fit | more<br>text | 4567 |\n" +
"| bye | done | -15 |\n";

assertEquals(writer.getBuffer().toString(), expected);
}

static Column column(String name, String type)
{
return new Column(name, type, new ClientTypeSignature(type));
}

static List<?> row(Object... values)
{
return asList(values);
}

static List<List<?>> rows(List<?>... rows)
{
return asList(rows);
}

static byte[] bytes(String s)
{
return s.getBytes(UTF_8);
}
}
2 changes: 2 additions & 0 deletions docs/src/main/sphinx/client/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,8 @@ and `CSV` in non-interactive mode.
* - ``AUTO``
- Same as ``ALIGNED`` if output would fit the current terminal width,
and ``VERTICAL`` otherwise.
* - ``MARKDOWN``
- Output emitted as a Markdown table.
* - ``NULL``
- Suppresses normal query results. This can be useful during development
to test a query's shell return code or to see whether it results in
Expand Down

0 comments on commit 109762e

Please sign in to comment.