5
\$\begingroup\$

I went from manually outputting escape codes in each string to creating a utility struct/class full of static functions and variables that needed to be initialized in source files (static private members are required to) which was a bit ugly for me. Then I was searching for something different and while playing with enums, I found it interesting for my scenario. And then I carefully used namespaces to construct my sort-of function only header and here it is. Also, I'm not supporting pre-C++11 version of compilers.

My aims were – not to use classes since they became ugly with lots of static members and creating an object and using it for colouring doesn't looks so good (however I might try that in future), and to make it fast and also modern C++ish. Along the way, some neat tricks with C++ namespaces made it possible to sort of hide (well at least to code completions) my non-interface elements and give a nice shape to the code.

*I also want to reduce some not so necessary headers (e.g. <cstring>) if possible.

#ifndef RANG_H
#define RANG_H

#include <iostream>
#include <cstdlib>
#include <cstring>
extern "C" {
#include <unistd.h>
}

namespace rang {

    enum class style : unsigned char {
        Reset     = 0,
        bold      = 1,
        dim       = 2,
        italic    = 3,
        underline = 4,
        blink     = 5,
        reversed  = 6,
        conceal   = 7,
        crossed   = 8
    };
    enum class fg : unsigned char {
        def     = 39,
        black   = 30,
        red     = 31,
        green   = 32,
        yellow  = 33,
        blue    = 34,
        magenta = 35,
        cyan    = 36,
        gray    = 37
    };
    enum class bg : unsigned char {
        def     = 49,
        black   = 40,
        red     = 41,
        green   = 42,
        yellow  = 43,
        blue    = 44,
        magenta = 45,
        cyan    = 46,
        gray    = 47
    };
}


namespace {
    bool isAllowed = false;
    bool isTerminal()
    {
        return isatty(STDOUT_FILENO);
    }
    bool supportsColor()
    {
        if(const char *env_p = std::getenv("TERM")) {
            const char *const term[8] = {
                "xterm", "xterm-256", "xterm-256color", "vt100",
                "color", "ansi",      "cygwin",         "linux"};
            for(unsigned int i = 0; i < 8; ++i) {
                if(std::strcmp(env_p, term[i]) == 0) return true;
            }
        }
        return false;
    }
    std::ostream &operator<<(std::ostream &os, rang::style v)
    {
        return isAllowed ? os << "\e[" << static_cast<int>(v) << "m" : os;
    }
    std::ostream &operator<<(std::ostream &os, rang::fg v)
    {
        return isAllowed ? os << "\e[" << static_cast<int>(v) << "m" : os;
    }
    std::ostream &operator<<(std::ostream &os, rang::bg v)
    {
        return isAllowed ? os << "\e[" << static_cast<int>(v) << "m" : os;
    }
    namespace init {
        void rang()
        {
            isAllowed = isTerminal() && supportsColor() ? true : false;
        }
    }
}
#endif /* ifndef RANG_H*/

And to test:

#include <iostream>
#include "rang.h"

int main()
{
    init::rang();
    std::cout << rang::style::bold << rang::fg::red << "ERROR HERE! "
              << std::endl
              << rang::bg::red << rang::fg::gray << "ERROR INVERSE?"
              << rang::style::Reset << std::endl;
    return 0;
}

And here are warnings if you don't want to compile:

