#!/usr/bin/env bash # 1. Shebang: run this file with the first `bash` on the PATH. On macOS that # can be the old /bin/bash 3.2, so this script avoids newer features. # # sample-script.sh: summarise a log file by level (INFO, WARN, ERROR). # Free sample from 99helpers.com/file-types/sh # Run: bash sample-script.sh (uses a built-in sample log) # bash sample-script.sh -n 2 app.log (show the last 2 errors) # bash sample-script.sh -f app.log (exit with status 1 if errors) # chmod +x sample-script.sh && ./sample-script.sh -h # 2. "Strict mode": stop on the first failing command (-e), treat unset # variables as errors (-u), and let a failure anywhere in a pipeline # count (-o pipefail). Not a cure-all, but it catches many mistakes. set -euo pipefail # 3. Read-only settings. ${0##*/} strips the folder from the script's path. readonly SCRIPT_NAME="${0##*/}" readonly VERSION="1.0" show_errors=3 fail_on_error=false # 4. A function. Here-documents (<&2; exit 2 ;; *) usage >&2; exit 2 ;; esac done shift $((OPTIND - 1)) case "$show_errors" in '' | *[!0-9]*) echo "$SCRIPT_NAME: -n needs a whole number" >&2; exit 2 ;; esac # 6. Pick the input: a readable file, or the built-in sample. # ${1:-} gives "" instead of an error under set -u when there is no $1. file="${1:-}" if [[ -n "$file" && ! -r "$file" ]]; then echo "$SCRIPT_NAME: cannot read '$file'" >&2 exit 1 fi info=0 warn=0 error=0 errors=() # 7. Read line by line. IFS= and -r keep spaces and backslashes intact; # case matches each line against glob patterns. while IFS= read -r line || [[ -n "$line" ]]; do case "$line" in *" INFO "*) info=$((info + 1)) ;; *" WARN "*) warn=$((warn + 1)) ;; *" ERROR "*) error=$((error + 1)); errors+=("$line") ;; esac done < <(if [[ -n "$file" ]]; then cat -- "$file"; else sample_log; fi) # 8. printf rather than echo: it behaves the same in every shell. # Always "quote" variables, so spaces in values can't split them apart. printf '%-6s %5d\n' INFO "$info" WARN "$warn" ERROR "$error" # 9. Arrays: ${#errors[@]} is the count. Checking it first keeps bash 3.2 # happy under set -u, where an empty array counts as unset. if (( ${#errors[@]} > 0 && show_errors > 0 )); then start=$(( ${#errors[@]} > show_errors ? ${#errors[@]} - show_errors : 0 )) echo "Last errors:" for (( i = start; i < ${#errors[@]}; i++ )); do printf ' %s\n' "${errors[i]}" done fi # 10. The exit status: 0 means success, anything else failure. With -f, a # calling script or CI job can stop when the log contains errors. if [[ "$fail_on_error" == true ]] && (( error > 0 )); then exit 1 fi exit 0