-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Markdown output format to the CLI
- Loading branch information
1 parent
c381c97
commit 109762e
Showing
5 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -285,6 +285,7 @@ public enum OutputFormat | |
CSV_UNQUOTED, | ||
CSV_HEADER_UNQUOTED, | ||
JSON, | ||
MARKDOWN, | ||
NULL | ||
} | ||
|
||
|
144 changes: 144 additions & 0 deletions
144
client/trino-cli/src/main/java/io/trino/cli/MarkdownTablePrinter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
183 changes: 183 additions & 0 deletions
183
client/trino-cli/src/test/java/io/trino/cli/TestMarkdownTablePrinter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters