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