You are not logged in.

#1 Yesterday 14:00:03

Bedna
Member
Registered: 2025-11-14
Posts: 54
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 (Yesterday 17:06:13)

Offline

#2 Yesterday 14:20:41

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

Re: [SOLVED] printf with alignment

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

Offline

#3 Yesterday 14:51:33

Bedna
Member
Registered: 2025-11-14
Posts: 54
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 Yesterday 14:58:40

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

Re: [SOLVED] printf with alignment

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

Offline

#5 Yesterday 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 Yesterday 15:18:08

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

Re: [SOLVED] printf with alignment

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

Offline

#7 Yesterday 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 (Yesterday 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 Yesterday 15:28:21

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

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 (Yesterday 15:47:54)


You'll definitely find me talking to a wall wink

Offline

#9 Yesterday 15:51:30

mpan
Member
Registered: 2012-08-01
Posts: 1,673
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 (Yesterday 15:52:54)

Offline

#10 Yesterday 16:48:14

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

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 (Yesterday 16:48:50)


You'll definitely find me talking to a wall wink

Offline

#11 Yesterday 17:03:08

Bedna
Member
Registered: 2025-11-14
Posts: 54
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 (Yesterday 17:08:08)

Offline

#12 Yesterday 17:15:37

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

Re: [SOLVED] printf with alignment

Interesting, any particular reason for using two passes over lsblk instead of capturing its output with mapfile?


You'll definitely find me talking to a wall wink

Offline

#13 Yesterday 17:47:02

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

Re: [SOLVED] printf with alignment

5hridhyan wrote:

Interesting, any particular reason for using two passes over lsblk instead of capturing its output with mapfile?

Good point, I'll change to using mapfile and for loops instead.

Offline

#14 Yesterday 19:24:07

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

Re: [SOLVED] printf with alignment

5hridhyan wrote:

@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. (…)

I was writing my post when you sent yours. I didn’t see it. smile

You are correct and, as long as we talk about Arch Linux only, I’d agree this may be the preferred solution. However, in general,⁽¹⁾ it’s more complicated.

The concept of a wide character set is so broken, nonportable, and full of traps, that even the C committee facepalmed and in 2011 introduced `char32_t` to amend the mess. Ignoring hypotheticals and exotics of non-Unicode `wchar_t`, in practice it may be one of: UTF-32, UTF-16, or UCS-2. Not necessarily correctly or fully implemented either.

I believe in 2026 all Linux distros use UTF-32. So all Archers and other penguin riders are safe, as long as we talk about glibc-based code and anything compatible. The moment we leave this safe area, monsters may await. I was fighting my first programming battles in the late 1990s and early 2000s, so I certainly am biased towards the even more enormous nonsense people encounter nowadays. Take this into account. But don’t blindly trust wide characters in general.

Also mind that both my and your solutions are still broken. Regardless of `wchar_t` mess, but merely because Unicode is variable-width encoding even with UTF-32. Here is a showcase

printf '\x6F\n\xC3\xB6\n\x6F\xCC\x88\n' | while read line; do
    printf '(% 2ls)\n' "$line";
done

Should we address this in this thread? In my opinion: no. There is a limit in how far we should go pruning shortcomings of an approach that would rather be replaced with a more appropiate and convenient solution. wink

Let’s not talk about glyphs that don’t fit into a single cell, a problem you also already mentioned. This is an entire separate can of worms.

____
⁽¹⁾ Any answer on this forum should be for Arch. But that doesn’t mean it can’t care about portability or warning about trouble the user may encounter elsewhere. As long as Arch is the primary focus.

Last edited by mpan (Yesterday 19:25:29)

Offline

#15 Yesterday 19:28:44

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

Re: [SOLVED] printf with alignment

more appropiate and convenient solution

lsblk | awk '{printf "# %-60s#\n", $0;}'

Offline

#16 Today 02:20:04

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

Re: [SOLVED] printf with alignment

@mpan
Indeed. IMO, software engineering isn't exclusively about finding the mathematically perfect, universally portable implementation wink
Just analyzing engineering trade-offs... like How broad is the intended use case? What assumptions are reasonable? How much complexity does additional robustness introduce? Does that complexity actually benefit the intended user? the usual yada yada yada lol

I was just trying to find an approach that would be visually convincing for a particular case... which was *just* `lsblk` here smile


Edit:
@seth I think I and OP wanted a complete box like to cover top, bottom, and both sides. I think the elegant solution would be

lsblk | awk '{L[NR]=$0; if(length($0)>w)w=length($0)} END{b="";for(i=0;i<w+4;i++)b=b"#";print b;for(i=1;i<=NR;i++)printf "# %-*.*s #\n",w,w,L[i];print b}'

wink

Or if one is happy w/ a fixed width of 60 (no need to buffer all lines):

