I discovered paste while trying to combine two exported columns from a database dump — one file with usernames, one with corresponding user IDs, generated by two separate queries because our reporting tool couldn’t export them side by side. My instinct was to write a small script with a loop and two file handles. Then someone pointed out that paste does exactly this in one line, and I felt a little silly for not knowing about it sooner. It’s one of those commands that’s narrow in scope but saves real time once it’s part of your muscle memory.
What paste Does
paste merges corresponding lines from multiple files (or standard input), joining them side by side with a delimiter — tab by default — rather than stacking them vertically like cat would.
paste [OPTION]... [FILE]...
I tested the basic behavior:
$ printf "a\nb\nc\n" > f1.txt
$ printf "1\n2\n3\n" > f2.txt
$ paste f1.txt f2.txt
a 1
b 2
c 3
Line 1 of f1.txt and line 1 of f2.txt are joined with a tab; line 2 of each file, and so on — a horizontal merge rather than a vertical concatenation.
Core Options and Parameters
-d DELIMITER — Custom Delimiter
By default, paste separates joined fields with a tab character. Use -d to specify something else:
$ paste -d',' f1.txt f2.txt
a,1
b,2
c,3
I tested this with a comma, which is the most common use case I run into — building CSV rows out of separately generated columns.
Cycling Multiple Delimiters
If you provide more than one character to -d, paste cycles through them between successive fields on the same output line, rather than repeating the last one:
$ printf "x\ny\nz\n" > f3.txt
$ paste -d',|' f1.txt f2.txt f3.txt
a,1|x
b,2|y
c,3|z
I tested this with three input files and a two-character delimiter string (,|). The first join (between f1 and f2) used ,, and the second join (between f2 and f3) used |, cycling back to , if there were a fourth file. This is genuinely handy when building output with mixed separators — for example, key: value | tag formatted reports.
-s — Serial Mode (Paste Lines of Each File Horizontally, Instead of Across Files)
Normal paste merges across files line by line. -s instead merges within a single file, turning all its lines into one row:
$ paste -s f1.txt
a b c
I tested this and it took the three separate lines of f1.txt (a, b, c) and joined them into a single tab-separated line. When given multiple files with -s, each file becomes its own output row:
$ paste -s f1.txt f2.txt
a b c
1 2 3
This mode is extremely useful for flattening a list of items — like a file listing one hostname per line — into a single space- or comma-separated line for use in another command’s argument list:
paste -sd',' hostnames.txt
Reading from Standard Input with -
You can mix a real file with STDIN using a single dash as a placeholder:
$ cat f1.txt | paste - f2.txt
a 1
b 2
c 3
I confirmed this works exactly as expected — - represents wherever STDIN should be positioned relative to the other named files, which is useful when one of your “columns” is generated on the fly by an earlier pipeline stage rather than existing as a file on disk.
How paste Works Internally
paste reads one line at a time from each input source in lockstep — line 1 from every file, then line 2 from every file, and so on — buffering only the current line from each source rather than loading entire files into memory. This makes it efficient even on fairly large files, since memory usage scales with the number of input sources and the length of the current lines, not the total size of any file. If files have different lengths, paste fills in missing fields with empty strings once a file is exhausted, rather than erroring out or stopping early — so the output row count matches the longest input file.
Practical, Real-World Examples
1. Combining Two Related Columns into a CSV
paste -d',' usernames.txt user_ids.txt > users.csv
2. Flattening a List for a Command Argument
HOSTS=$(paste -sd',' hostlist.txt)
ansible-playbook -i "$HOSTS," deploy.yml
3. Building a Simple Key-Value Report
paste -d': ' keys.txt values.txt
4. Merging Three Separately Generated Data Streams
paste -d',' names.txt scores.txt grades.txt > report.csv
5. Interleaving Output from Two Commands
paste <(seq 1 5) <(seq 6 10)
Using process substitution (<(...)), you can paste the live output of two commands side by side without writing intermediate files:
1 6
2 7
3 8
4 9
5 10
6. Joining Every N Lines into One Row
Combined with xargs or a loop, paste can group lines — for example, turning a file with 3 lines per logical record into one row per record:
paste -d' ' - - - < three_lines_per_record.txt
Passing - three times tells paste to read three consecutive lines from the same input stream and join them into a single output row — a neat trick for reshaping fixed-format multi-line records into a single line each.
paste in Shell Scripting and Automation
A pattern I use when generating deployment manifests from two loosely related data sources — a list of services and their corresponding ports pulled from separate config lookups:
#!/bin/bash
grep -oP '(?<=service_name: ).*' services.yml > /tmp/names.txt
grep -oP '(?<=port: )\d+' services.yml > /tmp/ports.txt
paste -d':' /tmp/names.txt /tmp/ports.txt > service_endpoints.txt
Another pattern: quickly building a comma-separated argument list for a command from a file listing, without writing a loop:
tar -czf archive.tar.gz $(paste -sd' ' files_to_include.txt)
Comparing paste to Related Commands
| Task | Best Tool |
|---|---|
| Merging lines from multiple files side by side | paste |
| Stacking files vertically (concatenation) | cat |
| Joining files on a matching key field (SQL-style join) | join |
| Column-wise extraction from a single file | cut |
| Transposing rows and columns | awk (no direct built-in for this) |
paste and join are frequently confused because both combine data from multiple files, but they solve different problems. paste merges purely by line position — line 1 with line 1, line 2 with line 2 — with no regard for content. join matches lines based on a shared key field, similar to a SQL join, and requires both input files to be sorted on the join field. If your two files have data in the same order already, paste is simpler; if they need to be matched by an ID or key that may not be in the same order, join is the correct tool.
Troubleshooting Common paste Issues
Output has more rows than expected, with blank fields — this happens when input files have different line counts; paste pads short files with empty fields rather than truncating to the shortest file. Verify with wc -l on each input if the row count is unexpected.
Delimiter not applying to every join point — remember multiple characters passed to -d cycle rather than apply globally; if you want the same delimiter everywhere, pass just one character.
Tabs displaying oddly in terminal output — the default tab delimiter can look inconsistent depending on terminal tab-stop settings; specify an explicit delimiter with -d if visual alignment matters for a report you’re reading directly in the terminal.
Performance Optimization
paste is inherently efficient for its use case, streaming line by line without materializing entire files in memory. For very large files, performance is generally I/O-bound rather than CPU-bound; there’s little to tune beyond ensuring source files are on fast storage. If you find yourself calling paste repeatedly in a loop over many small files, consider whether you can restructure the operation to pass all files to a single paste invocation instead, since spawning paste per iteration adds unnecessary process overhead.
Security Implications
paste doesn’t execute code or interpret its input beyond delimiter handling, so it carries minimal direct security risk. As with any tool handling multiple data files, be mindful when merging files from different trust levels or sensitivity classifications — the resulting combined file inherits the sensitivity of its most sensitive input column, and it’s easy to accidentally create a new file that combines two individually low-sensitivity columns (like a name list and an internal ID list) into something more sensitive (a name-to-ID mapping) than either source alone.
Compatibility Across Distributions
paste is part of GNU coreutils (tested here at version 9.4) and is standard on Ubuntu, Debian, Fedora, RHEL/CentOS, Arch, and openSUSE. It’s also part of the POSIX specification, so BSD and macOS paste implementations support the same core behavior and options (-d, -s), making it one of the more reliably portable text utilities across Unix-like systems.
paste and the Broader Unix Philosophy
paste is a good example of the classic Unix design principle of building small, composable tools rather than one monolithic tool that tries to do everything. On its own, paste looks almost too simple to be useful — it just zips lines together. Its real value only becomes obvious once you start combining it with other tools in a pipeline: cut to extract specific columns from a wider file, sort to order data before merging, awk to transform a column’s values before pasting, and process substitution (<(...)) to feed it live command output instead of static files. Each of these tools does one narrow thing well, and paste‘s narrowness is precisely what makes it easy to reason about and combine predictably with the others.
Deeper Look at How paste Handles Uneven Input
It’s worth understanding exactly what happens when input files have different numbers of lines, since this is the single most common source of confusing paste output. Consider two files, one with 3 lines and one with 5:
$ printf "a\nb\nc\n" > short.txt
$ printf "1\n2\n3\n4\n5\n" > long.txt
$ paste short.txt long.txt
a 1
b 2
c 3
4
5
Once short.txt is exhausted after line 3, paste continues pulling lines from long.txt alone, filling in an empty field (just the delimiter, no content) where the shorter file has run out. This is deliberate, predictable behavior — paste never truncates to the shortest file, and it never errors out over a length mismatch. If your workflow requires that mismatched lengths be treated as an error condition instead, you’ll need an explicit check (comparing wc -l output on each file) before calling paste, since paste itself won’t flag the discrepancy.
Extracting Columns Before Pasting
A common real-world pattern is generating the “columns” to be pasted together on the fly using cut or awk, rather than starting from pre-existing separate files:
cut -d',' -f1 accounts.csv > /tmp/names.txt
cut -d',' -f3 accounts.csv > /tmp/balances.txt
paste -d',' /tmp/names.txt /tmp/balances.txt > name_balance_pairs.csv
This effectively lets you reorder or select a subset of columns from a wider CSV without needing a full spreadsheet tool or a more heavyweight awk script — though for anything beyond simple column selection and reordering, awk alone (using -F',' and print $1","$3) is usually a cleaner single-command alternative to this cut-then-paste combination.
Building Tabular Reports From Multiple Command Outputs
A pattern I’ve used for quick capacity-planning snapshots, combining live output from two separate monitoring commands into one aligned report:
paste <(free -h | awk 'NR==2{print $2,$3}') <(df -h / | awk 'NR==2{print $2,$4}')
This produces a single row combining memory total/used with root filesystem total/available, generated fresh from two live commands via process substitution rather than from static files — a lightweight way to build a custom one-line system snapshot without writing a longer script.
Summary
paste fills a narrow but genuinely common gap: combining data that’s already organized line by line across separate sources into a single merged output. The essentials are -d for custom delimiters, -s for flattening a single file’s lines into one row, and the - placeholder for mixing STDIN with real files. It’s not trying to be a database join tool — for that, reach for join — but for straightforward positional merging, nothing is simpler or faster.
References
- GNU Coreutils Manual —
paste: https://www.gnu.org/software/coreutils/manual/html_node/paste-invocation.html - POSIX Specification for
paste: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/paste.html man paste(local manual page)
