#!/bin/sh # The MIT License (MIT) # # Copyright (c) 2026 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 -q -E '^[+-]?[0-9]+$'; then num_columns="$1" shift fi fancy_sep=1 case "$1" in -no-sep|--no-sep|-s|--s|-simple|--simple) fancy_sep=0 shift ;; esac [ "$1" = '--' ] && shift # show all non-existing files given failed=0 for arg in "$@"; do if [ "${arg}" = "-" ]; then continue fi if [ ! -e "${arg}" ]; then printf "\e[38;2;204;0;0mno file named \"%s\"\e[0m\n" "${arg}" >&2 failed=1 fi done # in case of errors, avoid showing an empty screen if [ "${failed}" -gt 0 ]; then exit 2 fi command='awk' if [ -e /usr/bin/gawk ]; then command='gawk' fi # allow loading lines from multiple files, ensuring no lines are accidentally # joined across inputs awk 1 "$@" | # ignore leading UTF-8 BOMs (byte-order marks) and trailing carriage-returns: # the latter in particular will ruin side-by-side output; doing it separately # using `sed` is faster overall for the script sed 's-^\xef\xbb\xbf--; s-\r$--' | # before laying out lines side-by-side, expand all tabs, using 4 as the # tabstop width expand -t 4 | # lay lines into side-by-side columns ${command} -v fancy="${fancy_sep}" -v num_columns="${num_columns}" ' BEGIN { sep = fancy ? " █" : " " 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-Za-z]/, "", 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 = 1; i <= NR; i++) 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] } # 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) printf "%*s", pad, "" } } print "" } } ' | # view the result interactively less -MKiCRS