27

I have written a Makefile which works fine; I am just posting the part of it under investigation:

BUILD_PRINT = @echo -e "\e[1;34mBuilding $<\e[0m"
COMPILE_cpp = $(CXX) $(CFLAGS) -o $@ -c $< $(MAKEDEP) $(INCLUDES)
%.o : %.cpp
    $(BUILD_PRINT)
    $(COMPILE_cpp)
.SUFFIXES: .o .cpp

I would like to highlight the warnings and errors given by the compiler without using external tools (such as colorgcc or CMake); I thought that a good way to hack it was via "bash script tricks". Looking at the solution posted in How Can I highlight the warning and error lines in the make output? I have tried the following:

pathpat="(/[^/]*)+:[0-9]+"
ccred=$(echo -e "\033[0;31m")
ccyellow=$(echo -e "\033[0;33m")
ccend=$(echo -e "\033[0m")

BUILD_PRINT = @echo -e "\e[1;34mBuilding $<\e[0m"
COMPILE_cpp = $(CXX) $(CFLAGS) -o $@ -c $< $(MAKEDEP) $(INCLUDES)
%.o : %.cpp
    $(BUILD_PRINT)
    $(COMPILE_cpp) 2>&1 | sed -e "/[Ee]rror[: ]/ s%$pathpat%$ccred&$ccend%g" -e "/[Ww]arning[: ]/ s%$pathpat%$ccyellow&$ccend%g" echo "${PIPESTATUS[0]}"
.SUFFIXES: .o .cpp

but it is not working. I get the following output

Building main.cpp
g++ -o main.o -c main.cpp  2>&1 | sed -e "/[Ee]rror[: ]/ s%athpat%cred&cend%g" -e "/[Ww]arning[: ]/ s%athpat%cyellow&cend%g" echo ""
sed: can't read echo: No such file or directory
sed: can't read : No such file or directory

Thanks in advance!

Community
  • 1
  • 1
