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 [options...] [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 # The options are, available both in single and double-dash versions 49 # 50 # -h, -help show this help message 51 # -sep use next argument as the column-separator symbol/string 52 # -t, -tab, -tabs use a single tab as the column-separator 53 54 55 sep='█' 56 57 while [ $# -gt 0 ]; do 58 case "$1" in 59 -h|--h|-help|--help) 60 awk '/^# +bsbs /, /^$/ { gsub(/^# ?/, ""); print }' "$0" 61 exit 0 62 ;; 63 64 -no-sep|--no-sep|-s|--s|-simple|--simple) 65 sep='' 66 shift 67 continue 68 ;; 69 70 -sep|--sep) 71 if [ $# -lt 2 ]; then 72 printf "forgot the separator value\n" >&2 73 exit 1 74 fi 75 shift 76 sep="$1" 77 shift 78 continue 79 ;; 80 81 -t|--t|-tab|--tab|-tabs|--tabs) 82 sep="\t" 83 shift 84 continue 85 ;; 86 87 --) break ;; 88 89 -*) 90 printf "unsupported option '%s'\n" "$1" >&2 91 exit 1 92 ;; 93 esac 94 95 break 96 done 97 98 if [ $# -eq 0 ] && [ ! -p /dev/stdin ]; then 99 awk '/^# +bsbs /, /^$/ { gsub(/^# ?/, ""); print }' "$0" 100 printf "no files given and stdin not being piped\n" 101 exit 0 102 fi 103 104 num_columns=2 105 if echo "$1" | grep -q -E '^[+-]?[0-9]+$'; then 106 num_columns="$1" 107 shift 108 fi 109 110 if [ "${num_columns}" -lt 1 ]; then 111 num_columns=1 112 fi 113 114 [ "$1" = '--' ] && shift 115 116 # use the current screen height 117 height="$(tput -T xterm lines)" 118 119 if [ "${height}" -lt 2 ]; then 120 printf "screen/window is too short\n" >&2 121 exit 1 122 fi 123 124 # show all non-existing files given 125 failed=0 126 for arg in "$@"; do 127 [ "${arg}" = "-" ] && continue 128 [ -e "${arg}" ] && continue 129 printf "no file named \"%s\"\n" "${arg}" >&2 130 failed=1 131 done 132 133 # in case of errors, avoid showing an empty screen 134 [ "${failed}" -gt 0 ] && exit 2 135 136 # allow loading lines from multiple files, ensuring no lines are accidentally 137 # joined across inputs 138 awk 1 "$@" | 139 140 # ignore leading UTF-8 BOMs (byte-order marks) and trailing carriage-returns: 141 # the latter in particular will ruin side-by-side output; doing it separately 142 # using `sed` is faster overall for the script 143 sed 's-^\xef\xbb\xbf--; s-\r$--' | 144 145 # before laying out lines side-by-side, expand all tabs 146 expand -t 4 | 147 148 # lay things side-by-side, like pages/faces in a book 149 awk -v sep="${sep}" -v num_columns="${num_columns}" -v height="${height}" ' 150 BEGIN { 151 sep = " " sep 152 inner_rows = height - 2 153 } 154 155 # remember all lines; assumes carriage-returns are already removed 156 { 157 p = NR - 1 158 lines[p] = $0 159 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "") 160 widths[p] = length($0) 161 } 162 163 # round up non-integers 164 function ceil(n) { 165 return (n % 1) ? n - (n % 1) + 1 : n 166 } 167 168 END { 169 # if a single column is enough for all lines, just show lines as given 170 if (NR <= inner_rows) { 171 for (i = 0; i < NR; i++) print lines[i] 172 exit 173 } 174 175 # avoid empty trailing columns 176 if (NR < inner_rows * num_columns) num_columns = ceil(NR / inner_rows) 177 # ensure number of columns is valid 178 if (num_columns < 1) num_columns = 1 179 180 for (i = 0; i < NR; i += inner_rows * num_columns) { 181 for (j = 0; j < inner_rows; j++) { 182 for (k = 0; k < num_columns; k++) { 183 w = widths[i + k * inner_rows + j] 184 if (max_widths[k] < w) max_widths[k] = w 185 } 186 } 187 } 188 189 widest = 0 190 for (i in max_widths) { 191 if (widest < max_widths[i]) widest = max_widths[i] 192 } 193 194 total_max_width = 0 195 for (i = 0; i < num_columns; i++) { 196 total_max_width += max_widths[i] 197 } 198 # also count separators, which are 3-items wide 199 if (num_columns > 0) total_max_width += 3 * (num_columns - 1) 200 201 # make a separator wide enough to match the length of any output line 202 bottom_sep = "································" 203 for (nsep = 32; nsep < total_max_width; nsep *= 2) { 204 bottom_sep = bottom_sep bottom_sep 205 } 206 # separator is used directly, so match the needed width exactly 207 bottom_sep = substr(bottom_sep, 1, total_max_width) 208 209 # emit lines side by side 210 for (i = 0; i < NR; i += inner_rows * num_columns) { 211 # emit a page-bottom/separator line between pages of columns 212 if (i > 0) print bottom_sep 213 214 for (j = 0; j < inner_rows; j++) { 215 # bottom-pad last page-pair with empty lines, so page-scrolling 216 # on viewers like `less` stays in sync with the page boundaries 217 if (NR - i - j <= 0) { 218 print "" 219 continue 220 } 221 222 for (k = 0; k < num_columns; k++) { 223 p = i + k * inner_rows + j 224 l = lines[p] 225 226 if (k > 0) { 227 printf "\x1b[0m%s", sep 228 if (k != num_columns - 1 || l != "") printf " " 229 } 230 231 printf "%s", l 232 233 if (k < num_columns - 1) { 234 pad = max_widths[k] - widths[p] 235 # if (pad > 0) printf "%*s", pad, "" 236 for (l = 1; l <= pad; l++) printf " " 237 } 238 } 239 240 print "\x1b[0m" 241 } 242 } 243 244 # end last page with an empty line, instead of the usual page-separator 245 if (NR > 0) print "" 246 } 247 ' | 248 249 # view the result interactively 250 less -MKiCRS