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.
Đâ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. Phần 1–5 đã nói về shell, biến, điều kiện, vòng lặp, và hàm. Giờ ta đi vào mảng và thao 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.
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. 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.
Mảng & chuỗi — khái niệm
Bash có hai loại mảng và bộ parameter expansion phong phú trên chuỗi:
- Mảng chỉ số — ô đánh số
0,1,2, … như một danh sách. - Mảng kết hợp — key chuỗi ánh xạ tới giá trị, như hash map (bash 4+).
- 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.
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. Giữ #!/usr/bin/env bash trong shebang.
Mảng chỉ số — tạo, truy cập, duyệt
Khai báo và gán mảng chỉ số trong một dòng:
arr=(apple banana cherry)
Truy cập một phần tử theo index (bắt đầu từ 0):
echo "${arr[0]}" # apple
echo "${arr[2]}" # cherry
Lấy mọi phần tử thành các từ riêng — dạng subscript @:
echo "${arr[@]}" # apple banana cherry (three words)
Đếm phần tử bằng toán tử length trên mảng:
echo "${#arr[@]}" # 3
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
Thêm phần tử mà không gõ lại cả danh sách:
arr+=(date)
echo "${arr[@]}" # apple banana cherry date
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)
Duyệt an toàn — luôn quote expansion
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
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
Không quote, bash word-split expansion và bạn có bốn lần lặp thay vì hai. Ta quay lại phần này ở Lỗi người mới hay mắc.
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
Dạng subscript @ giữ mỗi flag là một đối số; dạng subscript * với IFS nối chúng lại. Chọn @ cho danh sách đối số, * khi cố ý muốn một chuỗi.
Mảng kết hợp — map key/value
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
Đọc giá trị theo key:
echo "${map[user]}" # vinxi
echo "${map[role]}" # admin
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
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}"
Default :- trên lookup xử lý key không tồn tại một cách an toàn.
Thao tác chuỗi — độ dài, cắt, tỉa, thay
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"
Độ 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)
Bỏ prefix — # và ##
# 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)
Bỏ suffix — % và %%
% 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)
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)
Đổ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)
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
:- thay thế để hiển thị; := gán khi unset hoặc rỗng. 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.
Tham chiếu nhanh — so sánh toán tử expansion
| Toán tử | Làm gì | Ví dụ |
|---|---|---|
# prefix | Bỏ prefix match ngắn nhất | path=/a/b/c → /b/c with pattern /a |
## prefix | Bỏ prefix match dài nhất | path=/a/b/c → b/c with pattern /a/ |
% suffix | Bỏ suffix match ngắn nhất | file.tar.gz → file.tar with pattern .gz |
%% suffix | Bỏ suffix match dài nhất | file.tar.gz → file with pattern .* |
/old/new | Thay match đầu tiên | a-b-c → a_x_c replacing - with _ |
//old/new | Thay mọi match | a-b-c → a_x_x replacing - with _ |
^^ | Viết hoa toàn chuỗi | hello → HELLO |
,, | Viết thường toàn chuỗi | HELLO → hello |
:- default | Dùng fallback nếu unset hoặc rỗng | unset x → default |
@ subscript | Mọi phần tử mảng thành từ riêng | 3-element array → 3 words |
* subscript | Mọi phần tử nối (một từ) | 3-element array → 1 word |
Lỗi người mới hay mắc
- 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 …
Luôn quote dạng subscript @ trong vòng lặp.
- Dùng
$arrthay vì dạng all-elements —$arrtrần (hoặc"$arr") chỉ bung phần tử 0, không phải cả mảng. Dùng subscript@khi bạn muốn mọi phần tử. - Quên
declare -Atrướ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ênmap[key], không phải hash map. - Nhầm
#/##(prefix) với%/%%(suffix) — nhớ:#là “trái/đầu”,%là “phải/cuối”. - Tưởng
:-sẽ gán — nó chỉ thay thế cho expansion đó; dùng:=khi cần lưu default lại.
Bài tập
Thử từng bài trước khi mở lời giải.
- 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.
- Tạo mảng kết hợp map tên service (
web,api,db) sang số port. Nhận tên service qua$1và in port, hoặcunknownnếu key không có. - 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.
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"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ó.
Điều cốt lõi
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. Toán tử chuỗi cho phép tỉa path, đổi separator, và default config thiếu mà không cần subprocess. Giữ mọi expansion trong quote cho đến khi bạn cố ý không quote.