File: pbn.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 # pbn [options...] [line numbers...]
  27 #
  28 # Pick (lines) By Number, using all the 1-based line-numbers given, or even
  29 # negative ones, which count backward from the last line. Input lines always
  30 # come from the standard input.
  31 #
  32 # The options are, available both in single and double-dash versions
  33 #
  34 #   -h, -help    show this help message
  35 
  36 
  37 buffered=0
  38 
  39 case "$1" in
  40     -b|--b|-buffered|--buffered) buffered=1; shift ;;
  41 
  42     -h|--h|-help|--help)
  43         awk '/^# +pbn /, /^$/ { gsub(/^# ?/, ""); print }' "$0"
  44         exit 0
  45     ;;
  46 
  47     --) shift ;;
  48 
  49     -*)
  50         if echo "$1" | grep -E -q '^-[^0-9]+$'; then
  51             printf "unsupported option '%s'\n" "$1" >&2
  52             exit 1
  53         fi
  54     ;;
  55 esac
  56 
  57 if [ $# -eq 0 ]; then
  58     awk '/^# +pbn /, /^$/ { gsub(/^# ?/, ""); print }' "$0"
  59     exit 1
  60 fi
  61 
  62 flush=0
  63 if [ "${buffered}" -eq 0 ] && { [ -p /dev/stdout ] || [ -t 1 ]; }; then
  64     flush=1
  65 fi
  66 
  67 awk -v flush="${flush}" '
  68     BEGIN {
  69         for (i = 1; i < ARGC; i++) {
  70             p = ARGV[i]
  71             delete ARGV[i]
  72             if (p != 0) picks[++npicks] = p
  73         }
  74 
  75         j = 1
  76         out = 0
  77         if (npicks == 0) exit 1
  78 
  79         min = max = picks[1]
  80         for (i = 1; i <= npicks; i++) {
  81             p = picks[i]
  82             if (min > p) min = p
  83             if (max < p) max = p
  84         }
  85     }
  86 
  87     {
  88         # try to emit leading picks right away, in case of live-lines output
  89         while ((j <= npicks) && (picks[j] == NR)) {
  90             print
  91             if (flush) fflush()
  92             out++
  93 
  94             picks[j] = 0
  95             j++
  96         }
  97 
  98         # no need to read more input if all picks have already been output
  99         if (j > npicks) exit
 100 
 101         # no need to remember lines before the first pick: `NR` is always
 102         # at least 1, so the condition is always false with a negative `min`
 103         if (NR < min) next
 104 
 105         # no need to remember lines past the last index, when no index is
 106         # negative/backwards
 107         if ((min >= 0) && (NR > max)) exit
 108 
 109         lines[NR] = $0
 110     }
 111 
 112     END {
 113         if (npicks == 0) exit 1
 114 
 115         # output any picks not yet emitted
 116         for (i = j; i <= npicks; i++) {
 117             p = picks[i]
 118             if (p < 0) p += NR + 1
 119             if (1 <= p && p <= NR) { print lines[p]; out++ }
 120         }
 121 
 122         exit(out == 0)
 123     }
 124 ' "$@"