lsblk | awk 'BEGIN{for(i=0;i<64;i++)b=b"#";print b}{printf "# %-60.60s #\n",$0}END{print b}'

Last edited by 5hridhyan (Today 02:22:43)


You'll definitely find me talking to a wall wink

Offline

#17 Today 08:02:18

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

Re: [SOLVED] printf with alignment

printf '%0.1s' "#"{1..60}; echo; lsblk

Offline

#18 Today 10:36:44

dimich
Member
From: Kharkiv, Ukraine
Registered: 2009-11-03
Posts: 834

Re: [SOLVED] printf with alignment

#!/usr/bin/env bash

mapfile -t output < <(lsblk)

declare -a lens

max=0
for i in "${!output[@]}"; do
    len=${#output[$i]}
    lens[$i]=$len
    [[ $len -gt $max ]] && max=$len
done

hline=$(for ((i=0; i<$max; i++)); do printf '─'; done)

printf '┌%s┐\n' "$hline"

for i in "${!output[@]}"; do
    printf '│%s%*s│\n' "${output[$i]}" $(($max - ${lens[i]})) ''
done

printf '└%s┘\n' "$hline"

This works only if terminal and bash use same rules for grapheme cluster segmentation.

Last edited by dimich (Today 10:39:03)

Offline

#19 Today 10:38:46

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

Re: [SOLVED] printf with alignment

Even though

lsblk | awk '{L[NR]=$0; if(length($0)>w)w=length($0)} END{b="";for(i=0;i<w+4;i++)b=b"#";print b;for(i=1;i<=NR;i++)printf "# %-*.*s #\n",w,w,L[i];print b}'

works exactly like how I want, I also have no clue as to how it works, and I DO need to understand the code I use, or I could use AI instead of learning. xD

The line

lsblk | awk '{printf "# %-60s#\n", $0;}'

is clean (and I understand it) and does produce something I could potentially work with, but I'm not sure I can make the width modular with a variable with awk?
Maybe what I should take from this is that it's time for me to learn awk. smile

Edit.
Snippet I currently decided to go with (unless something else comes up here in the thread)

do_loop # function in the script that loops an img file

LOOP=/dev/loop0 # this variable is set in the do_loop function, I just added it here for clarity
IMG_FILE=/some/path/to/file.img # this is provided by user and set at the start of the script, just here for clarity
# All colors are also set in another part of the script, just here for clarity
White='\033[0;37m' # white
IWhite='\033[0;97m' # high intensity white
Green='\033[0;92m' # high intensity green
Purple='\033[0;95m' # high intensity purple
NC='\033[0m' # Text Reset - No colors

echo -e "${White}## ${Green}$IMG_FILE ${IWhite}is looped to ${Green}$LOOP"
mapfile -t looprun_output < <(lsblk $LOOP)
width=0
for line in "${lsblk_output[@]}"; do
  [ "${#line}" -gt "$width" ] && width="${#line}" COLS="$((${#line} + 4))"
done
BREAK="$(printf "%${COLS}s" | tr ' ' '#')"
echo -e "${Purple}$BREAK"
for line in "${looprun_output[@]}"; do
  printf "# %b%*s ${Purple}#\n" "${Green}$line" "$((COLS - ${#line} - 4))" ''
done
echo -e "${Purple}$BREAK"
echo -e "${White}## ${Green}Done."
echo -e "${White}## ${IWhite}To remove loop, type: ${Green}sudo losetup -d ${LOOP}${NC}"

Last edited by Bedna (Today 12:00:49)

Offline

#20 Today 13:20:19

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

Re: [SOLVED] printf with alignment

I also have no clue as to how it works, and I DO need to understand the code I use, or I could use AI instead of learning.

Why fear when a hooman is here? wink

lsblk | awk '{
    L[NR]=$0;                          # store each line in array L (indexed by line number NR)
    if(length($0)>w) w=length($0)      # track the longest line length in w
}
END {
    b="";
    for(i=0;i<w+4;i++) b=b"#"          # build border: w content + "# " + " #" = w+4 '#'s
    print b;                           # top border
    for(i=1;i<=NR;i++)
        printf "# %-*.*s #\n", w, w, L[i]   # each row: "# " + left-justified, truncated-to-w line or "left-justify each line and pad it to width w"
    print b                            # bottom border
}'

In short: it takes lsblk output into memory(Sam Altman's fetish btw), remembering the longest line width (w)
END runs once after all input has been processed. It builds a # border exactly w+4 characters wide, prints the top border, then prints every stored line padded to w and framed by # ... #, followed by the bottom border.

What would a hooman see? A neatly aligned # box, with all four sides lining up.

Last edited by 5hridhyan (Today 13:37:35)


You'll definitely find me talking to a wall wink

Offline

Board footer

Powered by FluxBB