#!/bin/sh # The MIT License (MIT) # # Copyright © 2024 pacman64 # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # sbs [column count...] [filepaths...] # # Side-By-Side lays out lines read from all inputs given into several columns, # separating them with a special symbol. If no named inputs are given, lines # are read from the standard input, instead of files. # # If a column-count isn't given, the script tries to find the most columns # which can fit a reasonable width-limit; when even a single column can't fit # that limit, it simply emits all lines, which is the same as using 1 column. case "$1" in -h|--h|-help|--help) awk '/^# +sbs/, /^$/ { gsub(/^# ?/, ""); print }' "$0" exit 0 ;; esac num_columns=0 if [ "$(echo "$1" | grep -E '^[+-]?[0-9]+$' 2> /dev/null)" ]; then num_columns="$1" shift fi awk ' # ignore leading UTF-8 BOMs (byte-order marks) FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } # carriage-returns will ruin side-by-side output, so remove them { gsub(/\r$/, ""); print } ' "$@" | # before laying out lines side-by-side, expand all tabs, using 4 as the # tabstop width expand -t 4 | awk -v num_columns="${num_columns}" ' BEGIN { sep = " █" sep_width = width(sep) total_max_width = 79 max_auto_cols = 26 # num_columns = 0 # detect a leading number, and use it as the max-number of columns if (ARGV[1] + 0 != 0) { num_columns = ARGV[1] + 0 delete ARGV[1] } } # remember all lines; carriage-returns are already removed { lines[NR] = $0 } # width counts items in the string given, ignoring ANSI-style sequences function width(s) { gsub(/\x1b\[([0-9]*[A-HJKST]|[0-9;]*m)/, "", s) return length(s) } # pick a line using a row-column pair as an index function pick(lines, height, row, col) { return lines[(col - 1) * height + row] } # see if a list of lines can fit the number of columns given exactly function fits(lines, line_count, num_cols, height, max_widths, i, j, w, l) { height = ceil(line_count / num_cols) # prevent too many empty columns if (height * num_cols - line_count >= height) { return 0 } for (i = 1; i <= height; i++) { for (j = 1; j <= num_cols; j++) { w = width(pick(lines, height, i, j)) if (w > total_max_width) return 0 if (max_widths[j] < w) max_widths[j] = w } } for (i = 1; i <= height; i++) { w = 0 for (j = 1; j <= num_cols; j++) { if (j > 1) w += sep_width l = pick(lines, height, i, j) if (j > 1 && l != "") w++ w += width(l) if (j < num_cols) { pad = max_widths[j] - width(l) if (pad > 0) { if (l == "") pad++ w += pad } } if (w > total_max_width) return 0 } } return 1 } # auto-detect an appropriate column-count for the list of lines given function find_max_fit(lines, num_columns) { if (!fits(lines, NR, 1)) { return 1 } for (num_columns = max_auto_cols; num_columns >= 2; num_columns--) { if (fits(lines, NR, num_columns)) { return num_columns } } return 1 } # round up non-integers function ceil(n) { return (n % 1) ? n - (n % 1) + 1 : n } END { # prevent errors when input has no lines if (NR < 1) exit # auto-fit when given a non-positive count, or when not given a count if (num_columns < 1) num_columns = find_max_fit(lines) # prevent column-count from exceeding the number of input lines if (num_columns > NR) num_columns = NR # emit a single column as lines, quitting right away if (num_columns < 2) { for (i in lines) print lines[i] exit } # prevent a trailing empty column num_lines = ceil(NR / num_columns) if (num_lines * num_columns - NR >= num_lines) { num_columns-- } num_lines = ceil(NR / num_columns) for (i = 1; i <= num_lines; i++) { for (j = 1; j <= num_columns; j++) { w = width(pick(lines, num_lines, i, j)) if (max_widths[j] < w) max_widths[j] = w } } widest = 0 for (i in max_widths) { if (widest < max_widths[i]) widest = max_widths[i] } # make enough spaces to hand-pad lines later; these spaces can exceed # the max-count needed, since they are always subsliced when used later spaces = " " nspaces = length(spaces) while (nspaces < widest) { spaces = spaces spaces nspaces *= 2 } # emit lines side by side for (i = 1; i <= num_lines; i++) { for (j = 1; j <= num_columns; j++) { l = pick(lines, num_lines, i, j) if (j > 1) { printf "%s", sep if (j != num_columns || l != "") printf " " } printf "%s", l if (j < num_columns) { pad = max_widths[j] - width(l) if (pad > 0) { s = substr(spaces, 1, pad) printf "%s", s } } } print "" } } '