[AWK] Phần 2 - Một vài AWK one-liner

Trong phần 1, chúng ta đã tìm hiểu cơ bản về AWK. Tôi đã đưa ra câu lệnh awk '!a[$0]++' file và đặt bốn câu hỏi:

  • Câu lệnh trên làm gì?
  • Nó thực sự hoạt động ra sao?
  • Đó có phải là cách tốt và hiệu năng nhất không?
  • Nếu không thì có cách nào tốt hơn và tại sao?

Phần này chúng ta sẽ cùng trả lời các câu hỏi trên và xem xét thêm một số ứng dụng của AWK trong việc xử lý text file.

I. Xóa dòng trùng lặp trong file

awk '!a[$0]++' file  

Ví dụ:

$ printf '1\n2\n1\n2\n4\n3\n' > test.txt
$ awk '!a[$0]++' <test.txt
1  
2  
4  
3  
  • Câu lệnh trên làm gì: đúng như tiêu đề, câu lệnh này dùng để xóa các dòng trùng lặp trong file

  • Nó thực sự hoạt động ra sao: Số lần xuất hiện của mỗi dòng sẽ được đếm dựa vào một associative array a, với key là nội dung của dòng, giá trị sẽ tăng dần qua mỗi lần xuất hiện. Lần đầu tiên xuất hiện, a[$0] chưa tồn tại, a[$0]++ tăng giá trị a[$0] thêm 1 và trả về 0, lúc này ! của 0 là 1, là một giá trị boolean true, action mặc định print $0 thực thi. Các lần sau, a[$0] có giá trị lớn hơn hoặc bằng 1, phủ định của nó luôn trả về 0, là một giá trị boolean false, awk bỏ qua dòng đó.

  • Đó có phải là cách tốt và hiệu năng nhất không: Câu trả lời là không. Với mỗi 1 dòng, awk sẽ phải làm các việc sau:

    • Tạo một giá trị mới trong associative array a, với key là nội dung và giá trị undef nếu key chưa tồn tại
    • Tăng giá trị của key đó thêm 1
    • Phủ định giá trị đó, kiểm tra giá trị boolean trả về

Như vậy với cách nay, lúc nào trong bộ nhớ của chương trình cũng phải lưu giá trị hash của dòng (để lưu key của associative array a) và số lần xuất hiện của dòng.

  • Nếu không thì có cách nào tốt hơn và tại sao:
awk '!($0 in a) {a[$0];print}' file  

Với cách này, awk chỉ kiểm tra dòng hiện tại có tồn tại trong các key của associative array a hay không, nếu có bỏ qua, nếu không sẽ lưu và in dòng hiện tại ra màn hình. Cách này cũng sẽ dùng ít bộ nhớ hơn, khi mọi thời điểm chỉ cần lưu giá trị hash của dòng.

II. Xóa các khoảng trắng ở đầu và cuối dòng:

awk '{$1=$1};1' file  

Ví dụ:

$ printf '   1 2   \n' > test.txt
$ od -t a <test.txt
0000000  sp  sp  sp   1  sp   2  sp  sp  sp  nl  
0000012  
$ awk '{$1=$1};1' <test.txt | od -t a
0000000   1  sp   2  nl  
0000004  

Khi một field có thay đổi, awk sẽ tạo lại $0 bằng cách nối các field bằng giá trị hiện tại của OFS, mặc định là một khoảng trắng.

Bằng cách sử dụng od, chúng ta có thể thấy ouput đã không còn các khoảng trắng ở đầu và cuối dòng.

III. Thêm dòng trống với mỗi dòng input:

awk 'BEGIN{ORS="\n\n"};1' file  

Ví dụ:

$ printf '1\n2\n' > test.txt
$ awk 'BEGIN{ORS="\n\n"};1' <test.txt
1  
<blank line>  
2  
<blank line>  

ORS, mặc định là một blank line, là biến awk dùng để phân cách các dòng output. Bằng cách set giá trị nhiều hơn một blank line, chúng ta có thể thêm bao nhiêu blank line vào tùy ý.

Chú ý là ORS được set trong BEGIN block, để tránh phải gán nhiều lần như trong trường hợp sau:

awk 'ORS="\n\n"' file  

hoặc một cách khác tương đương với việc dùng BEGIN block:

awk -vORS='\n\n' 1 file  

(Một lưu ý là không sử dụng awk -v var="$shell_var" khi $shell_var có chứa các escape sequences, awk sẽ expand chúng. Để truyền biến từ shell vào awk một cách tin cậy, an toàn, truyền qua ARGV hoặc ENVIRON. Thử echo | awk '{print a,ARGV[1]}' a='\t' | od -t a để thấy sự khác biệt)

IV. Indent source code:

Bạn có thể sử dụng AWK để format source code, text file đẹp hơn. Giả sử có file có nội dung như sau:

describe "a" do  
describe "b" do  
stuff  
more stuff  
end  
end  

Chúng ta sẽ thêm indent cho file trên với 2 spaces, với luật là với mỗi dòng bắt đầu là describe thì tăng indent thêm 2 spaces, với mỗi dòng bắt đầu là end thì giảm đi 2. Output mong muốn:

describe "a" do  
  describe "b" do
    stuff
    more stuff
  end
end  

Rất dễ dàng với awk:

awk '  
  /^end/ { indent = substr(indent, 3) }
  { print indent, $0 }
  /^describe/ { indent = indent"  " }
' <file  

(Bạn có thể xem ví dụ tại đây và câu trả lời của tôi)

V. Xóa các dòng chẵn trong file:

awk 'FNR%2' file  

Ví dụ:

$ seq 10 | awk 'FNR%2'
1  
3  
5  
7  
9  

Đây là một trong những ví dụ hay về sự đơn giản và gọn gàng của AWK. FNR là giá trị số thứ tự của dòng trong file hiện tại. Với các dòng lẻ FNR%2 bằng 1, là giá trị true nên awk sẽ in các dòng lẻ, bằng 0 với các dòng chẵn, awk bỏ qua.

Chú ý việc sử dụng FNR (thay vì NR) giúp bạn làm việc với nhiều file:

awk 'FNR == 1 {print FILENAME}; FNR%2' file_1 file_2 ... file_n  

Lời kết

AWK là một ngôn ngữ rất thú vị và rất "fun" để học. Là một System Admin hay một Developer, tôi đều cải thiện được rất nhiều hiệu suất làm việc của mình nhờ AWK.

Hi vọng qua loạt bài viết này, mọi người sẽ cảm thấy hứng thú hơn trong việc tìm hiểu về AWK.

Mọi thắc mắc về kỹ thuật, thảo luận sâu hơn, các bạn có thể để lại comment hoặc email đến địa chỉ cuonglm@vccloud.vn.