#!/bin/bash # # Copyright 2025 Rowan Rodrik van der Molen # # Released under the MIT license: https://opensource.org/license/mit SCRIPT_NAME=$(basename "$0") SCRIPT_VERSION="1.0.0" FRAGMENT_START_RE="]+)>" FRAGMENT_CLOSE_RE="]+)>" IGNORE_START_TAG="" IGNORE_END_TAG="" SINGLE_LINE_IGNORE_TAG="" DEFAULT_LOG_LEVEL="info" DEFAULT_DEBUG_LEVEL=1 b="\x1b[1m" B="\x1b[22m" u="\x1b[4m" U="\x1b[24m" usage() { local sep="\x1b[2m|\x1b[22m" local repeat="\x1b[2m…\x1b[22m" local opt="\x1b[2m[\x1b[22m" local Opt="\x1b[2m]\x1b[22m" echo -e "${b}$SCRIPT_NAME – ${u}W${U}rite ${u}E${U}verything ${u}T${U}wice, because ${u}W${U}e ${u}E${U}njoy ${u}T${U}yping – v.$SCRIPT_VERSION${B} In many contexts, it's actually good to repeat yourself. This script helps you to make sure that, if you do repeat yourself, each repetition is the same, except where the differences are marked to be specifically allowed. Usage: ${b}$SCRIPT_NAME ${opt}${b}option${repeat}${Opt}${b} ${u}file${U}${repeat}${B} ${b}$SCRIPT_NAME -h${B}$sep${b}--help ${b}$SCRIPT_NAME -v${B}$sep${b}--version ${b}$SCRIPT_NAME -s${B}$sep${b}--self-test${B} Options: ${b}-h${B}${sep}${b}--help${B} Show this help. ${b}-l${B}${set}${b}--log-level info${sep}${b}error${sep}${b}debug${opt}${b}${u}debug_level${U}${Opt}${B} Which level of log messages to show; default: ${b}$DEFAULT_LOG_LEVEL${B} For ${b}--log-level debug${B}, a ${b}${u}debug_level${U}${B} between 1 and 5 may be suffixed. The default ${b}${u}debug_level${U}${B} is ${b}$DEFAULT_DEBUG_LEVEL${B}. ${b}-1${B}${sep}${b}--single-line-comment ${u}comment_prefix${U}${B} The beginning characters of single-line comments in the given ${u}file${U}s. ${b}-2${B}${sep}${b}--multi-line-comment ${u}comment_prefix${U} ${u}comment_suffix${U}${B} The beginning and endings of multi-line comments in the given ${u}file${U}s. ${b}-d${B}${sep}${b}--working-dir ${u}path${U}${B} ${b}-k${B}$sep${b}--keep-working-dir on_failure${B}${sep}${b}always${B}${sep}${b}never${B} ${b}-e${B}$sep${b}--file-extension ${u}.ext${U} The file extension to assume and use for each fragment repetition file in the fragment's subdir of the working directory. ${b}-s${B}$sep${b}--self-test${B} ${b}-v${B}$sep${b}--version${B} Show version information. Exit codes: \x1b[32m${b}0${B}\x1b[39m when all the fragments are in sync. \x1b[31m${b}1${B}\x1b[39m never intentionally, because too many shell crashes can cause it. \x1b[31m${b}2${B}\x1b[39m on option and argument errors and missing files. \x1b[31m${b}4${B}\x1b[39m when fragments are demarcated incorrectly. \x1b[31m${b}8${B}\x1b[39m when fragments have diverged. \x1b[31m${b}12${B}\x1b[39m when fragments are demarcated incorrectly ${b}and${B} also fragments diverged. Example: \x1b[36mbash$\x1b[39m ${b}$SCRIPT_NAME -1 '\x1b[35m--\x1b[39m' - <\x1b[39m code that you wish to repeat code that you wish to repeat \x1b[35m--\x1b[39m line(s) that may deviate from the other repetitions of this fragment. \x1b[35m--\x1b[39m more code that you wish to retype more code that you wish to retype another line to ignore \x1b[35m--\x1b[39m \x1b[35m--\x1b[39m ${b}EOF${B} " } usage_error() { 1>&2 echo -e "\x1b[31mUsage error: $1\x1b[39m" 1>&2 echo 1>&2 usage exit 2 } log_info() { [[ "$log_level" == 'info' || "$log_level" == debug* ]] || return 1>&2 echo -e "\x1b[42;30;1m$SCRIPT_NAME\x1b[22;32;49m $1\x1b[0m" } log_debug() { [[ "$log_level" =~ debug* ]] || return local msg_debug_level=$1 [[ "$msg_debug_level" -le "$debug_level" ]] || return local msg="${b}$2${B} line ${b}$3${B}: $4" 1>&2 echo -e "\x1b[43;30;1m$SCRIPT_NAME\x1b[22;33;49m $msg\x1b[0m" } log_error() { local msg if [[ -n "$3" ]]; then msg="${b}$1${B} line ${b}$2${B}: $3" elif [[ -n "$2" ]]; then msg="${b}$1${B}: $3" elif [[ -n "$1" ]]; then msg="$1" fi 1>&2 echo -e "\x1b[41;30;1m$SCRIPT_NAME\x1b[22;31;49m $msg\x1b[0m" } self_test() { 1>&2 echo -e "Nope, not yet." exit 10 } log_level="$DEFAULT_LOG_LEVEL" debug_level="$DEFAULT_DEBUG_LEVEL" working_dir= keep_working_dir= single_line_comment_prefix= multi_line_comment_prefix= multi_line_comment_suffix= file_extension= files=() while [[ -n "$1" ]]; do case "$1" in -h|--help) usage exit 0 ;; -v|--version) echo "$SCRIPT_NAME $SCRIPT_VERSION" exit 0 ;; -s|--self-test) self_test exit 0 ;; -l|--log-level) [[ -n "$2" ]] || usage_error "Missing argument to ${b}$1${B} option." log_level="$2" if [[ "$log_level" =~ ^debug([1-5])$ ]]; then debug_level="${BASH_REMATCH[1]}" fi shift ;; -1|--single-line-comment) [[ -n "$2" ]] || usage_error "Missing argument to ${b}$1${B} option." single_line_comment_prefix="$2" shift ;; -2|--multi-line-comment) [[ -n "$3" ]] || usage_error "The \x1b[1m$1\x1b[22m option requires 2 arguments." multi_line_comment_prefix="$2" multi_line_comment_suffix="$3" shift 2 ;; -d|--working-dir) [[ -n "$2" ]] || usage_error "Missing argument to ${b}$1${B} option." working_dir="$2" shift ;; -k|--keep-working-dir) [[ -n "$2" ]] || usage_error "Missing argument to ${b}$1${B} option." keep_working_dir="$2" shift ;; -e|--file-extension) [[ -n "$2" ]] || usage_error "Missing argument to ${b}$1${B} option." file_extension="$2" shift ;; -*) usage_error "Unknown option: \x1b[1m$1\x1b[22m" ;; *) files+=("$1") ;; esac shift done [[ ${#files[@]} -gt 0 ]] || usage_error "You need to specify at least one ${u}file{$u} argument." for file in "${files[@]}"; do if [[ ! -f "$file" ]]; then usage_error "File ${u}$file${U} doesn't exist." fi done if [[ -z "$single_line_comment_prefix" && -z "$multi_line_comment_prefix" ]]; then usage_error "Your need at least one of the ${b}--single-line-comment${B} or ${b}--multi-line-comment${B} options." fi single_line_comment_re="$single_line_comment_prefix" if [[ -n "$single_line_comment_prefix" ]]; then ignored_line_marker="$single_line_comment_prefix" else ignored_line_marker="$multi_line_comment_prefix$multi_line_comment_suffix" fi if [[ -z "$working_dir" ]]; then working_dir="$(mktemp -d -t wet-fragments.XXXXXXXXXX)" log_info "Generated working dir: ${u}$working_dir${U}" implicitly_keep_working_dir=never elif [[ ! -d "$working_dir" ]]; then mkdir -p "$working_dir" log_info "Created working dir: ${u}$working_dir${U}" implicitly_keep_working_dir=never else log_info "Working dir ${u}$working_dir${U} exists." implicitly_keep_working_dir=always fi if [[ -z "$file_extension" ]]; then file_extension=$(echo "${files[0]}" | sed -E 's/^.*(\.[^.]+)$/\1/') log_info "Assumed ${b}--file-extension ${u}$file_extension${U}${B} from first file argument: ${u}${files[0]}${U}" if [[ -z "$file_extension" ]]; then usage_error "Couldn't determine file extension; please supply ${b}--file-extension ${u}.ext${U}${B} option." fi fi if [[ -z "$keep_working_dir" && "$implicitly_keep_working_dir" ]]; then keep_working_dir="$implicitly_keep_working_dir" log_info "Implicitly set option: ${b}--keep-working-dir $keep_working_dir${B}" fi # Let's loop through all the files to locate all the fragments. declare -A fragment_repetitions for file in "${files[@]}"; do line_no=0 comment_start_line_no= commen_end_line_no= fragment_start_line_no= fragment_end_line_no= fragment_name= fragment_dir= fragment_lines=() ignore_start_line_no= ignore_end_line_no= while IFS= read -r line; do line_no=$((line_no + 1)) # The first step in each iteration is to determine whether this line is (part of) a comment or not. if [[ -n "$multi_line_comment_prefix" && -z "$comment_start_line_no" && "$line" =~ ^[[:blank:]]*"$multi_line_comment_prefix" ]]; then log_debug 5 "$file" "$line_no" "Multiline comment started: $line" comment_start_line_no="$line_no" if [[ "$line" =~ ^\s*"$multi_line_comment_prefix".*$multi_line_comment_suffix\s*$ ]]; then log_debug 5 "$file" "$line_no" "Multiline comment ended on the same line." comment_end_line_no="$line_no" fi elif [[ -n "$multi_line_comment_suffix" && -n "$comment_start_line_no" && "$line" =~ "$multi_line_comment_suffix"$ ]]; then log_debug 5 "$file" "$line_no" "Ended multiline comment, started on line ${b}$comment_start_line_no${B}: $line" comment_end_line_no="$line_no" elif [[ -n "$single_line_comment_prefix" && "$line" =~ ^[[:blank:]]*"$single_line_comment_prefix" ]]; then log_debug 5 "$file" "$line_no" "Single-line comment found: $line" comment_start_line_no="$line_no" comment_end_line_no="$line_no" fi # The second step in each iteration is to determine if this line has a comment with # a `` tag and thus has to be ignored, or whether it's part of a a range of lines # between `` and `` tags. if [[ -n "$comment_start_line_no" && "$line" =~ "$SINGLE_LINE_IGNORE_TAG" ]] then if [[ -z "$fragment_start_line_no" ]]; then log_error "$file" "$line_no" "${b}SINGLE_LINE_IGNORE_TAG${B} tag found outside of ${b}${B} context." break fi if [[ -n "$ignore_start_line_no" ]]; then log_error "$file" "$line_no" "Stumbled on an ${b}${B} element while already in another ${b}${B} context, started on ${comment_start_line_no}." break fi ignore_start_line_no=$line_no ignore_end_line_no=$line_no log_debug 3 "$file" "$line_no" "${b}${B} tag found." elif [[ -n "$comment_start_line_no" && "$line" =~ "$IGNORE_START_TAG" ]]; then if [[ -z "$fragment_start_line_no" ]]; then log_error "$file" "$line_no" "${b}$IGNORE_START_TAG${B} tag found outside of ${b}${B} context." break fi if [[ -n "$ignore_start_line_no" ]]; then log_error "$file" "$line_no" "Stumbled on an ${b}${B} element while already in another ${b}${B} context, started on ${ignore_start_line_no}." break fi ignore_start_line_no=$line_no log_debug 3 "$file" "$line_no" "${b}${B} tag found." elif [[ -n "$comment_start_line_no" && "$line" =~ "$IGNORE_END_TAG" ]]; then if [[ -z "$ignore_start_line_no" ]]; then log_error "$file" "$line_no" "Stumbled on an unopened ${b}${B} closing tag." break fi ignore_end_line_no=$line_no log_debug 3 "$file" "$line_no" "${b}${B} tag found." fi # If we _are_ ignoring lines, we will replace them with a `` tag. if [[ -n "$ignore_start_line_no" ]]; then if [[ -n "$ignore_end_line_no" ]]; then ignored_line_count=$((ignore_end_line_no - ignore_start_line_no + 1)) replacement_line=$(echo "$ignored_line_marker" | sed -E "s/\{lines\}/$ignored_line_count/") fragment_lines+=("$replacement_line") log_debug 1 "$file" "$line_no" "Substituted ${b}$ignored_line_count${B} lines with: $replacement_line" ignore_start_line_no= ignore_end_line_no= fi continue fi if [[ -n "$comment_start_line_no" && "$line" =~ $FRAGMENT_START_RE ]]; then new_fragment_name="${BASH_REMATCH[1]}" log_debug 1 "$file" "$line_no" "${b}${B} tag encountered in comment." if [[ -n "$new_fragment_name" && "$new_fragment_name" == "$fragment_name" ]]; then log_error "$file" "$line_no" "New fragment ${b}${B} while fragment ${b}${B}, started on line $fragment_start_line_no, is still unclosed." break # Break out, back into outer loop. fi fragment_name="$new_fragment_name" fragment_start_line_no="$comment_start_line_no" fragment_repetitions[$fragment_name]=$((${fragment_repetitions[$fragment_name]:0} + 1)) elif [[ -n "$comment_start_line_no" && "$line" =~ $FRAGMENT_CLOSE_RE ]]; then ended_fragment_name="${BASH_REMATCH[1]}" log_debug 1 "$file" "$line_no" "${b}${B} tag encountered in comment." if [[ -n "$ended_fragment_name" && "$ended_fragment_name" != "$fragment_name" ]]; then log_error "$file" "$line_no" "Fragment ${b}$fragment_name${B} closed, but the unclused fragment started on line $fragment_start_line_no was called ${b}$fragment_name${B}." break # Break out, back into outer loop. fi fragment_end_line_no="$line_no" fi if [[ -n "$fragment_start_line_no" ]]; then fragment_lines+=("$line") fi if [[ -n "$fragment_start_line_no" && -n "$fragment_end_line_no" ]]; then if [[ -n "$comment_start_line_no" && -n "$comment_end_line_no" ]]; then # Push fragment end line no. to potentially extend beyond the end tag, # to the end of the comment. fragment_end_line_no="$comment_end_line_no" fi fragment_repeat="${fragment_repetitions[$fragment_name]}" fragment_dir="$working_dir/$fragment_name" mkdir -p "$fragment_dir" fragment_file="$fragment_dir/$fragment_repeat$file_extension" meta_var_file="$fragment_dir/$fragment_repeat.wet.sh" printf '%s\n' "${fragment_lines[@]}" > "$fragment_file" log_info "Wrote fragment ${u}$fragment_name${U} to: ${u}$fragment_file${U}" echo "wet_fragment_name=\"$fragment_name\" wet_file=\"$file\" wet_fragment_start_line_no=$fragment_start_line_no wet_fragment_end_line_no=$fragment_end_line_no wet_rep_filename=\"$(basename "$fragment_file")\" wet_rep_no=$fragment_repeat" > "$meta_var_file" log_info "Wrote fragment ${u}$fragment_name${U} meta data to: ${u}$meta_var_file${U}" fragment_name= fragment_start_line_no= fragment_end_line_no= fragment_lines=() fi if [[ -n "$comment_start_line_no" && -n "$comment_end_line_no" ]]; then comment_start_line_no= comment_end_line_no= fi done < "$file" if [[ -n "$comment_start_line_no" && -z "$comment_end_line_no" ]]; then log_error "$file" "EOF" "Unclosed multi-line comment that started on line ${b}$comment_start_line_no${B}." fi done for fragment_dir in "$working_dir"/*; do rep_count=1 meta_var_file="$fragment_dir/$rep_count.wet.sh" source <(sed -E "s/^wet_/wet_1_/" "$meta_var_file") rep_a="$fragment_dir/$wet_1_rep_filename" while true; do rep_count=$((rep_count + 1)) meta_var_file="$fragment_dir/$rep_count.wet.sh" [[ -f "$meta_var_file" ]] || break source <(sed -E "s/^wet_/wet_n_/" "$meta_var_file") rep_b="$fragment_dir/$wet_n_rep_filename" diff_file="$fragment_dir/1--$rep_count.diff" if ! diff \ --ignore-space-change \ --ignore-blank-lines \ --ignore-matching-lines ']*/>$' \ "$rep_a" "$rep_b" > "$diff_file" then log_error "Rep ${b}#1${B} of fragment ${b}$wet_1_fragment_name${B} differs from rep ${b}#$rep_count${B}: $diff_file" exit 8 fi done log_info "All $((rep_count - 1)) reps of ${u}$wet_1_fragment_name${U} fragment in sync." done if [[ -d "$working_dir" && "$keep_working_dir" == 'never' ]]; then rm -r "$working_dir" log_info "Deleted working dir: ${u}$working_dir${U}" fi