CSI: Echo, Make, and ANSI

    Earth, Grass, and CSI

    I recently heard about the EGOS project and, being interested in Operating Systems, RISC-V, and systems and embedded programming, naturally, I wanted to check it out.

    The project revolves around understanding EGOS-2000: an Operating System designed for teaching about Operating Systems, with only ~2000 lines of code. The E stands for the Earth layer, which involves abstracting away the RISC-V ISA, while the G signifies the Grass layer, or hardware-independent abstractions, upon which the remaineder of the OS is built.

    I'll leave it at that for now, but hope to post more about the project in the near future. If you're interested, please do check out the link above.

    Make it Work

    More to the point of this post: I had a small struggle to the get the project up and running on my system. I had to set up QEMU on Linux for starters, which ultimately was not terribly difficult, but is not as simple as, say, grabbing a singular "qemu" package. The darn thing wouldn't compile either, but again, I'll save that for later...

    The Makefile worked fine. Being an Arch user, and as most, already having the base-devel package (which is pretty much essential for installing/building packages from the AUR), I of course had the GNU make utility already installed. As a developer, I'm no stranger to Makefiles, but in the course of my particular career path, it hasn't been often that I've needed to deal with them, much less create or edit them (though I've been learning more about them due to their popularity with the Golang crowd).

    That being said, it didn't work fine. Yes, it ran the specified commands, but the output was a mess. Let me illustrate:

    % make
    Compile app\033[1;36m cat \033[0m=> build/release/user/cat.elf
    Compile app\033[1;36m cd \033[0m=> build/release/user/cd.elf
    Compile app\033[1;36m crash1 \033[0m=> build/release/user/crash1.elf
    Compile app\033[1;36m crash2 \033[0m=> build/release/user/crash2.elf
    Compile app\033[1;36m echo \033[0m=> build/release/user/echo.elf
    Compile app\033[1;36m loop \033[0m=> build/release/user/loop.elf
    Compile app\033[1;36m ls \033[0m=> build/release/user/ls.elf
    Compile app\033[1;36m udp_demo \033[0m=> build/release/user/udp_demo.elf
    Compile app\033[1;36m vga_demo \033[0m=> build/release/user/vga_demo.elf
    Compile app\033[1;36m sys_file \033[0m=> build/release/sys_file.elf
    Compile app\033[1;36m sys_proc \033[0m=> build/release/sys_proc.elf
    Compile app\033[1;36m sys_shell \033[0m=> build/release/sys_shell.elf
    Compile app\033[1;36m sys_terminal \033[0m=> build/release/sys_terminal.elf
    \033[1;33m-------- Compile EGOS --------\033[0m
    riscv-none-elf-gcc -march=rv32ima_zicsr -mabi=ilp32 -Wl,--gc-sections -ffunction-sections -fdata-sections -fdiagnostics-show-option -Ilibrary -Ilibrary/elf -Ilibrary/file -Ilibrary/libc -Ilibrary/syscall -DKERNEL earth/boot.s grass/kernel.s earth/boot.c earth/cpu_intr.c earth/cpu_mmu.c earth/dev_disk.c earth/dev_tty.c grass/init.c grass/kernel.c grass/process.c library/elf/elf.c library/file/file0.c library/file/file1.c library/libc/malloc.c library/libc/print.c library/syscall/servers.c library/syscall/syscall.c -Tlibrary/elf/egos.lds -nostdlib -lc -lgcc -o build/release/egos.elf
    

    Notice anything odd in the output? Yeah, the little \033[1;36m and similar entries, spread a couple times each on multiple lines in the output. Almost immediately, I recognized what I was seeing, since I have the same type of entries in my Bash prompt:

    RED="\[\033[0;31m\]"
    YELLOW="\[\033[0;33m\]"
    GREEN="\[\033[0;32m\]"
    WHITE="\[\033[1;37m\]"
    GRAY="\[\033[0;37m\]"
    NONE="\[\033[0m\]"
    
    BG_GRAY=$(tput setab 240)
    BG_CYAN=$(tput setab 45)
    reset=$(tput sgr0)
    cyan=$(tput setaf 45)
    white=$(tput setaf 7)
    gray=$(tput setaf 240)
    magenta=$(tput setaf 213)
    
    git_status="\$(__git_ps1 '\[${magenta}\][%s]\[${reset}\]' )"
    tmsp="\[${BG_CYAN}${gray}\] \t \[${reset}\]"
    
    corner_top="\[${cyan}\]╭\[${reset}\]${tmsp}"
    row_top="\[${cyan}\]-{\[${reset}\]"
    corner_bottom="\[${reset}\]\[${cyan}\]╰{\[${reset}\]"
    row_bottom="\[${magenta}\]%\[${reset}\]"
    
    ps1_top="${corner_top}${row_top} $GRAY\w ${git_status}"
    ps1_bottom="${corner_bottom} ${row_bottom} "
    
    PS1="\r\n${ps1_top}\r\n${ps1_bottom}"
    

    Here's how it looks:

    my Bash PS1 prompt

    (Yes, that is quite a PS1 and the code snippet doesn't even include the Git-checking bit, but is it any worse than Starship!? Also, you may notice the use of tput in place of those cryptic entries I mentioned; this is a GNU utility that essentially looks up a terminal capability and translates it to your specific terminal emulator -- the setaf code essentially means, "set ANSI foreground". See man terminfo for more).

    That's right, you guessed it: ANSI escape codes! Specifically, the \033[ equates to ESC [, which is an ANSI Control Sequence Introducer. The values between there and the letter m basically tell the terminal to print the characters that follow in bold or in a certain color. Another magical string tells it to stop doing all that and go back to normal -- kinda like wrapping your webpage text in a <span> with attributes or CSS to change the look of the text between. In this case, the 1 means bold, and the 36 is a color code. Select Graphics Rendition covers this font stuff and other display changing abilities, including blink, strikethrough, background color, etc.

    Echo-o-o-o-o-o

    Now that we know what we're dealing with, why the heck does it work in my PS1 prompt but not when I run the make command? I experienced the same whether using tmux or no, Alacritty (Bash), or Ghostty (Bash).

    I confirmed that issuing printf and even echo -e commands would properly interpret the escape sequences, but running the make command or a plain echo always resulted in the escape sequences printed out instead of interpreted.

    Well, it boils down to that echo command, in my case (and likely yours), supplied by GNU Echo in the coreutils package (which for me seems to exist at both /usr/bin/echo and /bin/echo -- without symbolic link). Apparently, some shells even have their own versions of the command. In any case, whatever is echoing text may or may not interpret those escape codes, depending on how it's configured. Given all the variables at play, it can actually be quite complicated, especially when you figure in cross-compatibility between Linux, BSD, MacOS, and Windows!

    Use tput you say? This is a good cross-terminal solution, looking up the correct ANSI interpretations via terminfo, but not for Windows users. What about that echo -e I mentioned? There doesn't seem to be a consensus on which systems support that flag, which might mean it fixes the problem, is totally ignored, or completely breaks the Makefile.

    I did some research and I discovered a couple of things:

    • For one, enabling the Shell Option shopt -s xpg_echo solves the problem, if your environment supports it. This enables the parsing of those escape sequences.
    • Secondly, using printf is the preferred cross-platform solution.

    So, there you have it.

    And I'm proud to say, I didn't keep this information to myself. Not only am I sharing it here with you today, but I also updated the Makefile and submitted a pull request to the EGOS project, which was accepted by the maintainer (who also thanked me for the thorough explain -- yes, I know I am a bit wordy).

    Hope it helps someone if they happen across this one day.