fmt.Printf with verbs for producing colored output

1,949 views
Skip to first unread message

Anmol Sethi

unread,
Mar 28, 2016, 1:03:41 PM3/28/16
to golang-nuts
Hello everyone,

I recently created my first package.
It extends fmt.Printf with verbs for producing colored output.

https://github.com/nhooyr/color

I’m looking for feedback, comments, thoughts etc.

Konstantin Khomoutov

unread,
Mar 28, 2016, 3:51:13 PM3/28/16
to Anmol Sethi, golang-nuts
I, for one, think that it puts the cart before the horse: using colors
in the output is a _formatting detail,_ not the primary goal of
outputting stuff. I hence think that the approach taken by, say, [1]
(there are many other packages on github implementing ANSI
color-controlling sequences for Go [2]) is more "natural": it allows you
to create little string values which you then just literally embed in
the strings you send to fmt.Print*() and it just forwards them to the
terminal driver.

On the other hand, supporting Windows console, which does not know
about terminal control sequences, requires using some sort of translator
"wrapping" os.Stdout and parsing and interpreting those control
sequences (and [1] refers to one such package). The approach
implemented by your package could hence be more direct in its
handling of the Windows console but I'm not exactly sure it's a clear
win.

1. https://github.com/mgutz/ansi
2. https://github.com/search?q=golang+ansi

Anmol Sethi

unread,
Mar 28, 2016, 4:29:18 PM3/28/16
to Konstantin Khomoutov, golang-nuts
Thanks for the feedback.

I find the approach of such other packages to be extremely verbose. For each different color/formatting detail I have to call a function. I find the succinctness of using verbs to color the output much more natural for longer strings with multiple colors. fmt.Printf is all about formatting details and color fits right in.

Paul Borman

unread,
Mar 28, 2016, 7:07:13 PM3/28/16
to Anmol Sethi, Konstantin Khomoutov, golang-nuts
I think you are doing exactly the right thing, you are creating a package that wraps fmt.  It would be wrong to add this into the standard fmt package, however.  I think you can modify this to handle windows, but it would probably only work when talking to os.Stdout directly, according to what Konstantin said about windows and color.  You would need some way to indicate which one to use, of course.  Or you can just let it only be useful for ANSI devices (there are other encodings besides ANSI).

    -Paul

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

roger peppe

unread,
Mar 29, 2016, 4:08:49 AM3/29/16
to Anmol Sethi, golang-nuts
One thing: please respect the value of $TERM.
If it's "dumb", then please don't output the ANSI control codes.
Not everyone uses an ANSI terminal for shell output
and it's frustrating when commands don't provide a way
of silencing the noise.

You should probably also consider turning off the colours
if the output is not a terminal, as it's useful to be able
to use grep.

Alex Bligh

unread,
Mar 29, 2016, 4:26:11 AM3/29/16
to roger peppe, Alex Bligh, Anmol Sethi, golang-nuts

On 29 Mar 2016, at 09:08, roger peppe <rogp...@gmail.com> wrote:

> You should probably also consider turning off the colours
> if the output is not a terminal, as it's useful to be able
> to use grep.

isatty() is your friend

--
Alex Bligh




Anmol Sethi

unread,
Mar 29, 2016, 6:28:47 AM3/29/16
to Alex Bligh, roger peppe, golang-nuts
Will do, thanks

Konstantin Khomoutov

unread,
Mar 29, 2016, 7:34:37 AM3/29/16
to roger peppe, Anmol Sethi, golang-nuts
On Tue, 29 Mar 2016 09:08:16 +0100
roger peppe <rogp...@gmail.com> wrote:

> One thing: please respect the value of $TERM.
> If it's "dumb", then please don't output the ANSI control codes.
> Not everyone uses an ANSI terminal for shell output
> and it's frustrating when commands don't provide a way
> of silencing the noise.

It's not that simple. The names of terminal types are free-form,
and it's somewhat customary to have user-provided terminal descriptions.

