v0.4.1: each / each-site / len2nl / csv-to-table / table-to-csv
Five small Unix-style loop & format helpers, fully offline:
lib/each.sh
- replaces v1 `each`
- run a CMD per item: args, stdin lines, or {}-placeholder substitution
- example: tbn adt | awk '{print $2}' | each.sh 'route_test {}'
lib/each-site.sh
- replaces v1 each_site / each_site_hdr / each_site_tcl patterns
- iterates every site under $HCIROOT with HCISITE/HCISITEDIR auto-exported
- --filter REGEX limits which sites; --hdr prints a header before each
lib/len2nl.sh
- replaces v1 `len2nl`
- strict superset: handles length-prefixed (digits before MSH),
MLLP (\x0b...\x1c\x0d), and segment CRs (→ LF)
- works as stdin filter or with file arg
lib/csv-to-table.sh
- 2-column CSV → Cloverleaf .tbl format
- emits proper prologue (who, date, bidir, type, version)
- --has-header --default VALUE --bidir 0|1 --in-delim CHAR --user NAME --out PATH
lib/table-to-csv.sh
- reverse: .tbl → CSV
- --with-header --delim CHAR --include-meta
- confirmed clean round-trip: CSV → table → CSV byte-identical for the data rows
All 5 are pipeable, have --help, zero external deps beyond bash+awk+sed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
47e44c2289
commit
3eb88f86c8
@ -94,6 +94,11 @@ fetch lib/oauth.sh "$LARRY_HOME/lib/oauth.sh"
|
|||||||
fetch lib/lessons.sh "$LARRY_HOME/lib/lessons.sh"
|
fetch lib/lessons.sh "$LARRY_HOME/lib/lessons.sh"
|
||||||
fetch lib/hl7-sanitize.sh "$LARRY_HOME/lib/hl7-sanitize.sh"
|
fetch lib/hl7-sanitize.sh "$LARRY_HOME/lib/hl7-sanitize.sh"
|
||||||
fetch lib/hl7-desanitize.sh "$LARRY_HOME/lib/hl7-desanitize.sh"
|
fetch lib/hl7-desanitize.sh "$LARRY_HOME/lib/hl7-desanitize.sh"
|
||||||
|
fetch lib/each.sh "$LARRY_HOME/lib/each.sh"
|
||||||
|
fetch lib/each-site.sh "$LARRY_HOME/lib/each-site.sh"
|
||||||
|
fetch lib/len2nl.sh "$LARRY_HOME/lib/len2nl.sh"
|
||||||
|
fetch lib/csv-to-table.sh "$LARRY_HOME/lib/csv-to-table.sh"
|
||||||
|
fetch lib/table-to-csv.sh "$LARRY_HOME/lib/table-to-csv.sh"
|
||||||
fetch lib/nc-parse.sh "$LARRY_HOME/lib/nc-parse.sh"
|
fetch lib/nc-parse.sh "$LARRY_HOME/lib/nc-parse.sh"
|
||||||
fetch lib/nc-inbound.sh "$LARRY_HOME/lib/nc-inbound.sh"
|
fetch lib/nc-inbound.sh "$LARRY_HOME/lib/nc-inbound.sh"
|
||||||
fetch lib/nc-make-jump.sh "$LARRY_HOME/lib/nc-make-jump.sh"
|
fetch lib/nc-make-jump.sh "$LARRY_HOME/lib/nc-make-jump.sh"
|
||||||
|
|||||||
2
larry.sh
2
larry.sh
@ -32,7 +32,7 @@ set -o pipefail
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Config
|
# Config
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
LARRY_VERSION="0.4.0"
|
LARRY_VERSION="0.4.1"
|
||||||
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
||||||
LARRY_UPDATE_URL="${LARRY_UPDATE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/larry.sh}"
|
LARRY_UPDATE_URL="${LARRY_UPDATE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/larry.sh}"
|
||||||
LARRY_AGENTS_URL="${LARRY_AGENTS_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/agents}"
|
LARRY_AGENTS_URL="${LARRY_AGENTS_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/agents}"
|
||||||
|
|||||||
102
lib/csv-to-table.sh
Executable file
102
lib/csv-to-table.sh
Executable file
@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# csv-to-table.sh — convert a 2-column CSV into Cloverleaf .tbl format.
|
||||||
|
#
|
||||||
|
# Input CSV (with or without header):
|
||||||
|
# input,output
|
||||||
|
# 12345,M
|
||||||
|
# 12346,F
|
||||||
|
# ...
|
||||||
|
#
|
||||||
|
# Output Cloverleaf .tbl:
|
||||||
|
# # Generated by csv-to-table.sh
|
||||||
|
# prologue
|
||||||
|
# who: <user>
|
||||||
|
# date: <iso8601>
|
||||||
|
# outname: output
|
||||||
|
# inname: input
|
||||||
|
# bidir: 0
|
||||||
|
# type: tbl
|
||||||
|
# version: 7.0
|
||||||
|
# end_prologue
|
||||||
|
# dflt=__non-existent__
|
||||||
|
# #
|
||||||
|
# 12345
|
||||||
|
# M
|
||||||
|
# encoded=0,0
|
||||||
|
# #
|
||||||
|
# 12346
|
||||||
|
# F
|
||||||
|
# ...
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# csv-to-table.sh [FILE] [--default VALUE] [--bidir 0|1] [--has-header]
|
||||||
|
# [--in-delim CHAR] # default ','
|
||||||
|
# [--user NAME] # default $USER
|
||||||
|
# [--out PATH] # default stdout
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
usage() { sed -n '2,30p' "$0"; exit 0; }
|
||||||
|
|
||||||
|
INPUT=""
|
||||||
|
DEFAULT="__non-existent__"
|
||||||
|
BIDIR=0
|
||||||
|
HAS_HEADER=0
|
||||||
|
DELIM=','
|
||||||
|
USER_NAME="${USER:-larry}"
|
||||||
|
OUT_FILE=""
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--default) shift; DEFAULT="$1" ;;
|
||||||
|
--bidir) shift; BIDIR="$1" ;;
|
||||||
|
--has-header) HAS_HEADER=1 ;;
|
||||||
|
--in-delim) shift; DELIM="$1" ;;
|
||||||
|
--user) shift; USER_NAME="$1" ;;
|
||||||
|
--out) shift; OUT_FILE="$1" ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
-*) echo "csv-to-table: unknown flag: $1" >&2; exit 2 ;;
|
||||||
|
*) INPUT="$1" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -z "$INPUT" ] || [ -f "$INPUT" ] || { echo "csv-to-table: no such file: $INPUT" >&2; exit 2; }
|
||||||
|
|
||||||
|
emit() {
|
||||||
|
cat <<EOF
|
||||||
|
# Generated by larry-anywhere csv-to-table.sh
|
||||||
|
prologue
|
||||||
|
who: ${USER_NAME}
|
||||||
|
date: $(date -Iseconds 2>/dev/null || date)
|
||||||
|
outname: output
|
||||||
|
inname: input
|
||||||
|
bidir: ${BIDIR}
|
||||||
|
type: tbl
|
||||||
|
version: 7.0
|
||||||
|
end_prologue
|
||||||
|
#
|
||||||
|
dflt=${DEFAULT}
|
||||||
|
#
|
||||||
|
EOF
|
||||||
|
awk -F"$DELIM" -v has_header="$HAS_HEADER" '
|
||||||
|
NR == 1 && has_header == 1 { next }
|
||||||
|
NF >= 2 {
|
||||||
|
# Trim whitespace
|
||||||
|
gsub(/^[ \t"]+|[ \t"]+$/, "", $1)
|
||||||
|
gsub(/^[ \t"]+|[ \t"]+$/, "", $2)
|
||||||
|
if ($1 == "") next
|
||||||
|
print $1
|
||||||
|
print $2
|
||||||
|
print "encoded=0,0"
|
||||||
|
print "#"
|
||||||
|
}
|
||||||
|
' "${INPUT:-/dev/stdin}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -n "$OUT_FILE" ]; then
|
||||||
|
mkdir -p "$(dirname "$OUT_FILE")" 2>/dev/null
|
||||||
|
emit > "$OUT_FILE"
|
||||||
|
printf 'csv-to-table: wrote %s\n' "$OUT_FILE" >&2
|
||||||
|
else
|
||||||
|
emit
|
||||||
|
fi
|
||||||
54
lib/each-site.sh
Executable file
54
lib/each-site.sh
Executable file
@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# each-site.sh — run a command for each site under $HCIROOT.
|
||||||
|
# Replaces v1 `each_site` / `each_site_hdr` / `each_site_tcl` patterns.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# each-site.sh [--hciroot DIR] [--hdr] [--filter REGEX] <CMD ...>
|
||||||
|
#
|
||||||
|
# The current site is exposed to CMD as the env vars HCISITE and HCISITEDIR.
|
||||||
|
# The literal token {} in CMD, if present, is replaced with the site name.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# each-site.sh 'lib/nc-find.sh --name adt --netconfigs "$HCISITEDIR/NetConfig"'
|
||||||
|
# each-site.sh --hdr 'echo "Hey from $HCISITE; netconfig=$HCISITEDIR/NetConfig"'
|
||||||
|
# each-site.sh --filter '^(ancout|epic)$' 'wc -l "$HCISITEDIR/NetConfig"'
|
||||||
|
# each-site.sh 'lib/nc-parse.sh list-protocols "$HCISITEDIR/NetConfig" | wc -l'
|
||||||
|
#
|
||||||
|
# --hdr prints a "===== <site> =====" header before each run.
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
HCIROOT_OVR=""
|
||||||
|
HEADER=0
|
||||||
|
FILTER=""
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--hciroot) shift; HCIROOT_OVR="$1" ;;
|
||||||
|
--hdr) HEADER=1 ;;
|
||||||
|
--filter) shift; FILTER="$1" ;;
|
||||||
|
-h|--help) sed -n '2,18p' "$0"; exit 0 ;;
|
||||||
|
--) shift; break ;;
|
||||||
|
-*) echo "each-site: unknown flag: $1" >&2; exit 2 ;;
|
||||||
|
*) break ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
[ $# -ge 1 ] || { echo "each-site: needs a CMD" >&2; exit 2; }
|
||||||
|
CMD="$*"
|
||||||
|
|
||||||
|
ROOT="${HCIROOT_OVR:-${HCIROOT:-}}"
|
||||||
|
[ -n "$ROOT" ] && [ -d "$ROOT" ] || { echo "each-site: no \$HCIROOT (or --hciroot)" >&2; exit 2; }
|
||||||
|
|
||||||
|
# Site = subdir of ROOT that contains a NetConfig file.
|
||||||
|
SITES=$(find "$ROOT" -mindepth 1 -maxdepth 2 -name NetConfig -type f 2>/dev/null \
|
||||||
|
| xargs -I{} dirname {} 2>/dev/null \
|
||||||
|
| xargs -I{} basename {} 2>/dev/null \
|
||||||
|
| sort -u)
|
||||||
|
[ -n "$SITES" ] || { echo "each-site: no sites with NetConfig under $ROOT" >&2; exit 1; }
|
||||||
|
|
||||||
|
while IFS= read -r site; do
|
||||||
|
[ -z "$site" ] && continue
|
||||||
|
if [ -n "$FILTER" ] && ! printf '%s' "$site" | grep -Eq -- "$FILTER"; then continue; fi
|
||||||
|
[ "$HEADER" = "1" ] && printf '\n===== %s =====\n' "$site"
|
||||||
|
HCISITE="$site" HCISITEDIR="$ROOT/$site" eval "${CMD//\{\}/$site}"
|
||||||
|
done <<< "$SITES"
|
||||||
45
lib/each.sh
Executable file
45
lib/each.sh
Executable file
@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# each.sh — run a command for each item. Replaces v1 `each`.
|
||||||
|
#
|
||||||
|
# Two input modes:
|
||||||
|
# each.sh <CMD> <ARG1> [ARG2 ... ARGN] — args as items
|
||||||
|
# <stream> | each.sh <CMD> — stdin lines as items (one per line)
|
||||||
|
#
|
||||||
|
# The literal token {} in CMD, if present, gets replaced with the current item.
|
||||||
|
# Otherwise the item is appended as the last arg.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# each.sh 'echo got:' /etc/hosts /etc/passwd
|
||||||
|
# tbn adt | awk 'NR>1{print $2}' | each.sh 'echo route_test for' {}
|
||||||
|
# nc-find.sh --process codametrix --format tsv \
|
||||||
|
# | awk -F'\t' 'NR>1{print $2}' \
|
||||||
|
# | each.sh 'lib/nc-msgs.sh' {} '--limit' '1' '--format' 'count'
|
||||||
|
#
|
||||||
|
# Each invocation is a separate process; failures are not aggregated. To
|
||||||
|
# stop on first error, prepend `set -e` in your own wrapper.
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
usage() { sed -n '2,20p' "$0"; exit 0; }
|
||||||
|
[ $# -ge 1 ] || usage
|
||||||
|
case "$1" in -h|--help) usage ;; esac
|
||||||
|
|
||||||
|
CMD="$1"; shift
|
||||||
|
|
||||||
|
run_one() {
|
||||||
|
local item="$1"
|
||||||
|
if [[ "$CMD" == *"{}"* ]]; then
|
||||||
|
eval "${CMD//\{\}/\"\$item\"}"
|
||||||
|
else
|
||||||
|
# Append item as last arg
|
||||||
|
eval "$CMD \"\$item\""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
for item in "$@"; do run_one "$item"; done
|
||||||
|
else
|
||||||
|
while IFS= read -r item; do
|
||||||
|
[ -z "$item" ] && continue
|
||||||
|
run_one "$item"
|
||||||
|
done
|
||||||
|
fi
|
||||||
38
lib/len2nl.sh
Executable file
38
lib/len2nl.sh
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# len2nl.sh — convert length-prefixed and/or MLLP-framed HL7 to newline-readable.
|
||||||
|
#
|
||||||
|
# Handles both common framing patterns:
|
||||||
|
# 1. Length-prefix: `<digits>MSH|...` → strip leading digits before MSH
|
||||||
|
# 2. MLLP: `\x0b...\x1c\x0d` → strip VT and FS+CR end-block
|
||||||
|
# 3. Segment CRs: `<seg>\r<seg>\r...` → replace \r with \n
|
||||||
|
#
|
||||||
|
# After conversion, each segment is on its own line (LF), making the message
|
||||||
|
# greppable / pageable.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# len2nl.sh [FILE] # file or stdin
|
||||||
|
# cat raw.hl7 | len2nl.sh
|
||||||
|
# tcpdump ... | len2nl.sh
|
||||||
|
#
|
||||||
|
# This is a strict superset of the v1 `len2nl` behavior.
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
usage() { sed -n '2,18p' "$0"; exit 0; }
|
||||||
|
case "${1:-}" in -h|--help) usage ;; esac
|
||||||
|
|
||||||
|
INPUT="${1:-}"
|
||||||
|
if [ -n "$INPUT" ] && [ ! -f "$INPUT" ]; then
|
||||||
|
echo "len2nl: no such file: $INPUT" >&2; exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$INPUT" ]; then
|
||||||
|
CMD=(cat "$INPUT")
|
||||||
|
else
|
||||||
|
CMD=(cat)
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${CMD[@]}" \
|
||||||
|
| tr -d '\013' `# strip MLLP VT (0x0B = start-block)` \
|
||||||
|
| sed 's/\x1c\x0d//g' `# strip MLLP FS+CR (0x1C 0x0D = end-block)` \
|
||||||
|
| sed -E 's/[0-9]+MSH\|/MSH|/g' `# strip numeric length-prefix before MSH` \
|
||||||
|
| tr '\r' '\n' `# CR-separated segments → LF`
|
||||||
70
lib/table-to-csv.sh
Executable file
70
lib/table-to-csv.sh
Executable file
@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# table-to-csv.sh — convert a Cloverleaf .tbl file to CSV.
|
||||||
|
#
|
||||||
|
# Reverses csv-to-table.sh. Skips the prologue, dflt= line, encoded= meta,
|
||||||
|
# comment lines, and #-separator lines; emits "input,output" rows.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# table-to-csv.sh [FILE] # file or stdin
|
||||||
|
# table-to-csv.sh --with-header [FILE] # emit "input,output" header
|
||||||
|
# table-to-csv.sh --delim ';' [FILE] # output delimiter (default ',')
|
||||||
|
# table-to-csv.sh --include-meta [FILE] # also emit prologue/default rows
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
usage() { sed -n '2,12p' "$0"; exit 0; }
|
||||||
|
|
||||||
|
INPUT=""
|
||||||
|
WITH_HEADER=0
|
||||||
|
DELIM=','
|
||||||
|
INCLUDE_META=0
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--with-header) WITH_HEADER=1 ;;
|
||||||
|
--delim) shift; DELIM="$1" ;;
|
||||||
|
--include-meta) INCLUDE_META=1 ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
-*) echo "table-to-csv: unknown flag: $1" >&2; exit 2 ;;
|
||||||
|
*) INPUT="$1" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -z "$INPUT" ] || [ -f "$INPUT" ] || { echo "table-to-csv: no such file: $INPUT" >&2; exit 2; }
|
||||||
|
|
||||||
|
[ "$WITH_HEADER" = "1" ] && printf 'input%soutput\n' "$DELIM"
|
||||||
|
|
||||||
|
awk -v D="$DELIM" -v META="$INCLUDE_META" '
|
||||||
|
function csv_escape(s, needs_q) {
|
||||||
|
needs_q = (index(s, D) > 0 || index(s, "\"") > 0 || index(s, "\n") > 0)
|
||||||
|
gsub(/"/, "\"\"", s)
|
||||||
|
if (needs_q) return "\"" s "\""
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
/^[[:space:]]*$/ { next }
|
||||||
|
/^[[:space:]]*#/ { next }
|
||||||
|
/^prologue$/ { in_prologue=1; next }
|
||||||
|
/^end_prologue$/ { in_prologue=0; next }
|
||||||
|
in_prologue { next }
|
||||||
|
/^dflt=/ {
|
||||||
|
if (META == "1") {
|
||||||
|
val = $0; sub(/^dflt=/, "", val)
|
||||||
|
print "__dflt__" D csv_escape(val)
|
||||||
|
}
|
||||||
|
next
|
||||||
|
}
|
||||||
|
/^encoded=/ { next }
|
||||||
|
{
|
||||||
|
line = $0
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
|
||||||
|
if (line == "") next
|
||||||
|
if (waiting_for_output) {
|
||||||
|
print csv_escape(pending_input) D csv_escape(line)
|
||||||
|
pending_input = ""
|
||||||
|
waiting_for_output = 0
|
||||||
|
} else {
|
||||||
|
pending_input = line
|
||||||
|
waiting_for_output = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "${INPUT:-/dev/stdin}"
|
||||||
Loading…
Reference in New Issue
Block a user