File: hawk.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 # hawk [options...] [awk condition] [files...]
  27 #
  28 #
  29 # Highlight (lines) satisfying an AWK condition/expression, keeping other
  30 # lines the same. When not given a condition, all lines are highlighted.
  31 #
  32 # The handy case-insensitive shortcut options may cause this tool to fail,
  33 # if the main AWK tool installed doesn't support the special IGNORECASE
  34 # variable.
  35 #
  36 # The AWK options available only in single-dash versions are
  37 #
  38 #   -f fs, -F fs, -F=fs
  39 #                use `fs` for the field separator, changing the `FS` variable
  40 #
  41 # The other options are, available both in single and double-dash versions
  42 #
  43 #   -h, -help    show this help message
  44 #   -i           match regexes case-insensitively; may fail the default `awk`
  45 #   -tsv         split fields using tabs, same as using -F "\t"
  46 #
  47 #   -blue                 use a blue style
  48 #   -gray                 use a gray style
  49 #   -green                use a green style
  50 #   -orange               use an orange style
  51 #   -purple               use a purple style
  52 #   -red                  use a red style
  53 #
  54 #   -bb, -blueback        use a blue-background style
  55 #        -grayback        use a gray-background style
  56 #   -gb, -greenback       use a green-background style
  57 #   -ob, -orangeback      use an orange-background style
  58 #   -pb, -purpleback      use a purple-background style
  59 #   -rb, -redback         use a red-background style
  60 
  61 
  62 case "$1" in
  63     -h|--h|-help|--help)
  64         awk '/^# +hawk /, /^$/ { gsub(/^# ?/, ""); print }' "$0"
  65         exit 0
  66     ;;
  67 esac
  68 
  69 flush=''
  70 if [ -p /dev/stdout ] || [ -t 1 ]; then
  71     flush='if (flush) fflush()'
  72 fi
  73 
  74 tsv=0
  75 ansi='7m'
  76 case_insensitive=0
  77 command='awk'
  78 
  79 while [ $# -gt 0 ]; do
  80     arg="$1"
  81 
  82     if [ "${arg}" = "--" ]; then
  83         shift
  84         break
  85     fi
  86 
  87     case "${arg}" in
  88         -f|-F)
  89             shift
  90             if [ $# -eq 0 ]; then
  91                 printf "expected value after -F option\n" >&2
  92                 exit 1
  93             fi
  94             command="${command} -F $1"
  95             shift
  96             continue
  97         ;;
  98 
  99         -F*)
 100             command="${command} ${arg}"
 101             shift
 102             continue
 103         ;;
 104 
 105         -i|--i)
 106             case_insensitive=1
 107             shift
 108             continue
 109         ;;
 110 
 111         -tsv|--tsv)
 112             tsv=1
 113             shift
 114             continue
 115         ;;
 116 
 117         -blue|--blue)
 118             ansi='38;2;0;95;215m'
 119             shift
 120             continue
 121         ;;
 122 
 123         -bb|--bb|-blueback|--blueback)
 124             ansi='48;2;0;95;215m\x1b[38;2;238;238;238m'
 125             shift
 126             continue
 127         ;;
 128 
 129         -bold|--bold|-bolded|--bolded)
 130             ansi='1m'
 131             shift
 132             continue
 133         ;;
 134 
 135         -dim|--dim|-faint|--faint|-gray|--gray)
 136             ansi='38;2;168;168;168m'
 137             shift
 138             continue
 139         ;;
 140 
 141         -grayback|--grayback)
 142             ansi='48;2;168;168;168m\x1b[38;2;238;238;238m'
 143             shift
 144             continue
 145         ;;
 146 
 147         -green|--green)
 148             ansi='38;2;0;135;95m'
 149             shift
 150             continue
 151         ;;
 152 
 153         -gb|--gb|-greenback|--greenback)
 154             ansi='48;2;0;135;95m\x1b[38;2;238;238;238m'
 155             shift
 156             continue
 157         ;;
 158 
 159         -inv|--inv|-inverse|--inverse|-invert|--invert|-inverted|--inverted)
 160             ansi='7m'
 161             shift
 162             continue
 163         ;;
 164 
 165         -orange|--orange)
 166             ansi='38;2;215;95;0m'
 167             shift
 168             continue
 169         ;;
 170 
 171         -ob|--ob|-orangeback|--orangeback)
 172             ansi='48;2;215;95;0m\x1b[38;2;238;238;238m'
 173             shift
 174             continue
 175         ;;
 176 
 177         -purple|--purple)
 178             ansi='38;2;135;95;255m'
 179             shift
 180             continue
 181         ;;
 182 
 183         -pb|--pb|-purpleback|--purpleback)
 184             ansi='48;2;135;95;255m\x1b[38;2;238;238;238m'
 185             shift
 186             continue
 187         ;;
 188 
 189         -red|--red)
 190             ansi='38;2;204;0;0m'
 191             shift
 192             continue
 193         ;;
 194 
 195         -rb|--rb|-redback|--redback)
 196             ansi='48;2;204;0;0m\x1b[38;2;238;238;238m'
 197             shift
 198             continue
 199         ;;
 200 
 201         -strike|--strike|-striked|--striked|-stricken|--stricken)
 202             ansi='9m'
 203             shift
 204             continue
 205         ;;
 206 
 207         -underline|--underline|-underlined|--underlined)
 208             ansi='4m'
 209             shift
 210             continue
 211         ;;
 212     esac
 213 
 214     break
 215 done
 216 
 217 cond="${1:-1}"
 218 [ $# -gt 0 ] && shift
 219 
 220 # show all non-existing files given
 221 failed=0
 222 for arg in "$@"; do
 223     if [ "${arg}" = "-" ]; then
 224         continue
 225     fi
 226     if [ ! -e "${arg}" ]; then
 227         printf "no file named \"%s\"\n" "${arg}" >&2
 228         failed=1
 229     fi
 230 done
 231 
 232 if [ "${failed}" -gt 0 ]; then
 233     exit 2
 234 fi
 235 
 236 ci='
 237     BEGIN {
 238         if (IGNORECASE == "") {
 239             m = "your `awk` command lacks case-insensitive regex-matching"
 240             print(m) > "/dev/stderr"
 241             exit 125
 242         }
 243         IGNORECASE = 1
 244     }
 245 '
 246 if [ "${case_insensitive}" -eq 0 ]; then
 247     ci=''
 248 fi
 249 
 250 src="${ci}"'
 251     '"${cond}"' {
 252         gsub(/\x1b\[0m/, "\x1b[0m\x1b['"${ansi}"'")
 253         printf "\x1b['"${ansi}"'%s\x1b[0m%s", $0, ORS
 254         '"${flush}"'
 255         next
 256     }
 257     1
 258 '
 259 
 260 if [ "${tsv}" -eq 1 ]; then
 261     ${command} -F "\t" "${src}" "$@"
 262 else
 263     ${command} "${src}" "$@"
 264 fi