Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Printing and padding strings with bash/printf

In bash scripts I like to use printf "%-20s" "some string" to create columns that line up. That works great with regular text, but not really for multi-byte unicode, nor if using any kind of terminal decoration.

Works great:

for i in string longer_string Some_kind_of_monstrosity ; do
   printf "%-20s" $i ; echo " OK"
done

Everything is reasonably well lined up:

string               OK
longer_string        OK
Some_kind_of_monstrosity OK

However - it doesn't work very well with multi-byte unicode or colour codes:

printred () { tput setaf 1; printf %b "$*"; tput sgr0; }
printf "%-20s" test             ; echo " NOK"
printf "%-20s" $(printred RED)  ; echo " NOK"
printf "%-20s" "★★★★"         ; echo " NOK"

It looks like both the bash builtin printf and the coreutils/printf simply count the number of bytes in the string, rather than how many character that will be visible on the output:

test                 NOK
RED       NOK
★★★★         NOK

Is there a way to achieve this nicely in bash? (I'm using bash 5.0.17, but I'm not averse to using some other tool.)

like image 631
Popup Avatar asked Oct 21 '25 14:10

Popup


2 Answers

Here is a sample library I wrote for an utf-8-compatible string alignment:

align.sh:

#!/bin/false
# UTF-8-compatible string alignment library

# Space pad align string to width
# @params
# $1: The alignment width
# $2: The string to align
# @stdout
# aligned string
align::left() {
  local -i width=${1:?} # Mandatory column width
  local -- str=${2:?} # Mandatory input string
  local -i length=$((${#str} > width ? width : ${#str}))
  local -i pad_right=$((width - length))
  printf '%s%*s' "${str:0:length}" $pad_right ''
}
align::right() {
  local -i width=${1:?} # Mandatory column width
  local -- str=${2:?} # Mandatory input string
  local -i length=$((${#str} > width ? width : ${#str}))
  local -i offset=$((${#str} - length))
  local -i pad_left=$((width - length))
  printf '%*s%s' $pad_left '' "${str:offset:length}"
}
align::center() {
  local -i width=${1:?} # Mandatory column width
  local -- str=${2:?} # Mandatory input string
  local -i length=$((${#str} > width ? width : ${#str}))
  local -i offset=$(((${#str} - length) / 2))
  local -i pad_left=$(((width - length) / 2))
  local -i pad_right=$((width - length - pad_left))
  printf '%*s%s%*s' $pad_left '' "${str:offset:length}" $pad_right ''
}

demo:

#!/usr/bin/env bash

# Demonstrates the align library
. ./align.sh

strings=(
  'Früchte und Gemüse'
  'Milchprodukte'
  'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
)

printf '%s\n' 'Left-aligned:'
for str in "${strings[@]}"; do
  printf "| %s |\n" "$(align::left 20 "$str")"
done
printf '\n%s\n' 'Right-aligned:'
for str in "${strings[@]}"; do
  printf "| %s |\n" "$(align::right 20 "$str")"
done
printf '\n%s\n' 'Center-aligned:'
for str in "${strings[@]}"; do
  printf "| %s |\n" "$(align::center 20 "$str")"
done

Demonstration output:

Left-aligned:
| Früchte und Gemüse   |
| Milchprodukte        |
| ABCDEFGHIJKLMNOPQRST |

Right-aligned:
|   Früchte und Gemüse |
|        Milchprodukte |
| GHIJKLMNOPQRSTUVWXYZ |

Center-aligned:
|  Früchte und Gemüse  |
|    Milchprodukte     |
| DEFGHIJKLMNOPQRSTUVW |
like image 171
Léa Gris Avatar answered Oct 23 '25 05:10

Léa Gris


simple command column meets your need.

column -t < input.txt
like image 20
mariolu Avatar answered Oct 23 '25 04:10

mariolu



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!