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 # 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 -E '^[+-]?[0-9]+$' 2> /dev/null)" ]; 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 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 command='awk' 100 if [ -e /usr/bin/gawk ]; then 101 command='gawk' 102 fi 103 104 # allow loading lines from multiple files, ensuring no lines are accidentally 105 # joined across inputs 106 awk 1 "$@" | 107 108 # ignore leading UTF-8 BOMs (byte-order marks) and trailing carriage-returns: 109 # the latter in particular will ruin side-by-side output; doing it separately 110 # using `sed` is faster overall for the script 111 sed 's-^\xef\xbb\xbf--; s-\r$--' | 112 113 # before laying out lines side-by-side, expand all tabs 114 expand -t 4 | 115 116 # lay things side-by-side, like pages/faces in a book 117 ${command} -v num_columns="${num_columns}" -v height="${height}" ' 118 BEGIN { 119 inner_rows = height - 2 120 } 121 122 # remember all lines; assumes carriage-returns are already removed 123 { 124 p = NR - 1 125 lines[p] = $0 126 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "") 127 widths[p] = length($0) 128 } 129 130 # round up non-integers 131 function ceil(n) { 132 return (n % 1) ? n - (n % 1) + 1 : n 133 } 134 135 END { 136 # if a single column is enough for all lines, just show lines as given 137 if (NR <= inner_rows) { 138 for (i = 0; i < NR; i++) print lines[i] 139 exit 140 } 141 142 # avoid empty trailing columns 143 if (NR < inner_rows * num_columns) num_columns = ceil(NR / inner_rows) 144 # ensure number of columns is valid 145 if (num_columns < 1) num_columns = 1 146 147 for (i = 0; i < NR; i += inner_rows * num_columns) { 148 for (j = 0; j < inner_rows; j++) { 149 for (k = 0; k < num_columns; k++) { 150 w = widths[i + k * inner_rows + j] 151 if (max_widths[k] < w) max_widths[k] = w 152 } 153 } 154 } 155 156 widest = 0 157 for (i in max_widths) { 158 if (widest < max_widths[i]) widest = max_widths[i] 159 } 160 161 total_max_width = 0 162 for (i = 0; i < num_columns; i++) { 163 total_max_width += max_widths[i] 164 } 165 # also count separators, which are 3-items wide 166 if (num_columns > 0) total_max_width += 3 * (num_columns - 1) 167 168 # make a separator wide enough to match the length of any output line 169 bottom_sep = "································" 170 for (nsep = 32; nsep < total_max_width; nsep *= 2) { 171 bottom_sep = bottom_sep bottom_sep 172 } 173 # separator is used directly, so match the needed width exactly 174 bottom_sep = substr(bottom_sep, 1, total_max_width) 175 176 # emit lines side by side 177 for (i = 0; i < NR; i += inner_rows * num_columns) { 178 # emit a page-bottom/separator line between pages of columns 179 if (i > 0) print bottom_sep 180 181 for (j = 0; j < inner_rows; j++) { 182 # bottom-pad last page-pair with empty lines, so page-scrolling 183 # on viewers like `less` stays in sync with the page boundaries 184 if (NR - i - j <= 0) { 185 print "" 186 continue 187 } 188 189 for (k = 0; k < num_columns; k++) { 190 p = i + k * inner_rows + j 191 l = lines[p] 192 193 if (k > 0) { 194 printf "\x1b[0m █" 195 if (k != num_columns - 1 || l != "") printf " " 196 } 197 198 printf "%s", l 199 200 if (k < num_columns - 1) { 201 pad = max_widths[k] - widths[p] 202 if (pad > 0) printf "%*s", pad, "" 203 } 204 } 205 206 print "\x1b[0m" 207 } 208 } 209 210 # end last page with an empty line, instead of the usual page-separator 211 if (NR > 0) print "" 212 } 213 ' | 214 215 # view the result interactively 216 less -MKiCRS