opscape

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]

10 of 10 examples run in Debian 13.7 on 2026-10-31 · manual: bash(1), SHELL BUILTIN COMMANDS

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]}"
output
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
output
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]}"
output
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[@]}"
output
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"
output
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[@]}"
output
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
output
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[@]}"
output
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[@]}"
output
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[*]}"
output
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 arr fills the array in a subshell; afterward it is empty. Use mapfile -t arr < <(cmd).

  • mapfile reads all input into memory at once. For huge files, process them line by line with while IFS= read -r.

  • macOS ships bash 3.2 as /bin/bash, which has no mapfile; scripts using it fail there with command 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/readarray are bash 4+ only; dash and POSIX sh have no arrays at all. zsh users write arr=("${(@f)$(< file)}") instead.

Options worth knowing

A selection, described in our own words. For the full list, see bash(1), SHELL BUILTIN COMMANDS.

OptionWhat it does
-tRemove the trailing delimiter (newline by default) from each element.
-d DELIMEnd each record at DELIM instead of newline; an empty string means NUL.
-n COUNTRead at most COUNT lines; 0 means all.
-s COUNTDiscard the first COUNT lines.
-O ORIGINStart assigning at index ORIGIN, without clearing the array.
-u FDRead from file descriptor FD instead of standard input.
-C CALLBACKRun CALLBACK every QUANTUM lines with the index and the line as arguments.
-c QUANTUMSet how many lines pass between CALLBACK calls (default 5000).

Related