jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Bash & Shell Scripting · Part 6 — Arrays & String Manipulation

Indexed and associative bash arrays, string slicing, prefix/suffix removal, search/replace, case conversion, safe iteration, and parameter defaults — bilingual, with exercises.

This is Part 6 of a 10-part series that takes you from “I copy-paste commands” to writing robust, production-grade shell scripts {Đây là Phần 6 của series 10 bài đưa bạn từ “copy-paste lệnh” đến viết shell script chắc chắn, mức production}. Parts 1–5 covered the shell, variables, conditionals, loops, and functions {Phần 1–5 đã nói về shell, biến, điều kiện, vòng lặp, và hàm}. Now we tackle arrays and string manipulation — the bash features that turn a flat list of variables into structured data and let you reshape text without reaching for sed or awk {Giờ ta đi vào mảngthao tác chuỗi — tính năng bash biến danh sách biến phẳng thành dữ liệu có cấu trúc và cho phép biến đổi text mà không cần sed hay awk}.

Arrays hold multiple values under one name; parameter expansion operators slice, trim, replace, and default strings — all in pure bash {Mảng giữ nhiều giá trị dưới một tên; các toán tử parameter expansion cắt, tỉa, thay thế, và gán mặc định cho chuỗi — tất cả trong bash thuần}. Master these and your scripts become shorter, clearer, and far less fragile around filenames and config keys {Nắm vững phần này, script ngắn hơn, rõ hơn, và ít vỡ hơn nhiều khi xử lý tên file và key cấu hình}.


Arrays & strings — the concept {Mảng & chuỗi — khái niệm}

Bash gives you two array types and a rich set of parameter expansion operators on strings {Bash có hai loại mảng và bộ parameter expansion phong phú trên chuỗi}:

  • Indexed arrays — numbered slots 0, 1, 2, … like a list {Mảng chỉ số — ô đánh số 0, 1, 2, … như một danh sách}.
  • Associative arrays — string keys mapped to values, like a hash map (bash 4+) {Mảng kết hợp — key chuỗi ánh xạ tới giá trị, như hash map (bash 4+)}.
  • String operators — length, substring, prefix/suffix removal, search/replace, case change, and defaults — all applied via the same brace syntax you learned in Part 2 {Toán tử chuỗi — độ dài, substring, bỏ prefix/suffix, tìm/thay, đổi hoa thường, và default — cùng cú pháp ngoặc nhọn đã học ở Phần 2}.
