7

I have a function in a bash script: message_offset which is used to print the status of a bash script.
i.e. you would call it passing a message into it and a status, like this

message_offset "install font library" "[   OK   ]" 

and it would print into the terminal where printf's %*s format is used to always set the rightmost character of [ OK ] at 80 columns wide e.g. output would be

install font library                              [   OK   ]
update configuration file on server               [   ERR  ]
                                                           ^
                                                           |
                                                      always
                                                        at 80

If echo was used output would look like this

install font library                 [   OK   ]
update configuration file on server               [   ERR  ]

code:

#!/usr/bin/env bash

function message_offset() {

    local message="$1"
    local status="$2"

    # compensate for the message length by reducing the offset 
    # by the length of the message, 
    (( offset = 80 - ${#message} ))

    # add a $(tput sgr0) to the end to "exit attributes" whether a color was
    # set or not
    printf "%s%*s%s" "${message}" 80 "$status" "$(tput sgr0)"

}

this all works ok, until I try to use tput to add some color sequences into the string, i.e. to make "[ ERR ]" red.
It seems that the printf "%*s" formatting is counting the tput character sequences when its setting the offset, so if I call the function like this

message_offset "update configuration file on server"  "$(tput setaf 1)[   ERR  ]"

the output will look something like:

install font library                              [   OK   ]
update configuration file on server          [   ERR  ]

because printf "%*s" is saying hey this string has got all the "[ ERR ]" characters, plus the "$(tput setaf 1) chars, but obviously the "$(tput setaf 1) chars are not printed, so don't actually affect the padding.
Is there a way I can add color the "status" messages, and also use the tput style color sequences?

Jeff Schaller
  • 66,199
  • 35
  • 114
  • 250
the_velour_fog
  • 11,840
  • 16
  • 64
  • 109

2 Answers2

8

You're making this a lot more complicated than it should be. You can handle alignment with $message and not care about the width of ANSI sequences:

#! /usr/bin/env bash

message() {
    [ x"$2" = xOK ] && color=2 || color=1
    let offset=$(tput cols)-4-${#2}
    printf "%-*s[ %s%s%s ]\n" $offset "$1" "$(tput setaf "$color")"  "$2" "$(tput sgr0)"
}  

message "install font library" "OK"
message "update configuration file on server" "ERR"

Edit: Please note that most printf(1) implementations don't cope well with lengths calculations for multibyte charsets. So if you want to print messages with accented characters in UTF-8 you might need a different approach. shrug

Satō Katsura
  • 13,138
  • 2
  • 31
  • 48
  • thanks that works and is definitely cleaner, now Im trying to understand how your code is working. for example, can you tell me what the difference between the printf `%*s` and the printf `%-*s` formats are? – the_velour_fog Mar 19 '17 at 09:16
  • @the_velour_fog `$(tput cols)` = terminal width, `printf '%-*s' $offset "$1"` prints the message left-aligned, and pads it to the right with blanks up to width `$offset`. That's essentially all there is to it. – Satō Katsura Mar 19 '17 at 09:19
  • hmm, still not getting it, but it seems, `printf '%-*s'` is the key here? – the_velour_fog Mar 19 '17 at 09:21
  • Try this: `printf '%10s|\n' aaa`, then `printf '%-10s|\n' aaa`, then `printf '%-*s|\n' 10 aaa` – Satō Katsura Mar 19 '17 at 09:27
  • ah, thanks, got it. yes `%-*s` is essentially "absolute" padding regardless of how long `%s` is, whereas `%*s` is "relative to the right edge of the previous word. thanks Sato :) – the_velour_fog Mar 19 '17 at 09:33
  • @the_velour_fog Nope, they both have the same width. The difference is with `%*s` the string is printed flushed right, while with `%-*s` it's printed flushed left. But the width is still the same (namely the one specified by the argument corresponding to `*`). – Satō Katsura Mar 19 '17 at 09:36
  • ah, ok thats pretty easy then. – the_velour_fog Mar 19 '17 at 09:46
  • Your comment about "Linux specific" is backwards: SVr4 tput defaults to filling in zero's for unspecified command-line parameters. If BSD tput doesn't do that, it's a bug (since it was written to imitate SVr4). – Thomas Dickey Mar 19 '17 at 10:57
  • @ThomasDickey Hmm, right: this actually seems to be specific to OpenBSD. – Satō Katsura Mar 19 '17 at 11:59
  • Well good luck with that - their record on ncurses has been unsatisfactory. – Thomas Dickey Mar 19 '17 at 12:50
  • Try `message "this éíó" "ERR"`, it doesn't align on the right (at least here). –  Mar 19 '17 at 15:30
  • @sorontar Yup, that depends on locales. It works fine if the charset is `ISO_8859_*`, and it most likely breaks for `UTF-8`. Feel free to report that as a bug in `printf(1)` and / or `printf(3)`. – Satō Katsura Mar 19 '17 at 16:12
  • @SatoKatsura This is not a bug in `printf(3)` but a deliberate design decision to count in bytes. Doing differently would potentially cause buffer overruns in millions of apps. Moreover, width is a way more difficult concept in Unicode (e.g. what about characters that are double-wide in terminals, what about combining marks...). Handling them requires higher level functions than `printf(3)`. For the command line utility `printf(1)` it would cause a whole lot of confusion to do things differently than the `printf(3)` library call, let alone that it obviously uses that one. – egmont Mar 19 '17 at 22:15
  • On Linux systems you can use e.g. `wc -L` to compute the terminal display width of one-line strings. – egmont Mar 19 '17 at 22:16
  • @egmont when I try `echo "$(tput setaf 2)abc" | wc -L` output is: `7` shouldn't it be the correct printed terminal length be `3`? (using gnu `wc`) – the_velour_fog Mar 20 '17 at 00:14
  • @the_velour_fog You're right, I implicitly thought we were within the design proposed by Sato Katsura. That is: leave the terminal control sequences totally out of this game. Hardly any apps other than terminal emulators themselves recognize them, so `wc` doesn't either. It knows about UTF-8 and double-wide characters, though, so it correctly reports the display width of _printable_ text. Sorry for the confusion. – egmont Mar 20 '17 at 07:22
  • Also note that `wc -L` is specific to GNU coreutils and maybe a few more implementations, but is not portable across all kinds of Unixes. – egmont Mar 20 '17 at 07:24
  • Another totally different approach is to use a cursor positioning escape sequence, e.g. `\e[70G` to move the cursor to the 70th column in the current row. Sure there's a `tput` command that emits this too, I'm lazy to look that up. Or, alternatively, `\r\e[70C` is a carriage return followed by 70 cursor right movements. – egmont Mar 20 '17 at 07:27
  • @egmont _what about characters that are double-wide in terminals, what about combining marks_ - Don't forget the evergreen tabs. Which is why Vim has `strlen()`, `strwidth()`, `strchars()`, and `strdisplaywidth()` just to deal with length calculations, and some of them also depend on `ambiwidth`. Which is, of course, [the way things are meant to be](https://www.google.com/search?q=how+projects+really+work&tbm=isch). – Satō Katsura Mar 20 '17 at 08:40
  • @egmont _Sure there's a `tput` command that emits this too_ - `infocmp -1 | grep G,` reveals that as `hpa` (i.e. `tput hpa 70`). Probably worth posting as an answer. – Satō Katsura Mar 20 '17 at 08:42
1

an easy approach is to colorize everything after it has been aligned

In a nutshell you need

  • a function (or better, external script) to colorize string with colors (for example using perl's s,$regex,$color$&$resetcolor,gi

  • and you call it after you did the printing. color escape codes won't change the alignement that way.

for example: let's say you created a script named "colorize" that takes colors arguments, followed by regexes to be colorized with that color: for exemple colorize -blue 'regex1' -green 'regex2' you call it when needed:

 { code
   that
  formats and display things
 } | colorize -red 'ERR' -green 'OK'

Having that as a script by itself allow you to use it everywhere, for exemple:

 df -h | colorize -red '[890].%'
Olivier Dulac
  • 5,924
  • 1
  • 23
  • 35