You are not logged in.

#1 Today 14:00:03

Bedna
Member
Registered: 2025-11-14
Posts: 52
Website

[SOLVED] printf with alignment

I'm trying to create a "border" around an lsblk output in a bash script.

I have following code, but printf is not creating the rightmost # as I interpret it should.

COLS=60
BREAK="$(printf "%${COLS}s" | tr ' ' '#')"
mapfile -t lsblk_output < <(lsblk)
echo "$BREAK"
for line in "${lsblk_output[@]}"; do
  printf "# %-${COLS}s%s\n" "$line" '#'
done
echo "$BREAK"

The output becomes this:

############################################################
# NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS           #
# sda           8:0    0 465,8G  0 disk                       #
# ├─sda1        8:1    0   384M  0 part                   #
# ├─sda2        8:2    0    38G  0 part                   #
# ├─sda3        8:3    0   128G  0 part                   #
# └─sda4        8:4    0 299,4G  0 part                   #
# sdb           8:16   0 167,7G  0 disk                       #
# └─sdb1        8:17   0 167,7G  0 part [SWAP]            #
# sdc           8:32   0   2,7T  0 disk                       #
# └─sdc1        8:33   0   2,7T  0 part /media/fil-fitt   #
# nvme0n1     259:0    0   1,8T  0 disk                       #
# └─nvme0n1p1 259:1    0   1,8T  0 part /media/games      #
# nvme1n1     259:2    0 931,5G  0 disk                       #
# ├─nvme1n1p1 259:3    0     1G  0 part /boot/efi         #
# ├─nvme1n1p2 259:4    0  99,5G  0 part /var/cache        #
# │                                     /var/log            #
# │                                     /                   #
# ├─nvme1n1p3 259:5    0   783G  0 part /home             #
# └─nvme1n1p4 259:6    0    48G  0 part [SWAP]            #
############################################################

It doesn't make sense to me. The -60 is from the left border right? Not after the "# ", if it starts after the initial "# ", it should be "printf "# %-$(( COLS - 2 ))s%s\n" "$line" '#'", but that doesn't align with anything either. Besides, the main problem is the rightmost # is not consistent, and I can't figure out what is making it shift randomly.
What am I doing wrong?

I could use tput, catch current cursor location and yadayada, but in my mind, printf should be able to do this right?
I have also tried counting characters or bytes in $line within the for loop and use that for alignment, but wc seems to count all the spaces as one byte (or something) so I can't get correct alignment with that either.

Thank you in advance.

Last edited by Bedna (Today 17:06:13)

Offline

#2 Today 14:20:41

seth
Member
From: Won't reply 2 private help req
Registered: 2012-09-03
Posts: 78,705

Re: [SOLVED] printf with alignment

while read line; do printf "# %-60s%s\n" "$line" '#'; done < <(lsblk)

Offline

#3 Today 14:51:33

Bedna
Member
Registered: 2025-11-14
Posts: 52
Website

Re: [SOLVED] printf with alignment

seth wrote:
while read line; do printf "# %-60s%s\n" "$line" '#'; done < <(lsblk)

Gets the exact same result, ie the right is not aligned.

Offline

#4 Today 14:58:40

seth
Member
From: Won't reply 2 private help req
Registered: 2012-09-03
Posts: 78,705

Re: [SOLVED] printf with alignment

Oh, it's bash… zsh does what you expect hmm

Offline

#5 Today 15:15:37

Trilby
Inspector Parrot
Registered: 2011-11-29
Posts: 30,532
Website

Re: [SOLVED] printf with alignment

Ha ... and busybox's ash gets it right, but I can confirm here that bash's output isn't aligned.

Now I'm curious why.


"UNIX is simple and coherent" - Dennis Ritchie; "GNU's Not Unix" - Richard Stallman

Offline

#6 Today 15:18:08

seth
Member
From: Won't reply 2 private help req
Registered: 2012-09-03
Posts: 78,705

