linux-commands

Linux join Command: Match Records by Field

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

TaskCommand
Match the first field in both filesjoin file1.txt file2.txt
Match field 2 in file 1 to field 1 in file 2join -1 2 -2 1 file1.txt file2.txt
Use a comma as the delimiterjoin -t, file1.csv file2.csv
Choose output fieldsjoin -o 1.1,1.2,2.2 file1.txt file2.txt
Include unmatched lines from file 1join -a 1 file1.txt file2.txt
Show only unmatched lines from file 1join -v 1 file1.txt file2.txt
Fill missing output fieldsjoin -a 1 -e MISSING -o 1.1,1.2,2.2 file1.txt file2.txt
Ignore case while matchingjoin -i file1.txt file2.txt
Check input order with GNU joinjoin --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:

  1. field 1 from file 1: hostname;
  2. field 2 from file 1: IP address;
  3. 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.

  1. Save the inventory and health exports unchanged.
  2. Confirm that both use the same stable hostname or asset ID.
  3. Inspect delimiters, headers, capitalization, and sample rows.
  4. Create working copies sorted by the matching field under LC_ALL=C.
  5. Check for duplicate keys and verify sort order.
  6. Run a normal join on a few records to confirm the selected fields.
  7. Use join -v 1 to list inventory records without a health match.
  8. Validate several results in the source systems before escalating or changing anything.
  9. 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.