Skip to content
Linux Basics: Extract Fields with Bash Regex Captures

Linux Basics: Extract Fields with Bash Regex Captures

October 21, 2026

A shell script sometimes needs just one field from a small, predictable string. Bash can match a regular expression and keep its captured groups without starting a separate text-processing command. Practice extracting fields, rejecting incomplete input, and keeping a match before another test replaces it.

These examples were tested on Linux with GNU Bash 5.2.37. Use Bash rather than sh. The expressions use ASCII letters and digits under the C locale; they are deliberately small formats, not general parsers.

Prepare a scratch directory

You need Bash, a text editor, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file in this directory before running it. The guards stop the shell if setup fails; invoking bash explicitly avoids changing executable permissions. No administrator access is needed.

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

1. Read the captured groups

Save this as regex-fields.sh:

#!/usr/bin/env bash
export LC_ALL=C
line='build-42:ready'
pattern='^([a-z]+)-([0-9]+):([a-z]+)$'
if [[ $line =~ $pattern ]]; then
  printf 'Full match: %s\n' "${BASH_REMATCH[0]}"
  printf 'Name: %s\n' "${BASH_REMATCH[1]}"
  printf 'Number: %s\n' "${BASH_REMATCH[2]}"
  printf 'State: %s\n' "${BASH_REMATCH[3]}"
else
  printf 'Unexpected record\n' >&2
  exit 1
fi

Run it:

bash regex-fields.sh

Output:

Full match: build-42:ready
Name: build
Number: 42
State: ready

Each parenthesized group gets an array element. Index zero contains the whole match, while indexes one through three follow the opening parentheses from left to right. The ^ and $ anchors require the entire value to fit. Without anchors, a matching substring would be enough.

Store the expression in a single-quoted variable assignment to make its punctuation easier to read. Keep the expansion after =~ unquoted when you want its regex operators to work. Inside [[ ... ]], that expansion does not undergo ordinary word splitting or filename expansion.

2. Reject a record with extra text

Save this as regex-setting.sh:

#!/usr/bin/env bash
export LC_ALL=C
pattern='^([a-z][a-z0-9_-]{0,15})=([0-9]{1,4})$'
if [[ $# -ne 1 ]]; then
  printf 'Expected one setting\n' >&2
  exit 2
fi
if [[ $1 =~ $pattern ]]; then
  printf 'Key: %s; digits: %s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"
else
  printf 'Invalid setting\n' >&2
  exit 2
fi

Run it:

bash regex-setting.sh threads=0012
bash regex-setting.sh "threads=12 extra" 2>&1
printf 'Rejected status: %d\n' "$?"

Output:

Key: threads; digits: 0012
Invalid setting
Rejected status: 2

The key starts with a lowercase letter and contains at most sixteen characters. The value contains one to four decimal digits. Leading zeros remain text; matching digits does not convert them into a number or check an application-specific range.

The second invocation intentionally fails. Its error is redirected to standard output so the displayed transcript has a predictable order. Argument count is checked separately, so missing input and multiple arguments cannot silently become a valid record.

3. Understand quoting and changing match state

Save this as regex-state.sh:

#!/usr/bin/env bash
export LC_ALL=C
pattern='[0-9]+'
text='jobs=12'
if [[ $text =~ "$pattern" ]]; then
  printf 'Quoted pattern: matched\n'
else
  printf 'Quoted pattern: no match\n'
fi
if [[ $text =~ $pattern ]]; then
  digits=${BASH_REMATCH[0]}
  [[ label =~ ^([a-z]+)$ ]] || exit 1
  printf 'Saved digits: %s\n' "$digits"
  printf 'Latest match: %s\n' "${BASH_REMATCH[0]}"
fi

Run it:

bash regex-state.sh

Output:

Quoted pattern: no match
Saved digits: 12
Latest match: label

Quoting "$pattern" makes this pattern literal, so Bash looks for the characters [0-9]+ rather than a run of digits. The unquoted version finds 12 inside the larger string.

Read captures only after a successful match, and copy anything needed later. A subsequent regex test updates BASH_REMATCH; it is shared shell state, including across function calls. Declaring a local variable with that special name is not a reliable way to isolate matches.

Keep in mind

These are POSIX extended regular expressions, not PCRE patterns. A match returns zero, no match returns one, and an invalid expression returns two. Keep patterns fixed when possible, and distinguish malformed patterns from ordinary rejected input when patterns come from configuration.

Clean up

In the same terminal, remove only the named practice files, return to your original directory, and remove the empty scratch directory:

rm -- regex-fields.sh regex-setting.sh regex-state.sh
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

References

Acknowledgements

This post was created with assistance from AI.

Last updated on