hl7probe
A friendly command-line tool for reading and checking HL7 v2 messages.
Hospital systems talk to each other in HL7 v2 — dense lines of text full of pipes and carets that look like this:
PID|1||123456^^^MERCY^MR||Smith^John^A^^Mr||19850312|M
Reading that by hand is slow and error-prone. hl7probe turns those lines into
something a person can read, and tells you what a receiving hospital system
would reject.
Contents
- Why you'd use it
- Install
- Quick start
- Reading the output
- Using it as a library
- Interactive viewer
- Using it in scripts and CI
- All the options
- What it checks
- What it accepts
- Performance
- Building from source
- Contributing
- License
Why you'd use it
You are wiring up an interface between two health systems and a message is being rejected. You need to know what is in the message and what is wrong with it — quickly, without opening a heavyweight integration engine.
hl7probe answers both in one command:
- Every field is shown with its real name —
PID-5becomes Patient Name. - Codes are translated —
Mbecomes Male,Ibecomes Inpatient. - Dates become readable —
19850312becomes 1985-03-12, age 41. - Problems are listed with the exact field, the line number and why it matters.
No configuration, no database, no server. One binary, one file, one answer.
Install
Homebrew (macOS and Linux)
Recent Homebrew versions ask you to trust a third-party tap the first time; if
you see that prompt, run brew trust sudhi001/tap and install again.
Download a prebuilt binary
Grab the archive for your platform from the
releases page, unpack it and
put hl7probe somewhere on your PATH. The Linux builds are static, so they
run on any distribution regardless of its glibc version:
On Windows, download the x86_64-pc-windows-msvc.zip archive, unpack it and put
hl7probe.exe in a folder on your PATH:
Expand-Archive hl7probe-*-x86_64-pc-windows-msvc.zip -DestinationPath .
With Cargo
Check it works:
$ hl7probe --version
hl7probe 0.5.0
None of the commands above pin a version: they each fetch the current release, which the badges at the top of this page track.
Quick start
Point it at a message file:
Or pipe one in:
|
Try it on the samples that ship with the project:
Reading the output
The report has three parts.
1. What the message is. The HL7 version, the message type, a plain-English description, who sent it and when.
HL7 v2.5.1 ADT^A01 Admit / Visit Notification
MSG00001 · 2024-01-15 14:32:00 · HIS/MERCY → LIS/LAB · Production
2. What is inside it. Each segment is listed with a status mark, then each
field is shown with its name, its raw value, and — after the › — the same
value in plain language.
Segments
────────────────────────────────────────────
MSH ✓ Message Header
EVN ✓ Event Type
PID ✓ Patient Identification
PV1 ✓ Patient Visit
PID · Patient Identification line 3
──────────────────────────────────────────────────────────────
3 Patient Identifier List 123456^^^MERCY^MR › 123456 (MR, MERCY)
~ rep 2 987654321^^^SSA^SS › 987654321 (SS, SSA)
5 Patient Name Smith^John^A^^Mr › Mr John A Smith
7 Date/Time of Birth 19850312 › 1985-03-12, age 41
8 Administrative Sex M › Male
⚠ 11 Patient Address (empty) recommended
3. What is wrong with it. Five groups of checks, then the individual findings, each pointing at the field responsible.
Validation
────────────────────────────────────────────
✗ Structure ADT^A01
⚠ Required fields
✗ Data types
⚠ Code tables
✗ Consistency
✗ EVN required segment is missing — ADT^A01 requires EVN (Event Type)
✗ PID-7 not a valid date/time — day 32 does not exist in 1985-03
✗ PV1-3 invalid location — component 1 (point of care) is empty
⚠ PID-11 missing — Patient Address should be populated when the value is known
5 errors · 8 warnings
The three marks mean:
| Mark | Meaning |
|---|---|
| ✓ | Fine |
| ⚠ | Works, but a receiving system may complain — a missing recommended field, an unusual code |
| ✗ | Wrong — this will be rejected |
Using it as a library
The parser and validator are a library as well as a command, so the same checks can run inside your own code:
use ;
let text = read_to_string?;
let message = parse?;
println!;
// PID-5.1 is the patient's family name.
let pid = message.first.expect;
println!;
let report = validate;
for finding in &report.findings
A message borrows the text it was read from rather than copying it, so keep
that string alive for as long as you use the message. For a file holding
several messages, walk them with parser::split_messages:
let = split_messages;
for raw in &raws
The four public modules are parser (decomposition), validate (the checks
and their findings), spec (the HL7 dictionary the checks read) and
datetime (HL7 timestamp handling). Full API documentation is on
docs.rs.
Interactive viewer
For bigger messages, browse instead of scroll:
Segments on the left, decoded fields on the right, problems underneath. Move
with the arrow keys, press ? for help and q to quit.
| Key | Action |
|---|---|
↑ ↓ or j k |
Move within the focused panel |
← → or h l |
Jump between segments and fields |
tab |
Cycle segments → fields → validation |
n / p |
Next / previous message in the file |
a |
Also show fields that were left empty |
v |
Include informational notes |
r |
Show the raw segment line |
f |
Findings for this segment only, or the whole message |
? |
Help |
q or esc |
Quit |
Using it in scripts and CI
One line per message, ideal for checking a folder full of test messages:
Exit codes make it a gate in a build pipeline:
| Code | Meaning |
|---|---|
0 |
No errors (warnings are allowed unless you pass --strict) |
1 |
At least one validation error |
2 |
The input could not be read, or contained no HL7 message |
||
Pull out a single value without writing a parser:
HL7 escape sequences are already decoded, so the output drops straight into a shell script.
Machine-readable reports for dashboards and tests:
|
All the options
hl7probe [OPTIONS] [FILE]...
FILE can be given more than once. Use -, or no file at all, to read from
standard input.
| Option | What it does |
|---|---|
-t, --tui |
Open the interactive viewer |
--json |
Print the whole report as JSON |
-q, --quiet |
Print one verdict line per message |
-v, --verbose |
Include informational notes |
-a, --all |
Show fields that were left empty |
-s, --segment PID,PV1 |
Only show these segments in detail |
--summary |
Segment list and verdict only, no field tables |
--raw |
Print the original segment line above each table |
-f, --field PID-5.1 |
Print one value and nothing else |
-m, --message N |
Only inspect the Nth message in the file |
--strict |
Count warnings as failures in the exit code |
--color auto|always|never |
Colour control (NO_COLOR is respected) |
--width N |
Wrap at N columns instead of the terminal width |
-h, --help |
Full help |
What it checks
Structure — the message type in MSH-9 is matched against the official
message layout (ADT, ORU, ORM/OML, ACK, SIU, MDM, VXU, DFT, BAR, RDE, QRY and
others). A missing required segment is an error; an unexpected or out-of-order
segment is a warning. Site-specific Z segments are left alone.
Required fields — fields the standard marks as required are errors when absent. Fields that should be filled in whenever the value is known — patient address, visit number, observation time — are warnings.
Data types — dates and times are checked against the real calendar, so
19850332 and 20230229 are caught, along with bad timezone offsets,
non-numeric numbers, identifiers with no ID, and locations with no ward.
Code tables — coded values are looked up in their HL7 table, so Q in the
patient class field is flagged. Unknown codes are warnings, because local code
sets are normal; tables that allow no local values — processing ID, version,
acknowledgement code, yes/no — are errors.
Consistency — the cross-field rules that catch real interface bugs:
- the event code in
EVN-1disagreeing with the trigger inMSH-9 - a discharge time earlier than the admission time
- a date of birth in the future, or an implausible age
- a discharge message with no discharge time
- an inpatient with no assigned location
- an observation value that contradicts its declared type (
NMholding text) - set IDs on repeating segments that do not count up
- the same patient identifier repeated twice
- accented characters sent with no character set declared in
MSH-18 - a message control ID too long for the receiving system
Every finding carries a severity, the exact field, the line number in the file and an explanation of why it matters.
What is not checked
Free-text fields (ST, TX, FT) have nothing to check beyond their
presence. Coded fields (ID, IS) are checked against the HL7 table they name
and not otherwise, since without a table any value is legal. OBX-5 carries
whatever type OBX-2 declares and is checked against that.
Beyond those, several composite types are read and displayed but their
components are not individually validated: addresses (XAD), phone numbers
(XTN), organisation names (XON), and the timing, price and quantity types
(TQ, CP, CQ). A malformed phone number will be shown, not reported.
Which HL7 version the checks come from
The field definitions, usages and code tables are taken from HL7 2.5.1.
Messages declaring any version are read and checked, but field numbering,
names, whether a field is required and which table it names all change between
versions of HL7 v2. When a message declares a different version the report says
so against MSH-12, so version-sensitive findings can be weighed accordingly:
ℹ MSH-12 checked against HL7 2.5.1 — this message declares 2.3; field
numbering, names, usage and code tables differ between versions, so
field-level findings may not all apply
Per-version dictionaries are not implemented. spec::DICTIONARY_VERSION names
the version in use, for callers that need to reason about it.
What it accepts
Real-world message files are messy. hl7probe copes with:
- Windows, Unix or classic Mac line endings (
CRLF,LF,CR) - MLLP framing bytes left over from a network capture
- Batch files with
FHS/BHS/BTS/FTSwrappers - Several messages in one file, each reported separately
- Non-UTF-8 (latin-1) text, decoded instead of rejected
- Custom delimiters — whatever
MSH-1andMSH-2declare is what is used
Performance
A batch file is read into memory once and never copied. Messages are parsed, written and dropped one at a time, and a parsed message is a set of views over the file text rather than an owned tree, so peak memory is close to the size of the input regardless of which output mode you ask for.
Measured on an Apple M4 (macOS 26.6, rustc 1.97.1, --release), best of five
runs, against a file of 50,000 copies of examples/adt_a01.hl7 — 26 MB, or
about 300,000 segments:
| Command | Time | Peak memory |
|---|---|---|
hl7probe -q (validate only) |
0.67s | 33 MB |
hl7probe -f PID-5.1 (one field per message) |
0.14s | 33 MB |
hl7probe (full report) |
2.73s | 34 MB |
hl7probe --json |
4.90s | 34 MB |
That is about 1.3x the file, and most of it is the file. At the sizes most runs actually are, none of this matters: a single message reports in about 4 ms end to end — most of that being process startup — using 2.3 MB, and 5,000 messages (2.6 MB) validate in 0.07s using 5.7 MB.
Memory used to scale with the batch rather than the message, because every
message was parsed up front and every field, component and subcomponent became
its own String:
| Command | Peak memory before | After |
|---|---|---|
hl7probe -q |
2,234 MB | 33 MB |
hl7probe -f PID-5.1 |
2,234 MB | 33 MB |
hl7probe |
2,417 MB | 34 MB |
hl7probe --json |
4,598 MB | 34 MB |
A parsed message costs 2.4 KB and 35 allocations, down from 40 KB and 482. That is what the interactive viewer pays per message, since it has to hold the whole batch to let you page back and forth. Validating one costs a further 19 allocations when it is clean, and beyond that only what its findings need.
To reproduce:
Building from source
Requires Rust 1.88 or newer.
The code is organised as:
| File | Responsibility |
|---|---|
src/parser.rs |
Splitting messages into segments, fields, components |
src/spec.rs |
The HL7 dictionary: field names, code tables, message layouts |
src/validate.rs |
The checks, one rule per concern |
src/view.rs |
The decoded field model both output modes share |
src/render.rs |
The printed report |
src/tui.rs |
The interactive viewer |
src/datetime.rs |
HL7 date and time handling |
src/text.rs |
Padding and truncation helpers |
src/main.rs |
Command-line interface |
Adding a validation check means writing one Rule implementation in
src/validate.rs and listing it in RULES; nothing else changes.
Changelog
Release notes live in CHANGELOG.md.
Contributing
Issues and pull requests are welcome. Please make sure cargo test and
cargo clippy --all-targets pass, and add a test alongside any behaviour
change — the fastest way to describe an HL7 bug is a message that reproduces it.
Note your change under Unreleased in CHANGELOG.md.
License
MIT — see LICENSE.