Add ColumnFormatter/StringUtil classes for text output
The ColumnFormatter class will simplify output of column-oriented text for SSH commands like ls-groups and others (including HTTP counterparts) that may print user-supplied text that can contain tab and newline characters that would screw up scripts using these commands for automation. To deal with this, all text output is subject to C-style escaping of all non-printable characters. The escaping in done in the new StringUtil class. So far this new functionality is only used by the ls-projects command when the project description is printed. The escaping method used there previously only took care of newline characters. Change-Id: Ia605bbc44fc82d69d2f40b337319368aefd01eee
This commit is contained in:
@@ -46,8 +46,11 @@ OPTIONS
|
||||
Allows listing of projects together with their respective
|
||||
description.
|
||||
+
|
||||
Line-feeds are escaped to allow ls-project to keep the
|
||||
"one project per line"-style.
|
||||
For text format output, all non-printable characters (ASCII value 31 or
|
||||
less) are escaped according to the conventions used in languages like C,
|
||||
Python, and Perl, employing standard sequences like `\n` and `\t`, and
|
||||
`\xNN` for all others. In shell scripts, the `printf` command can be
|
||||
used to unescape the output.
|
||||
|
||||
--tree::
|
||||
-t::
|
||||
|
@@ -0,0 +1,54 @@
|
||||
// Copyright (C) 2012 The Android Open Source Project
|
||||
//
|
||||
// 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 com.google.gerrit.server;
|
||||
|
||||
public class StringUtil {
|
||||
/**
|
||||
* An array of the string representations that should be used in place
|
||||
* of the non-printable characters in the beginning of the ASCII table
|
||||
* when escaping a string. The index of each element in the array
|
||||
* corresponds to its ASCII value, i.e. the string representation of
|
||||
* ASCII 0 is found in the first element of this array.
|
||||
*/
|
||||
static String[] NON_PRINTABLE_CHARS =
|
||||
{ "\\x00", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\a",
|
||||
"\\b", "\\t", "\\n", "\\v", "\\f", "\\r", "\\x0e", "\\x0f",
|
||||
"\\x10", "\\x11", "\\x12", "\\x13", "\\x14", "\\x15", "\\x16", "\\x17",
|
||||
"\\x18", "\\x19", "\\x1a", "\\x1b", "\\x1c", "\\x1d", "\\x1e", "\\x1f" };
|
||||
|
||||
/**
|
||||
* Escapes the input string so that all non-printable characters
|
||||
* (0x00-0x1f) are represented as a hex escape (\x00, \x01, ...)
|
||||
* or as a C-style escape sequence (\a, \b, \t, \n, \v, \f, or \r).
|
||||
* Backslashes in the input string are doubled (\\).
|
||||
*/
|
||||
public static String escapeString(final String str) {
|
||||
// Allocate a buffer big enough to cover the case with a string needed
|
||||
// very excessive escaping without having to reallocate the buffer.
|
||||
final StringBuilder result = new StringBuilder(3 * str.length());
|
||||
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
char c = str.charAt(i);
|
||||
if (c < NON_PRINTABLE_CHARS.length) {
|
||||
result.append(NON_PRINTABLE_CHARS[c]);
|
||||
} else if (c == '\\') {
|
||||
result.append("\\\\");
|
||||
} else {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
// Copyright (C) 2012 The Android Open Source Project
|
||||
//
|
||||
// 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 com.google.gerrit.server.ioutil;
|
||||
|
||||
import com.google.gerrit.server.StringUtil;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* Simple output formatter for column-oriented data, writing its output to
|
||||
* a {@link java.io.PrintWriter} object. Handles escaping of the column
|
||||
* data so that the resulting output is unambiguous and reasonably safe and
|
||||
* machine parsable.
|
||||
*/
|
||||
public class ColumnFormatter {
|
||||
private char columnSeparator;
|
||||
private boolean firstColumn;
|
||||
private final PrintWriter out;
|
||||
|
||||
/**
|
||||
* @param out The writer to which output should be sent.
|
||||
* @param columnSeparator A character that should serve as the separator
|
||||
* token between columns of output. As only non-printable characters
|
||||
* in the column text are ever escaped, the column separator must be
|
||||
* a non-printable character if the output needs to be unambiguously
|
||||
* parsed.
|
||||
*/
|
||||
public ColumnFormatter(final PrintWriter out, final char columnSeparator) {
|
||||
this.out = out;
|
||||
this.columnSeparator = columnSeparator;
|
||||
this.firstColumn = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a text string as a new column in the current line of output,
|
||||
* taking care of escaping as necessary.
|
||||
*
|
||||
* @param content the string to add.
|
||||
*/
|
||||
public void addColumn(final String content) {
|
||||
if (!firstColumn) {
|
||||
out.print(columnSeparator);
|
||||
}
|
||||
out.print(StringUtil.escapeString(content));
|
||||
firstColumn = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes the output by flushing the current line and takes care of any
|
||||
* other cleanup action.
|
||||
*/
|
||||
public void finish() {
|
||||
nextLine();
|
||||
out.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the current line of output and makes the formatter ready to
|
||||
* start receiving new column data for a new line (or end-of-file).
|
||||
* If the current line is empty nothing is done, i.e. consecutive calls
|
||||
* to this method without intervening calls to {@link #addColumn} will
|
||||
* be squashed.
|
||||
*/
|
||||
public void nextLine() {
|
||||
if (!firstColumn) {
|
||||
out.print('\n');
|
||||
firstColumn = true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -19,6 +19,7 @@ import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.reviewdb.client.Project.NameKey;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.OutputFormat;
|
||||
import com.google.gerrit.server.StringUtil;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.util.TreeFormatter;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
@@ -269,7 +270,7 @@ public class ListProjects {
|
||||
|
||||
if (info.description != null) {
|
||||
// We still want to list every project as one-liners, hence escaping \n.
|
||||
stdout.print(" - " + info.description.replace("\n", "\\n"));
|
||||
stdout.print(" - " + StringUtil.escapeString(info.description));
|
||||
}
|
||||
stdout.print('\n');
|
||||
}
|
||||
|
@@ -0,0 +1,54 @@
|
||||
// Copyright (C) 2012 The Android Open Source Project
|
||||
//
|
||||
// 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 com.google.gerrit.server;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class StringUtilTest extends TestCase {
|
||||
/**
|
||||
* Test the boundary condition that the first character of a string
|
||||
* should be escaped.
|
||||
*/
|
||||
public void testEscapeFirstChar() {
|
||||
assertEquals(StringUtil.escapeString("\tLeading tab"), "\\tLeading tab");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the boundary condition that the last character of a string
|
||||
* should be escaped.
|
||||
*/
|
||||
public void testEscapeLastChar() {
|
||||
assertEquals(StringUtil.escapeString("Trailing tab\t"), "Trailing tab\\t");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that various forms of input strings are escaped (or left as-is)
|
||||
* in the expected way.
|
||||
*/
|
||||
public void testEscapeString() {
|
||||
final String[] testPairs =
|
||||
{ "", "",
|
||||
"plain string", "plain string",
|
||||
"string with \"quotes\"", "string with \"quotes\"",
|
||||
"string with 'quotes'", "string with 'quotes'",
|
||||
"string with 'quotes'", "string with 'quotes'",
|
||||
"C:\\Program Files\\MyProgram", "C:\\\\Program Files\\\\MyProgram",
|
||||
"string\nwith\nnewlines", "string\\nwith\\nnewlines",
|
||||
"string\twith\ttabs", "string\\twith\\ttabs" };
|
||||
for (int i = 0; i < testPairs.length; i += 2) {
|
||||
assertEquals(StringUtil.escapeString(testPairs[i]), testPairs[i + 1]);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,138 @@
|
||||
// Copyright (C) 2012 The Android Open Source Project
|
||||
//
|
||||
// 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 com.google.gerrit.server.ioutil;
|
||||
|
||||
import com.google.gerrit.server.ioutil.ColumnFormatter;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
public class ColumnFormatterTest extends TestCase {
|
||||
/**
|
||||
* Holds an in-memory {@link java.io.PrintWriter} object and allows
|
||||
* comparisons of its contents to a supplied string via an assert statement.
|
||||
*/
|
||||
class PrintWriterComparator {
|
||||
private PrintWriter printWriter;
|
||||
private StringWriter stringWriter;
|
||||
|
||||
public PrintWriterComparator() {
|
||||
stringWriter = new StringWriter();
|
||||
printWriter = new PrintWriter(stringWriter);
|
||||
}
|
||||
|
||||
public void assertEquals(String str) {
|
||||
printWriter.flush();
|
||||
TestCase.assertEquals(stringWriter.toString(), str);
|
||||
}
|
||||
|
||||
public PrintWriter getPrintWriter() {
|
||||
return printWriter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only lines with at least one column of text emit output.
|
||||
*/
|
||||
public void testEmptyLine() {
|
||||
final PrintWriterComparator comparator = new PrintWriterComparator();
|
||||
final ColumnFormatter formatter =
|
||||
new ColumnFormatter(comparator.getPrintWriter(), '\t');
|
||||
formatter.addColumn("foo");
|
||||
formatter.addColumn("bar");
|
||||
formatter.nextLine();
|
||||
formatter.nextLine();
|
||||
formatter.nextLine();
|
||||
formatter.addColumn("foo");
|
||||
formatter.addColumn("bar");
|
||||
formatter.finish();
|
||||
comparator.assertEquals("foo\tbar\nfoo\tbar\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that there is no output if no columns are ever added.
|
||||
*/
|
||||
public void testEmptyOutput() {
|
||||
final PrintWriterComparator comparator = new PrintWriterComparator();
|
||||
final ColumnFormatter formatter =
|
||||
new ColumnFormatter(comparator.getPrintWriter(), '\t');
|
||||
formatter.nextLine();
|
||||
formatter.nextLine();
|
||||
formatter.finish();
|
||||
comparator.assertEquals("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that there is no output (nor any exceptions) if we finalize
|
||||
* the output immediately after the creation of the {@link ColumnFormatter}.
|
||||
*/
|
||||
public void testNoNextLine() {
|
||||
final PrintWriterComparator comparator = new PrintWriterComparator();
|
||||
final ColumnFormatter formatter =
|
||||
new ColumnFormatter(comparator.getPrintWriter(), '\t');
|
||||
formatter.finish();
|
||||
comparator.assertEquals("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the text in added columns is escaped while the column separator
|
||||
* (which of course shouldn't be escaped) is left alone.
|
||||
*/
|
||||
public void testEscapingTakesPlace() {
|
||||
final PrintWriterComparator comparator = new PrintWriterComparator();
|
||||
final ColumnFormatter formatter =
|
||||
new ColumnFormatter(comparator.getPrintWriter(), '\t');
|
||||
formatter.addColumn("foo");
|
||||
formatter.addColumn(
|
||||
"\tan indented multi-line\ntext");
|
||||
formatter.nextLine();
|
||||
formatter.finish();
|
||||
comparator.assertEquals("foo\t\\tan indented multi-line\\ntext\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that we get the correct output with multi-line input where the number
|
||||
* of columns in each line varies.
|
||||
*/
|
||||
public void testMultiLineDifferentColumnCount() {
|
||||
final PrintWriterComparator comparator = new PrintWriterComparator();
|
||||
final ColumnFormatter formatter =
|
||||
new ColumnFormatter(comparator.getPrintWriter(), '\t');
|
||||
formatter.addColumn("foo");
|
||||
formatter.addColumn("bar");
|
||||
formatter.addColumn("baz");
|
||||
formatter.nextLine();
|
||||
formatter.addColumn("foo");
|
||||
formatter.addColumn("bar");
|
||||
formatter.nextLine();
|
||||
formatter.finish();
|
||||
comparator.assertEquals("foo\tbar\tbaz\nfoo\tbar\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that we get the correct output with a single column of input.
|
||||
*/
|
||||
public void testOneColumn() {
|
||||
final PrintWriterComparator comparator = new PrintWriterComparator();
|
||||
final ColumnFormatter formatter =
|
||||
new ColumnFormatter(comparator.getPrintWriter(), '\t');
|
||||
formatter.addColumn("foo");
|
||||
formatter.nextLine();
|
||||
formatter.finish();
|
||||
comparator.assertEquals("foo\n");
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user