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 (Yesterday 17:06:13)
Offline
while read line; do printf "# %-60s%s\n" "$line" '#'; done < <(lsblk)Offline
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 ![]()
Offline
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 ![]()
Offline
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 (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
[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 (Yesterday 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 (Yesterday 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 (Yesterday 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 (Yesterday 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 (Yesterday 19:25:29)
Offline
more appropiate and convenient solution
lsblk | awk '{printf "# %-60s#\n", $0;}'Offline
@mpan
Indeed. IMO, software engineering isn't exclusively about finding the mathematically perfect, universally portable implementation ![]()
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 ![]()
I was just trying to find an approach that would be visually convincing for a particular case... which was *just* `lsblk` here ![]()
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}'![]()
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 ![]()
Offline
printf '%0.1s' "#"{1..60}; echo; lsblkOffline
#!/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
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. ![]()
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
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? ![]()
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 ![]()
Offline
Pages: 1