Formatting the output: Underlining

7,921

Solution 1

The core of your question is building a string consisting entirely of underscores that is of the same length as an existing string. In recent enough versions of bash, ksh or zsh you can build this string with the ${VARIABLE//PATTERN/REPLACEMENT} construct: underlines=${word//?/_}. But this construct doesn't exist in ksh88.

In any shell, you can use tr instead. POSIX-compliant implementations of tr let you write this:

underlines=$(printf %s "$word" | tr -c '_' '[_*]')

I think Solaris 10 has a POSIX-compliant tr by default, but there might be a historical implementation (compatible with earlier Solaris releases). Historical implementations of tr might not understand the [x*] syntax, but they tend to accept the following syntax instead (which isn't guaranteed by POSIX), to mean “replace everything that isn't a newline by a _”:

underlines=$(echo "$word" | tr -c '\010' '_')
underlines=${underlines%_}

And here's a slightly crazy method that doesn't use any loop or external program and should work in any Bourne shell (at least since set -f was introduced — though running in an empty directory would mitigate the lack of set -f). Unfortunately, it only works if the string doesn't contain any whitespace.

set -f          # turn off globbing
IFS=$word       # split at any character in $word
set a $word     # split $word into one word between each character, i.e. empty words
shift           # remove the leading a (needed in case $word starts with -)
IFS=_
underlines=$*   # join the empty words, separated with the new value of $IFS

A more complex variant deals with whitespace, but only if there isn't any consecutive whitespace sequence. I don't think you can go any further with this trick, since sequences of whitespace characters in IFS are always collapsed.

set -f
unset IFS; set a $0    # split at whitespace
IFS=$*; set $*         # split into empty words
IFS=_; underlines=$*   # collect the empty

Solution 2

In newer shells you can do printf %s\\n "${word//?/-}". I don't think ksh88 has that particular expansion.

If you don't mind an extra process, you could do printf %s\\n "${word}" | sed -e 's/./-/g'.

Your approach is fine as well, although I would make the following tiny change:

print_underlined () {
        word=$1
        printf %s\\n "$word"
        i=${#word}
        while (( i )); do
                printf -
                (( i = i - 1 ))
        done
        echo
}

And for a completely different approach, use the terminal's ability to display true underlines, if available:

tput smul; printf %s\\n "$word"; tput rmul

Of course, that approach only works if you know the terminal the script runs on supports it.

Solution 3

If the output is to a terminal that supports underlined graphic rendition, as is the case with most terminal emulators nowadays (including dtterm but not wscons) and quite a few real terminals historically, then true underline qualifies as "more elegant":

print_underlined () {
  word=$1
  tput smul # set mode underline
  print -r -- "$word"
  tput rmul # reset mode underline
}

On a system with termcap and not terminfo, which is luckily not the case for Solaris 10, the capability names are different:

print_underlined () {
  word=$1
  tput us
  print -r -- "$word"
  tput ue
}

On an old Teletype Model 37, one would have done underline by backspacing and overstriking with the underscore character, _.

print_underlined () {
  word=$1
  print -r -- "$word" | sed -e 's/./&'$'\b''_/g'
}

This is better done the other way around if the output is not actually to a TTY-37. On an actual TTY-37 it does not matter which order things are overstruck, obviously. On "glass TTYs" overstriking does not exist, so it is better to "overstrike" the underscore with the character to be underlined, rather than vice versa.

print_underlined () {
  word=$1
  print -r -- "$word" | sed -e 's/./_'$'\b''&/g'
}

Strictly speaking, termcap/terminfo parameterize what the characters to emit are for underline current position and move on one and for backspace to previous position. In practice, the termcap/terminfo databases only define the uc capability for a handful of very rare terminals, and do not define it for the terminals and terminal emulators that you will encounter in the 21st century (or even the late 20th), making this parameterization largely pointless.

Ironically, the TTY-37 way is still the way to underline in some parts of Unix, all these decades later. Feed such input to more or less, or even ul, and they will render it in their outputs as true underlining, using the terminal's underlining control sequences.

print_underlined wibble | more

You can even feed it to the colcrt tool, which will turn the (sadly still widely used) TTY-37 underlining into underlining with minus signs.

$ print_underlined wibble | colcrt
wibble
------
$

Solution 4

I found this simply by googling it:

underline() { echo $1; echo "${1//?/${2:--}}";}

Basically the same thing but much more compact. If you find it confusing, more info on the curly bracket substitution can be found here. Very similar to the sed syntax.

Solution 5

This is a POSIX compliant way that will work on Solaris 10 & ksh88:

print_underlined () {
  printf "%s\n%s\n" "$1" $(printf "%s\n" "$1" | sed "s/./-/g")
}

$ print_underlined "hello world"
hello world
-----------
Share:
7,921

Related videos on Youtube

rahmu
Author by

rahmu

Updated on September 18, 2022

Comments

  • rahmu
    rahmu almost 2 years

    I wrote the following function in ksh that prints its first argument to the screen and underlines it with the appropriate ammount of - character:

    print_underlined () {
        word=$1 
        echo $word
    
        i=${#word}
        while [[ i -gt 0 ]]; do
            printf "-"
            (( i = $i - 1 ))
        done
        printf "\n"
    }
    

    example:

    $ print_underlined foobar
    foobar
    ------
    $
    

    I wonder if there is a simpler and more elegant way to display an underlined word to the screen.

    For the record I am using:

    • Solaris 10
    • ksh88
  • rahmu
    rahmu about 12 years
    Unfortunately, except for the sed process, none of the solutions suggested seem to work on such an old shell (although I tested them in bash and they seem to work). The decrement operator in particular is annoyingly missing from the shell, hence my original clumsy syntax. Oh and the tput solution didn't work either... it was too good to be true :(
  • jw013
    jw013 about 12 years
    @rahmu Sorry, I got carried away and didn't realize ksh88 lacked the decrement operator :). A pure POSIX-sh approach would not be much different from what you wrote, just with different test and arithmetic operators. The tput approach is entirely dependent on the terminal. If you want your script to be portable, it's probably not a good idea to assume things about the user's terminal.
  • nikitautiu
    nikitautiu about 12 years
    Also a second optional argument can be provided for the underline character.
  • rahmu
    rahmu about 12 years
    Nope won't work on my old shell. The answer is similar to the first one suggested by jw013 and is analogous to what I am looking for.
  • Stéphane Chazelas
    Stéphane Chazelas over 4 years
    ${var//pattern/replacement} was introduced by ksh93. It wasn't there in ksh88
  • Marius
    Marius over 4 years
    The comment and example about Solaris 10 is incorrect. It uses the terminfo names. Actually, about half the answer needs a rewrite...