agauniyal@lenovo > rang[master] » clang++ test.cpp -std=c++11 -o rang.app -Wall -Weverything
In file included from test.cpp:2:
./rang.h:13:7: warning: scoped enumerations are incompatible with C++98 [-Wc++98-compat]
        enum class style : unsigned char {
             ^
./rang.h:13:2: warning: enumeration types with a fixed underlying type are incompatible with C++98 [-Wc++98-compat]
        enum class style : unsigned char {
        ^
./rang.h:24:7: warning: scoped enumerations are incompatible with C++98 [-Wc++98-compat]
        enum class fg : unsigned char {
             ^
./rang.h:24:2: warning: enumeration types with a fixed underlying type are incompatible with C++98 [-Wc++98-compat]
        enum class fg : unsigned char {
        ^
./rang.h:35:7: warning: scoped enumerations are incompatible with C++98 [-Wc++98-compat]
        enum class bg : unsigned char {
             ^
./rang.h:35:2: warning: enumeration types with a fixed underlying type are incompatible with C++98 [-Wc++98-compat]
        enum class bg : unsigned char {
        ^
./rang.h:69:29: warning: use of non-standard escape character '\e' [-Wpedantic]
                return isAllowed ? os << "\e[" << static_cast<int>(v) << "m" : os;
                                          ^~
./rang.h:73:29: warning: use of non-standard escape character '\e' [-Wpedantic]
                return isAllowed ? os << "\e[" << static_cast<int>(v) << "m" : os;
                                          ^~
./rang.h:77:29: warning: use of non-standard escape character '\e' [-Wpedantic]
                return isAllowed ? os << "\e[" << static_cast<int>(v) << "m" : os;
                                          ^~
./rang.h:80:8: warning: no previous prototype for function 'rang' [-Wmissing-prototypes]
                void rang()
                     ^
test.cpp:7:21: warning: enumeration type in nested name specifier is incompatible with C++98 [-Wc++98-compat]
        std::cout << rang::style::bold << rang::fg::red << "ERROR HERE! "
                           ^
test.cpp:7:42: warning: enumeration type in nested name specifier is incompatible with C++98 [-Wc++98-compat]
        std::cout << rang::style::bold << rang::fg::red << "ERROR HERE! "
                                                ^
test.cpp:9:21: warning: enumeration type in nested name specifier is incompatible with C++98 [-Wc++98-compat]
                  << rang::bg::red << rang::fg::gray << "ERROR INVERSE?"
                           ^
test.cpp:9:38: warning: enumeration type in nested name specifier is incompatible with C++98 [-Wc++98-compat]
                  << rang::bg::red << rang::fg::gray << "ERROR INVERSE?"
                                            ^
test.cpp:10:21: warning: enumeration type in nested name specifier is incompatible with C++98 [-Wc++98-compat]
                  << rang::style::Reset << std::endl;
                           ^
15 warnings generated.
\$\endgroup\$
  • 1
    \$\begingroup\$ Why are you compiling with -Wc++98-compat if your code isn't fully backwards compatible? \$\endgroup\$ – glampert Feb 22 '16 at 18:28
  • \$\begingroup\$ @glampert that's -Weverything in its full glory so that no warning gets away. \$\endgroup\$ – Abhinav Gauniyal Feb 23 '16 at 2:37
  • \$\begingroup\$ Hehe, I don't think -Weverything is meant for everyday use. I seem to have read somewhere that they added it so you could test all the warnings, then select the ones that make sense... \$\endgroup\$ – glampert Feb 23 '16 at 4:16
  • \$\begingroup\$ Anyways, if you're into that, be sure to also give the static analyzer a run: clang-analyzer.llvm.org \$\endgroup\$ – glampert Feb 23 '16 at 4:20
2
\$\begingroup\$

I made a couple of small changes for you:

bool supportsColor()
{
    if(const char *env_p = std::getenv("TERM")) {
        const char *const terms[] = {
            "xterm", "xterm-256", "xterm-256color", "vt100",
            "color", "ansi",      "cygwin",         "linux"};
        // Change 1
        for(auto const term: terms) {
            if(std::strcmp(env_p, term) == 0) return true;
        }
    }
    return false;
}

I swapped out your for loop with a range for loop which, apart from being all the rage, actually improve read-ability as well. Literally, for every term in terms. The compiler will optimise this in the same way as the original for loop.

To follow up on point 2 from Jerry Coffin's answer, using some template black magic allows us to reduce the number of overloads for operator<< to just one:

// Change 2
template<typename T>
using enable = typename std::enable_if
<
    std::is_same<T, rang::style>::value ||
    std::is_same<T, rang::fg>::value ||
    std::is_same<T, rang::bg>::value,
    std::ostream&
>::type;

template<typename T>
enable<T> operator<<(std::ostream& os, T const value)
{
    ...
}

Change 2 relies on a technique known as SFINAE (Substitution Failure Is Not An Error) whereby any substitutions that result in erroneous candidates for overload resolution, are simply removed. In our case, the return type is well-defined iff T has type rang::{style,fg,bg}.

One last change that I made is in the body of operator<<:

// Change 3
if(isAllowed) {
    os << "\e[" << static_cast< int >(value) << "m";
}
return os;

While I am a proponent of the ternary operator, I question it's use here. The above if statement is more obvious. Since we return the ostream regardless of whether or not we used it, the result is the same as before.

You can see my modifications in action here sans the colourful output, unfortunately. Testing in my linux machine with g++ 4.8 yields a lovely colourful output.

\$\endgroup\$
5
\$\begingroup\$

I have a few comments.

  1. You use isatty to check whether standard output is connected to a terminal, regardless of what stream you're writing to. This means the rest of the functions only work correctly if you pass std::cout as the stream to which they're going to write. Otherwise, you may allow formatting when writing to something that's not a TTY, and you may prohibit formatting when writing to something that is a TTY.

  2. The repetition of having three implementations of operator<< that differ solely in the type of one parameter is...less than pleasing. Ideally, you'd want one template to handle all three. Less ideally (but maybe more practically) you'd move the "guts" to a single template, and have three really "thin" functions that just pass through to the template.

  3. Maybe it's just me, but every time I see code like:

    isAllowed = isTerminal() && supportsColor() ? true : false;
    

    ... I grit my teeth. The result from isTeriminal() && supportsColor() is already a perfectly good bool--you can use it directly:

    isAllowed = isTerminal() && supportsColor();
    

    This seems much cleaner and more readable, at least to me.

\$\endgroup\$
  • \$\begingroup\$ Thankyou, I was definitely considering template as well and I'll correctly write that bool expression too. However I am not able to understand 1st point you've mentioned. This means the rest of the functions only work correctly if you pass std::cout as the stream to which they're going to write., well I'm targetting cout only and what else should I , and this won't write to a file with cout because of isatty. Could you explain a bit? \$\endgroup\$ – Abhinav Gauniyal Feb 23 '16 at 2:47
  • \$\begingroup\$ @AbhinavGauniyal: As one obvious example, you might have cout directed to a file, but want to highlight an error message (being written to std::cerr and still going to the console) in red. \$\endgroup\$ – Jerry Coffin Feb 23 '16 at 2:49
  • \$\begingroup\$ I tried to study more about it today and got to know that cout/cin can be redirected to files too. tbh I wasn't even aware of it. How do you recommend to tackle that problem? Is this what I should be doing - stackoverflow.com/questions/18081392/… \$\endgroup\$ – Abhinav Gauniyal Feb 23 '16 at 15:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.