diff --git a/zrec b/zrec index 04779ff..b864909 100755 --- a/zrec +++ b/zrec @@ -14,38 +14,38 @@ set -o pipefail # Step 0: Verify recutils (recins/recsel) is installed # --------------------------------------------------------------------------- check_recutils() { - if ! command -v recins >/dev/null 2>&1; then - echo "Error: 'recins' not found. Please install recutils." >&2 - echo " On macOS: brew install recutils" >&2 - exit 1 - fi - if ! command -v recinf >/dev/null 2>&1; then - echo "Error: 'recinf' not found. Please install recutils." >&2 - exit 1 - fi + if ! command -v recins >/dev/null 2>&1; then + echo "Error: 'recins' not found. Please install recutils." >&2 + echo " On macOS: brew install recutils" >&2 + exit 1 + fi + if ! command -v recinf >/dev/null 2>&1; then + echo "Error: 'recinf' not found. Please install recutils." >&2 + exit 1 + fi - local ver - ver=$(recins --version 2>/dev/null | head -n1) - echo "Using: ${ver:-recutils (version unknown)}" + local ver + ver=$(recins --version 2>/dev/null | head -n1) + echo "Using: ${ver:-recutils (version unknown)}" } # --------------------------------------------------------------------------- # Step 1: Usage / argument handling # --------------------------------------------------------------------------- usage() { - echo "Usage: $(basename "$0") [record-type]" >&2 - echo " Interactively build and insert a record into a recfile." >&2 - exit 1 + echo "Usage: $(basename "$0") [record-type]" >&2 + echo " Interactively build and insert a record into a recfile." >&2 + exit 1 } # --------------------------------------------------------------------------- # Step 2: Extract the descriptor block for a given record type. # --------------------------------------------------------------------------- get_descriptor_block() { - local file="$1" - local rectype="$2" + local file="$1" + local rectype="$2" - awk -v rt="$rectype" ' + awk -v rt="$rectype" ' BEGIN { RS=""; FS="\n" } { for (i = 1; i <= NF; i++) { @@ -67,10 +67,10 @@ get_descriptor_block() { # Step 3: List all record types defined in the file. # --------------------------------------------------------------------------- list_record_types() { - local file="$1" + local file="$1" - if command -v recinf >/dev/null 2>&1; then - recinf "$file" 2>/dev/null | awk ' + if command -v recinf >/dev/null 2>&1; then + recinf "$file" 2>/dev/null | awk ' { if (NF == 0) next name = $NF @@ -81,19 +81,19 @@ list_record_types() { } } ' - else - grep '^%rec:' "$file" | sed 's/^%rec:[[:space:]]*//; s/[[:space:]]*$//' - fi + else + grep '^%rec:' "$file" | sed 's/^%rec:[[:space:]]*//; s/[[:space:]]*$//' + fi } # --------------------------------------------------------------------------- # Step 4: Pull a space-separated list of fields from a directive. # --------------------------------------------------------------------------- extract_fields() { - local block="$1" - local directive="$2" + local block="$1" + local directive="$2" - echo "$block" | awk -v d="$directive" ' + echo "$block" | awk -v d="$directive" ' index($0, d) == 1 { line = substr($0, length(d) + 1) print line @@ -113,10 +113,10 @@ extract_fields() { # -> returns "range 1 100" # --------------------------------------------------------------------------- resolve_typedef() { - local block="$1" - local alias="$2" + local block="$1" + local alias="$2" - echo "$block" | awk -v a="$alias" ' + echo "$block" | awk -v a="$alias" ' index($0, "%typedef:") == 1 { rest = substr($0, length("%typedef:") + 1) sub(/^[[:space:]]+/, "", rest) @@ -156,12 +156,12 @@ resolve_typedef() { # This performs a single awk pass over the descriptor block. # --------------------------------------------------------------------------- get_field_type_info() { - local block="$1" - local field="$2" + local block="$1" + local field="$2" - # Single awk pass: pull the raw %type: token(s) for this field. - local raw_type - raw_type=$(echo "$block" | awk -v f="$field" ' + # Single awk pass: pull the raw %type: token(s) for this field. + local raw_type + raw_type=$(echo "$block" | awk -v f="$field" ' index($0, "%type:") == 1 { rest = substr($0, length("%type:") + 1) sub(/^[[:space:]]+/, "", rest) @@ -181,51 +181,51 @@ get_field_type_info() { } ') - # No declared type: emit an empty resolved/label pair. - if [ -z "$raw_type" ]; then - printf '\t' - return 0 - fi + # No declared type: emit an empty resolved/label pair. + if [ -z "$raw_type" ]; then + printf '\t' + return 0 + fi - local base="${raw_type%% *}" - local resolved label + local base="${raw_type%% *}" + local resolved label - case "$base" in - int | real | bool | date | line | url | email | uuid | regexp | range) - # Concrete inline type. - resolved="$raw_type" - # Hide the regexp pattern in the label; show just the keyword. - label="$base" - ;; - *) - # Named alias: resolve it (one typedef lookup, only when needed). - local typedef - typedef=$(resolve_typedef "$block" "$base") - if [ -n "$typedef" ]; then - resolved="$typedef" # validate against the underlying type - label="$base" # but display the friendly alias name - else - # Unknown alias: fall back to raw for both. - resolved="$raw_type" - label="$raw_type" - fi - ;; - esac + case "$base" in + int | real | bool | date | line | url | email | uuid | regexp | range) + # Concrete inline type. + resolved="$raw_type" + # Hide the regexp pattern in the label; show just the keyword. + label="$base" + ;; + *) + # Named alias: resolve it (one typedef lookup, only when needed). + local typedef + typedef=$(resolve_typedef "$block" "$base") + if [ -n "$typedef" ]; then + resolved="$typedef" # validate against the underlying type + label="$base" # but display the friendly alias name + else + # Unknown alias: fall back to raw for both. + resolved="$raw_type" + label="$raw_type" + fi + ;; + esac - printf '%s\t%s' "$resolved" "$label" + printf '%s\t%s' "$resolved" "$label" } # --------------------------------------------------------------------------- # Helper: test membership of a word in a space-separated list. # --------------------------------------------------------------------------- in_list() { - local needle="$1" - shift - local item - for item in "$@"; do - [ "$item" = "$needle" ] && return 0 - done - return 1 + local needle="$1" + shift + local item + for item in "$@"; do + [ "$item" = "$needle" ] && return 0 + done + return 1 } # --------------------------------------------------------------------------- @@ -247,142 +247,142 @@ in_list() { # range [min] [max] - min, max, or both may be omitted # --------------------------------------------------------------------------- validate_field_type() { - local value="$1" - local ftype="$2" + local value="$1" + local ftype="$2" - local base_type="${ftype%% *}" + local base_type="${ftype%% *}" - case "$base_type" in + case "$base_type" in - int) - if printf '%s' "$value" | grep -qE '^-?[0-9]+$'; then - printf '%s' "$value" - return 0 - fi - echo " -> Expected an integer (e.g. 42, -7)." >&2 - return 1 - ;; + int) + if printf '%s' "$value" | grep -qE '^-?[0-9]+$'; then + printf '%s' "$value" + return 0 + fi + echo " -> Expected an integer (e.g. 42, -7)." >&2 + return 1 + ;; - real) - if printf '%s' "$value" | grep -qE '^-?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?$'; then - printf '%s' "$value" - return 0 - fi - echo " -> Expected a real number (e.g. 3.14, -2.0, 1e10)." >&2 - return 1 - ;; + real) + if printf '%s' "$value" | grep -qE '^-?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?$'; then + printf '%s' "$value" + return 0 + fi + echo " -> Expected a real number (e.g. 3.14, -2.0, 1e10)." >&2 + return 1 + ;; - bool) - case "$value" in - 1 | [Yy][Ee][Ss] | [Tt][Rr][Uu][Ee]) - printf 'yes' - return 0 - ;; - 0 | [Nn][Oo] | [Ff][Aa][Ll][Ss][Ee]) - printf 'no' - return 0 - ;; - esac - echo " -> Expected a boolean: yes/no, true/false, or 1/0." >&2 - return 1 - ;; + bool) + case "$value" in + 1 | [Yy][Ee][Ss] | [Tt][Rr][Uu][Ee]) + printf 'yes' + return 0 + ;; + 0 | [Nn][Oo] | [Ff][Aa][Ll][Ss][Ee]) + printf 'no' + return 0 + ;; + esac + echo " -> Expected a boolean: yes/no, true/false, or 1/0." >&2 + return 1 + ;; - date) - if date -d "$value" >/dev/null 2>&1 || - date -j -f "%Y-%m-%d" "$value" "+%Y-%m-%d" >/dev/null 2>&1; then - printf '%s' "$value" - return 0 - fi - echo " -> Expected a valid date (e.g. 2024-07-04)." >&2 - return 1 - ;; + date) + if date -d "$value" >/dev/null 2>&1 || + date -j -f "%Y-%m-%d" "$value" "+%Y-%m-%d" >/dev/null 2>&1; then + printf '%s' "$value" + return 0 + fi + echo " -> Expected a valid date (e.g. 2024-07-04)." >&2 + return 1 + ;; - line) - printf '%s' "$value" - return 0 - ;; + line) + printf '%s' "$value" + return 0 + ;; - url) - if printf '%s' "$value" | - grep -qiE '^(https?|ftp|ftps|sftp|file)://[^[:space:]]+$'; then - printf '%s' "$value" - return 0 - fi - echo " -> Expected a URL beginning with http://, https://, ftp://, etc." >&2 - return 1 - ;; + url) + if printf '%s' "$value" | + grep -qiE '^(https?|ftp|ftps|sftp|file)://[^[:space:]]+$'; then + printf '%s' "$value" + return 0 + fi + echo " -> Expected a URL beginning with http://, https://, ftp://, etc." >&2 + return 1 + ;; - email) - if printf '%s' "$value" | grep -qE '^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$'; then - printf '%s' "$value" - return 0 - fi - echo " -> Expected an e-mail address (e.g. user@example.com)." >&2 - return 1 - ;; + email) + if printf '%s' "$value" | grep -qE '^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$'; then + printf '%s' "$value" + return 0 + fi + echo " -> Expected an e-mail address (e.g. user@example.com)." >&2 + return 1 + ;; - uuid) - if printf '%s' "$value" | - grep -qiE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then - printf '%s' "$value" - return 0 - fi - echo " -> Expected a UUID (e.g. 550e8400-e29b-41d4-a716-446655440000)." >&2 - return 1 - ;; + uuid) + if printf '%s' "$value" | + grep -qiE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then + printf '%s' "$value" + return 0 + fi + echo " -> Expected a UUID (e.g. 550e8400-e29b-41d4-a716-446655440000)." >&2 + return 1 + ;; - regexp) - local pattern="${ftype#regexp }" - pattern="${pattern#/}" - pattern="${pattern%/}" + regexp) + local pattern="${ftype#regexp }" + pattern="${pattern#/}" + pattern="${pattern%/}" - # ### CHANGED: Guard against empty pattern before attempting match. - if [ -z "$pattern" ]; then - echo " -> Warning: empty regexp in type declaration; skipping validation." >&2 - printf '%s' "$value" - return 0 - fi + # ### CHANGED: Guard against empty pattern before attempting match. + if [ -z "$pattern" ]; then + echo " -> Warning: empty regexp in type declaration; skipping validation." >&2 + printf '%s' "$value" + return 0 + fi - # ### CHANGED: Use awk for ERE matching instead of grep -E for - # consistent behaviour across BSD (macOS) and GNU/Linux platforms. - if printf '%s' "$value" | - awk -v pat="$pattern" '$0 ~ pat { found=1 } END { exit !found }'; then - printf '%s' "$value" - return 0 - fi - echo " -> Value does not match required pattern: /${pattern}/." >&2 - return 1 - ;; + # ### CHANGED: Use awk for ERE matching instead of grep -E for + # consistent behaviour across BSD (macOS) and GNU/Linux platforms. + if printf '%s' "$value" | + awk -v pat="$pattern" '$0 ~ pat { found=1 } END { exit !found }'; then + printf '%s' "$value" + return 0 + fi + echo " -> Value does not match required pattern: /${pattern}/." >&2 + return 1 + ;; - range) - if ! printf '%s' "$value" | grep -qE '^-?[0-9]+(\.[0-9]+)?$'; then - echo " -> Expected a number for a range field." >&2 - return 1 - fi + range) + if ! printf '%s' "$value" | grep -qE '^-?[0-9]+(\.[0-9]+)?$'; then + echo " -> Expected a number for a range field." >&2 + return 1 + fi - local lo hi - lo=$(echo "$ftype" | awk '{print $2}') - hi=$(echo "$ftype" | awk '{print $3}') + local lo hi + lo=$(echo "$ftype" | awk '{print $2}') + hi=$(echo "$ftype" | awk '{print $3}') - if [ -n "$lo" ] && - awk -v v="$value" -v lo="$lo" 'BEGIN { exit !(v < lo) }'; then - echo " -> Value must be >= ${lo}." >&2 - return 1 - fi - if [ -n "$hi" ] && - awk -v v="$value" -v hi="$hi" 'BEGIN { exit !(v > hi) }'; then - echo " -> Value must be <= ${hi}." >&2 - return 1 - fi - printf '%s' "$value" - return 0 - ;; + if [ -n "$lo" ] && + awk -v v="$value" -v lo="$lo" 'BEGIN { exit !(v < lo) }'; then + echo " -> Value must be >= ${lo}." >&2 + return 1 + fi + if [ -n "$hi" ] && + awk -v v="$value" -v hi="$hi" 'BEGIN { exit !(v > hi) }'; then + echo " -> Value must be <= ${hi}." >&2 + return 1 + fi + printf '%s' "$value" + return 0 + ;; - *) - printf '%s' "$value" - return 0 - ;; - esac + *) + printf '%s' "$value" + return 0 + ;; + esac } # --------------------------------------------------------------------------- @@ -391,276 +391,276 @@ validate_field_type() { # Mandatory fields loop until non-empty; typed fields loop until valid. # --------------------------------------------------------------------------- prompt_field() { - local field="$1" - local is_mandatory="$2" # "yes" / "no" - local ftype="$3" # resolved type, used for validation - local type_label="$4" # friendly label, used for display + local field="$1" + local is_mandatory="$2" # "yes" / "no" + local ftype="$3" # resolved type, used for validation + local type_label="$4" # friendly label, used for display - local label="$field" - local tags="" - [ "$is_mandatory" = "yes" ] && tags="${tags} [MANDATORY]" - [ -n "$type_label" ] && tags="${tags} (type: ${type_label})" + local label="$field" + local tags="" + [ "$is_mandatory" = "yes" ] && tags="${tags} [MANDATORY]" + [ -n "$type_label" ] && tags="${tags} (type: ${type_label})" - local value validated - while true; do - printf "%s%s: " "$label" "$tags" >&2 - IFS= read -r value + local value validated + while true; do + printf "%s%s: " "$label" "$tags" >&2 + IFS= read -r value - # Mandatory-empty check. - if [ "$is_mandatory" = "yes" ] && [ -z "$value" ]; then - echo " -> This field is mandatory and cannot be empty." >&2 - continue - fi + # Mandatory-empty check. + if [ "$is_mandatory" = "yes" ] && [ -z "$value" ]; then + echo " -> This field is mandatory and cannot be empty." >&2 + continue + fi - # Skip type validation for empty optional fields. - if [ -z "$value" ]; then - printf '%s' "$value" - return 0 - fi + # Skip type validation for empty optional fields. + if [ -z "$value" ]; then + printf '%s' "$value" + return 0 + fi - # ### CHANGED: If a type is declared, validate (and possibly normalise) - # the input; re-prompt on failure. - if [ -n "$ftype" ]; then - if validated=$(validate_field_type "$value" "$ftype"); then - printf '%s' "$validated" - else - continue - fi - else - printf '%s' "$value" - fi - return 0 - done + # ### CHANGED: If a type is declared, validate (and possibly normalise) + # the input; re-prompt on failure. + if [ -n "$ftype" ]; then + if validated=$(validate_field_type "$value" "$ftype"); then + printf '%s' "$validated" + else + continue + fi + else + printf '%s' "$value" + fi + return 0 + done } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { - check_recutils + check_recutils - [ "$#" -ge 1 ] || usage - local recfile="$1" - local rectype="$2" + [ "$#" -ge 1 ] || usage + local recfile="$1" + local rectype="$2" - if [ ! -f "$recfile" ]; then - echo "Error: recfile '$recfile' does not exist." >&2 - exit 1 - fi + if [ ! -f "$recfile" ]; then + echo "Error: recfile '$recfile' does not exist." >&2 + exit 1 + fi - # ---- Determine record type -------------------------------------------- - local available_types - available_types=$(list_record_types "$recfile") + # ---- Determine record type -------------------------------------------- + local available_types + available_types=$(list_record_types "$recfile") - if [ -z "$rectype" ]; then - if [ -z "$available_types" ]; then - echo "No %rec: types found. This recfile uses free-form fields." >&2 - else - local -a display_list - local -a name_list - display_list=() - name_list=() + if [ -z "$rectype" ]; then + if [ -z "$available_types" ]; then + echo "No %rec: types found. This recfile uses free-form fields." >&2 + else + local -a display_list + local -a name_list + display_list=() + name_list=() - local _line - while IFS= read -r _line; do - [ -z "$_line" ] && continue - display_list+=("$_line") - local plain="${_line%% \[count:*}" - name_list+=("$plain") - done <&2 - printf "Press Enter to use it, or type a different type: " >&2 - IFS= read -r rectype - [ -z "$rectype" ] && rectype="$only_type" + if [ "$count" -eq 1 ]; then + local only_type="${name_list[0]}" + echo "Only one record type found: '$only_type'" >&2 + printf "Press Enter to use it, or type a different type: " >&2 + IFS= read -r rectype + [ -z "$rectype" ] && rectype="$only_type" - else - # ----- Multiple types: single-keypress numbered menu ------- - echo "Available record types:" >&2 - local i=1 - local label - for label in "${display_list[@]}"; do - printf " %d) %s\n" "$i" "$label" >&2 - i=$((i + 1)) - done + else + # ----- Multiple types: single-keypress numbered menu ------- + echo "Available record types:" >&2 + local i=1 + local label + for label in "${display_list[@]}"; do + printf " %d) %s\n" "$i" "$label" >&2 + i=$((i + 1)) + done - while true; do - # ### CHANGED: -n1 reads exactly one character without - # requiring Enter. -s suppresses echo so we can echo - # the digit ourselves on the same line for clarity. - printf "Select a record type (1-%d): " "$count" >&2 - local choice - IFS= read -r -n1 -s choice - # Echo the character the user pressed so they can see it. - printf '%s\n' "$choice" >&2 + while true; do + # ### CHANGED: -n1 reads exactly one character without + # requiring Enter. -s suppresses echo so we can echo + # the digit ourselves on the same line for clarity. + printf "Select a record type (1-%d): " "$count" >&2 + local choice + IFS= read -r -n1 -s choice + # Echo the character the user pressed so they can see it. + printf '%s\n' "$choice" >&2 - # Blank (Enter pressed directly) → re-prompt. - if [ -z "$choice" ]; then - echo " -> Please press a digit between 1 and $count." >&2 - continue - fi + # Blank (Enter pressed directly) → re-prompt. + if [ -z "$choice" ]; then + echo " -> Please press a digit between 1 and $count." >&2 + continue + fi - # Reject non-digits immediately. - case "$choice" in - *[!0-9]*) - echo " -> Please press a digit between 1 and $count." >&2 - continue - ;; - esac + # Reject non-digits immediately. + case "$choice" in + *[!0-9]*) + echo " -> Please press a digit between 1 and $count." >&2 + continue + ;; + esac - if [ "$choice" -ge 1 ] && [ "$choice" -le "$count" ]; then - rectype="${name_list[$((choice - 1))]}" - break - else - echo " -> Out of range. Choose between 1 and $count." >&2 - fi - done - fi + if [ "$choice" -ge 1 ] && [ "$choice" -le "$count" ]; then + rectype="${name_list[$((choice - 1))]}" + break + else + echo " -> Out of range. Choose between 1 and $count." >&2 + fi + done + fi - echo "Selected record type: $rectype" >&2 - fi - fi + echo "Selected record type: $rectype" >&2 + fi + fi - # ---- Load the descriptor block ---------------------------------------- - local block="" - if [ -n "$rectype" ]; then - block=$(get_descriptor_block "$recfile" "$rectype") - if [ -z "$block" ]; then - echo "Warning: no descriptor block found for type '$rectype'." >&2 - fi - fi + # ---- Load the descriptor block ---------------------------------------- + local block="" + if [ -n "$rectype" ]; then + block=$(get_descriptor_block "$recfile" "$rectype") + if [ -z "$block" ]; then + echo "Warning: no descriptor block found for type '$rectype'." >&2 + fi + fi - # ---- Gather field rules ----------------------------------------------- - local mandatory_fields allowed_fields - mandatory_fields=$(extract_fields "$block" "%mandatory:") - allowed_fields=$(extract_fields "$block" "%allowed:") + # ---- Gather field rules ----------------------------------------------- + local mandatory_fields allowed_fields + mandatory_fields=$(extract_fields "$block" "%mandatory:") + allowed_fields=$(extract_fields "$block" "%allowed:") - echo "" >&2 - echo "----- Building a new record -----" >&2 - [ -n "$rectype" ] && echo "Record type: $rectype" >&2 - echo "" >&2 + echo "" >&2 + echo "----- Building a new record -----" >&2 + [ -n "$rectype" ] && echo "Record type: $rectype" >&2 + echo "" >&2 - local -a args - args=() + local -a args + args=() - local prompted="" + local prompted="" - # ---- 1. Prompt for ALLOWED fields (if defined) ------------------------ - if [ -n "$allowed_fields" ]; then - echo "== Allowed fields ==" >&2 - local f - for f in $allowed_fields; do - # ### CHANGED: Skip the %rec: type-name field. When -t is passed - # to recins it writes the %rec: line itself; supplying it again as - # a -f/-v pair produces a malformed record with a duplicate or - # misplaced %rec: field. - if [ -n "$rectype" ] && [ "$f" = "$rectype" ]; then - continue - fi + # ---- 1. Prompt for ALLOWED fields (if defined) ------------------------ + if [ -n "$allowed_fields" ]; then + echo "== Allowed fields ==" >&2 + local f + for f in $allowed_fields; do + # ### CHANGED: Skip the %rec: type-name field. When -t is passed + # to recins it writes the %rec: line itself; supplying it again as + # a -f/-v pair produces a malformed record with a duplicate or + # misplaced %rec: field. + if [ -n "$rectype" ] && [ "$f" = "$rectype" ]; then + continue + fi - local mand="no" - in_list "$f" $mandatory_fields && mand="yes" - local info ftype label - info=$(get_field_type_info "$block" "$f") - ftype="${info%%"$TAB"*}" # everything before the tab -> resolved type - label="${info#*"$TAB"}" # everything after the tab -> display label + local mand="no" + in_list "$f" $mandatory_fields && mand="yes" + local info ftype label + info=$(get_field_type_info "$block" "$f") + ftype="${info%%"$TAB"*}" # everything before the tab -> resolved type + label="${info#*"$TAB"}" # everything after the tab -> display label - local val - val=$(prompt_field "$f" "$mand" "$ftype" "$label") - if [ -n "$val" ]; then - args+=(-f "$f" -v "$val") - fi - prompted="$prompted $f" - done - fi + local val + val=$(prompt_field "$f" "$mand" "$ftype" "$label") + if [ -n "$val" ]; then + args+=(-f "$f" -v "$val") + fi + prompted="$prompted $f" + done + fi - # ---- 2. Prompt for any MANDATORY fields not in the allowed list ------- - if [ -n "$mandatory_fields" ]; then - local f - local printed_header="no" - for f in $mandatory_fields; do - if ! in_list "$f" $prompted; then - if [ "$printed_header" = "no" ]; then - echo "== Mandatory fields ==" >&2 - printed_header="yes" - fi - local info ftype label - info=$(get_field_type_info "$block" "$f") - ftype="${info%%"$TAB"*}" # everything before the tab -> resolved type - label="${info#*"$TAB"}" # everything after the tab -> display label + # ---- 2. Prompt for any MANDATORY fields not in the allowed list ------- + if [ -n "$mandatory_fields" ]; then + local f + local printed_header="no" + for f in $mandatory_fields; do + if ! in_list "$f" $prompted; then + if [ "$printed_header" = "no" ]; then + echo "== Mandatory fields ==" >&2 + printed_header="yes" + fi + local info ftype label + info=$(get_field_type_info "$block" "$f") + ftype="${info%%"$TAB"*}" # everything before the tab -> resolved type + label="${info#*"$TAB"}" # everything after the tab -> display label - local val - val=$(prompt_field "$f" "$mand" "$ftype" "$label") - args+=(-f "$f" -v "$val") - prompted="$prompted $f" - fi - done - fi + local val + val=$(prompt_field "$f" "yes" "$ftype" "$label") + args+=(-f "$f" -v "$val") + prompted="$prompted $f" + fi + done + fi - # ---- 3. Prompt for free-form / user-defined fields -------------------- - # ### CHANGED: Only offer free-form entry when no %allowed: constraint - # exists for this record type. If %allowed: is defined, recins will - # reject any field not listed there, so presenting the prompt would - # only allow the user to produce a record that fails on insert. - if [ -z "$allowed_fields" ]; then - echo "" >&2 - echo "== Additional fields ==" >&2 - echo "Enter a field name (or press Enter / type 'done' to finish):" >&2 - while true; do - printf "Field name: " >&2 - local newfield - IFS= read -r newfield - case "$newfield" in - "" | done | DONE | q | quit) break ;; - esac + # ---- 3. Prompt for free-form / user-defined fields -------------------- + # ### CHANGED: Only offer free-form entry when no %allowed: constraint + # exists for this record type. If %allowed: is defined, recins will + # reject any field not listed there, so presenting the prompt would + # only allow the user to produce a record that fails on insert. + if [ -z "$allowed_fields" ]; then + echo "" >&2 + echo "== Additional fields ==" >&2 + echo "Enter a field name (or press Enter / type 'done' to finish):" >&2 + while true; do + printf "Field name: " >&2 + local newfield + IFS= read -r newfield + case "$newfield" in + "" | done | DONE | q | quit) break ;; + esac - local info ftype label - info=$(get_field_type_info "$block" "$newfield") - ftype="${info%%"$TAB"*}" # everything before the tab -> resolved type - label="${info#*"$TAB"}" # everything after the tab -> display label + local info ftype label + info=$(get_field_type_info "$block" "$newfield") + ftype="${info%%"$TAB"*}" # everything before the tab -> resolved type + label="${info#*"$TAB"}" # everything after the tab -> display label - local val - val=$(prompt_field "$newfield" "$mand" "$ftype" "$label") + local val + val=$(prompt_field "$newfield" "no" "$ftype" "$label") - args+=(-f "$newfield" -v "$val") - done - else - echo "" >&2 - echo "== Additional fields ==" >&2 - echo "(Skipped: this record type has %allowed: constraints.)" >&2 - fi + args+=(-f "$newfield" -v "$val") + done + else + echo "" >&2 + echo "== Additional fields ==" >&2 + echo "(Skipped: this record type has %allowed: constraints.)" >&2 + fi - # ---- Sanity check: did we collect anything? --------------------------- - if [ "${#args[@]}" -eq 0 ]; then - echo "No fields entered. Aborting." >&2 - exit 1 - fi + # ---- Sanity check: did we collect anything? --------------------------- + if [ "${#args[@]}" -eq 0 ]; then + echo "No fields entered. Aborting." >&2 + exit 1 + fi - # ---- Build and run recins --------------------------------------------- - echo "" >&2 - echo "Inserting record..." >&2 + # ---- Build and run recins --------------------------------------------- + echo "" >&2 + echo "Inserting record..." >&2 - local status - if [ -n "$rectype" ]; then - recins -t "$rectype" "${args[@]}" "$recfile" - status=$? - else - recins "${args[@]}" "$recfile" - status=$? - fi + local status + if [ -n "$rectype" ]; then + recins -t "$rectype" "${args[@]}" "$recfile" + status=$? + else + recins "${args[@]}" "$recfile" + status=$? + fi - if [ "$status" -eq 0 ]; then - echo "Record inserted successfully into '$recfile'." >&2 - else - echo "recins failed (exit code $status)." >&2 - exit "$status" - fi + if [ "$status" -eq 0 ]; then + echo "Record inserted successfully into '$recfile'." >&2 + else + echo "recins failed (exit code $status)." >&2 + exit "$status" + fi } diff --git a/zrec-age b/zrec-age new file mode 100755 index 0000000..f74a2f5 --- /dev/null +++ b/zrec-age @@ -0,0 +1,284 @@ +#!/usr/bin/env bash +# +# zrec-age - Encrypted-recfile wrapper around zrec. +# +# Decrypts an age-encrypted recfile to a temporary plaintext file, runs zrec +# against it, then re-encrypts the result back to the original location. +# +# Usage: +# zrec-age [options] [record-type] +# +# Options: +# -i age identity (private key) file for DECRYPTION. +# May be repeated. (Implies key-based mode.) +# -r age recipient (public key) for ENCRYPTION. +# May be repeated. (Implies key-based mode.) +# -R File containing one recipient per line. +# -p Use passphrase (symmetric) mode for both +# decrypt and encrypt. (Default if no -i/-r/-R given.) +# -z Path to the zrec script (default: looks on PATH, +# then alongside this script). +# -h Show this help. +# +# Environment: +# ZREC_BIN Overrides the zrec location (same as -z). +# +# Notes: +# * In passphrase mode you will be prompted up to three times by age +# (decrypt, then encrypt). This is unavoidable with symmetric age. +# * Plaintext only ever lives in a 0700 temp directory and is removed +# (best-effort shredded) on exit. +# +set -o pipefail + +# --------------------------------------------------------------------------- +# Globals +# --------------------------------------------------------------------------- +PROG="$(basename "$0")" +WORKDIR="" +PLAINTEXT="" +PASSPHRASE_MODE="auto" # auto | yes | no +declare -a IDENTITIES=() +declare -a RECIPIENTS=() +declare -a RECIPIENT_FILES=() +ZREC_BIN="${ZREC_BIN:-}" + +# --------------------------------------------------------------------------- +# Logging helpers (all to stderr; stdout stays clean) +# --------------------------------------------------------------------------- +log() { printf '%s\n' "$*" >&2; } +err() { printf 'Error: %s\n' "$PROG" "$*" >&2; } +die() { + err "$*" + exit 1 +} + +usage() { + sed -n '3,40p' "$0" | sed 's/^# \{0,1\}//' >&2 + exit "${1:-1}" +} + +# --------------------------------------------------------------------------- +# Dependency checks +# --------------------------------------------------------------------------- +check_deps() { + command -v age >/dev/null 2>&1 || die "'age' not found. Install it (brew install age / apt install age)." + + if [ -z "$ZREC_BIN" ]; then + if command -v zrec >/dev/null 2>&1; then + ZREC_BIN="$(command -v zrec)" + else + # Fall back to a zrec sitting next to this wrapper. + local self_dir + self_dir="$(cd "$(dirname "$0")" && pwd)" + if [ -x "$self_dir/zrec" ]; then + ZREC_BIN="$self_dir/zrec" + fi + fi + fi + [ -n "$ZREC_BIN" ] && [ -x "$ZREC_BIN" ] || + die "Could not find an executable 'zrec' (use -z or set ZREC_BIN)." +} + +# --------------------------------------------------------------------------- +# Cross-platform secure temp dir creation. +# --------------------------------------------------------------------------- +make_workdir() { + # mktemp -d differs slightly across platforms but -d