#!/bin/bash # snippet - extract lines between start and end patterns from files or stdin # # Usage: # snippet -s pattern [-e pattern] [-f num] [-l num] [file ...] # snippet -s pattern [-e pattern] [-t num] [file ...] # snippet [-h] # # Options: # -s, --start pattern Pattern indicating the beginning of the desired snippet # -e, --end pattern Pattern indicating the end of the desired snippet # If not provided, reads to end of input # -f, --trim-first num Exclude the first num lines of the snippet # -l, --trim-last num Exclude the last num lines of the snippet # -t, --trim num Exclude the first and last num lines of the snippet # -h, --help Show this help message _snippet() ( local SCRIPT_NAME; SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" case "${BASH_SOURCE[0]}" in /dev/*|/proc/*) SCRIPT_NAME="" ;; esac case "$SCRIPT_NAME" in ""|bash|sh|zsh|dash) SCRIPT_NAME="snippet" ;; esac local args=() # positional args local start_pattern local end_pattern local trim_first=0 local trim_last=0 # Print error message to stderr _error() { echo "[ERR][$SCRIPT_NAME] $*" >&2; } # Print usage (help) info _show_help() { # Underline only if output is a terminal (not a pipe or file) local s; [ -t 1 ] && s=$'\033[4m' local r; [ -t 1 ] && r=$'\033[24m' echo "Usage:" echo " $SCRIPT_NAME -s ${s}pattern${r} [-e ${s}pattern${r}] [-f ${s}num${r}] [-l ${s}num${r}] [${s}file${r} ${s}...${r}]" echo " $SCRIPT_NAME -s ${s}pattern${r} [-e ${s}pattern${r}] [-t ${s}num${r}] [${s}file${r} ${s}...${r}]" echo " $SCRIPT_NAME [-h]" echo "Options:" echo " -s, --start ${s}pattern${r} Specify the pattern indicating the beginning of the desired snippet" echo " -e, --end ${s}pattern${r} Specify the pattern indicating the end of the desired snippet" echo " If not provided, reads to end of input" echo " -f, --trim-first ${s}num${r} Exclude the first ${s}num${r} lines of the snippet" echo " -l, --trim-last ${s}num${r} Exclude the last ${s}num${r} lines of the snippet" echo " -t, --trim ${s}num${r} Exclude the first and last ${s}num${r} lines of the snippet" echo " -h, --help Show this help message" echo "Exit status:" echo " 0 Success" echo " 2 Usage error (missing flag value, missing start pattern, unknown option)" } # Drop first n lines of input _dropfirst() { # macOS `tail` does not support negative numbers, so we need another approach local lines="${1:-0}" if [ "$lines" -gt 0 ]; then sed "1,${lines}d" else cat # print input as-is fi } # Drop last n lines of input _droplast() { # macOS `head` does not support negative numbers, so we need another approach local lines="${1:-0}" if [ "$lines" -gt 0 ]; then # Maintains a buffer of n lines and prints all but the last buffer awk -v n="$lines" ' NR > n { # Once we have read more than n lines, print buffer[NR % n] # print the "oldest" line in the cyclic buffer } { buffer[NR % n] = $0 } # Store the current line in the cyclic buffer ' # Alternative: sed -n -e :a -e "1,${1:-1}!{P;N;D;};N;ba" else cat # print input as-is fi } _expand_short_opts() { # $1 = string of short-opt letters that take a value (e.g. "nXHd"); "" for flag-only scripts # $2..$N = "$@" # Populates _EXPANDED; caller does: set -- "${_EXPANDED[@]}"; unset _EXPANDED local value_opts="$1"; shift _EXPANDED=() local passthru="" local arg local rest local c for arg in "$@"; do if [ -n "$passthru" ]; then _EXPANDED+=("$arg"); continue; fi case "$arg" in --) passthru=1; _EXPANDED+=("$arg") ;; --*|-|"") _EXPANDED+=("$arg") ;; -[a-zA-Z]?*) rest="${arg#-}" while [ -n "$rest" ]; do c="${rest%"${rest#?}"}"; rest="${rest#?}" _EXPANDED+=("-$c") case "$value_opts" in *"$c"*) [ -n "$rest" ] && _EXPANDED+=("$rest") rest="" ;; esac done ;; *) _EXPANDED+=("$arg") ;; esac done } # Unset helper functions before returning # Just print help if no arguments are provided if [ $# -eq 0 ]; then _show_help return 0 fi _expand_short_opts "seflt" "$@" set -- "${_EXPANDED[@]}"; unset _EXPANDED # Parse parameters while [ $# -gt 0 ]; do case "$1" in -s|--start|--start-pattern) # Required value: Ensure $2 exists if [ -z "$2" ]; then _error "Missing value for '$1'. Run \`$SCRIPT_NAME -h\` for usage" return 2 fi start_pattern="$2" shift 2 ;; --start=*|--start-pattern=*) start_pattern="${1#*=}" [ -n "$start_pattern" ] || { _error "Missing value for '${1%%=*}'. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } shift ;; -e|--end|--end-pattern) # Required value: Ensure $2 exists if [ -z "$2" ]; then _error "Missing value for '$1'. Run \`$SCRIPT_NAME -h\` for usage" return 2 fi end_pattern="$2" shift 2 ;; --end=*|--end-pattern=*) end_pattern="${1#*=}" [ -n "$end_pattern" ] || { _error "Missing value for '${1%%=*}'. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } shift ;; # Drop first n lines of snippet -f|--trim-first|--trim-start) # Required value: Ensure $2 exists if [ -z "$2" ]; then _error "Missing value for '$1'. Run \`$SCRIPT_NAME -h\` for usage" return 2 fi trim_first="$2" shift 2 ;; --trim-first=*|--trim-start=*) trim_first="${1#*=}" [ -n "$trim_first" ] || { _error "Missing value for '${1%%=*}'. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } shift ;; # Drop last n lines of snippet -l|--trim-last|--trim-end) # Required value: Ensure $2 exists if [ -z "$2" ]; then _error "Missing value for '$1'. Run \`$SCRIPT_NAME -h\` for usage" return 2 fi trim_last="$2" shift 2 ;; --trim-last=*|--trim-end=*) trim_last="${1#*=}" [ -n "$trim_last" ] || { _error "Missing value for '${1%%=*}'. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } shift ;; # Drop first and last n lines of snippet -t|--trim) # Required value: Ensure $2 exists if [ -z "$2" ]; then _error "Missing value for '$1'. Run \`$SCRIPT_NAME -h\` for usage" return 2 fi trim_first="$2" trim_last="$2" shift 2 ;; --trim=*) trim_first="${1#*=}" [ -n "$trim_first" ] || { _error "Missing value for '--trim'. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } trim_last="$trim_first" shift ;; -h|--help) _show_help return 0 ;; # End of options, parse remainder as positional --) while [ $# -gt 0 ]; do args+=("$1") shift done break ;; # Unknown option -*) _error "Unknown argument '$1'. Run \`$SCRIPT_NAME -h\` for usage" return 2 ;; # Positional argument *) args+=("$1") shift ;; esac done # Error if no start pattern provided if [ -z "$start_pattern" ]; then _error "Start pattern is required. Run \`$SCRIPT_NAME -h\` for usage" return 2 fi # If no end pattern provided, read to end of string local sed_cmd if [ "$end_pattern" ]; then sed_cmd="/$start_pattern/,/$end_pattern/p" else sed_cmd="/$start_pattern/,\$p" fi # Print file contents if filenames provided, otherwise use piped/redirected input if [ ${#args[@]} -gt 0 ]; then sed -n "$sed_cmd" "${args[@]}" | _dropfirst "$trim_first" | _droplast "$trim_last" else sed -n "$sed_cmd" | _dropfirst "$trim_first" | _droplast "$trim_last" fi ) _snippet "$@" __snippet_rc=$? unset -f _snippet if [ -n "${BASH_SOURCE[0]}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then eval "unset __snippet_rc; return $__snippet_rc" fi eval "unset __snippet_rc; exit $__snippet_rc"