awk Part I · Module 3 · Fields, Records, and Separators
Field Separators: FS and OFS
Every example so far has relied on awk's default whitespace splitting, which is why the padded, human-formatted reports in Modules 2 and 3 parsed so cleanly. This chapter is where splitting stops being a default and becomes something you set. FS controls how an incoming record is cut into fields, which is what lets awk read a CSV, a TSV, or a colon-separated tool export. OFS controls how your printed values are joined back together, which is what lets you convert between those formats. The two names look like a matched pair and they are not: one acts on the way in, the other on the way out and only where a comma appears in your print. This chapter separates them properly, and covers the single-space rule that catches everyone exactly once.
Foundation17 min readawkPart ISeparatorsFS and OFSCSV
Part I · Module 3 · Chapter 3.3 · Field Separators: FS and OFS
1. Why This Matters
awk's default splitting is generous and it carried Modules 1 and 2 entirely. Give it a report padded out so the columns line up and it finds exactly the fields you wanted.
Give it this and the default fails completely:
alu:120:1820
regfile:45:180
io:18:144There is no whitespace on those lines at all. The default rule finds one field per record — the whole line — and $2 is empty. Every command you have learned returns nothing useful.
The fix is one setting, and the two settings this chapter covers are the most common cure for an awk command that runs without error and tells you nothing:
FSsays how to split the input.OFSsays how to join your output. They act in opposite directions and are not a matched pair.
2. FS: Telling awk How to Split
FS is the field separator for input. Set it to a colon and the file above parses:
awk -F: '{ print $1 }' cells.colonalu
regfile
io-F: is a command-line option meaning "set FS to a colon". awk now cuts each record at every colon, so $1 is the name, $2 the count and $3 the area.
A tab-separated coverage file, which is extremely common as a tool export:
alu_decode 184 200
alu_flags 96 96
fifo_ctrl 41 75awk -F'\t' '{ print $1, $3 }' cov.tsvalu_decode 200
alu_flags 96
fifo_ctrl 75The \t is a tab, and the single quotes keep the shell from touching it. Note that the default rule would also have worked on this particular file, because a tab is whitespace — but only by luck. If any scope name contained a space, the default would split it and every column after it would shift. Saying -F'\t' states that the file is tab-delimited, which is both more precise and self-documenting.
3. Two Ways to Set FS
-F on the command line and FS in a BEGIN block do the same thing:
awk 'BEGIN { FS = ":" } { print $1 }' cells.colonalu
regfile
ioIdentical to section 2's output. Use whichever reads better: -F: is shorter for a one-liner, and BEGIN { FS = ":" } is clearer in a script file where it sits beside the other setup. Section 10 explains why BEGIN specifically, and not just anywhere.
4. OFS: How Your Output Is Joined
OFS is the output field separator. It is the character awk puts between values when you separate them with a comma in print.
Read a tab-separated file and emit comma-separated output — a format conversion in one command:
awk 'BEGIN { FS = "\t"; OFS = "," } { print $1, $2 }' cov.tsvalu_decode,184
alu_flags,96
fifo_ctrl,41FS did the reading, OFS did the writing, and the two are set independently because they are doing independent jobs.
5. OFS Applies Only Where There Is a Comma
This is the first surprise, and it catches people who set OFS and see no effect.
With a comma, OFS is used:
awk 'BEGIN { OFS = "," } { print $1, $2 }' cells.rptGiven the whitespace-separated report:
DFF 120 1820
AND 45 180
MUX 18 144DFF,120
AND,45
MUX,18Without the comma, it is not:
awk 'BEGIN { OFS = "," } { print $1 $2 }' cells.rptDFF120
AND45
MUX18No separator at all. print $1 $2 is not a list of two values — it is one value, the two fields concatenated. awk had nothing to put a separator between.
6. OFS Does Not Touch $0
The second surprise. Setting OFS has no effect on printing the whole record:
awk 'BEGIN { OFS = "," } { print $0 }' cells.rptDFF 120 1820
AND 45 180
MUX 18 144Still spaces. $0 is the record as it was read — Chapter 2.2 established that it is the original text, not a re-join of the fields — so OFS never enters into it.
This is a frequent disappointment: someone sets OFS = ",", prints the record, and sees no commas. The record is not built from the fields, so there is nothing for OFS to do.
There is a way to make awk rebuild $0 using OFS, and it is the subject of Chapter 3.6 — Rebuilding Records. It is deliberately not shown here, because the useful thing to learn in this chapter is that printing a record and printing its fields are two different operations.
7. The Single-Character Rule
The third surprise, and it is the one Chapter 2.2 promised. The default whitespace rule collapses runs of separators. A single-character FS does not.
The default, on a line with three spaces between two values:
awk '{ print NF }' spaces.txta b2Two fields, as Chapter 2.2 showed: a run of whitespace is one separator.
Now a single-character FS on adjacent delimiters:
awk -F, '{ print NF, "[" $2 "]" }' adjacent.csva,,c3 []Three fields, and the middle one is empty. The two adjacent commas did not collapse into one separator — they delimited an empty field between them.
That is the correct behaviour for delimited data. In a CSV, a,,c means three columns where the middle value is missing, and that is information you want preserved. It is only surprising if you learned the whitespace rule first and assumed it was universal.
| default whitespace | single-character FS | |
|---|---|---|
| run of separators | one separator | one separator per character |
| adjacent separators | collapse | produce empty fields |
| leading separator | ignored | produces an empty $1 |
| trailing separator | ignored | produces an empty last field |
| right for | human-formatted, padded reports | machine-written delimited data |
8. FS = " " Is Special
The fourth surprise, and the sharpest. Setting FS to a single space does not mean "split on one space". It means "use the default whitespace rule".
awk 'BEGIN { FS = " " } { print NF }' spaces.txt2Two fields on a b — the generous default behaviour, not three separators producing four fields.
To get a genuine single-space separator you have to say so in a way that is not the literal single space, which means a regular expression:
awk 'BEGIN { FS = "[ ]" } { print NF }' spaces.txt4Four fields: a, empty, empty, b. Three spaces are now three separators, with two empty fields between them.
9. FS Can Be a Regular Expression
Any FS longer than one character is treated as a regular expression, which is occasionally exactly what you need:
awk 'BEGIN { FS = "[0-9]+" } { print $1, $2, $3 }' re.txtab12cd34efab cd ef[0-9]+ means "one or more digits", so the runs 12 and 34 each acted as a separator. Regular expressions are Module 4's subject; the point here is that FS is where they first become useful, and that this is why a two-character FS behaves differently from a one-character one.
A practical case: a report separated by a colon that may have spaces around it. FS = " *: *" would absorb the spaces. Chapter 3.5 covers the delimiters you actually meet.
10. The Gotcha: Setting FS in a Rule
The fifth surprise, and the reason section 3 said BEGIN rather than "anywhere".
Setting FS inside an ordinary rule does not re-split the record that has already been read:
awk 'NR == 1 { FS = ":" } { print $1 }' cells.colonalu:120:1820
regfile
ioLook at the first line of output. Record 1 printed whole, because it had already been split — using the old FS, whitespace — before the rule ran. The new FS took effect from record 2 onwards, which is why records 2 and 3 print correctly.
The result is a program that is wrong on exactly one record. On a three-line file that is obvious. On a 50,000-line regression summary it is one bad row in the middle of a correct-looking report.
awk 'BEGIN { FS = ":" } { print $1 }' cells.colonalu
regfile
ioBEGIN runs before any input is read — Chapter 2.4's phase model — so the setting is in place when the first record is split. That is the whole reason FS belongs there.
11. An Engineering Example
A synthetic coverage export, tab-separated, with explicit column meanings:
alu_decode 184 200
alu_flags 96 96
fifo_ctrl 41 75Which scopes are not finished, reading tab-delimited input:
awk -F'\t' '$2 < $3 { print $1 }' cov.tsvalu_decode
fifo_ctrlThe same data converted to CSV for a spreadsheet, with a header:
awk 'BEGIN { FS = "\t"; OFS = "," ; print "scope,covered,total" } { print $1, $2, $3 }' cov.tsvscope,covered,total
alu_decode,184,200
alu_flags,96,96
fifo_ctrl,41,75One command: FS reads tabs, OFS writes commas, BEGIN adds the header line. This is the most common real use of the two settings together.
12. Common Mistakes
Setting OFS and printing $0. Section 6. $0 is the original text; nothing for OFS to do. Rebuilding is Chapter 3.6.
Forgetting the comma in print. Section 5. print $1 $2 concatenates into one value, so OFS is never consulted.
Setting FS in an ordinary rule. Section 10. The first record is already split, so it comes out wrong and everything after it is right.
Expecting FS = " " to mean one space. Section 8. It means the default whitespace rule; FS = "[ ]" is the literal single space, and you rarely want it.
Assuming a single-character FS collapses runs. Section 7. It does not, and that is correct for delimited data.
Using -F'\t' without quotes. The shell may mangle the backslash. Single-quote it.
Relying on the default for tab-separated data. It usually works, because tabs are whitespace — right up until a value contains a space. Say -F'\t' and the file documents itself.
Assuming FS affects the output or OFS affects the input. They act in opposite directions, which is the point of Figure 1.
13. Exercises
Exercise 1 — Predict the output
alu:120:1820
regfile:45:180
io:18:144awk '{ print NF }' cells.colon1
1
1One field per record. There is no whitespace on these lines, so the default rule finds nothing to split on and the whole record is $1. This is what "awk printed nothing useful" looks like from the inside.
Exercise 2 — Modify a command
Print the area column — the third — from the colon-separated file.
awk -F: '{ print $3 }' cells.colon1820
180
144One option changes the whole picture. Note $NF would also work here; which to prefer is Chapter 3.2's decision.
Exercise 3 — Trace the separator rules
Predict NF for each of these three commands on the same one-line file before running them.
a bawk '{ print NF }' spaces.txt2awk 'BEGIN { FS = " " } { print NF }' spaces.txt2awk 'BEGIN { FS = "[ ]" } { print NF }' spaces.txt4The default and FS = " " are the same thing — that is section 8's rule. FS = "[ ]" is a genuine single-space regular expression, so three spaces are three separators and the record has four fields, two of them empty. If you predicted 2, 4, 4 you had the special case backwards, which is the normal first guess.
Exercise 4 — Debug the one bad record
An engineer runs the command below on a colon-separated file of 2,000 rows and reports that "the first line is broken but the rest are fine". Explain the cause and fix it.
awk 'NR == 1 { FS = ":" } { print $1 }' cells.colonalu:120:1820
regfile
ioRecord 1 was split before the rule that set FS ran, so it was split on whitespace and the entire line became $1. From record 2 onwards the new FS was already in place. The bug is in the timing, not the separator.
awk 'BEGIN { FS = ":" } { print $1 }' cells.colonalu
regfile
ioBEGIN runs before any record is read, so the setting applies to all of them. This is Chapter 2.4's phase model doing real work.
Exercise 5 — Engineering application
Convert the tab-separated coverage export to a pipe-separated file, keeping only the scope and the covered-bin count, with no header.
awk 'BEGIN { FS = "\t"; OFS = "|" } { print $1, $2 }' cov.tsvalu_decode|184
alu_flags|96
fifo_ctrl|41FS and OFS set independently in BEGIN, and the comma in print is what lets OFS apply. Changing the output delimiter needed no change to how the input is read, which is Figure 1's asymmetry being useful rather than confusing.
Exercise 6 — Challenge: two separators, one answer
A tool exports the same three columns in two formats on different machines — one colon-separated, one tab-separated. Write a single command that reads either and prints the first and third columns, then explain what it assumes.
alu:120:1820
regfile:45:180
io:18:144awk -F'[:\t]' '{ print $1, $3 }' cells.colonalu 1820
regfile 180
io 144awk -F'[:\t]' '{ print $1, $3 }' cov.tsvalu_decode 200
alu_flags 96
fifo_ctrl 75One command, both files. [:\t] is a two-character-class regular expression meaning "a colon or a tab", and because it is longer than one character awk treats FS as a regex — section 9.
What it assumes is worth stating. It assumes neither a colon nor a tab ever appears inside a value, and that both formats put the same three columns in the same order. The first assumption is the dangerous one: a scope name containing a colon would split into two fields and shift everything after it. A single FS is always a claim about what cannot appear in the data, which is precisely the limitation Chapter 3.5 takes up.
14. Summary
FSsplits the input;OFSjoins your output. They act in opposite directions and changing one does not affect the other.-F:andBEGIN { FS = ":" }are equivalent. PreferBEGINin a script where it sits with the other setup.OFSis used only where a comma appears inprint.print $1 $2concatenates into one value andOFSis never consulted.OFSdoes not affect$0. The record is the original text, so printing it shows the original separators. Rebuilding it is Chapter 3.6.- A single-character
FSdoes not collapse runs.a,,cis three fields with an empty middle, which is correct for delimited data and the opposite of the whitespace default. FS = " "means the default whitespace rule, not one space.FS = "[ ]"is the literal single space, and it will manufacture empty fields across any padded report.- An
FSlonger than one character is a regular expression, which is how one command reads two delimiters. - Setting
FSin an ordinary rule leaves the first record split by the old one. It belongs inBEGIN, for the reason Chapter 2.4 explains. - Any single
FSis a claim about what cannot appear inside a value.
You can now read a file in whatever delimited shape a tool gave you, and emit it in whatever shape the next stage wants.
What has not changed in this chapter is what counts as a record: every example still read one line at a time. That is a separate setting, and it is the one that unlocks the report shape Chapter 1.2 kept deferring — a timing path whose startpoint, endpoint and slack sit on three consecutive lines, which no amount of field splitting can reach. Chapter 3.4 — Record Separators: RS and ORS is where a record stops being a line.
Where this fits
Part of the awk curriculum.
