File: chopleads.sh
   1 #!/bin/sh
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright (c) 2026 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 # chopleads [options...] [files...]
  27 #
  28 # Ignore the longest common leading parts of all non-empty lines. A typical
  29 # use case is when dealing with lines starting with full filenames, where
  30 # all files come from the same subfolder, as getting rid of the long common
  31 # starts can vastly reduce the clutter.
  32 #
  33 # The options are, available both in single and double-dash versions
  34 #
  35 #   -h, -help    show this help message
  36 
  37 
  38 buf=0
  39 
  40 while [ $# -gt 0 ]; do
  41     case "$1" in
  42         -b|--b|-buffered|--buffered) buf=1; shift; continue ;;
  43 
  44         -h|--h|-help|--help)
  45             awk '/^# +chopleads /, /^$/ { gsub(/^# ?/, ""); print }' "$0"
  46             exit 0
  47         ;;
  48 
  49         -) break ;;
  50 
  51         --) shift; break ;;
  52 
  53         -*)
  54             printf "unsupported option '%s'\n" "$1" >&2
  55             exit 1
  56         ;;
  57     esac
  58 
  59     break
  60 done
  61 
  62 # show all non-existing files given
  63 failed=0
  64 for arg in "$@"; do
  65     [ "${arg}" = "-" ] && continue
  66     [ -e "${arg}" ] && continue
  67     printf "no file named \"%s\"\n" "${arg}" >&2
  68     failed=1
  69 done
  70 
  71 [ "${failed}" -gt 0 ] && exit 2
  72 
  73 flush=0
  74 if [ "${buf}" -eq 0 ] && { [ -p /dev/stdout ] || [ -t 1 ]; }; then
  75     flush=1
  76 fi
  77 
  78 awk -v flush="${flush}" '
  79     BEGIN { leadlen = -1 }
  80 
  81     function dump() {
  82         for (i = 1; i <= kept; i++) {
  83             print lines[i]
  84             delete lines[i]
  85             if (flush) fflush()
  86         }
  87 
  88         # prevent output of many empty lines at the end
  89         kept = 0
  90     }
  91 
  92     {
  93         if (leadlen == 0) {
  94             dump()
  95             print
  96             if (flush) fflush()
  97             next
  98         }
  99 
 100         lines[++kept] = $0
 101     }
 102 
 103     {
 104         if ($0 == "") next
 105 
 106         # the full first non-empty line becomes the temporary lead
 107         if (leadlen < 0) {
 108             lead = $0
 109             first = substr(lead, 1, 1)
 110             leadlen = length
 111             next
 112         }
 113 
 114         # avoid loops if very start of a line implies no common lead
 115         if (first != substr($0, 1, 1)) {
 116             lead = ""
 117             leadlen = 0
 118             dump()
 119             next
 120         }
 121 
 122         # common lead never exceeds the shortest non-empty line
 123         l = length
 124         if (leadlen > l) {
 125             leadlen = l
 126             lead = substr(lead, 1, leadlen)
 127         }
 128 
 129         while (leadlen > 0) {
 130             s = substr($0, 1, leadlen)
 131             if (s == lead) {
 132                 lead = s
 133                 break
 134             }
 135 
 136             leadlen--
 137             lead = substr(lead, 1, leadlen)
 138         }
 139 
 140         # if no leads are common, show all previous lines right away
 141         if (leadlen == 0) dump()
 142     }
 143 
 144     END {
 145         # with only 1 line of input, avoid emitting a single empty line
 146         # for sure
 147         skip = (NR == 1) ? 0 : leadlen + 1
 148 
 149         for (i = 1; i <= kept; i++) {
 150             line = lines[i]
 151             if ((skip > 0) && (line != "")) line = substr(line, skip)
 152             print line
 153         }
 154     }
 155 ' "$@"