helmet_23
  • 405
  • 1
  • 5
  • 10
  • What is (or isn't) happening with that makefile exactly? From a quick glance you probably just need to escape the `$` in the body rule so `s%$pathpat%$ccred&$ccend%g` needs to be `s%$$pathpat%$$ccred&$$ccend%g`, etc. Also that pipestatus echo is not going to work on its own line like that it would need to be on the same command line as the compile line to work the way you want. – Etan Reisner Jun 10 '14 at 15:33
  • I have edited the post with the error I get – helmet_23 Jun 10 '14 at 16:07
  • 1
    Your command line says, basically, `sed -e "...1" -e "...2" echo ""`. That tells `sed` to execute the sed commands `...1` and `...2` on the files whose names are `echo` and the empty string. The error message is, therefore, logical. Also, inside a makefile, a command to set a `make` variable can't use bash syntax. So `ccred=$(echo -e "\033[0;31m")` doesn't do what you think it does. Finally, as Etan points out, `$` substitutions in makefiles are handled before the command line is passed to bash, so you need to escape the `$` for bash substitution or put `()` around the name for make variables. – rici Jun 10 '14 at 16:44
  • 1
    You need to put a semicolon before the `echo` to separate it into a second command, not just attach it to the sed command. I'd misread `$(echo -e "\033[0;31m")` originally, you either want to remove the `$()` from that line and use `$$(echo "$$ccred")` in the rule body or use `$(shell echo ...)` to get the output from the command. – Etan Reisner Jun 10 '14 at 16:52
  • By the way, aside from the value in learning more about `bash` and `make`, I don't see the rationale for avoiding simple tools like `colorgcc` or `colormake`. But possibly the best solution is to compile with `clang`, which colorizes error messages and warnings all by itself. – rici Jun 10 '14 at 16:58
  • @EtanReisner: Really, all these comments should be an answer instead. I'm leaving it up to you :) – rici Jun 10 '14 at 16:59
  • @rici Indeed, when I first answered I wasn't sure I had the full answer. It seems we now do. I think I covered it all in my answer. – Etan Reisner Jun 10 '14 at 18:31

4 Answers4

16

In make context the following lines do not behave the way they would in the shell:

ccred=$(echo -e "\033[0;31m")
ccyellow=$(echo -e "\033[0;33m")
ccend=$(echo -e "\033[0m")

In the shell those would put the echoed output into those variables. In make that tries to run the echo command, which doesn't exist, and ends up creating empty variables.

Those lines should either be

  • ccred=$(shell echo -e "\033[0;31m") to run the commands through the shell and store the output and then used as $(ccred) in the body
  • ccred=\033[0;31m to store the string in the variable and then used as $$(echo -e '$(ccred)') in the body
  • ccred=echo -e "\033[0;31m" to store the command in the variable and then used as $$($(ccred) in the body

Either of the first or second options is likely fine. (Use := instead of = in the first option to have make only run the echo command once, at make parse time, instead of every time ccred is used.)

make and shell variables share a prefix sigil $. As such to use shell variables in make contexts requires escaping the $ in shell contexts by doubling it to $$. As such s%$pathpat%$ccred&$ccend%g needs to be s%$$pathpat%$$ccred&$$ccend%g, etc.

Each line of a make rule body is executed as a distinct shell command, as such the commands cannot interact with each other. In order to use constructs like echo "${PIPESTATUS[0]}" meaningfully therefore requires that they be on the same command line in make. As such the compile line in that pattern rule would need to be $(COMPILE_cpp) 2>&1 | sed ...; echo "${PIPESTATUS[0]}".

However, even that isn't going to do what you want since you don't need to echo the exit status from the compilation you need to exit with it, so you probably want ; exit "${PIPESTATUS[0]}" there instead.

Etan Reisner
  • 77,877
  • 8
  • 106
  • 148
  • Etan Reisner and rici, thank you for the detailed explanation. I am quite new to this topic and I understand I am making some trivial mistake. Now I have modified my Makefile following your hints like this: `$(COMPILE_cpp) 2>&1 | sed -e s/error/"\\\e[1;31merror\\\e[0m"/g` but, as a result, calling the make target, I get the whole compiler output not with the `error` word written in red bold but just with the replaced `\e[1;31merror\e[0m` in plain text. Where am I wrong? – helmet_23 Jun 11 '14 at 15:57
  • @helmet_23 You need to use echo (or similar) to evaluate those escape characters. Not just send them to the output. Notice how in my variations echo always existed somewhere. – Etan Reisner Jun 11 '14 at 17:21
  • if you want something a little more readable, you can use `ccred=$(shell tput setaf 001)`, and use https://unix.stackexchange.com/questions/269077/tput-setaf-color-table-how-to-determine-color-codes to select colors – Shawn Rubie May 03 '19 at 20:01
9

Got it working.

First of all thanks @EtanReisner and @rici. Here's the code:

BUILD_PRINT = \e[1;34mBuilding $<\e[0m

COMPILE_cpp = $(CXX) $(CFLAGS) -o $@ -c $< $(MAKEDEP) $(INCLUDES)
COMPILE_cpp_OUT=$$($(COMPILE_cpp) 2>&1 | sed -e 's/error/\\\e[1;31merror\\\e[0m/g' -e s/warning/\\\e[1;33mwarning\\\e[0m/g')

%.o : %.cpp
    @echo -e "$(BUILD_PRINT)\n$(COMPILE_cpp)\n$(COMPILE_cpp_OUT)"
.SUFFIXES: .o .cpp

All the commands are invoked by only one echo because I want all the outputs (command string and warnings/errors) coherently grouped for each file built when I launch a parallel build with make -j.

$(BUILD_PRINT) just prints out the path of the file currently being built.

$(COMPILE_cpp) prints out the string of the compiler, so that I can see the command with all the flags/dependencies/etc...

$(COMPILE_cpp_OUT) stores the output of the compiler and change some relevant word colour via sed command.

helmet_23
  • 405
  • 1
  • 5
  • 10
  • 2
    This didn't work for me, on debian jessie. I had to include the complete path to echo (e.g. @/bin/echo) to avoid the shell builtin command. – Matt Apr 25 '18 at 17:00
1

Here's a makefile that uses echo with colors:

GREEN='\033[0;32m'
NC='\033[0m'

all:
    @echo -e ${GREEN}BUILD SUCCESSFUL${NC}

I'm on Debian 11.

Benjamin Smus
  • 103
  • 1
  • 7
1

Best solution for my projects:

COLOUR_GREEN=\033[0;32m
COLOUR_RED=\033[0;31m
COLOUR_BLUE=\033[0;34m
END_COLOUR=\033[0m

test:
    @echo "$(COLOUR_GREEN)Test Passed$(END_COLOUR)"

Then i made separate file for colours colours.mk and included it to Makefile with include .make/colours.mk

Max Siratskyi
  • 38
  • 1
  • 6