Computer >> Máy Tính >  >> Lập trình >> Lập trình BASH

Bạn chưa biết Bash:Giới thiệu về mảng Bash

Mặc dù các kỹ sư phần mềm thường xuyên sử dụng dòng lệnh cho nhiều khía cạnh phát triển, nhưng mảng có thể là một trong những tính năng khó hiểu hơn của dòng lệnh (mặc dù không tối nghĩa như toán tử regex =~ ). Nhưng bỏ qua sự mù mờ và cú pháp đáng ngờ, mảng Bash có thể rất mạnh mẽ.

Chờ, nhưng tại sao?

Viết về Bash là một thách thức vì rất dễ dàng để một bài báo có thể chuyển thành một cuốn sổ tay hướng dẫn tập trung vào những điểm kỳ quặc về cú pháp. Hãy yên tâm, tuy nhiên, mục đích của bài viết này là để tránh bạn có RTFM.

Một ví dụ thực tế (thực sự hữu ích)

Để đạt được điều đó, chúng ta hãy xem xét một kịch bản trong thế giới thực và cách Bash có thể giúp:Bạn đang dẫn đầu một nỗ lực mới tại công ty của mình để đánh giá và tối ưu hóa thời gian chạy của đường ống dữ liệu nội bộ của bạn. Bước đầu tiên, bạn muốn thực hiện quét tham số để đánh giá xem đường ống sử dụng các luồng tốt như thế nào. Để đơn giản hơn, chúng tôi sẽ coi đường ống như một hộp đen C ++ đã biên dịch trong đó tham số duy nhất mà chúng tôi có thể tinh chỉnh là số luồng dành riêng cho xử lý dữ liệu:./pipeline --threads 4 .

Khái niệm cơ bản

Điều đầu tiên chúng ta sẽ làm là xác định một mảng chứa các giá trị của --threads tham số mà chúng tôi muốn kiểm tra:

allThreads=(1 2 4 8 16 32 64 128)

Trong ví dụ này, tất cả các phần tử đều là số, nhưng không nhất thiết phải là chữ thường — các mảng trong Bash có thể chứa cả số và chuỗi, ví dụ:myArray=(1 2 "three" 4 "five") là một biểu thức hợp lệ. Và cũng giống như với bất kỳ biến Bash nào khác, hãy đảm bảo không để lại khoảng trống xung quanh dấu bằng. Nếu không, Bash sẽ coi tên biến như một chương trình để thực thi và = là tham số đầu tiên của nó!

Bây giờ chúng ta đã khởi tạo mảng, hãy truy xuất một vài phần tử của nó. Bạn sẽ nhận thấy rằng chỉ cần thực hiện echo $allThreads sẽ chỉ xuất phần tử đầu tiên.

Để hiểu tại sao lại như vậy, chúng ta hãy lùi lại một bước và xem lại cách chúng ta thường xuất các biến trong Bash. Hãy xem xét tình huống sau:

type="article"
echo "Found 42 $type"

Nói biến $type được đặt cho chúng tôi như một danh từ số ít và chúng tôi muốn thêm một s ở cuối câu của chúng tôi. Chúng tôi không thể chỉ thêm một s thành $type vì điều đó sẽ biến nó thành một biến khác, $types . Và mặc dù chúng tôi có thể sử dụng các biến dạng mã như echo "Found 42 "$type"s" , cách tốt nhất để giải quyết vấn đề này là sử dụng dấu ngoặc nhọn:echo "Found 42 ${type}s" , cho phép chúng tôi cho Bash biết vị trí bắt đầu và kết thúc của tên biến (thú vị là, đây là cú pháp tương tự được sử dụng trong JavaScript / ES6 để đưa các biến và biểu thức vào các ký tự mẫu).

Hóa ra, mặc dù các biến Bash thường không yêu cầu dấu ngoặc nhọn, nhưng chúng được yêu cầu cho các mảng. Đổi lại, điều này cho phép chúng tôi 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 dấu ngoặc, ví dụ:echo $allThreads[1] , dẫn đến việc Bash xử lý [1] dưới dạng một 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 được lập chỉ mục bằng 0, không giống như một số ngôn ngữ khác (Tôi đang nhìn bạn, R ).

Vòng qua các mảng

