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 ' "$@"