arr=(apple banana cherry) apple index 0 → ${arr[0]} banana index 1 → ${arr[1]} cherry index 2 → ${arr[2]} ${#arr[@]} = 3 associative: declare -A m; m[user]=vinxi → ${m[user]}
Indexed arrays use numeric slots; associative arrays map string keys to values — both share the same expansion patterns

Everything in this part is bash-specific — POSIX sh has no arrays and fewer expansion forms {Mọi thứ trong phần này chỉ có trên bash — POSIX sh không có mảng và ít dạng expansion hơn}. Keep #!/usr/bin/env bash in your shebang {Giữ #!/usr/bin/env bash trong shebang}.


Indexed arrays — create, access, iterate {Mảng chỉ số — tạo, truy cập, duyệt}

Declare and populate an indexed array in one line {Khai báo và gán mảng chỉ số trong một dòng}:

arr=(apple banana cherry)

Access a single element by index (zero-based) {Truy cập một phần tử theo index (bắt đầu từ 0)}:

echo "${arr[0]}"    # apple
echo "${arr[2]}"    # cherry

Get all elements as separate words — the @ subscript form {Lấy mọi phần tử thành các từ riêng — dạng subscript @}:

echo "${arr[@]}"    # apple banana cherry  (three words)

Count elements with the length operator on the array {Đếm phần tử bằng toán tử length trên mảng}:

echo "${#arr[@]}"   # 3

List which indices are set (useful after sparse or selective deletes) {Liệt kê index nào đang có giá trị (hữu ích sau khi xóa chọn lọc hoặc mảng thưa)}:

echo "${!arr[@]}"   # 0 1 2

Append without retyping the whole list {Thêm phần tử mà không gõ lại cả danh sách}:

arr+=(date)
echo "${arr[@]}"    # apple banana cherry date

Slice a range of elements — same offset:length syntax as strings, but applied to the array expansion {Cắt một đoạn phần tử — cùng cú pháp offset:length như chuỗi, nhưng áp lên expansion mảng}:

echo "${arr[@]:1:2}"   # banana cherry  (start at index 1, take 2)

Iterate safely — always quote the expansion {Duyệt an toàn — luôn quote expansion}

The correct loop pattern from Part 4, now with the full picture {Mẫu vòng lặp đúng từ Phần 4, giờ với bức tranh đầy đủ}:

for item in "${arr[@]}"; do
  echo "→ $item"
done

Quoting preserves each element as one word, even when values contain spaces {Quote giữ mỗi phần tử là một từ, kể cả khi giá trị có dấu cách}:

paths=("/tmp/my docs/report.pdf" "/var/log")

for p in "${paths[@]}"; do
  echo "File: $p"
done

Without quotes, bash word-splits the expansion and you get four iterations instead of two {Không quote, bash word-split expansion và bạn có bốn lần lặp thay vì hai}. We revisit this in Mistakes beginners make {Ta quay lại phần này ở Lỗi người mới hay mắc}.

Practical example — build a command from an array {Ví dụ thực tế — ghép lệnh từ mảng}

#!/usr/bin/env bash
flags=(-v --recursive --exclude=.git)

# Pass the array as separate arguments to a command:
grep "${flags[@]}" "TODO" src/

# Or join into one string when you need it:
joined=$(IFS=','; echo "${flags[*]}")
echo "$joined"    # -v,--recursive,--exclude=.git

The @ subscript form keeps each flag as its own argument; the * subscript form with IFS joins them {Dạng subscript @ giữ mỗi flag là một đối số; dạng subscript * với IFS nối chúng lại}. Pick @ for argument lists, * when you deliberately want one string {Chọn @ cho danh sách đối số, * khi cố ý muốn một chuỗi}.


Associative arrays — key/value maps {Mảng kết hợp — map key/value}

Associative arrays require an explicit declaration before use {Mảng kết hợp cần khai báo rõ trước khi dùng}:

declare -A map

map[user]=vinxi
map[role]=admin
map[env]=production

Read a value by key {Đọc giá trị theo key}:

echo "${map[user]}"      # vinxi
echo "${map[role]}"      # admin

Iterate over keys with the indirect-expansion form, then look up each value {Duyệt key bằng dạng indirect expansion, rồi tra từng value}:

for key in "${!map[@]}"; do
  echo "$key → ${map[$key]}"
done

Practical example — simple config lookup {Ví dụ thực tế — tra cấu hình đơn giản}

#!/usr/bin/env bash
declare -A ports=(
  [web]=8080
  [api]=3000
  [db]=5432
)

service="${1:-web}"
echo "Connecting to port ${ports[$service]:-unknown}"

The :- default on the lookup handles a missing key gracefully {Default :- trên lookup xử lý key không tồn tại một cách an toàn}.


String manipulation — length, slice, trim, replace {Thao tác chuỗi — độ dài, cắt, tỉa, thay}

All of the following work on ordinary string variables. Set up a sample {Tất cả dưới đây dùng trên biến chuỗi thường. Chuẩn bị ví dụ}:

str="Hello-World-Bash"
path="/var/www/html/index.html"

Length and substring {Độ dài và substring}

echo "${#str}"        # 16

echo "${str:0:5}"     # Hello   (offset 0, length 5)
echo "${str:6}"       # World-Bash  (offset 6, rest of string)
echo "${str: -4}"     # Bash  (note the space before -4 — last 4 chars)

Prefix removal — # and ## {Bỏ prefix — ###}

# removes the shortest match; ## removes the longest {# bỏ match ngắn nhất; ## bỏ match dài nhất}:

echo "${path#/}"           # var/www/html/index.html
echo "${path##*/}"         # index.html  (strip through last slash)
echo "${str#Hello-}"       # World-Bash
echo "${str##Hello-}"      # World-Bash  (same here — only one match)

Suffix removal — % and %% {Bỏ suffix — %%%}

% removes the shortest suffix match; %% removes the longest {% bỏ suffix match ngắn nhất; %% bỏ suffix match dài nhất}:

echo "${path%.html}"       # /var/www/html/index
echo "${path%%/*}"         #   (empty — strips longest prefix through slash)
echo "${path%/*}"          # /var/www/html
echo "${str%-Bash}"        # Hello-World
echo "${str%%-*}"          # Hello  (strip from last dash)

Search and replace {Tìm và thay}

echo "${str/World/Earth}"    # Hello-Earth-Bash  (first match only)
echo "${str//-/_}"           # Hello_World_Bash  (all matches — global replace)
echo "${str/#Hello/Hi}"      # Hi-World-Bash  (anchor: start of string)
echo "${str/%Bash/Script}"   # Hello-World-Script  (anchor: end of string)

Case conversion (bash 4+) {Đổi hoa thường (bash 4+)}

msg="Hello World"
echo "${msg^^}"      # HELLO WORLD  (uppercase all)
echo "${msg,,}"      # hello world  (lowercase all)
echo "${msg^}"       # Hello world  (uppercase first char only)
echo "${msg,}"       # hello World  (lowercase first char only)

Defaults — :- when a value might be empty {Default — :- khi giá trị có thể rỗng}

name=""
echo "${name:-Guest}"     # Guest  (name is empty → use default)
echo "$name"              # still empty — :- does not assign

: "${CONFIG_DIR:=/etc/myapp}"   # assign default if unset OR empty
echo "$CONFIG_DIR"              # /etc/myapp

:- substitutes for display; := assigns when unset or empty {:- thay thế để hiển thị; := gán khi unset hoặc rỗng}. You met the basics in Part 2; arrays and string ops use the same brace machinery {Bạn đã gặp cơ bản ở Phần 2; mảng và thao tác chuỗi dùng cùng cơ chế ngoặc nhọn}.


Quick reference — expansion operators compared {Tham chiếu nhanh — so sánh toán tử expansion}

Operator {Toán tử}What it does {Làm gì}Example input → output {Ví dụ}
# prefixRemove shortest prefix match {Bỏ prefix match ngắn nhất}path=/a/b/c/b/c with pattern /a
## prefixRemove longest prefix match {Bỏ prefix match dài nhất}path=/a/b/cb/c with pattern /a/
% suffixRemove shortest suffix match {Bỏ suffix match ngắn nhất}file.tar.gzfile.tar with pattern .gz
%% suffixRemove longest suffix match {Bỏ suffix match dài nhất}file.tar.gzfile with pattern .*
/old/newReplace first match {Thay match đầu tiên}a-b-ca_x_c replacing - with _
//old/newReplace all matches {Thay mọi match}a-b-ca_x_x replacing - with _
^^Uppercase entire string {Viết hoa toàn chuỗi}helloHELLO
,,Lowercase entire string {Viết thường toàn chuỗi}HELLOhello
:- defaultUse fallback if unset or empty {Dùng fallback nếu unset hoặc rỗng}unset xdefault
@ subscriptAll array elements as separate words {Mọi phần tử mảng thành từ riêng}3-element array → 3 words
* subscriptAll elements joined (one word) {Mọi phần tử nối (một từ)}3-element array → 1 word

Mistakes beginners make {Lỗi người mới hay mắc}

  • Unquoted array expansion — looping with an unquoted @ subscript expansion word-splits and glob-expands each element, breaking paths with spaces and mangling * in filenames {Expansion mảng không quote — lặp với expansion subscript @ không quote sẽ word-split và glob-expand từng phần tử, làm vỡ path có dấu cách và phá * trong tên file}:
# ❌ wrong — splits "my file.txt" into two iterations
for x in ${arr[@]}; do

Always quote the @ subscript form in loops {Luôn quote dạng subscript @ trong vòng lặp}.

  • Using $arr instead of the all-elements form — bare $arr (or "$arr") expands to only element 0, not the whole array {Dùng $arr thay vì dạng all-elements$arr trần (hoặc "$arr") chỉ bung phần tử 0, không phải cả mảng}. Use the @ subscript when you mean every element {Dùng subscript @ khi bạn muốn mọi phần tử}.
  • Forgetting declare -A before using associative arrays — without it, map[key]=value is treated as a plain string variable named map[key], not a hash map {Quên declare -A trước khi dùng mảng kết hợp — không có nó, map[key]=value được coi là biến chuỗi tên map[key], không phải hash map}.
  • Mixing up #/## (prefix) with %/%% (suffix) — remember: # is “left/start”, % is “right/end” (like comment # at line start and % modulo at the end of an expression) {Nhầm #/## (prefix) với %/%% (suffix) — nhớ: # là “trái/đầu”, % là “phải/cuối”}.
  • Expecting :- to assign — it only substitutes for that expansion; use := when you need to persist the default {Tưởng :- sẽ gán — nó chỉ thay thế cho expansion đó; dùng := khi cần lưu default lại}.

Exercises {Bài tập}

Try each before opening the solution {Thử từng bài trước khi mở lời giải}.

  1. Create an indexed array of three server hostnames. Print each on its own line, then print the count and a slice of the first two elements {Tạo mảng chỉ số ba hostname server. In từng cái một dòng, rồi in số lượng và slice hai phần tử đầu}.
  2. Build an associative array mapping service names (web, api, db) to port numbers. Accept a service name as $1 and print its port, or unknown if the key is missing {Tạo mảng kết hợp map tên service (web, api, db) sang số port. Nhận tên service qua $1 và in port, hoặc unknown nếu key không có}.
  3. Given filename="backup-2025-02-10.tar.gz", use only parameter expansion (no external tools) to extract: the full name without .gz, the base name without any extension, and an uppercase version {Cho filename="backup-2025-02-10.tar.gz", chỉ dùng parameter expansion (không tool ngoài) để lấy: tên đầy đủ bỏ .gz, tên gốc bỏ mọi extension, và bản viết hoa}.
Solution {Lời giải}
#!/usr/bin/env bash
# Exercise 1
servers=(web-01 api-02 db-03)

for host in "${servers[@]}"; do
  echo "Host: $host"
done

echo "Count: ${#servers[@]}"
echo "First two: ${servers[@]:0:2}"
#!/usr/bin/env bash
# Exercise 2
declare -A ports=(
  [web]=8080
  [api]=3000
  [db]=5432
)

service="${1:-}"
if [[ -n "${ports[$service]+x}" ]]; then
  echo "Port for $service: ${ports[$service]}"
else
  echo "unknown"
fi
#!/usr/bin/env bash
# Exercise 3
filename="backup-2025-02-10.tar.gz"

no_gz="${filename%.gz}"           # backup-2025-02-10.tar
base="${filename%%.*}"            # backup-2025-02-10
upper="${filename^^}"             # BACKUP-2025-02-10.TAR.GZ

echo "without .gz:  $no_gz"
echo "base name:     $base"
echo "uppercase:     $upper"

The +x indirect subscript test (shown in Exercise 2) checks whether the key exists without triggering set -u errors on a missing subscript {Test indirect subscript +x (trong Bài 2) kiểm tra key có tồn tại mà không gây lỗi set -u khi subscript không có}.


Takeaway {Điều cốt lõi}

Indexed arrays store ordered lists; associative arrays store key/value maps — both need the right subscript (@ for separate words, [n] or [key] for one slot) and quotes when iterating {Mảng chỉ số lưu danh sách có thứ tự; mảng kết hợp lưu map key/value — cả hai cần subscript đúng (@ cho từ riêng, [n] hoặc [key] cho một ô) và quote khi duyệt}. String operators let you trim paths, swap separators, and default missing config without spawning subprocesses {Toán tử chuỗi cho phép tỉa path, đổi separator, và default config thiếu mà không cần subprocess}. Keep every expansion example in quotes until you have a deliberate reason not to {Giữ mọi expansion trong quote cho đến khi bạn cố ý không quote}.