File: bsbs.sh 1 #!/bin/sh 2 3 # The MIT License (MIT) 4 # 5 # Copyright (c) 2026 pacman64 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the "Software"), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be included in 15 # all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 # SOFTWARE. 24 25 26 # bsbs [column count...] [files...] 27 # 28 # 29 # Book-like Side-By-Side lays out text lines into several columns, separating 30 # them with a special symbol. This script lets you see more data at once, as 31 # monitors are wider than tall and most text content has fairly short lines. 32 # 33 # If a column-count isn't given, it's 2 by default, just like with books. If 34 # no named inputs are given, lines are read from the standard input. 35 # 36 # Hint: you can make handy aliases for this tool 37 # 38 # alias 1='bsbs 1' 39 # alias 2='bsbs 2' 40 # alias 3='bsbs 3' 41 # alias 4='bsbs 4' 42 # alias 5='bsbs 5' 43 # alias 6='bsbs 6' 44 # alias 7='bsbs 7' 45 # alias 8='bsbs 8' 46 # alias 9='bsbs 9' 47 48 49 case "$1" in 50 -h|--h|-help|--help) 51 awk '/^# +bsbs /, /^$/ { gsub(/^# ?/, ""); print }' "$0" 52 exit 0 53 ;; 54 esac 55 56 if [ $# -eq 0 ] && [ ! -p /dev/stdin ]; then 57 awk '/^# +bsbs /, /^$/ { gsub(/^# ?/, ""); print }' "$0" 58 printf "\e[38;2;204;0;0mno files given and stdin not being piped\e[0m\n" 59 exit 0 60 fi 61 62 num_columns=2 63 if echo "$1" | grep -q -E '^[+-]?[0-9]+$'; then 64 num_columns="$1" 65 shift 66 fi 67 68 [ "$1" = '--' ] && shift 69 70 if [ "${num_columns}" -lt 1 ]; then 71 num_columns=1 72 fi 73 74 # use the current screen height 75 height="$(tput -T xterm lines)" 76 77 if [ "${height}" -lt 2 ]; then 78 printf "\e[38;2;204;0;0mscreen/window is too short\e[0m\n" >&2 79 exit 1 80 fi 81 82 # show all non-existing files given 83 failed=0 84 for arg in "$@"; do 85 if [ "${arg}" = "-" ]; then 86 continue 87 fi 88 if [ ! -e "${arg}" ]; then 89 printf "\e[38;2;204;0;0mno file named \"%s\"\e[0m\n" "${arg}" >&2 90 failed=1 91 fi 92 done 93 94 # in case of errors, avoid showing an empty screen 95 if [ "${failed}" -gt 0 ]; then 96 exit 2 97 fi 98 99 # allow loading lines from multiple files, ensuring no lines are accidentally 100 # joined across inputs 101 awk 1 "$@" | 102 103 # ignore leading UTF-8 BOMs (byte-order marks) and trailing carriage-returns: 104 # the latter in particular will ruin side-by-side output; doing it separately 105 # using `sed` is faster overall for the script 106 sed 's-^\xef\xbb\xbf--; s-\r$--' | 107 108 # before laying out lines side-by-side, expand all tabs 109 expand -t 4 | 110 111 # lay things side-by-side, like pages/faces in a book 112 awk -v num_columns="${num_columns}" -v height="${height}" ' 113 BEGIN { 114 inner_rows = height - 2 115 } 116 117 # remember all lines; assumes carriage-returns are already removed 118 { 119 p = NR - 1 120 lines[p] = $0 121 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "") 122 widths[p] = length($0) 123 } 124 125 # round up non-integers 126 function ceil(n) { 127 return (n % 1) ? n - (n % 1) + 1 : n 128 } 129 130 END { 131 # if a single column is enough for all lines, just show lines as given 132 if (NR <= inner_rows) { 133 for (i = 0; i < NR; i++) print lines[i] 134 exit 135 } 136 137 # avoid empty trailing columns 138 if (NR < inner_rows * num_columns) num_columns = ceil(NR / inner_rows) 139 # ensure number of columns is valid 140 if (num_columns < 1) num_columns = 1 141 142 for (i = 0; i < NR; i += inner_rows * num_columns) { 143 for (j = 0; j < inner_rows; j++) { 144 for (k = 0; k < num_columns; k++) { 145 w = widths[i + k * inner_rows + j] 146 if (max_widths[k] < w) max_widths[k] = w 147 } 148 } 149 } 150 151 widest = 0 152 for (i in max_widths) { 153 if (widest < max_widths[i]) widest = max_widths[i] 154 } 155 156 total_max_width = 0 157 for (i = 0; i < num_columns; i++) { 158 total_max_width += max_widths[i] 159 } 160 # also count separators, which are 3-items wide 161 if (num_columns > 0) total_max_width += 3 * (num_columns - 1) 162 163 # make a separator wide enough to match the length of any output line 164 bottom_sep = "································" 165 for (nsep = 32; nsep < total_max_width; nsep *= 2) { 166 bottom_sep = bottom_sep bottom_sep 167 } 168 # separator is used directly, so match the needed width exactly 169 bottom_sep = substr(bottom_sep, 1, total_max_width) 170 171 # emit lines side by side 172 for (i = 0; i < NR; i += inner_rows * num_columns) { 173 # emit a page-bottom/separator line between pages of columns 174 if (i > 0) print bottom_sep 175 176 for (j = 0; j < inner_rows; j++) { 177 # bottom-pad last page-pair with empty lines, so page-scrolling 178 # on viewers like `less` stays in sync with the page boundaries 179 if (NR - i - j <= 0) { 180 print "" 181 continue 182 } 183 184 for (k = 0; k < num_columns; k++) { 185 p = i + k * inner_rows + j 186 l = lines[p] 187 188 if (k > 0) { 189 printf "\x1b[0m █" 190 if (k != num_columns - 1 || l != "") printf " " 191 } 192 193 printf "%s", l 194 195 if (k < num_columns - 1) { 196 pad = max_widths[k] - widths[p] 197 if (pad > 0) printf "%*s", pad, "" 198 } 199 } 200 201 print "\x1b[0m" 202 } 203 } 204 205 # end last page with an empty line, instead of the usual page-separator 206 if (NR > 0) print "" 207 } 208 ' | 209 210 # view the result interactively 211 less -MKiCRS