Re: [SOLVED] printf with alignment

coreutils /usr/bin/printf acts like bash - the trigger are probably the line drawing elements hmm

Offline

#7 Today 15:27:18

GerBra
Forum Fellow
From: Bingen/Germany
Registered: 2007-05-10
Posts: 299

Re: [SOLVED] printf with alignment

It's the drawing char output from lsblk.
If using:

-i, --ascii          use ascii characters only
-l, --list           use list format output

then it works.

while read line; do printf "# %-60s #\n" "$line" ; done < <(lsblk -i)
while read line; do printf "# %-60s #\n" "$line" ; done < <(lsblk -l)

Last edited by GerBra (Today 15:27:51)


My avatar: "It's not just a toilet... a FERGUSON. The King of bowls. Sit down and give me your best shot." Al Bundy

Offline

#8 Today 15:28:21

5hridhyan
Member
Registered: 2025-12-25
Posts: 1,119

Re: [SOLVED] printf with alignment

[shridhyan@archlinux ~]$ export LC_ALL=C.UTF-8
s='├─sda1'
echo "chars: ${#s}   bytes: $(printf '%s' "$s" | wc -c)"
chars: 6   bytes: 10
[shridhyan@archlinux ~]$ 
COLS=60
BREAK="$(printf '%*s' "$COLS" '' | tr ' ' '#')"

mapfile -t lsblk_output < <(lsblk)

printf '%s\n' "$BREAK"