Instead, one should query the terminal description file for the number
of colors the terminal supports. Ncurses comes with a dedicated binary,
`tput` which can be used for this -- say, on my current system I have:

~% echo $TERM
rxvt-unicode-256color
~% tput colors
256
~% TERM=dumb tput colors
-1
~%

The `tput` binary uses libtinfo library which is a part of ncurses, and
is responsible for accessing the terminal description database and
parsing its data.

termbox-go contains reasonable amount of code to query this database
but my cursory look at it failed to locate anything related to querying
this parameter. I'd say extending termbox-go with the approach
libtinfo implements to read the number of colors supported by a
terminal could work. ;-)

> You should probably also consider turning off the colours
> if the output is not a terminal, as it's useful to be able
> to use grep.

That's simpler -- as Alex Bligh suggested, the isatty() libc function
is typically used for this.

One problem with this is that it's a libc function and is hence
unavailable to Go unless cgo is used (which would be very much
desirable to avoid).

At least on Linux, isatty() internally calls fstat() on the file
descriptor associated with the stdout and them sees if the "mode" field
of the returned "stat data" block has bits indicating it's a "character
special" device set. On Linux, this mask, S_IFCHR, is defined to be
0020000. I don't know is this bitmask is portable or platform-specific
though.

In pure Go, Fstat is available via the syscall standard package.

I'm also pretty sure specialized packages for terminal handling, like,
say, termbox-go, should already contain code which mimics isatty().

Paul Borman

unread,
Mar 29, 2016, 11:27:12 AM3/29/16
to Konstantin Khomoutov, roger peppe, Anmol Sethi, golang-nuts
For terminal handling, see https://godoc.org/golang.org/x/crypto/ssh/terminal, it does have IsTerminal:

if terminal.IsTerminal(int(os.Stdout.Fd())) {
    use color
}

Not all systems have tput, though it is widely available.  There is a terminfo package for go, github.com/Nvveen/Gotty , but maybe you might enjoy writing one that does not use regular expressions, but a parser based on work Rob Pike did (http://blog.golang.org/two-go-talks-lexical-scanning-in-go-and), but not all systems have terminfo.

You might want a global public variable to disable coloring so a program could have a --nocolor flag, even if it does support coloring (I absolutely loathe standard commands (like ls) that display with coloring, even though my terminal handles it.  Others clearly like it.)  I would probably have an environment variable, other than TERM, to disable coloring.  Sadly there is already GREP_COLORS and LS_COLORS among others.  Maybe GO_COLORS and if it is set to "none" you don't do colors.

Anmol, what is your target audience for your package?

    -Paul

Anmol Sethi

unread,
Mar 29, 2016, 12:08:16 PM3/29/16
to Paul Borman, Konstantin Khomoutov, roger peppe, golang-nuts
I was just working on my own micro framework for net/http.
My logging/recovery middleware felt very bland without color.
I looked at some existing packages but I thought it could be simpler.

My target audience is people who need multiple colors/attributes in longer strings,
but I haven’t given it too much thought.

Anmol Sethi

unread,
Apr 8, 2016, 11:29:00 PM4/8/16
to Anmol Sethi, Paul Borman, Konstantin Khomoutov, roger peppe, golang-nuts
In the time since this post, I’ve given color a massive refactoring. I looked at how the fmt package's internals worked and realized that I was doing it all wrong. With proper buffering and pooling, the performance has been improved by over 2000%!

Next I also added support for disabling/enabling the colors. color.Fprintf() will only write in color if the writer is a terminal. For those who want full control, to say add a flag to disable coloured output, I added a new printer object that wraps an io.Writer. When created it will check if the writer is a terminal and enable coloured output accordingly. However you can easily override it with p.EnableColors()/p.DisableColors() on the fly.

I also added the same methods to the log.Logger wrapper.

Now, I’m gonna work on respecting the $TERM variable.

Once again, thanks for all the feedback.
Reply all
Reply to author
Forward
0 new messages