Linux join Command: Match Records by Field
The Linux join command matches records from two sorted text files using a shared field. If one file maps hostnames to IP addresses and another maps hostnames to health states, join can combine each matching hostname into one line.
join inventory.txt health.txt
join is useful for small, controlled support data sets such as:
- matching asset IDs to assigned users;
- adding IP addresses to a hostname report;
- comparing approved accounts with an application export;
- combining package names with version or status data.
Two rules matter immediately: both files must be sorted on the fields you are matching, and plain join is not a full CSV parser. If you skip either detail, you can produce a clean-looking report that is quietly wrong—the least entertaining kind of ticket update.
Quick join command reference
| Task | Command |
|---|---|
| Match the first field in both files | join file1.txt file2.txt |
| Match field 2 in file 1 to field 1 in file 2 | join -1 2 -2 1 file1.txt file2.txt |
| Use a comma as the delimiter | join -t, file1.csv file2.csv |
| Choose output fields | join -o 1.1,1.2,2.2 file1.txt file2.txt |
| Include unmatched lines from file 1 | join -a 1 file1.txt file2.txt |
| Show only unmatched lines from file 1 | join -v 1 file1.txt file2.txt |
| Fill missing output fields | join -a 1 -e MISSING -o 1.1,1.2,2.2 file1.txt file2.txt |
| Ignore case while matching | join -i file1.txt file2.txt |
| Check input order with GNU join | join --check-order file1.txt file2.txt |
The general syntax is:
join [OPTION]... FILE1 FILE2
By default, join treats runs of blanks as field separators and uses the first field in each file as the key.
A basic Linux join command example
Suppose inventory.txt contains:
app01 10.20.1.11
app02 10.20.1.12
db01 10.20.2.21
And health.txt contains:
app01 healthy
app02 warning
db01 healthy
Both files are sorted by hostname. Run:
join inventory.txt health.txt
Output:
app01 10.20.1.11 healthy
app02 10.20.1.12 warning
db01 10.20.2.21 healthy
The shared hostname appears once, followed by the remaining fields from file 1 and then file 2. join did not understand what a hostname, IP address, or health state means. It simply matched equal first fields.
This is different from paste, which combines lines by position. If health.txt were in a different order, paste could attach the wrong status to a host. join matches the key instead.
Sort files by the join field first
join expects each input to be sorted by its matching field. Preserve the original exports and create working copies:
LC_ALL=C sort -k1,1 inventory.txt > inventory.sorted
LC_ALL=C sort -k1,1 health.txt > health.sorted
LC_ALL=C join inventory.sorted health.sorted
-k1,1 tells sort to use only field 1 as the key. LC_ALL=C gives sorting and matching the same predictable byte-oriented locale.
Do not use a broad sort inventory.txt without thinking. Full-line sorting often works when the key is first, but explicitly naming the key documents your intent and matters when the join field is elsewhere.
GNU join can check order while it works:
LC_ALL=C join --check-order inventory.sorted health.sorted
If your installed implementation lacks --check-order, verify the files separately:
LC_ALL=C sort -c -k1,1 inventory.sorted
LC_ALL=C sort -c -k1,1 health.sorted
A successful sort -c normally prints nothing. The Linux sort guide explains keys, numeric sorting, and duplicate handling in more detail.
Match different fields with -1 and -2
The shared value is not always in the same position. Suppose assets.txt stores an asset tag in field 2:
laptop-amy LT-1042 Windows11
laptop-lee LT-1088 Ubuntu
And owners.txt starts with the asset tag:
LT-1042 [email protected]
LT-1088 [email protected]
Match field 2 from the first file with field 1 from the second:
join -1 2 -2 1 assets.txt owners.txt
Output:
LT-1042 laptop-amy Windows11 [email protected]
LT-1088 laptop-lee Ubuntu [email protected]
The options mean:
-1 2: use field 2 from file 1;-2 1: use field 1 from file 2.
Sort each file by its own join field first:
LC_ALL=C sort -k2,2 assets.txt > assets.sorted
LC_ALL=C sort -k1,1 owners.txt > owners.sorted
LC_ALL=C join -1 2 -2 1 assets.sorted owners.sorted
A common beginner mistake is sorting both files by field 1 even after choosing field 2 from the first file. The commands run, but the input order no longer matches the join key.
Choose output columns with -o
Default output is convenient for exploration, but reports usually need explicit columns. Field references use FILENUM.FIELDNUM:
join -o 1.1,1.2,2.2 inventory.sorted health.sorted
This requests:
- field 1 from file 1: hostname;
- field 2 from file 1: IP address;
- field 2 from file 2: health state.
Output:
app01 10.20.1.11 healthy
app02 10.20.1.12 warning
db01 10.20.2.21 healthy
Explicit output fields become more important when the source files contain extra notes, timestamps, or columns that should not go into a ticket attachment. The cut command guide covers simpler field extraction when you are working with one file rather than matching two.
Use a different delimiter with -t
For simple comma-delimited data, specify the separator:
LC_ALL=C sort -t, -k1,1 inventory.csv > inventory.sorted.csv
LC_ALL=C sort -t, -k1,1 health.csv > health.sorted.csv
LC_ALL=C join -t, inventory.sorted.csv health.sorted.csv
If the files contain:
app01,10.20.1.11
app02,10.20.1.12
and:
app01,healthy
app02,warning
then the result is:
app01,10.20.1.11,healthy
app02,10.20.1.12,warning
This is safe only for deliberately simple delimited text. Real CSV can contain quoted commas, embedded newlines, escaped quotes, and other cases that join -t, does not parse as CSV structure. Use a CSV-aware tool, database, or script for arbitrary exports.
Find unmatched records with -a and -v
A normal join prints matched records. Support work often needs the missing ones.
To include matched records plus unmatched records from file 1:
join -a 1 inventory.sorted health.sorted
To include unmatched records from both files:
join -a 1 -a 2 inventory.sorted health.sorted
To show only records from file 1 that have no match in file 2:
join -v 1 inventory.sorted health.sorted
That is useful when the inventory contains a server but the monitoring export does not. To find monitoring records absent from inventory, use:
join -v 2 inventory.sorted health.sorted
Treat unmatched records as leads, not proof of a failure. The exports may have different scopes, timestamps, filters, or identifier formats.
Fill missing fields with -e
When you include unpaired lines and select fields explicitly, use -e to make missing values visible:
join -a 1 -e NOT_REPORTED -o 1.1,1.2,2.2 inventory.sorted health.sorted
If db01 has no health record, output can look like:
db01 10.20.2.21 NOT_REPORTED
A label is safer than an empty-looking column because it distinguishes “no matching record” from “the status happens to be blank.” Choose a value your downstream tool cannot confuse with real data.
Handle capitalization and duplicate keys carefully
Use -i for a case-insensitive match:
join -i inventory.sorted health.sorted
The files still need sorting compatible with the comparison. For predictable automation, it is often clearer to normalize documented working copies, then sort and join them under the same locale.
Duplicate keys also need attention. If app01 appears twice in each file, join can produce multiple combinations. That may be correct, or it may expose a broken export. Check key counts before trusting the result:
cut -d' ' -f1 inventory.sorted | uniq -c
Do not automatically remove duplicates with sort -u unless the repeated records truly mean the same thing. A duplicate asset ID can be the problem you are supposed to investigate.
Common beginner mistakes
Joining unsorted files
This is the main failure. Sort each input by the exact field used for matching, with consistent delimiter and locale settings.
Mixing up -1 and -2
The first number identifies the input file. -1 2 means field 2 from file 1; it does not mean fields 1 through 2.
Expecting a database join
join is a focused text utility. It does not understand data types, quoted CSV, relational constraints, or whether two identifiers refer to the same real device.
Hiding unmatched records
A normal join omits records without a match. If you need completeness, deliberately use -a or inspect missing records with -v.
Overwriting source exports
Create sorted working copies. Keeping the originals makes the result reviewable and gives you somewhere to return when a delimiter or field assumption was wrong.
Matching inconsistent keys
app01, APP01, app01.example.test, and app01 are different text unless you deliberately normalize them. Document any normalization, especially for account or asset data.
A safe help desk workflow
Imagine a patching ticket asks which managed servers did not send a health result.
- Save the inventory and health exports unchanged.
- Confirm that both use the same stable hostname or asset ID.
- Inspect delimiters, headers, capitalization, and sample rows.
- Create working copies sorted by the matching field under
LC_ALL=C. - Check for duplicate keys and verify sort order.
- Run a normal join on a few records to confirm the selected fields.
- Use
join -v 1to list inventory records without a health match. - Validate several results in the source systems before escalating or changing anything.
- Record source timestamps, commands, and assumptions in the ticket.
Example:
LC_ALL=C sort -k1,1 inventory.txt > inventory.sorted
LC_ALL=C sort -k1,1 health.txt > health.sorted
LC_ALL=C sort -c -k1,1 inventory.sorted
LC_ALL=C sort -c -k1,1 health.sorted
LC_ALL=C join -v 1 inventory.sorted health.sorted > missing-health.txt
wc -l missing-health.txt
head missing-health.txt
Every command here is read-only with respect to the source exports. The output becomes a review list, not an excuse to disable accounts or remove devices automatically.
If you only need to compare complete lines rather than match fields, use the comm command guide. The distinction is simple: comm compares whole sorted lines; join matches sorted records by a chosen field.
Practice join without production data
Create two five-line sample files in a practice shell. Put a shared hostname in each, leave one hostname unmatched, and move the key to field 2 in one file. Then practice sorting by the right field, selecting output with -o, and finding missing records with -v.
The syntax is not the hard part. The useful skill is checking your key, delimiter, ordering, duplicates, and unmatched records before anyone acts on the output.
Practice Linux commands in Shell Samurai and build the command-line confidence to turn small text exports into safe, reviewable answers.
FAQ
What does the Linux join command do?
join matches lines from two sorted text files using a shared field. By default, it matches the first blank-separated field in each file.
Do files need to be sorted before join?
Yes. Sort each file by the exact field used for matching, using compatible delimiter and locale settings. Otherwise the result may be incomplete or incorrect.
How do I show records that did not match?
Use join -v 1 file1 file2 for unmatched records from file 1 or join -v 2 file1 file2 for unmatched records from file 2. Use -a when you want matched and unmatched records together.
Can join use different fields in each file?
Yes. For example, join -1 2 -2 1 file1 file2 matches field 2 in the first file against field 1 in the second.
Can join safely merge any CSV files?
No. join -t, can handle simple comma-delimited text, but it does not fully parse quoted CSV. Use a CSV-aware tool when fields can contain commas, quotes, or newlines.
Practice This in a Real Terminal
Shell Samurai gives you safe Linux missions so the commands actually stick. Chapter 1 is free; the full practice path is a one-time purchase, not another subscription.