for line in "${lsblk_output[@]}"; do
    w=${#line}
    pad=$((COLS - 3 - w))
    ((pad < 0)) && pad=0

    printf "# %s%*s#\n" "$line" "$pad" ''
done

printf '%s\n' "$BREAK"
############################################################
# NAME        MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS       #
# zram0       253:0    0   2.8G  0 disk  [SWAP]            #
# nvme0n1     259:0    0 476.9G  0 disk                    #
# ├─nvme0n1p1 259:1    0     1G  0 part  /boot             #
# ├─nvme0n1p2 259:2    0 347.9G  0 part                    #
# │ └─root    254:0    0 347.9G  0 crypt /var/log          #
# │                                      /home             #
# │                                      /var/cache/pacman/pkg#
# │                                      /                 #
# ├─nvme0n1p3 259:3    0 123.9G  0 part                    #
# └─nvme0n1p4 259:4    0   4.1G  0 part                    #
############################################################

HAHA

or

COLS=60
WIDTH=$((COLS - 4))

BREAK=$(printf '%*s' "$COLS" '' | tr ' ' '#')

printf '%s\n' "$BREAK"

while IFS= read -r line; do
    printf "# %-*s #\n" "$WIDTH" "$line"
done < <(lsblk)

printf '%s\n' "$BREAK"

I just need to remember that field width is a minimum, not a maximum. Longer lines will still overflow. hmm

Edit:
missed responses...

Edit2:
Bash has "%ls" right? LoL

[shridhyan@archlinux ~]$
COLS=60
WIDTH=$((COLS - 4))

printf -v BREAK '%*s' "$COLS" ''
BREAK=${BREAK// /#}

printf '%s\n' "$BREAK"

while IFS= read -r line; do
    printf '# %-*.*ls #\n' "$WIDTH" "$WIDTH" "$line"
done < <(lsblk)

printf '%s\n' "$BREAK"
############################################################
# NAME        MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS       #
# zram0       253:0    0   2.8G  0 disk  [SWAP]            #
# nvme0n1     259:0    0 476.9G  0 disk                    #
# ├─nvme0n1p1 259:1    0     1G  0 part  /boot             #
# ├─nvme0n1p2 259:2    0 347.9G  0 part                    #
# │ └─root    254:0    0 347.9G  0 crypt /var/log          #
# │                                      /home             #
# │                                      /var/cache/pacman #
# │                                      /                 #
# ├─nvme0n1p3 259:3    0 123.9G  0 part                    #
# └─nvme0n1p4 259:4    0   4.1G  0 part                    #
############################################################

Last edited by 5hridhyan (Today 15:47:54)


You'll definitely find me talking to a wall wink

Offline

#9 Today 15:51:30

mpan
Member
Registered: 2012-08-01
Posts: 1,672
Website

Re: [SOLVED] printf with alignment

Seth is correct: format specifiers in `printf` work on bytes,⁽¹⁾⁽²⁾⁽³⁾ not glyphs. Assuming UTF-8, glyphs like ‘├’ and ‘─’ are 3 bytes.⁽⁴⁾

There is no reliable method of solving this in a general case, other than calculating character length of each line and using that in calculations. Bash has built-in, locale-aware string length subsitution: `${#param}`.⁽⁵⁾⁽⁶⁾ You may try to match a specific situation, but it is bound to fail the moment your assumptions about the data are not met.

The original problem stems from an attempt to process data meant for humans, not computer processing.

____
⁽¹⁾ printf(1p)
⁽²⁾ fprintf(3p)
⁽³⁾ POSIX.1-2017, Chapter 5 (in 2024)
⁽⁴⁾ U+251C, encoded as (0xE2, 0x94, 0x9C), and U+2500, encoded as (0xE2, 0x94, 0x80).
⁽⁵⁾ bash(1): Parameter length substitution
⁽⁶⁾ Bash: 3.5.3 Shell Parameter Expansion

Last edited by mpan (Today 15:52:54)

Offline

#10 Today 16:48:14

5hridhyan
Member
Registered: 2025-12-25
Posts: 1,119

Re: [SOLVED] printf with alignment

@mpan I agree that `printf` field width isn't a terminal-column-width mechanism. However, `%ls` seems to behave differently from `%s` here: it performs wide-character conversion, and its precision and field width operate on wide characters rather than UTF-8 bytes.

*It isn't intended as a general Unicode display-width solution.

And I tried another approach...

export LC_ALL=C.UTF-8

COLS=60
WIDTH=$((COLS - 4))                    
BREAK=$(printf '%*s' "$COLS" '' | tr ' ' '#')

printf '%s\n' "$BREAK"

while IFS= read -r line; do
    line=${line:0:WIDTH}               
    w=${#line}
    pad=$((WIDTH - w))
    printf '# %s%*s #\n' "$line" "$pad" ''
done < <(lsblk)

printf '%s\n' "$BREAK"

Here:
* `${line:0:WIDTH}` truncates by characters rather than bytes, so it shouldn't cut a multibyte UTF-8 character such as `├` in half...
* `${#line}` counts characters under the utf-8 locale

Which ofc,  still assumes each character occupies one terminal column, and it isn't a general solution for arbitrary Unicode. But it works for this particular `lsblk` output...

Last edited by 5hridhyan (Today 16:48:50)


You'll definitely find me talking to a wall wink

Offline

#11 Today 17:03:08

Bedna
Member
Registered: 2025-11-14
Posts: 52
Website

Re: [SOLVED] printf with alignment

So I was on the right track with bytes vs characters.
I actually thought about ${#line} when in bed trying to go to sleep last night, I just forgot about it!

This is the final code making sure $BREAK is as short as possible while making sure there is room for the longest $line:

width=0
while read line; do
  if [ "${#line}" -gt "$width" ]; then
    width="${#line}"
    COLS="$((${#line} + 4))"
  fi
done < <(lsblk)

BREAK="$(printf "%${COLS}s" | tr ' ' '#')"
echo "$BREAK"
while read line; do
  printf "# %s%$((COLS - ${#line} - 4))s #\n" "$line"
done < <(lsblk)
echo "$BREAK"

Thank you all for helping me out finding a solution!

Last edited by Bedna (Today 17:08:08)

Offline

Board footer

Powered by FluxBB