Bash & the POSIX Shell Module 11
Text processing: the Unix toolbox interview questions
Interview questions on “Text processing: the Unix toolbox”: warm-ups, then what each harder question tests and how a good answer starts.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher to mid
How does cut -f2 differ from cut -c2, and when would you use each?
Answered in the lesson cut, tr, head, tail, wc, paste and friends
-
Fresher to mid
What does sort | uniq -c do, why does the sort come first, and how do you then order by count?
sort reads the file and puts the lines in order, which makes every copy of a line sit next to its siblings. uniq then collapses adjacent duplicates and -c prefixes each run with its count, so the result is one line per unique value with a count on the left. Without sort first, uniq only drops duplicates that happen to be adjacent, and a file with interleaved rows comes out almost unchanged. To order the counts largest first, a second sort -k1,1nr -k2,2 reads field 1 as a number and reverses, with a tie-break by field 2 for a deterministic order; head -n N keeps the top N.
From the lesson sort and uniq: counting and top-N reports
-
Fresher to mid
Name three options you reach for most often with grep, and say what each does.
Three that come up every day: -i ignores case, so a search for error picks error, Error and ERROR without listing every spelling; -r reads every file under a folder and tags each match with the file's path, which turns grep into a quick "find in files"; and -q prints nothing and sets the exit status, which is why if grep -q PATTERN FILE; then …; fi is the usual test for "the file contains PATTERN". After those, -n for line numbers in a long file, -A or -B to see what came before or after a match, and -F to tell grep the pattern is plain text are the ones most scripts need. Each follows the GNU grep manual and POSIX.1-2024, so a script that uses them works on dash and bash alike.
From the lesson grep: searching text and files
-
Fresher to mid
What are the two POSIX regex dialects, and which bash tools read each?
POSIX defines two dialects, basic regular expressions (BRE) and extended regular expressions (ERE), with the same engine and different metacharacters. BRE is the default of grep and of sed; ERE is what grep -E, sed -E, awk and Bash's [[ string =~ regex ]] read. In BRE the characters + ? | ( ) { are literal and need a backslash to become metacharacters, in ERE they are metacharacters and a backslash makes them literal. POSIX regex has no \d, \s or lookaround; GNU grep accepts \w and \s as extensions, but a script that must run on BSD tools writes [[:digit:]] and [[:space:]] instead. The leftmost-longest rule applies to both dialects.
From the lesson Regular expressions for grep, sed and awk
-
Fresher to mid
What does s/old/new/g do, and how is it different from s/old/new/?
Both are the substitute command, and both read the current line into the pattern space, scan it from the left and try to replace old with new. Without the g flag, s/// stops after the first match on the line, so sed 's/cat/dog/' on 'ERROR cat sat on cat' printed 'ERROR dog sat on cat', with the second cat left alone. With g, sed keeps scanning after each replacement and changes every match on the line, so the same input became 'ERROR dog sat on dog'. A number flag N, such as s/cat/dog/2, picks only the Nth match and stops; p prints the pattern space when s/// matched (useful with -n); and i makes the match case-insensitive, which POSIX.1-2024 added to standard sed and which BSD and GNU sed both accept.
From the lesson sed: substitute, delete and print
-
Fresher to mid
What are NR, NF and $NF in an awk program, and how is $NF different from NF?
Three of awk's built-in names a learner meets first. NR is the record number across all the input awk reads, counting from 1. After awk has read the six lines of people.csv, NR is 6; after a second file, NR keeps going. NF is the number of fields in the CURRENT record, after awk has split it on FS, so NF is 3 for every row of a comma-separated people.csv read with -F,, and 1 if the same file is read with the default white-space FS. $NF is the LAST field of the current record: the field whose index is the VALUE of NF, so on Asha,34,Pune with -F, it is the string Pune. NF is a count; $NF is a value. A sister name, FNR, is NR reset at the start of each file.
From the lesson awk: fields, patterns and actions
-
Fresher to mid
Why does for (k in array) come out in a different order on different awk versions, and how do you fix it?
Answered in the lesson awk programs: arrays, joins and reports
-
Fresher to mid
A loop over find . -type f output breaks on a file named note with spaces.txt; what is the shortest fix?
Pipe find's -print0 to a reader that splits on NUL instead of whitespace. The two readers that fit are xargs -0 and a Bash loop with while IFS= read -r -d '' name. The nul session's run on a tree with a tab-in-name file and a newline-in-name file showed every name arriving whole through -print0 | xargs -0, while the plain find | xargs form split the same four names into eight argument words. -print0 ends each path with a NUL byte, which no file name can hold, so the next tool has an unambiguous separator; POSIX.1-2024 added both -print0 and -0 to the standard. The alternative is find . -type f -exec cmd {} +, which fires far fewer processes than -exec cmd {} \; and hands each name over whole, but is less flexible when the command needs the path somewhere other than the end of its line.
From the lesson find and xargs: working with many files
-
Fresher to mid
What does
.[]do versus.in a jq filter, and when do you reach for each?.is the identity filter: it emits its input unchanged, sojq '.' orders.jsonpretty-prints the whole object..[]is the array or object iterator: it emits each element of an array (or each value of an object) as its own value, so the next stage of a filter sees one element at a time instead of the whole collection. Reach for.when you only want jq to validate and format, or at the start of a filter where the input flows in as is. Reach for.[]whenever the next stage is "do something per item":.orders[] | select(...),.orders[] | .id, or[.orders[] | .amount] | addto sum. The two forms combine:.orders[]is read as.orders | .[], and the jq manual's "Array/Object Value Iterator" entry gives both forms.From the lesson jq: query and reshape JSON from the shell
-
Fresher to mid
Why does cut -d, give wrong results on some CSV files, and what tool handles quoted fields correctly?
cut -d, splits each line on every comma without understanding CSV quoting. When a field contains a comma inside double quotes, like "Mehta, Ravi", cut sees two fields instead of one. The same happens with awk -F, and IFS=, read. RFC 4180 defines quoting, but these tools do not implement it. gawk --csv (gawk 5.3+) parses quoted fields correctly, keeping the comma inside its field and un-escaping doubled quotes.
From the lesson CSV, TSV and YAML from the shell
-
Fresher to mid
Why store timestamps as UTC epoch seconds or an ISO 8601 Z string, and format for a local zone only at the edge?
A UTC epoch names one instant in time; a UTC ISO 8601 string with a Z suffix says the same instant with the fields spelt out. Both are unambiguous because UTC has no daylight saving: the mapping to any local wall clock is a pure function of the tz database. A local time stored without a zone leaves the reader to guess, and even a local time stored with a zone goes through a DST-aware mapping that is not one-to-one in one hour each spring and autumn. Storing UTC avoids both problems: the field is unambiguous, every tool reads the same instant (jq, awk, GNU date, BSD date), and converting for a user is one strftime at the edge. The rule works the same for logs, queues, databases and schedulers, which is why it survives a move between machines or regions.
From the lesson Dates, times and non-integer numbers
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Fresher to mid
A script processes a CSV file by piping it straight to awk, but the header row appears in the output as if it were data. How would you exclude the header, and what are two different approaches?
- What it tests
- Understanding that CSV files have headers, knowing multiple ways to skip a header line, and choosing the right one for the context.
- First step
- The header is line 1. Use tail -n +2 to start from line 2, or pass NR > 1 to awk to skip the first record. The tail approach works before any processing, while awk's NR check works inside the awk program.
- Follow-ups
-
- What if the file has multi-line headers, such as a title row and a column-names row?
- Would sed 1d work, and how does it compare to tail -n +2?
- If you need to preserve the header for the output but process only the data, how would you do that?
From the lesson cut, tr, head, tail, wc, paste and friends
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Mid to senior
A deployment script counts active servers with curl | grep UP | wc -l. The API returns a JSON array, and the script works in staging. In production, where some hosts are down, it occasionally reports one fewer server than expected. What could cause the off-by-one, and how would you fix it?
- What it tests
- Recognising that wc -l counts newlines, that text without a trailing newline loses a count, and that a pipeline relying on line counts needs to ensure each record ends with a newline.
- First step
- wc -l counts newline characters, not lines of text. If the last line of the grep output has no trailing newline, it is not counted. Verify that the API response or the grep output ends with a newline, or use grep -c UP instead of grep | wc -l.
- Follow-ups
-
- Why might the issue appear only on some API responses and not others?
- What does grep -c do, and how does it avoid the newline problem?
- If you must use wc -l, how could you add a trailing newline to the pipeline?
From the lesson cut, tr, head, tail, wc, paste and friends
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Fresher to mid
A nightly job runs uniq access.log | wc -l to count unique request URLs. On a quiet Sunday the number is 480 and a hand-count agrees. On a busy weekday it says 94,000, while sort -u access.log | wc -l on the same file says 7,200. Why do the two counts disagree, and what is the one-stage fix?
- What it tests
- Knowing that uniq collapses only adjacent duplicates, that a request bursting from one client clumps the duplicates on a quiet day but not on a busy one, and the sort-first fix.
- First step
- uniq compares each line with the one before it. On a quiet day a client's repeated hits often arrive back to back, so uniq already drops most duplicates. On a busy day the requests of many clients interleave, so almost every neighbour is different and uniq keeps nearly every line, which is why the count is far too high. The fix is sort before uniq (or sort -u instead), so every copy of a URL sits beside its siblings.
- Follow-ups
-
- Why does the hand-count on Sunday agree with the broken script?
- Which is faster on a 100 GB log, sort -u or sort | uniq, and does the answer change with LC_ALL=C?
- How would you then build a top-10 of busiest URLs with the same file?
From the lesson sort and uniq: counting and top-N reports
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A dashboard runs sort hits.log | uniq -c | sort -r | head -n 3 for the three busiest paths. Yesterday the top three looked right (100, 20, 2). Today the counts crossed 1000, and the dashboard printed 999, 100 and 10 on top; a path with 1500 hits never reached head. Why, and what two sort options fix it?
- What it tests
- Reading that sort without -n compares bytes, so a count that leads with '1' lands below any count that leads with '2' through '9'; the fix is -k1,1nr (numeric, reversed) with -k2,2 for a tie-break.
- First step
- sort -r reverses the order sort already picked, which is byte-wise. In byte order '1500 x' is below '999 y' because '1' is a smaller byte than '9', so the 1500 row never climbs. Change -r to -k1,1nr: the first field reads as a number and reverses, so the largest count comes first. Add -k2,2 to break ties by name so the report is deterministic.
- Follow-ups
-
- Why did the pipeline look fine when the biggest count was 100?
- Would sort -nr (no -k) be enough on a file where every line starts with the count?
- What one change makes two rows with the same count come out in the same order on every run?
From the lesson sort and uniq: counting and top-N reports
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
An operator runs grep ERROR app.log on a mixed-case log and gets back one line out of four they expected. On the terminal they can see the other three with their eye: 'error disk is full', 'Error reading config' and 'ERROR tar failed'. Explain what grep did, and give two ways to find all four at once.
- What it tests
- Knowing that grep is case-sensitive by default, choosing -i or an alternation such as -E '[Ee][Rr][Rr][Oo][Rr]', and keeping the search's exit status useful for a later test.
- First step
- Plain grep ERROR app.log matches E, R, R, O, R exactly, so only the all-caps line qualifies. The three others hold the same letters in different case and are dropped. Add -i so grep ignores case, or write the pattern as grep -E '[Ee][Rr][Rr][Oo][Rr]' when a script must stay portable to a grep that lacks -i.
- Follow-ups
-
- When would you prefer the bracket-based pattern to -i?
- What does grep -ni ERROR app.log print, and what does -c on its own return?
- How would you count the errors by severity (ERROR, WARN, INFO) in one grep call?
From the lesson grep: searching text and files
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A developer runs grep -r 'addEventListener' . in a Node.js project and the search runs for 40 seconds, printing thousands of lines, most of them inside node_modules and dist. Explain where the time went, and show the one-line search that finds the three matches in src/ in under a second.
- What it tests
- Knowing that -r walks every file under the folder, that --exclude-dir prunes a whole branch of the walk, that --include narrows by name, and ordering the two for the clean result.
- First step
- -r reads every file in the tree. node_modules alone can hold tens of thousands of files, many of them bundled JavaScript that includes the pattern. Prune the two heavy folders and keep only source files: grep -rn --exclude-dir='node_modules' --exclude-dir='dist' --include='.js' --include='.jsx' 'addEventListener' src.
- Follow-ups
-
- Why does --exclude=node_modules not help, and when would --exclude be the right filter?
- How would the same search change for a TypeScript project (ts, tsx)?
- When should the developer reach for git grep or a tool like ripgrep instead?
From the lesson grep: searching text and files
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A clean-up script runs grep -c '^[a-z]' members.csv to count lines that start with a lowercase name. On a developer's Mac the count is 1,842, on the Linux build server it is 2,310 for the same file. Explain the gap, and fix the check so it gives the same answer wherever it runs.
- What it tests
- Knowing that [a-z] walks the collation order of the current locale, that two computers often disagree about what that order is, and switching to a POSIX character class or pinning LC_ALL.
- First step
- The range [a-z] is read in collation order: on a Mac with en_IN.UTF-8 the letters sort A, a, B, b, ..., so [a-z] covers most lowercase letters and most uppercase except Z. On the Linux server with LC_ALL=C.UTF-8, letters sort A, B, ..., a, b, ..., so [a-z] is only the 26 lowercase. Either write [[:lower:]], which is lowercase in every locale, or set LC_ALL=C before the grep.
- Follow-ups
-
- Why does the same file give different numbers at all?
- Would [a-z[:lower:]] fix it, or does the first range still bite?
- How would you check from the shell what collation grep is using right now?
From the lesson Regular expressions for grep, sed and awk
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
check.sh under Bash 5.3 has if [[ $line =~ "^[0-9]+$" ]]; then echo integer; fi. The author who wrote it swears it worked yesterday, but today every input is reported as not an integer. Explain what the quotes did, and give the form a Bash script should use for an [[ =~ ]] regex.
- What it tests
- Knowing that Bash 3.2 and later treat a quoted part of the regex as a literal string, how to see the trap without a long debugging session, and the safe form that keeps the regex a regex.
- First step
- Bash 3.2 changed the quoting rule of [[ =~ ]]: every quoted character is matched literally. "^[0-9]+$" becomes the eight-character string ^ [ 0 - 9 ] + $, so the match fails for any input that is not that exact string. Put the regex in a variable, with no quotes on the use: re='^[0-9]+$'; [[ $line =~ $re ]].
- Follow-ups
-
- Does putting the regex in single quotes on the left (so [[ "a" =~ 'a*' ]]) differ from double quotes?
- Why does the Bash manual recommend a variable rather than an unquoted literal?
- What does shopt -s compat31 change about this behaviour, and when would you use it?
From the lesson Regular expressions for grep, sed and awk
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A deploy script runs sed -i 's/host=localhost/host=app.example.com/' config.yaml on a Linux CI job, where it works, and on a developer's macOS laptop, where it fails with "No such file or directory: files/host=localhost/host=app.example.com/". Explain each symptom, and fix the script so one line works on both.
- What it tests
- Knowing that -i's argument shape differs between GNU and BSD sed, that macOS reads the script as the backup suffix, and choosing the portable rename pattern over an OS-specific branch.
- First step
- Read the macOS line as BSD sed does: -i takes a backup suffix next, so it took s/host=localhost/host=app.example.com/ as the suffix and expected the next word to be the script; the file it tried to open was config.yaml with that suffix appended, which does not exist. Rewrite the line as sed 's/host=localhost/host=app.example.com/' config.yaml > config.yaml.tmp && mv config.yaml.tmp config.yaml and add a trap EXIT that removes config.yaml.tmp on failure.
- Follow-ups
-
- Why does the same -i line not fail on Linux, where GNU sed is installed?
- Could you fix it with case $(uname -s) in Darwin) …; *) …; esac instead, and what does that cost?
- If the script also needs a backup copy, what is the portable way to make one?
From the lesson sed: substitute, delete and print
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
A rewrite step runs sed 's/\/user\/\([0-9]*\)\/profile/\/u\/\1/' routes.txt to shorten /user/42/profile to /u/42. The output has /u/ where the id should be, every id is lost, and the file is only partly rewritten. Explain each symptom, and rewrite the one line as a sed script a teammate can read.
- What it tests
- Choosing a delimiter that does not collide with the pattern, knowing that BRE needs \( \) for groups, and seeing that -E lets ( ) group without backslashes so the id is captured and written back.
- First step
- The pattern used / as the delimiter and / inside the path, which forced a backslash before every path /; the escapes are correct but hard to read, and a typo there is why the id was lost (the group probably ended one character too early). Switch the delimiter to | and open ERE so the groups need no backslashes: sed -E 's|/user/([0-9]+)/profile|/u/\1|' routes.txt. The id is now captured by \1 and written back cleanly.
- Follow-ups
-
- Why does -E only change the pattern, and not the replacement?
- What is the smallest valid change to the original BRE line that captures the id?
- Would /user/<id>/profile with Unicode digits still match? What would the pattern need?
From the lesson sed: substitute, delete and print
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A shell script joins continuation lines (those ending with a backslash) to the next line with a sed script that uses N and a branch loop. The output doubles every joined line: "line1 continued" appears twice. What could cause that, and how do you fix it?
- What it tests
- Understanding that N appends without clearing the pattern space, that sed prints the pattern space at the end of each cycle unless -n is set, and that a branch loop must eventually print and start fresh.
- First step
- Trace what the script does on a two-line continuation: read line 1, match the backslash, N appends line 2, the substitution removes the backslash and newline, the branch jumps back, but if the script does not delete or print before the next iteration, the joined line is still in the pattern space when the cycle ends. The cycle then prints it, and the next input line appends to it again.
- Follow-ups
-
- Why does the doubled line sometimes also get the following input appended?
- What does D do that d does not, and why does it matter here?
- How would you rewrite the script to use -n and P instead of relying on the default print?
From the lesson sed beyond s///: hold space and multiline
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
A sed one-liner meant to reverse the lines of a file dropped the last line from the output. The input had three lines, the output had two. The script was sed '1!G;h;d'. What happened, and what small change fixes it?
- What it tests
- Knowing that d deletes the pattern space and starts the next cycle, that $ addresses the last line, and that $!d deletes on every line except the last, leaving the last line to print.
- First step
- Walk through the script on a three-line file. On line 1: 1!G skips G, h copies line 1 to the hold space, d deletes and starts the next cycle. On line 2: G appends line 1, h saves line2\nline1, d deletes. On line 3: G appends, h saves line3\nline2\nline1, d deletes. No line ever prints because every cycle ends with d.
- Follow-ups
-
- Why does the classic idiom use $!d instead of d?
- How would you print only the last line without holding everything?
- What happens if the file has only one line?
From the lesson sed beyond s///: hold space and multiline
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
A script runs awk -F, '$3 == "Pune" { total += $2 } END { print total }' invoices.csv. On the author's Mac it prints 72500 (matches a hand count). On the Linux CI runner, same file and the same gawk 5.4.1, it prints 0. Explain the zero and give a portable fix.
- What it tests
- Noticing that the file is edited on Windows and carries CRLF line endings on the CI runner, so $3 is "Pune\\r" (five bytes), the string compare fails on every record, and total stays 0; naming a one-line fix.
- First step
- Open the file in a byte-aware viewer such as od -c or cat -v; a trailing \\r on every line is the clue. Because awk's default record separator is \\n, a CRLF file leaves \\r at the end of the last field of every record, so $NF (and $3 on this file) is "Pune\\r", not "Pune". The equality test fails on every row, no amounts are added, and the END block prints 0.
- Follow-ups
-
- Does the plain total of the second column, awk -F, "{ total += \$2 } END { print total }", also print 0 on the CI runner?
- What is the simplest one-line fix in the pipeline, and the simplest fix inside the awk program?
- On gawk, what does setting RS = "\\r?\\n" do, and how is it different from stripping \\r first?
From the lesson awk: fields, patterns and actions
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A dashboard runs awk -F, '{ print $2 }' hits.csv | sort | head to list the biggest counts. This week the top is 999, 100, 10; a row of 1500 never reaches head. Inside awk, the filter $2 < "100" also lets 1000 and 1500 through. Explain both symptoms and give the fix each needs.
- What it tests
- Reading that sort without -n is a byte-wise compare so 1 is below 2 through 9, that an awk comparison with a string constant on one side is byte-wise too, and the two fixes: sort -n and a numeric constant.
- First step
- sort without -n reads the start of each line as bytes: "1500" sorts below "999" because the first byte 1 is below 9. Switch to sort -nr for a numeric sort. Inside awk, "100" on the right of a compare is a STRING constant, which forces a byte compare: "1500" < "100" is true. Drop the quotes so the compare reads $2 < 100, and awk uses numbers.
- Follow-ups
-
- Why did the dashboard look right when the top count was 100 and the one below it was 10?
- How does awk know a FIELD is "numeric" when a CONSTANT at the same spot is a string?
- A column of ages: "100" < "50" is true; "100" < 50 is false. What is the rule in one sentence?
From the lesson awk: fields, patterns and actions
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A billing script joins orders to customers with awk 'FNR==NR { c[$1]=$2; next } { print $0, c[$2] }' customers orders. One day a customer complains that their invoice shows no name. What went wrong, and how would you make the script report the problem instead of hiding it?
- What it tests
- Knowing that a missing key returns empty, not an error, and adding a guard or a stderr message.
- First step
- The customer id in the order file was not in the customer file. awk's c[$2] returned the empty string, so the invoice printed a blank. Add ($2 in c) as a pattern to skip rows with no match, or an if block that prints missing ids to stderr so the billing team can fix the data.
- Follow-ups
-
- How would you also catch duplicate customer ids in the customer file?
- What does next do in the FNR==NR block, and what happens if you leave it out?
- If the customer file is larger than memory, how would you handle the join differently?
From the lesson awk programs: arrays, joins and reports
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
An awk script writes intermediate results to a temp file in the main loop, then reads them back with getline in END to produce a summary. The summary comes out empty on a small input and only partly filled on a large one. What is the likely cause, and what one-line fix makes it reliable?
- What it tests
- Recognising that writes are buffered, that nothing reaches disk until the buffer flushes or close() runs, and that close both flushes the buffer and resets the read position to the start.
- First step
- The write buffer was not flushed before the getline. A small input sits entirely in the buffer, so getline finds an empty file; a large input flushes in parts, so getline sees only some lines. Call close("tempfile") before the first getline: it flushes the buffer and resets the read position to the start, so getline reads every line.
- Follow-ups
-
- What would fflush do, and why is close better here?
- If you need to read and write the same file alternately, what pattern works?
- How does this interact with print | 'cmd' and reading cmd's output in the same run?
From the lesson awk programs: arrays, joins and reports
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A nightly cleanup runs find /var/data -name '*.log' -o -mtime +7 -delete to drop logs older than a week. A week later every old log is still there, and the uploads and backups older than a week are gone. Explain both symptoms, and give the fix and the dry run that catch it on GNU and BSD find.
- What it tests
- Reading find's precedence (-a binds tighter than -o), knowing that an action such as -delete turns off the implicit -print, and the habit of a -print dry run with the same grouping before -delete.
- First step
- find reads the expression as -name '.log' -o ( -mtime +7 -a -delete ). A .log file makes the left side true, so the -o stops there and nothing deletes it; any other path eight or more days old reaches -delete. State the intent as one AND chain: find /var/data -type f -name '.log' -mtime +7 -print as the dry run, read the list, then put -delete where -print is.
- Follow-ups
-
- What does a dry run of the broken form, with -print in place of -delete, list, and why are the logs missing from it?
- How would you write one find that removes the old logs and every file under /var/data/scratch?
- Which checks would you run before -delete touches a shared production tree?
From the lesson find and xargs: working with many files
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
A cron job runs find /tmp -name '*.sock' -mtime +1 | xargs rm. On a quiet day the walk is empty: on Linux cron mails "rm: missing operand", on macOS the job stays silent. On another day a socket named "my app.sock" qualifies. Explain both days and rewrite the line safely.
- What it tests
- GNU xargs runs the command once on empty input and BSD xargs does not; -r (POSIX.1-2024) skips that run; -print0 | xargs -0 keeps spaces and newlines in names from splitting the arguments.
- First step
- GNU xargs with no input still starts the command once with no arguments; BSD xargs does not. The "missing operand" line is rm complaining about that empty run. On the other day xargs splits "my app.sock" into two words, so rm looks for "/tmp/my" and "app.sock". Add -r and -print0 / -0: find /tmp -name '*.sock' -mtime +1 -print0 | xargs -0 -r rm.
- Follow-ups
-
- Why does the second word of a split name matter more than the first, and what decides which file rm finds?
- What else could make the walk's output empty on a busy day while the pipeline looks fine?
- Could find -delete replace the pipe entirely, and what does that cost?
From the lesson find and xargs: working with many files
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A nightly job runs
jq ".orders[] | select(.city == \"$CITY\")" orders.jsonwith $CITY read from a config. The usage report counted all 4,218 orders in the file; a hand-check says only 1,100 are in that city. Explain what the filter actually ran, and give the one-line fix that keeps the pipeline correct for any value of CITY.- What it tests
- Reading that a shell-interpolated filter is a two-stage parse, that a value with
"ororreshapes the jq program silently, and knowing the--argform. - First step
- Read what the shell handed to jq. If
$CITYwas the plain wordPune, jq saw.orders[] | select(.city == "Pune"). If it was the stringPune" or "Chennai, jq saw.orders[] | select(.city == "Pune" or "Chennai"), parsed as(.city == "Pune") or "Chennai"; the non-empty string is truthy, so every order matches. Rewrite asjq --arg city "$CITY" '.orders[] | select(.city == $city)' orders.json: the program is single-quoted and--argpasses the value as a jq string. - Follow-ups
-
- Why do the usage report and the hand-check disagree in the same way on every slice (city, status, date range)?
- If the config value is JSON (a number, a boolean, an object), what changes in the fix?
- How would you catch the problem in a code review before it runs?
From the lesson jq: query and reshape JSON from the shell
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A daily totals report runs
jq -r '.orders | group_by(.city) | map("\(.[0].city) \((map(.amount) | add))") | .[]'expecting three lines, one per city. When Chennai has no orders the report drops to two; when aMumbaiorder appears it has four. Rework so Chennai prints a zero and Mumbai still prints.- What it tests
- Knowing
group_byonly sees values present in the input (no zero rows); passing a fixed city list with--argjsonand pinning every city to a total so missing cities print 0 and extra cities still print. - First step
group_by(.city)builds one group per distinct.cityvalue in the input. No Chennai in the data means no Chennai group; aMumbairow makes a Mumbai group. Pin the output list:jq --argjson cities '["Chennai","Kochi","Pune"]' -r '(.orders | group_by(.city) | map({(.[0].city): (map(.amount)|add)}) | add) as $t | $cities | map("\(.) \($t[.] // 0)") | .[]' orders.json— every city in$citiesprints, with 0 when absent.- Follow-ups
-
- Why does the fix need
// 0, and what does it produce when a city has one or many orders? - What goes wrong if
$citiesincludes a city that is also missing when it is present (a stale list)? - How would you add a "others" line for any city not in $cities, with its own total?
- Why does the fix need
From the lesson jq: query and reshape JSON from the shell
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
An API-check runs
if ! jq -e '.enabled' response.json; then echo "feature off"; exit 1; fi. On{"enabled": false}it printsfeature off(correct). On a payload with.enabledmissing it does the same, but the product owner needs "missing" (a rollout bug) kept apart from "set to false". Explain whatjq -esaw and give a one-line fix.- What it tests
- Reading the jq manual's
--exit-statusrule (exit 1 onfalseornull, exit 4 on no output); using.enabled == nullorhas("enabled")to tell "missing" apart from "set to false". - First step
- The jq manual says
-ereturns exit 1 onfalseornulland exit 4 on no output. On{"enabled": false}the filter returnedfalse, exit 1. On the missing-field payload the filter returnednull(jq reads a missing key asnull), also exit 1 — the two cases match under-e. Switch to the real question:jq -e 'has("enabled")' response.json >/dev/null,truefor{"enabled": false}andfalsewhen the key is missing. - Follow-ups
-
- What does
jq -e ".enabled == null"return in each of the three cases (set true, set false, missing)? - Where does
|withset -o pipefailfit into this script, and what does${PIPESTATUS[@]}show? - How would you write the probe in one jq call that prints
on,offormissing?
- What does
From the lesson jq: query and reshape JSON from the shell
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A bash script sums the third column of a CSV. One row has a name with a comma, and the total is too high by exactly that row's amount. How do you diagnose and fix the script?
- What it tests
- Identify the quoted-comma row, explain the field shift, switch to gawk --csv.
- First step
- Run awk -F, '{ print NF }' file | sort | uniq -c to see the field count per row. A row with a quoted comma shows one extra field, confirming that simple splitting broke the data.
- Follow-ups
-
- Why does the total end up too high rather than too low?
- How would you rewrite the sum to handle any RFC 4180 CSV?
- What happens if the file also has a field with an embedded newline?
From the lesson CSV, TSV and YAML from the shell
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A YAML config uses anchors for shared defaults. A team member used sed to change a timeout value, and now two services have different timeouts though they should share the same default. What went wrong?
- What it tests
- Explain anchor expansion, show that sed edits text not structure, fix with yq.
- First step
- Open the YAML and find the anchor (&defaults) and its references (*defaults). sed changed the literal text in one place but did not know about the anchor relationship, so the other references still expand to the old value or the edit broke the anchor entirely.
- Follow-ups
-
- How would you use yq to change the shared default correctly?
- What other YAML features break when edited with sed?
- How can you test that a YAML edit did not break the file?
From the lesson CSV, TSV and YAML from the shell
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A crontab entry runs a billing summary at 02:30, and the server's zone is America/New_York. Before the spring and autumn clock changes, ops ask whether the summary can be skipped or run twice. What does cronie do on each day, which local times are at risk, and what change removes the question?
- What it tests
- Knowing which local hour is missing in spring and which repeats in autumn, how cronie handles clock changes of under three hours, and moving the schedule to UTC with the entry's time converted.
- First step
- Map both days. In spring the clock jumps from 02:00 to 03:00, so 02:30 does not exist; cronie's manual says jobs from a skipped interval run right after the change. In autumn the hour from 01:00 to 02:00 repeats; 02:30 comes once, and cronie avoids running a fixed-time job twice. To remove the question, set CRON_TZ=UTC and write the time in UTC.
- Follow-ups
-
- Which entries in the same crontab sit in the repeated hour, and what does cronie do with an hourly job there?
- Why is CRON_TZ not a portable fix across cron implementations, and how do you check what a host runs?
- How would a job runner that matches the wall clock every minute behave on the two days, and how would you test it?
From the lesson Dates, times and non-integer numbers
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
A nightly invoice job sums 20,000 subscription lines, each 24.99 rupees, with awk 'BEGIN { for (i=0; i<20000; i++) t += 24.99 } END { printf "%.2f", t }' and prints 499,799.99. The product owner expects 499,800.00. What happened, and what one change in the pipeline fixes it?
- What it tests
- Reading that 24.99 is not exact as an IEEE 754 double, that 20,000 additions accumulate the small error, and that the fix is to count in integer paise (minor units) and format at the end.
- First step
- 24.99 is not exactly representable in a double; the closest value is a shade below 24.99. Twenty thousand sums accumulate the shortfall into about 0.01 rupees of error, which the %.2f step rounds to 499,799.99. The one change is to count in paise: amounts become 2499 each; the awk accumulator is an integer t = 2499 * 20000 = 49980000; only the final line divides by 100 and formats, printing 499,800.00 cleanly.
- Follow-ups
-
- If the per-line amount is 24.995, how does the integer-paise rule read it, and whose job is it to decide which way to round?
- Would scale=2 in bc give the right total without the paise rewrite, and what does the pipeline then cost?
- In an Indian context, how would you format 49,980,000 paise as rupees with the lakh grouping, in one awk call?
From the lesson Dates, times and non-integer numbers
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 5 minutes
All Bash & the POSIX Shell interview questions · The Bash & the POSIX Shell track