Mặc dù trong các ví dụ trên, chúng tôi đã sử dụng các chỉ số nguyên trong các mảng của mình, chúng ta hãy xem xét hai trường hợp xảy ra trường hợp đó:Thứ nhất, nếu chúng tôi muốn $i -phần tử thứ của mảng, trong đó $i là một biến chứa chỉ mục quan tâm, chúng ta có thể truy xuất phần tử đó bằng cách sử dụng:echo ${allThreads[$i]} . Thứ hai, để xuất tất cả các phần tử của một mảng, chúng tôi thay thế chỉ mục số bằng @ biểu tượng (bạn có thể nghĩ đến @ như viết tắt của all ):echo ${allThreads[@]} .

Vòng qua các phần tử mảng

Với ý nghĩ đó, hãy lặp lại $allThreads và khởi chạy đường dẫn cho từng giá trị của --threads :

for t in ${allThreads[@]}; do
  ./pipeline --threads $t
done

Vòng qua các chỉ số mảng

Tiếp theo, hãy xem xét một cách tiếp cận hơi khác. Thay vì lặp qua mảng phần tử , chúng ta có thể lặp lại mảng chỉ số :

for i in ${!allThreads[@]}; do
  ./pipeline --threads ${allThreads[$i]}
done

Hãy phân tích điều đó:Như chúng ta đã thấy ở trên, ${allThreads[@]} đại diện cho tất cả các phần tử trong mảng của chúng tôi. Thêm dấu chấm than để đặt thành ${!allThreads[@]} sẽ trả về danh sách tất cả các chỉ số mảng (trong trường hợp của chúng ta là 0 đến 7). Nói cách khác, for vòng lặp đang lặp lại qua tất cả các chỉ số $i và đọc $i -th phần tử từ $allThreads để đặt giá trị của --threads tham số.

Điều này khắc nghiệt hơn nhiều đối với mắt, vì vậy bạn có thể tự hỏi tại sao tôi lại bận tâm giới thiệu nó ngay từ đầu. Đó là bởi vì đôi khi bạn cần biết cả chỉ mục và giá trị trong vòng lặp, ví dụ:nếu bạn muốn bỏ qua phần tử đầu tiên của một mảng, việc sử dụng chỉ số giúp bạn không phải tạo một biến bổ sung mà sau đó bạn tăng lên bên trong vòng lặp .

Điền mảng

Cho đến nay, chúng tôi đã có thể khởi chạy đường dẫn cho mỗi --threads lãi. Bây giờ, giả sử đầu ra cho đường ống của chúng ta là thời gian chạy tính bằng giây. Chúng tôi muốn nắm bắt đầu ra đó ở mỗi lần lặp lại và lưu nó vào một mảng khác để chúng tôi có thể thực hiện các thao tác khác nhau với nó ở cuối.

Một số cú pháp hữu ích

Nhưng trước khi đi sâu vào mã, chúng ta cần giới thiệu thêm một số cú pháp. Đầu tiên, chúng ta cần có thể truy xuất đầu ra của lệnh Bash. Để làm như vậy, hãy sử dụng cú pháp sau:output=$( ./my_script.sh ) , sẽ lưu trữ kết quả đầu ra của các lệnh của chúng ta vào biến $output .

Bit thứ hai của cú pháp chúng ta cần là cách nối giá trị mà chúng ta vừa truy xuất vào một mảng. Cú pháp để làm điều đó trông sẽ quen thuộc:

myArray+=( "newElement1" "newElement2" )

Quét thông số

Kết hợp mọi thứ lại với nhau, đây là tập lệnh của chúng tôi để khởi chạy quét thông số của chúng tôi:

allThreads=(1 2 4 8 16 32 64 128)
allRuntimes=()
for t in ${allThreads[@]}; do
  runtime=$(./pipeline --threads $t)
  allRuntimes+=( $runtime )
done

Và voilà!

Bạn còn gì nữa?

Trong bài viết này, chúng tôi đề cập đến kịch bản sử dụng mảng để quét tham số. Nhưng tôi hứa có nhiều lý do hơn để sử dụng mảng Bash — đây là hai ví dụ khác.

Cảnh báo nhật ký

