Mặc dù kỹ sư phần mềm thường dùng dòng lệnh cho nhiều khía cạnh phát triển, mảng có lẽ là một trong những tính năng bí ẩn hơn của dòng lệnh (dù không bí ẩn bằng toán tử regex =~). Nhưng bỏ qua sự bí ẩn và cú pháp đáng ngờ, mảng Bash có thể rất mạnh mẽ.
Tại Sao Lại Như Thế?
Viết về Bash thách thức vì rất dễ bài viết biến thành hướng dẫn tập trung vào sự kỳ lạ của cú pháp. Yên tâm, mục đích bài viết này là tránh việc bạn phải RTFM (Read The Fine Manual).
Một Ví Dụ Thực Tế (Thật Sự Hữu Ích)
Để đạt mục đích đó, hãy xem xét tình huống thực tế và cách Bash có thể giúp: Bạn dẫn dắt nỗ lực mới tại công ty để đánh giá và tối ưu thời gian chạy đường ống dữ liệu nội bộ. Là bước đầu, bạn muốn quét tham số để đánh giá đường ống dùng luồng (thread) tốt đến đâu. Để đơn giản, ta xem đường ống như hộp đen C++ đã biên dịch nơi tham số duy nhất có thể điều chỉnh là số luồng dành cho xử lý dữ liệu: ./pipeline --threads 4.
Cơ Bản
Điều đầu tiên chúng ta làm là định nghĩa mảng chứa giá trị tham số --threads muốn test:
allThreads=(1 2 4 8 16 32 64 128)
Trong ví dụ này, tất cả phần tử là số, nhưng không nhất thiết – mảng Bash có thể chứa cả số và chuỗi, ví dụ myArray=(1 2 "three" 4 "five") là biểu thức hợp lệ. Và giống như biến Bash khác, chắc chắn không để dấu cách quanh dấu bằng. Nếu không, Bash sẽ coi tên biến như chương trình để thực thi, và = như tham số đầu tiên!
Bây giờ chúng ta đã khởi tạo mảng, hãy lấy vài phần tử. Bạn sẽ thấy chỉ làm echo $allThreads chỉ xuất phần tử đầu tiên.
Để hiểu tại sao, hãy lùi lại và xem lại cách chúng ta thường xuất biến trong Bash. Xét tình huống sau:
type="article" echo "Found 42 $type"
Giả sử biến $type cho chúng ta như danh từ số ít và chúng ta muốn thêm s ở cuối câu. Chúng ta không thể chỉ thêm s vào $type vì sẽ biến nó thành biến khác, $types. Và dù có thể dùng contortion code như echo "Found 42 "$type"s", cách tốt nhất giải quyết vấn đề này là dùng ngoặc nhọn: echo "Found 42 ${type}s", cho phép nói cho Bash biết tên biến bắt đầu và kết thúc ở đâu (thú vị, cú pháp này giống JavaScript/ES6 để chèn biến và biểu thức trong template literals).
Vì vậy ra là mặc dù biến Bash không thường yêu cầu ngoặc nhọn, chúng bắt buộc cho mảng. Ngược lại, điều này cho phép chỉ định chỉ mục để truy cập, ví dụ echo ${allThreads[1]} trả về phần tử thứ hai của mảng. Không bao gồm ngoặc, ví dụ echo $allThreads[1], khiến Bash coi [1] như chuỗi và xuất nó như vậy.
Có, mảng Bash có cú pháp kỳ lạ, nhưng ít nhất chúng zero-indexed (chỉ mục từ 0), khác một số ngôn ngữ khác (tôi nhìn bạn, R).
Lặp Qua Mảng
Mặc dù trong ví dụ trên dùng chỉ mục số nguyên cho mảng, hãy xem hai trường hợp điều đó không đúng: Thứ nhất, nếu muốn phần tử $i-th của mảng, với $i là biến chứa chỉ mục quan tâm, ta có thể lấy phần tử đó dùng: echo ${allThreads[$i]}. Thứ hai, để xuất tất cả phần tử mảng, ta thay chỉ mục số bằng ký hiệu @ (bạn có thể nghĩ @ đại diện cho all): echo ${allThreads[@]}.
Lặp Qua Các Phần Tử Mảng
Với ý đó, hãy lặp qua $allThreads và chạy pipeline cho mỗi giá trị --threads:
for t in ${allThreads[@]}; do
./pipeline --threads $t
done
Lặp Qua Chỉ Mục Mảng
Tiếp theo, xem cách tiếp cận hơi khác. Thay vì lặp qua phần tử mảng, ta có thể lặp qua chỉ mục mảng:
for i in ${!allThreads[@]}; do
./pipeline --threads ${allThreads[$i]}
done
Hãy phân tích: Như thấy, ${allThreads[@]} đại diện tất cả phần tử mảng. Thêm dấu chấm than thành ${!allThreads[@]} sẽ trả về danh sách tất cả chỉ mục mảng (trong trường hợp 0 đến 7). Nói cách khác, vòng lặp for lặp qua tất cả chỉ mục $i và đọc phần tử $i-th từ $allThreads để đặt giá trị tham số --threads.
Cách này khô khan hơn, nên bạn có thể thắc tại sao tôi giới thiệu nó. Đó là vì có lúc bạn cần biết cả chỉ mục và giá trị trong vòng lặp, ví dụ nếu muốn bỏ qua phần tử đầu mảng, dùng chỉ mục giúp tránh tạo biến thêm rồi tăng trong vòng lặp.
Điền Giá Trị Vào Mảng
Cho đến nay, chúng ta đã chạy pipeline cho mỗi --threads quan tâm. Bây giờ, giả sử đầu ra pipeline là thời gian chạy tính bằng giây. Chúng ta muốn bắt đầu ra đó ở mỗi lần lặp và lưu vào mảng khác để thao tác cuối.
Một Số Cú Pháp Hữu Ích
Trước khi đi vào mã, cần giới thiệu thêm cú pháp. Thứ nhất, cần lấy đầu ra lệnh Bash. Để làm vậy, dùng cú pháp: output=$( ./my_script.sh ), sẽ lưu đầu ra lệnh vào biến $output.
Cú pháp thứ hai cần là cách thêm giá trị vừa lấy vào mảng. Cú pháp sẽ trông quen thuộc:
myArray+=( "newElement1" "newElement2" )
Quét Tham Số
Gộp tất cả, đây là script cho quét tham số:
allThreads=(1 2 4 8 16 32 64 128)
allRuntimes=()
for t in ${allThreads[@]}; do
runtime=$(./pipeline --threads $t)
allRuntimes+=( $runtime )
done
Và xong!
Còn Gì Khác?
Trong bài này, chúng ta bao phủ tình huống dùng mảng cho quét tham số. Tôi hứa có thêm lý do dùng mảng Bash – đây là hai ví dụ nữa.
Cảnh Báo Log
Trong tình huống này, app chia thành module, mỗi module có file log riêng. Chúng ta viết script cron job gửi email cho người phù hợp khi có dấu hiệu rắc rối trong module:
# List of logs and who should be notified of issues
logPaths=("api.log" "auth.log" "jenkins.log" "data.log")
logEmails=("jay@email" "emma@email" "jon@email" "sophia@email")
# Look for signs of trouble in each log
for i in ${!logPaths[@]};
do
log=${logPaths[$i]}
stakeholder=${logEmails[$i]}
numErrors=$( tail -n 100 "$log" | grep "ERROR" | wc -l )
# Warn stakeholders if recently saw > 5 errors
if [[ "$numErrors" -gt 5 ]];
then
emailRecipient="$stakeholder"
emailSubject="WARNING: ${log} showing unusual levels of errors"
emailBody="${numErrors} errors found in log ${log}"
echo "$emailBody" | mailx -s "$emailSubject" "$emailRecipient"
fi
done
Truy Vấn API
Giả sử bạn muốn tạo phân tích về user comment nhiều nhất trên bài Medium. Vì không có truy cập database trực tiếp, SQL không khả thi, nhưng ta dùng API!
Để tránh thảo luận dài về xác thực API và token, ta dùng JSONPlaceholder, dịch vụ test API công khai, làm endpoint. Khi truy vấn mỗi bài và lấy email mọi người comment, ta nối email vào mảng kết quả:
endpoint="https://jsonplaceholder.typicode.com/comments"
allEmails=()
# Query first 10 posts
for postId in {1..10};
do
# Make API call to fetch emails of this posts's commenters
response=$(curl "${endpoint}?postId=${postId}")
# Use jq to parse the JSON response into an array
allEmails+=( $( jq '.[].email' <<< "$response" ) )
done
Lưu ý tôi dùng công cụ jq để parse JSON từ dòng lệnh. Cú pháp jq ngoài phạm vi bài này, nhưng tôi mạnh mẽ khuyên bạn tìm hiểu nó.
Như bạn tưởng tượng, có vô số tình huống khác dùng mảng Bash giúp, và hy vọng ví dụ trong bài đã cho bạn tư duy. Nếu bạn có ví dụ khác từ công việc, hãy để bình luận dưới.
Nhưng Đợi Dé, Còn Thêm!
Vì đã bao phủ khá nhiều cú pháp mảng, đây là tóm tắt những gì bao phủ, cùng vài thủ thuật nâng cao chưa bao phủ:
| Cú pháp | Kết quả |
|---|---|
arr=() |
Tạo mảng rỗng |
arr=(1 2 3) |
Khởi tạo mảng |
${arr[2]} |
Lấy phần tử thứ 3 |
${arr[@]} |
Lấy tất cả phần tử |
${!arr[@]} |
Lấy chỉ mục mảng |
${#arr[@]} |
Tính kích thước mảng |
arr[0]=3 |
Ghi đè phần tử 1 |
arr+=(4) |
Thêm giá trị |
str=$(ls) |
Lưu đầu ra ls làm chuỗi |
arr=( $(ls) ) |
Lưu đầu ra ls làm mảng tệp |
${arr[@]:s:n} |
Lấy n phần tử bắt đầu tại chỉ mục s |
Một Suy Nghĩ Cuối
Như khám phá, mảng Bash chắc chắn có cú pháp kỳ quặc, nhưng hy vọng bài này thuyết phục bạn chúng cực mạnh. Khi nắm cú pháp, bạn sẽ thấy mình dùng mảng Bash khá thường.
Bash Hay Python?
Điều nảy ra câu hỏi: Khi nào nên dùng mảng Bash thay vì ngôn ngữ script khác như Python?
Đối với tôi, tất cả归结为 phụ thuộc (dependencies) – nếu giải quyết vấn đề chỉ dùng gọi công cụ dòng lệnh, không sao dùng Bash. Nhưng khi script là phần của dự án Python lớn hơn, tốt hơn dùng Python.
Ví dụ, ta có thể chuyển sang Python thực hiện quét tham số, nhưng sẽ chỉ viết wrapper quanh Bash:
import subprocess
all_threads = [1, 2, 4, 8, 16, 32, 64, 128]
all_runtimes = []
# Launch pipeline on each number of threads
for t in all_threads:
cmd = './pipeline --threads {}'.format(t)
# Use the subprocess module to fetch the return output
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
output = p.communicate()[0]
all_runtimes.append(output)
Vì không tránh được dòng lệnh trong ví dụ này, dùng Bash trực tiếp ưu việt.
Thời Gian Cho Quà Tặng Thô Lỗ
Bài viết này dựa trên talk tôi làm tại OSCON, nơi tôi trình bày workshop live-coding You Don't Know Bash. Không slide, không clicker – chỉ tôi và khán giả gõ phím tại dòng lệnh, khám phá thế giới kỳ diệu của Bash.