You are not logged in.
Pages: 1
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
while read line; do printf "# %-60s%s\n" "$line" '#'; done < <(lsblk)Online
while read line; do printf "# %-60s%s\n" "$line" '#'; done < <(lsblk)
Gets the exact same result, ie the right is not aligned.
Offline
Oh, it's bash… zsh does what you expect ![]()
Online
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
coreutils /usr/bin/printf acts like bash - the trigger are probably the line drawing elements ![]()
Online
It's the drawing char output from lsblk.
If using:
-i, --ascii use ascii characters only
-l, --list use list format outputthen 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
[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. ![]()
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 ![]()
Offline
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
@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 ![]()
Offline
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
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 ![]()
Offline
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
@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. ![]()
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";
doneShould 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. ![]()
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 (Today 19:25:29)
Offline
more appropiate and convenient solution
lsblk | awk '{printf "# %-60s#\n", $0;}'Online
Pages: 1