Shell builtins · bash builtin
mapfile
Read lines from standard input into an indexed array, one line per element (also called readarray).
mapfile [-d DELIM] [-n COUNT] [-O ORIGIN] [-s COUNT] [-t] [-u FD] [-C CALLBACK] [-c QUANTUM] [ARRAY]
mapfile reads all of its input and stores each line as one element of an array (named
MAPFILE if you give no name). readarray is the same builtin under a second name. It
replaces the slow and error-prone while read loop that appends to an array, and the
broken arr=($(cat file)), which splits on every space and expands globs.
Almost always add -t, which strips the newline from each element; without it every element
ends in \n. With -d '' it reads NUL-terminated records instead, which pairs with
find -print0 to collect file names safely.
Like read, it must run in the current shell to be useful: cmd | mapfile arr fills an
array in a subshell that is thrown away. Use mapfile -t arr < <(cmd). It needs bash 4 or
newer.
Examples
Read a file into an array
mapfile -t tasks < todo.txt
echo "${#tasks[@]} tasks"
echo "first: ${tasks[0]}"
echo "last: ${tasks[-1]}"
5 tasks first: buy milk last: deploy v2.4
Each line becomes one element, with spaces preserved, and -t removes the trailing
newline. Indices start at 0.
Without -t, each element keeps its newline
mapfile lines < data/poem.txt
declare -p lines
declare -a lines=([0]=$'The quick brown fox\n' [1]=$'jumps over the lazy dog.\n' [2]=$'The dog sleeps;\n' [3]=$'the fox runs on.\n')
declare -p shows the $'...\n' at the end of every element. Printing them with echo
would produce blank lines between entries. Use -t unless you really need the newlines.
Why not arr=($(cat file))
words=($(cat todo.txt))
mapfile -t lines < todo.txt
echo "word splitting: ${#words[@]} elements, [1]=${words[1]}"
echo "mapfile: ${#lines[@]} elements, [1]=${lines[1]}"
word splitting: 12 elements, [1]=milk mapfile: 5 elements, [1]=fix login bug
The unquoted command substitution splits on every space, so five lines become twelve
words. mapfile keeps one element per line.
Capture command output with process substitution
mapfile -t errors < <(grep ERROR logs/app.log)
echo "found ${#errors[@]} errors"
printf ' > %s\n' "${errors[@]}"
found 2 errors > 2026-03-14 08:16:40 ERROR health check failed: database timeout after 5000ms > 2026-03-14 08:30:00 ERROR disk usage on /var at 91%
< <(cmd) feeds the output of grep to mapfile while it runs in the current shell,
so the array survives.
Piping into mapfile loses the array
grep ERROR logs/app.log | mapfile -t errors
echo "found ${#errors[@]} errors"
found 0 errors
mapfile ran in a pipeline subshell and filled an array that vanished with it. The
count printed is 0. Use the process substitution form above, or shopt -s lastpipe in a
script.
Skip a header and limit the count with -s and -n
mapfile -t -s 1 -n 3 rows < data/users.csv
printf '%s\n' "${rows[@]}"
1,Alice Moreau,alice@example.com,engineering,2021-04-12 2,Bob Okafor,bob@example.com,sales,2019-11-03 3,Chen Wei,chen@example.com,engineering,2023-01-20
-s 1 discards the first line (the CSV header) and -n 3 stops after three lines. A
count of 0 for -n means all lines.
Collect file names safely with -d ''
touch 'photos/new
line.png'
mapfile -t -d '' pics < <(find photos -name '*.png' -print0)
echo "${#pics[@]} files"
printf '<%s>\n' "${pics[@]}" | sort
4 files <photos/IMG_2041.png> <photos/beach.png> <photos/forest.png> <photos/new line.png>
With NUL as the separator (bash 4.4+), even a name containing a newline stays one element, so there are four files. A newline-based read would have split it in two.
Append to an existing array with -O
mapfile -t logs < logs/error.log
mapfile -t -O "${#logs[@]}" logs < logs/app.log.1
echo "${#logs[@]} lines total"
printf '%s\n' "${logs[@]}"
5 lines total [2026-03-14 08:16:40] database timeout after 5000ms (host=db01) [2026-03-14 08:30:00] disk usage on /var at 91% 2026-03-13 23:58:12 INFO nightly backup started 2026-03-13 23:59:40 INFO nightly backup finished: 1.2G in 88s 2026-03-13 23:59:59 INFO log rotated
-O N starts filling at index N instead of 0, and does not clear the array first.
Passing the current length appends.
Process lines in batches with -C and -c
progress() { echo "reached line $(( $1 + 1 )): $2"; }
mapfile -t -C progress -c 4 lines < logs/app.log
echo "total ${#lines[@]}"
reached line 4: 2026-03-14 08:03:46 WARN failed login for user=alice from 198.51.100.7 reached line 8: 2026-03-14 08:16:50 INFO health check recovered reached line 12: 2026-03-14 08:30:05 INFO cleanup job removed 42 temp files total 12
Every 4 lines (-c 4), mapfile calls progress with the index the line is about to be
stored at (so line 4 is index 3) and the line itself. Rarely needed, but handy for a progress indicator on big inputs.
readarray is the same command
readarray -t nums < data/numbers.txt
echo "${nums[*]}"
42 7 19 3 100 7 58 21 3 64
readarray is another name for the mapfile builtin, with identical options. Some find the name clearer.
Common pitfalls
Forgetting
-t: every element ends with a newline, which breaks comparisons and file names built from the values.cmd | mapfile arrfills the array in a subshell; afterward it is empty. Usemapfile -t arr < <(cmd).mapfilereads all input into memory at once. For huge files, process them line by line withwhile IFS= read -r.macOS ships bash 3.2 as
/bin/bash, which has nomapfile; scripts using it fail there withcommand not found.
GNU vs BSD and macOS
The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).
mapfile/readarrayare bash 4+ only; dash and POSIXshhave no arrays at all. zsh users writearr=("${(@f)$(< file)}")instead.
Options worth knowing
A selection, described in our own words. For the full list, see bash(1), SHELL BUILTIN COMMANDS.
| Option | What it does |
|---|---|
-t | Remove the trailing delimiter (newline by default) from each element. |
-d DELIM | End each record at DELIM instead of newline; an empty string means NUL. |
-n COUNT | Read at most COUNT lines; 0 means all. |
-s COUNT | Discard the first COUNT lines. |
-O ORIGIN | Start assigning at index ORIGIN, without clearing the array. |
-u FD | Read from file descriptor FD instead of standard input. |
-C CALLBACK | Run CALLBACK every QUANTUM lines with the index and the line as arguments. |
-c QUANTUM | Set how many lines pass between CALLBACK calls (default 5000). |
Related
readRead one line from standard input and split it into shell variables.declareCreate variables with attributes (array, associative array, integer, read-only, exported), or show how variables are defined.xargsRead items from standard input and run a command with them as arguments, in batches or in parallel.- ArraysIndexed arrays in bash, creating, reading, appending, looping, slicing and removing elements, and using them to build command lines safely.
- Reading files line by lineThe correct `while IFS= read -r line` loop part by part, the missing last line, the pipe-subshell trap, CSV fields, mapfile and stdin-stealing commands.
- Process substitutionUse <(cmd) and >(cmd) to hand a command's output to a program that wants a file name, keep while-read loops in the current shell, and avoid its timing traps.