Trong trường hợp này, ứng dụng của bạn được chia thành các mô-đun, mỗi mô-đun có tệp nhật ký riêng. Chúng tôi có thể viết một tập lệnh cron job để gửi email cho đúng người khi có dấu hiệu trục trặc trong một số mô-đun nhất định:

# 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 một số phân tích về người dùng nào nhận xét nhiều nhất trên các bài đăng trên Phương tiện của bạn. Vì chúng tôi không có quyền truy cập trực tiếp vào cơ sở dữ liệu, nên SQL không có vấn đề gì, nhưng chúng tôi có thể sử dụng các API!

Để tránh thảo luận dài dòng về xác thực API và mã thông báo, thay vào đó, chúng tôi sẽ sử dụng JSONPlaceholder, một dịch vụ kiểm tra API công khai, làm điểm cuối của chúng tôi. Khi chúng tôi truy vấn từng bài đăng và truy xuất email của những người đã nhận xét, chúng tôi có thể nối các email đó vào mảng kết quả của mình:

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 ý ở đây rằng tôi đang sử dụng jq công cụ để phân tích cú pháp JSON từ dòng lệnh. Cú pháp của jq nằm ngoài phạm vi của bài viết này, nhưng tôi thực sự khuyên bạn nên xem xét nó.

Như bạn có thể tưởng tượng, có vô số tình huống khác mà việc sử dụng mảng Bash có thể hữu ích, và tôi hy vọng các ví dụ được nêu trong bài viết này sẽ giúp bạn suy nghĩ. Nếu bạn có những ví dụ khác để chia sẻ từ công việc của chính mình, vui lòng để lại bình luận bên dưới.

Nhưng chờ đã, còn nhiều thứ nữa!

Vì chúng tôi đã trình bày khá nhiều cú pháp mảng trong bài viết này, đây là bản tóm tắt về những gì chúng tôi đã đề cập, cùng với một số thủ thuật nâng cao hơn mà chúng tôi chưa đề cập:

Cú pháp
Kết quả
arr=() Tạo một mảng trống
arr=(1 2 3) Khởi tạo mảng
${arr[2]} Truy xuất phần tử thứ ba
${arr[@]} Truy xuất tất cả các phần tử
${!arr[@]} Truy xuất các chỉ số mảng
${#arr[@]} Tính kích thước mảng
arr[0]=3 Ghi đè phần tử thứ nhất
arr+=(4) Nối (các) giá trị
str=$(ls) Lưu ls xuất dưới dạng chuỗi
arr=( $(ls) ) Lưu ls xuất ra dưới dạng một mảng tệp
${arr[@]:s:n} Truy xuất n phần tử starting at index s

Một ý nghĩ cuối cùng

Như chúng tôi đã phát hiện ra, mảng Bash chắc chắn có cú pháp lạ, nhưng tôi hy vọng bài viết này thuyết phục bạn rằng chúng cực kỳ mạnh mẽ. Khi bạn nắm rõ cú pháp, bạn sẽ thấy mình sử dụng mảng Bash khá thường xuyên.

Bash hay Python?

Điều nào đặt ra câu hỏi: Khi nào bạn nên sử dụng mảng Bash thay vì các ngôn ngữ lập trình khác như Python?

Đối với tôi, tất cả chỉ tập trung vào sự phụ thuộc — nếu bạn có thể giải quyết vấn đề trong tầm tay chỉ bằng các lệnh gọi tới các công cụ dòng lệnh, thì bạn cũng có thể sử dụng Bash. Nhưng đối với những trường hợp tập lệnh của bạn là một phần của một dự án Python lớn hơn, bạn cũng có thể sử dụng Python.

Ví dụ:chúng tôi có thể đã chuyển sang Python để thực hiện quét tham số, nhưng cuối cùng chúng tôi sẽ chỉ viết một trình bao bọc xung 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 có dòng lệnh nào trong ví dụ này, nên sử dụng Bash trực tiếp sẽ thích hợp hơn.

Đã đến lúc cắm sừng vô liêm sỉ

Bài viết này dựa trên một bài nói chuyện mà tôi đã nói tại OSCON, nơi tôi đã trình bày về hội thảo viết mã trực tiếp You Don't Know Bash . Không có trang trình bày, không có trình nhấp chuột — chỉ tôi và khán giả đang gõ dòng lệnh, khám phá thế giới kỳ diệu của Bash.