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