File: bsbs.sh 1 #!/bin/sh 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2020-2025 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...] [filepaths...] 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 37 case "$1" in 38 -h|--h|-help|--help) 39 awk '/^# +bsbs /, /^$/ { gsub(/^# ?/, ""); print }' "$0" 40 exit 0 41 ;; 42 esac 43 44 if [ $# -eq 0 ] && [ ! -p /dev/stdin ]; then 45 awk '/^# +bsbs /, /^$/ { gsub(/^# ?/, ""); print }' "$0" 46 printf "\e[32mno files given and stdin not being piped into\e[0m\n" 47 exit 0 48 fi 49 50 num_columns=2 51 if [ "$(echo "$1" | grep -E '^[+-]?[0-9]+$' 2> /dev/null)" ]; then 52 num_columns="$1" 53 shift 54 fi 55 56 if [ "${num_columns}" -lt 1 ]; then 57 num_columns=1 58 fi 59 60 # use the current screen height 61 height="$(tput lines)" 62 63 if [ "${height}" -lt 2 ]; then 64 printf "screen/window isn't tall enough to show content\n" > /dev/stderr 65 exit 1 66 fi 67 68 # show all non-existing files given 69 failed=0 70 for arg in "$@"; do 71 if [ "${arg}" = "-" ]; then 72 continue 73 fi 74 if [ ! -e "${arg}" ]; then 75 printf "\e[31mno file named \"%s\"\e[0m\n" "${arg}" > /dev/stderr 76 failed=1 77 fi 78 done 79 80 # avoid showing an empty screen due to errors 81 if [ "${failed}" -gt 0 ]; then 82 exit 1 83 fi 84 85 # allow loading lines from multiple files, ensuring no lines are accidentally 86 # joined across inputs 87 awk 1 "$@" | 88 89 # ignore leading UTF-8 BOMs (byte-order marks) and trailing carriage-returns: 90 # the latter in particular will ruin side-by-side output 91 sed 's-^\xef\xbb\xbf--; s-\r$--' | 92 93 # before laying out lines side-by-side, expand all tabs, using 4 as the 94 # tabstop width 95 expand -t 4 | 96 97 # lay things side-by-side, like pages/faces in a book 98 awk -v num_columns="${num_columns}" -v height="${height}" ' 99 BEGIN { 100 sep = " █" 101 sep_width = 2 102 height -= 2 103 } 104 105 # remember all lines; carriage-returns are already removed 106 { lines[NR - 1] = $0 } 107 108 # width counts items in the string given, ignoring ANSI-style sequences 109 function width(s) { 110 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", s) 111 return length(s) 112 } 113 114 # pick a line using a row-column pair as an index 115 function pick(page, row, col) { 116 return lines[page + col * height + row] 117 } 118 119 # round up non-integers 120 function ceil(n) { 121 return (n % 1) ? n - (n % 1) + 1 : n 122 } 123 124 END { 125 # if a single column is enough for all lines, just show lines as they are 126 if (NR <= height) { 127 for (i = 0; i < NR; i++) print lines[i] 128 exit 129 } 130 131 # avoid empty trailing columns 132 if (NR < height * num_columns) num_columns = ceil(NR / height) 133 # ensure number of columns is valid 134 if (num_columns < 1) num_columns = 1 135 136 for (i = 0; i < NR; i += height * num_columns) { 137 for (j = 0; j < height; j++) { 138 for (k = 0; k < num_columns; k++) { 139 # w = width(pick(i, j, k)) 140 # w = width(lines[i + k * height + j]) 141 l = lines[i + k * height + j] 142 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", l) 143 w = length(l) 144 if (max_widths[k] < w) max_widths[k] = w 145 } 146 } 147 } 148 149 widest = 0 150 for (i in max_widths) { 151 if (widest < max_widths[i]) widest = max_widths[i] 152 } 153 154 total_max_width = 0 155 for (i = 0; i < num_columns; i++) { 156 if (i > 0) total_max_width += sep_width + 1 157 total_max_width += max_widths[i] 158 } 159 160 # make a separator wide enough to match the length of any output line 161 bottom_sep = "································" 162 nsep = length(bottom_sep) 163 while (nsep < total_max_width) { 164 bottom_sep = bottom_sep bottom_sep 165 nsep *= 2 166 } 167 # separator is used directly, so match the needed width exactly 168 bottom_sep = substr(bottom_sep, 1, total_max_width) 169 170 # emit lines side by side 171 for (i = 0; i < NR; i += height * num_columns) { 172 # emit a page-bottom/separator line between pages of columns 173 if (i > 0) print bottom_sep 174 175 for (j = 0; j < height; j++) { 176 # bottom-pad last page-pair with empty lines, so page-scrolling 177 # on viewers like `less` stays in sync with the page boundaries 178 if (NR - i - j <= 0) { 179 print "" 180 continue 181 } 182 183 for (k = 0; k < num_columns; k++) { 184 # l = pick(i, j, k) 185 l = lines[i + k * height + j] 186 187 if (k > 0) { 188 printf "%s", sep 189 if (k != num_columns - 1 || l != "") printf " " 190 } 191 192 printf "%s", l 193 194 if (k < num_columns - 1) { 195 # pad = max_widths[k] - width(l) 196 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", l) 197 pad = max_widths[k] - length(l) 198 if (pad > 0) printf "%*s", pad, "" 199 } 200 } 201 202 print "" 203 } 204 } 205 206 # end last page with an empty line, instead of the usual page-separator 207 if (NR > 0) print "" 208 } 209 ' | 210 211 # view the result interactively 212 less -JMKiCRS