Skip to content

Commit

Permalink
Add back support for --no-color and --force-color
Browse files Browse the repository at this point in the history
Per request, adding back support for these arguments. This was
achieved by separating the ColorFormatter from the CliPrinter.
Because CLI arguments aren't fully parsed when bootstrapping the
CLI and there needs to be a resolved ColorFormatter to create an
Environment to give to a comment, I added a ColorFormatter interface
that is implemented by AnsiColorFormatter (formally Ansi). The CLI
will create an instance of a ColorFormatter that defers whether color
is supported or not by creating a custom ColorFormatter that queries
StandardOptions#colorSetting before each write.
  • Loading branch information
mtdowling committed Dec 7, 2022
1 parent fbb1f61 commit d521dab
Show file tree
Hide file tree
Showing 21 changed files with 430 additions and 282 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,7 @@
/**
* Styles text using ANSI color codes.
*/
public enum Ansi {

/**
* Writes using ANSI colors if it detects that the environment supports color.
*/
AUTO {
private final Ansi delegate = Ansi.detect();

@Override
public String style(String text, Style... styles) {
return delegate.style(text, styles);
}

@Override
public void style(Appendable appendable, String text, Style... styles) {
delegate.style(appendable, text, styles);
}
},
public enum AnsiColorFormatter implements ColorFormatter {

/**
* Does not write any color.
Expand Down Expand Up @@ -74,54 +57,57 @@ public String style(String text, Style... styles) {
@Override
public void style(Appendable appendable, String text, Style... styles) {
try {
appendable.append("\033[");
boolean isAfterFirst = false;
for (Style style : styles) {
if (isAfterFirst) {
appendable.append(';');
if (styles.length == 0) {
appendable.append(text);
} else {
appendable.append("\033[");
boolean isAfterFirst = false;
for (Style style : styles) {
if (isAfterFirst) {
appendable.append(';');
}
appendable.append(style.getAnsiColorCode());
isAfterFirst = true;
}
appendable.append(style.toString());
isAfterFirst = true;
appendable.append('m');
appendable.append(text);
appendable.append("\033[0m");
}
appendable.append('m');
appendable.append(text);
appendable.append("\033[0m");
} catch (IOException e) {
throw new CliError("Error writing output", 2, e);
}
}
},

/**
* Writes using ANSI colors if it detects that the environment supports color.
*/
AUTO {
private final AnsiColorFormatter delegate = AnsiColorFormatter.detect();

@Override
public String style(String text, Style... styles) {
return delegate.style(text, styles);
}

@Override
public void style(Appendable appendable, String text, Style... styles) {
delegate.style(appendable, text, styles);
}
};

/**
* Detects if ANSI colors are supported and returns the appropriate Ansi enum variant.
*
* <p>This method differs from using the {@link Ansi#AUTO} variant directly because it will detect any changes
* to the environment that might enable or disable colors.
* <p>This method differs from using the {@link AnsiColorFormatter#AUTO} variant directly because it will
* detect any changes to the environment that might enable or disable colors.
*
* @return Returns the detected ANSI color enum variant.
*/
public static Ansi detect() {
public static AnsiColorFormatter detect() {
return isAnsiEnabled() ? FORCE_COLOR : NO_COLOR;
}

/**
* Styles text using ANSI color codes.
*
* @param text Text to style.
* @param styles Styles to apply.
* @return Returns the styled text.
*/
public abstract String style(String text, Style... styles);

/**
* Styles text using ANSI color codes and writes it to an Appendable.
*
* @param appendable Where to write styled text.
* @param text Text to write.
* @param styles Styles to apply.
*/
public abstract void style(Appendable appendable, String text, Style... styles);

private static boolean isAnsiEnabled() {
if (EnvironmentVariable.FORCE_COLOR.isSet()) {
return true;
Expand Down
50 changes: 37 additions & 13 deletions smithy-cli/src/main/java/software/amazon/smithy/cli/Cli.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.function.Supplier;
import java.util.logging.Logger;

/**
Expand All @@ -33,10 +34,12 @@ public final class Cli {

private static final Logger LOGGER = Logger.getLogger(Cli.class.getName());

private ColorFormatter colorFormatter;
private CliPrinter stdoutPrinter;
private CliPrinter stderrPrinter;
private final ClassLoader classLoader;
private final Command command;
private final StandardOptions standardOptions = new StandardOptions();

/**
* Creates a new CLI with the given name.
Expand All @@ -58,28 +61,31 @@ public Cli(Command command, ClassLoader classLoader) {
*/
public int run(String[] args) {
Arguments arguments = new Arguments(args);
StandardOptions standardOptions = new StandardOptions();
arguments.addReceiver(standardOptions);

if (stdoutPrinter == null || stderrPrinter == null) {
Ansi ansi = Ansi.detect();
if (stdoutPrinter == null) {
stdout(CliPrinter.fromOutputStream(ansi, System.out));
}
if (stderrPrinter == null) {
stderr(CliPrinter.fromOutputStream(ansi, System.err));
}
if (colorFormatter == null) {
// CLI arguments haven't been parsed yet, so the CLI doesn't know if --force-color or --no-color
// was passed. Defer the color setting implementation by asking StandardOptions before each write.
colorFormatter(createDelegatedColorFormatter(standardOptions::colorSetting));
}

if (stdoutPrinter == null) {
stdout(CliPrinter.fromOutputStream(System.out));
}

if (stderrPrinter == null) {
stderr(CliPrinter.fromOutputStream(System.err));
}

// Setup logging after parsing all arguments.
arguments.onComplete((opts, positional) -> {
LoggingUtil.configureLogging(opts.getReceiver(StandardOptions.class), stderrPrinter);
LoggingUtil.configureLogging(opts.getReceiver(StandardOptions.class), colorFormatter, stderrPrinter);
LOGGER.fine(() -> "Running CLI command: " + Arrays.toString(args));
});

try {
try {
Command.Env env = new Command.Env(stdoutPrinter, stderrPrinter, classLoader);
Command.Env env = new Command.Env(colorFormatter, stdoutPrinter, stderrPrinter, classLoader);
return command.execute(arguments, env);
} catch (Exception e) {
printException(e, standardOptions.stackTrace());
Expand All @@ -98,6 +104,10 @@ public int run(String[] args) {
}
}

public void colorFormatter(ColorFormatter colorFormatter) {
this.colorFormatter = colorFormatter;
}

public void stdout(CliPrinter stdoutPrinter) {
this.stdoutPrinter = stdoutPrinter;
}
Expand All @@ -108,9 +118,9 @@ public void stderr(CliPrinter stderrPrinter) {

private void printException(Throwable e, boolean stacktrace) {
if (!stacktrace) {
stderrPrinter.println(e.getMessage(), Style.RED);
colorFormatter.println(stderrPrinter, e.getMessage(), Style.RED);
} else {
try (CliPrinter.Buffer buffer = stderrPrinter.buffer()) {
try (ColorFormatter.PrinterBuffer buffer = colorFormatter.printerBuffer(stderrPrinter)) {
StringWriter writer = new StringWriter();
e.printStackTrace(new PrintWriter(writer));
String result = writer.toString();
Expand All @@ -120,4 +130,18 @@ private void printException(Throwable e, boolean stacktrace) {
}
}
}

private static ColorFormatter createDelegatedColorFormatter(Supplier<ColorFormatter> delegateSupplier) {
return new ColorFormatter() {
@Override
public String style(String text, Style... styles) {
return delegateSupplier.get().style(text, styles);
}

@Override
public void style(Appendable appendable, String text, Style... styles) {
delegateSupplier.get().style(appendable, text, styles);
}
};
}
}
141 changes: 15 additions & 126 deletions smithy-cli/src/main/java/software/amazon/smithy/cli/CliPrinter.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,31 @@
@FunctionalInterface
public interface CliPrinter extends Flushable {

/**
* Prints text to the writer and appends a new line.
*
* @param text Text to print.
*/
void println(String text);

/**
* Flushes any buffers in the printer.
*/
default void flush() {}

/**
* Create a new CliPrinter from a PrintWriter.
*
* @param ansi Ansi color settings to use.
* @param printWriter PrintWriter to write to.
* @return Returns the created CliPrinter.
*/
static CliPrinter fromPrintWriter(Ansi ansi, PrintWriter printWriter) {
static CliPrinter fromPrintWriter(PrintWriter printWriter) {
return new CliPrinter() {
@Override
public void println(String text) {
printWriter.println(text);
}

@Override
public Ansi ansi() {
return ansi;
}

@Override
public void flush() {
printWriter.flush();
Expand All @@ -58,129 +64,12 @@ public void flush() {
/**
* Create a new CliPrinter from an OutputStream.
*
* @param ansi Ansi color settings to use.
* @param stream OutputStream to write to.
* @return Returns the created CliPrinter.
*/
static CliPrinter fromOutputStream(Ansi ansi, OutputStream stream) {
static CliPrinter fromOutputStream(OutputStream stream) {
Charset charset = StandardCharsets.UTF_8;
PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(stream, charset)), false);
return fromPrintWriter(ansi, writer);
}

/**
* Prints text to the writer and appends a new line.
*
* @param text Text to print.
*/
void println(String text);

/**
* Prints a styled line of text using ANSI colors.
*
* @param text Text to style and write.
* @param styles Styles to apply.
*/
default void println(String text, Style... styles) {
println(ansi().style(text, styles));
}

/**
* Flushes any buffers in the printer.
*/
default void flush() {}

/**
* Gets the ANSI color style setting used by the printer.
*
* @return Returns the ANSI color style.
*/
default Ansi ansi() {
return Ansi.AUTO;
}

/**
* Creates a {@link Buffer} used to build up a long string of text.
*
* @return Returns the buffer. Call {@link Buffer#close()} or use try-with-resources to write to the printer.
*/
default Buffer buffer() {
return new Buffer(this);
}

/**
* A buffer associated with a {@link CliPrinter} used to build up a string of ANSI color stylized text.
*
* <p>This class is not thread safe; it's a convenient way to build up a long string of text that uses ANSI styles
* before ultimately calling {@link #close()}, which writes it to the attached printer.
*/
final class Buffer implements Appendable, AutoCloseable {
private final CliPrinter printer;
private final StringBuilder builder = new StringBuilder();

private Buffer(CliPrinter printer) {
this.printer = printer;
}

@Override
public String toString() {
return builder.toString();
}

@Override
public Buffer append(CharSequence csq) {
builder.append(csq);
return this;
}

@Override
public Buffer append(CharSequence csq, int start, int end) {
builder.append(csq, start, end);
return this;
}

@Override
public Buffer append(char c) {
builder.append(c);
return this;
}

/**
* Writes styled text to the builder using the CliPrinter's Ansi settings.
*
* @param text Text to write.
* @param styles Styles to apply to the text.
* @return Returns self.
*/
public Buffer print(String text, Style... styles) {
printer.ansi().style(this, text, styles);
return this;
}

/**
* Prints a line of styled text to the buffer.
*
* @param text Text to print.
* @param styles Styles to apply.
* @return Returns self.
*/
public Buffer println(String text, Style... styles) {
return print(text, styles).println();
}

/**
* Writes a system-dependent new line.
*
* @return Returns the buffer.
*/
public Buffer println() {
return append(System.lineSeparator());
}

@Override
public void close() {
printer.println(builder.toString());
builder.setLength(0);
}
return fromPrintWriter(writer);
}
}
Loading

0 comments on commit d521dab

Please sign in to comment.