4 điểm bởi GN⁺ 2023-08-29 | 1 bình luận | Chia sẻ qua WhatsApp
  • GNU awk tự động duyệt qua stdin và các tệp, xử lý tác vụ văn bản CLI bằng cách kết hợp lọc, thay thế và xử lý trường trong các lệnh một dòng
  • /regexp/!/regexp/ là cú pháp rút gọn để kiểm tra dòng đầu vào hiện tại; nếu điều kiện đúng thì mặc định $0 được in ra
  • sub chỉ thay thế lần khớp đầu tiên, còn gsub thay thế tất cả các lần khớp; nếu bỏ qua đối tượng đích thì phép thay thế được áp dụng lên dòng đầu vào hiện tại $0
  • Nhờ việc tách trường theo khoảng trắng cùng $N, NF, $NF, awk rất phù hợp với tác vụ dựa trên trường như chọn ra một cột cụ thể hoặc xử lý theo điều kiện
  • Bằng cách nối các đoạn cond{action} và thêm BEGIN{}·END{}, bạn có thể tạo các chương trình nhỏ bao gồm cả chuỗi, số và mảng kết hợp, vượt ra ngoài các bộ lọc đơn giản

Cách awk thực thi

  • awk có thể lọc văn bản giống như grep, sed, đồng thời cung cấp cả các tính năng lập trình cho những xử lý phức tạp hơn
  • Đầu vào có thể lấy từ stdin hoặc tệp; mặc định, nó duyệt theo từng dòng và áp dụng điều kiện cùng hành động
  • Toàn bộ dòng đầu vào được truy cập bằng biến đặc biệt $0
    • Thuật ngữ chính xác là record đầu vào, nhưng trong chương này sẽ giải thích theo đơn vị dòng

Lọc và đầu ra mặc định

  • Biểu thức chính quy được viết ở dạng /.../; khi kiểm tra một chuỗi có khớp với biểu thức chính quy hay không, dùng cú pháp string ~ /regexp/
  • Điều kiện phủ định được biểu diễn bằng string !~ /regexp/
  • Nếu bỏ qua chuỗi cần kiểm tra, $0 sẽ là đối tượng đích
  • Nếu chỉ có điều kiện mà không có hành động, khi điều kiện đúng thì $0 tự động được in ra
    • awk '/regexp/' là dạng rút gọn của awk '$0 ~ /regexp/{print $0}'
    • awk '!/regexp/' là dạng rút gọn của awk '$0 !~ /regexp/{print $0}'
  • Các ví dụ minh họa luồng lọc cơ bản bằng cách chỉ in những dòng chứa at, hoặc chỉ in những dòng không chứa e

Cách in theo thành ngữ với 1

  • Trong biểu thức điều kiện, số khác 0chuỗi không rỗng được đánh giá là đúng
  • awk '1' là cách viết thành ngữ để đưa ra một điều kiện luôn đúng cho mọi dòng đầu vào
  • Khi không có hành động, điều kiện đúng sẽ dẫn đến việc in $0, nên awk '1' in nguyên toàn bộ đầu vào
  • Kết quả giống với awk '{print $0}' hoặc cat đơn giản

Thay thế: subgsub

  • awk cung cấp các hàm subgsub để tìm kiếm và thay thế
  • sub(/:/, "-") chỉ đổi dấu : đầu tiên trong mỗi dòng đầu vào thành -
  • gsub(/:/, "-") đổi tất cả dấu : trong mỗi dòng đầu vào thành -
  • Hai hàm này nhận biểu thức chính quy cần khớp làm đối số thứ nhất, và chuỗi thay thế làm đối số thứ hai
  • Nếu không cung cấp riêng chuỗi đầu vào, đối tượng đích mặc định là $0
  • Khi thay thế thành công, đầu vào đích sẽ được sửa đổi
  • Số 1 sau khối thay thế được diễn giải là biểu thức điều kiện bên ngoài khối, khiến $0 đã sửa đổi được in lại
    • awk '{sub(/:/, "-")} 1' cho kết quả giống awk '{sub(/:/, "-"); print $0}'
    • Ngay cả khi chỉ viết print, đối tượng in mặc định cũng là $0

Tiêu chí chọn grep, sed, awk

  • Với lọc dòng đơn giản, grep vốn được tối ưu cho mục đích đó có thể nhanh hơn sed hoặc awk
  • Với tác vụ thay thế, sed có thể nhanh hơn awk
  • Chức năng giữa các công cụ không phải lúc nào cũng tương ứng 1:1
    • Để triển khai grep -o bằng sed hoặc awk, cần nhiều bước hơn
    • Chỉ grep cung cấp tìm kiếm đệ quy
  • Có thể xem thảo luận liên quan tại unix.stackexchange: When to use grep, sed, awk, perl, etc

Xử lý dựa trên trường

  • awk đặc biệt thường được dùng cho xử lý dựa trên trường
  • Mặc định, nó chia dòng đầu vào theo khoảng trắng, và truy cập từng trường bằng $N
    • $2 là trường thứ hai
    • $NF là trường cuối cùng
    • NF là tổng số trường của dòng đầu vào hiện tại
  • Ví dụ table.txt minh họa ngắn gọn cách xử lý theo cột
    • In trường thứ hai của từng dòng
    • Chỉ in các dòng có trường cuối cùng là số âm
    • Chỉ đổi b thành B trong trường đầu tiên
  • Tệp ví dụ nằm trong thư mục example_files

Cấu trúc lệnh một dòng

  • Một lệnh awk một dòng thông thường có dạng sau
awk 'cond1{action1} cond2{action2} ... condN{actionN}'
  • Nếu không có điều kiện, hành động luôn được thực thi
  • Nếu không có hành động, $0 được in ra khi điều kiện đúng
  • Bên trong khối, có thể phân tách nhiều câu lệnh bằng dấu chấm phẩy ;
  • Khi nối nhiều khối, trong hầu hết lệnh một dòng, bạn có thể biểu diễn hành động theo từng điều kiện mà không cần if tường minh
    • awk '$NF<0' table.txt chỉ in các dòng có trường cuối cùng là số âm
  • BEGIN{} được thực thi trước khi đọc đầu vào
  • END{} được thực thi sau khi xử lý xong toàn bộ đầu vào
  • Chi tiết về toán tử và biểu thức điều kiện có trong gawk manual: Operators, gawk manual: Truth Values and Conditions

Chuỗi và số

  • awk xác định kiểu của giá trị theo ngữ cảnh để có thể tạo các lời giải ngắn trên CLI
  • Literal chuỗi được viết trong dấu ngoặc kép
  • Số có thể dùng số nguyên, số dấu phẩy động và ký pháp khoa học
  • BEGIN{} cũng có thể được dùng khi chạy chương trình awk mà không có đầu vào bên ngoài
  • Biến có thể lưu số và chuỗi
    • Ví dụ: a=5; b=2.5; print a+b
    • Đặt các chuỗi cạnh nhau sẽ nối chúng lại
  • Biến chưa khởi tạo hoạt động như chuỗi rỗng trong ngữ cảnh chuỗi, và như 0 trong ngữ cảnh số
  • Khi dùng chuỗi trong biểu thức số, nó sẽ bị ép kiểu sang số
    • Nếu chuỗi không bắt đầu bằng một số hợp lệ, nó được xử lý là 0
    • Khoảng trắng ở đầu bị bỏ qua
  • Khi nối chuỗi vào số, số được chuyển thành chuỗi
  • Chi tiết có trong gawk manual: Constant Expressionsgawk manual: How awk Converts Between Strings and Numbers

Mảng

  • Mảng trong awkmảng kết hợp, hoạt động theo cặp khóa-giá trị
  • Khóa có thể là số hoặc chuỗi, nhưng khóa số được chuyển nội bộ thành chuỗi
  • Cũng có thể dùng mảng đa chiều
  • Trong ví dụ, giá trị được lưu và truy cập bằng khóa chuỗi như student["id"], student["name"]
  • Kiểm tra sự tồn tại của khóa bằng dạng "id" in student
  • Chi tiết có trong gawk manual: Arrays

Luyện tập và bước tiếp theo

  • Chương này giới thiệu ngắn gọn các cách viết rút gọn cơ bản trong cú pháp awk, lọc, thay thế, xử lý trường, chuyển đổi kiểu và mảng
  • Chương tiếp theo sẽ đề cập đến biểu thức chính quy, và các tính năng được giới thiệu trong chương này sẽ tiếp tục được dùng trong ví dụ
  • Bạn có thể làm quen với cú pháp bằng cách thay đổi số thứ tự trường, hoặc thử nghiệm các đầu vào khác như số âm và số thứ tự trường dạng dấu phẩy động
  • Ứng dụng TUI để luyện tập tương tác có thể được cài đặt từ kho AwkExercises, và cách sử dụng có trong app_guide.md
  • Tất cả bài tập được tập hợp trong Exercises.md, còn lời giải nằm trong Exercise_solutions.md

1 bình luận

 
GN⁺ 2023-08-29
Ý kiến trên Hacker News
  • Tôi thích awk nên dùng khá thường xuyên; một trong những cách dùng chính là như sed có trạng thái
    Ví dụ, chỉ khớp một dòng khi nó xuất hiện sau một dòng cụ thể trước đó, nên rất hữu ích khi tạo các linter tạm thời
    Gần đây tôi đã tạo một kiểm tra để tìm các file migration có CREATE INDEX có thể gây vấn đề trên các bảng rất lớn nhưng lại không dùng CONCURRENTLY; câu lệnh SQL có thể trải trên nhiều dòng nên việc khớp đơn giản khá khó
    awk có thể theo dõi trạng thái qua nhiều dòng, như “đang ở trong câu lệnh create”, “đang tạo index”, nên một script ghép vội đã chạy ổn khoảng 1 năm nay

    • Một ngày nào đó tôi sẽ phải học awk, nhưng trong lúc đó tôi đã học các tính năng dựa trên trạng thái sâu hơn của sed
      Nếu muốn chỉ in dòng hiện tại khi dòng trước đó khớp một mẫu nhất định, có thể làm như sed -ne 'x' -e '/PREV/ {x; /CURR/ p; x}'
      Ví dụ: echo -e "PREV\nCURR\nCURR\nCURR\nPREV\nRED" | sed -ne 'x' -e '/PREV/ {x; /CURR/ p; x}' chỉ in CURR
      Đây là cách dùng hold buffer của sed; dùng -n để chặn xuất mặc định rồi dùng p để chỉ in các dòng cần thiết
      x hoán đổi dòng hiện tại với hold buffer, rồi trong khối /PREV/ { ... } hoán đổi lại, sau đó chỉ in khi dòng hiện tại chứa CURR
      Lệnh x cuối cùng dùng để đưa mọi thứ trở lại như cũ trong trường hợp các lần khớp chồng lấn
      Tất nhiên nhiều khả năng script awk sẽ đơn giản và trực tiếp hơn nhiều, nhưng sed cũng có thể làm được như vậy
      Có lẽ thời gian học cái này đáng ra nên dành cho awk hơn, nhưng tutorial https://www.grymoire.com/Unix/Sed.html hay đến mức gần như mọi thứ tôi biết về sed đều học từ đó
    • Sẽ rất hay nếu bạn có thể chia sẻ ví dụ theo dõi trạng thái SQL bằng awk
  • Nếu đang đọc thread này thì có lẽ bạn cũng sẽ quan tâm đến “Ask HN: Share a shell script you like” từ 2 tuần trước
    Với 78 bình luận, nó không sôi nổi như kỳ vọng, nhưng vẫn đáng tham khảo: https://news.ycombinator.com/item?id=37112991

  • Tôi vẫn giữ chút hứng thú với Awk, cùng với Lisp và vài thứ khác, và năm 2022 tôi đã tạo cppawk: https://www.kylheku.com/cgit/cppawk/about/
    cppawk mở rộng Awk với khả năng tiền xử lý
    Nó có các macro vòng lặp hỗ trợ nhiều mệnh đề; có thể kết hợp các mệnh đề để tạo lặp song song hoặc lặp tích Descartes, và người dùng cũng có thể mở rộng
    Chỉ cần viết 5 macro đơn giản là có thể định nghĩa một mệnh đề mới
    Nếu bạn dùng Awk thì nó có thể hữu ích; nó được tài liệu hóa bằng nhiều man page và có unit test chạy trên cả gawk lẫn mawk

  • Có thể đây là cảm giác của một quản trị viên hệ thống kiểu cũ, nhưng tôi không rõ awk có lợi thế gì so với việc viết cùng việc đó bằng Perl
    Tôi đã thấy rất nhiều shell script kinh khủng do các quản trị viên hệ thống junior viết, và lần nào cũng nghĩ “phần xử lý văn bản mà dùng Perl thì đã gọn gàng hơn nhiều”

    • Nếu môi trường của bạn luôn dùng Perl thì nhận xét đó khá hợp lý
      Với tôi, awk là một trong số ít ngôn ngữ mà dù bỏ vài tháng, chỉ cần 10 phút là lấy lại cảm giác
      Nó có phần trực quan và phối hợp tự nhiên với các công cụ dòng lệnh phổ biến
    • Nếu so sánh Awk và Perl cho mục đích viết script, tôi thích Perl hoặc Python hơn
      Tuy nhiên bài này nói về các lệnh một dòng ngắn dùng tạm thời, và trong trường hợp đó sed/awk tốt hơn Perl
      Nếu đã biết Perl rồi thì cứ tiếp tục dùng Perl, không cần học thêm công cụ khác
    • Điểm mạnh của Awk là vòng lặp đọc có sẵn miễn phí, tách trường, và mô hình khớp mẫu/điều kiện
      Bản thân ngôn ngữ thì “tạm ổn”, nhưng đủ dùng
      Dĩ nhiên Perl cũng làm được tất cả, nhưng bạn phải tự viết phần boilerplate mà awk cung cấp sẵn
      Với tôi, ưu điểm của ngôn ngữ Perl không lớn đến mức bỏ awk; và vì tôi dùng awk chủ yếu cho xử lý dữ liệu và chỉnh sửa file một lần hơn là “scripting”, tôi không thấy nhớ độ sâu của Perl
      Nếu cần tính năng sâu hơn, tôi sẽ dùng thứ khác
    • Về mặt kỹ thuật thì không có lợi thế gì đặc biệt; các tính năng của Perl gần như cố ý là siêu tập của awk
      Có vẻ thế hệ ngày nay không học Perl, nên họ đang khám phá lại ý tưởng xử lý stream thông qua awk
      awk là một ý tưởng tuyệt vời theo chuẩn cuối thập niên 1970 và là một đổi mới thực sự đáng để bắt chước
      Sau đó Perl đã bắt chước và vượt qua nó nhưng rồi bị lãng quên, nên việc awk được khám phá lại trông hơi ngượng
    • Tôi không mỉa mai đâu, chỉ tò mò vì sao Perl lại tốt hơn Python cho scripting
  • Một điểm ít được biết đến trong gawk là nó thường đi kèm các tiện ích mở rộng hữu ích
    Bạn có thể truy cập các hàm như readdir(), ord(), chr(), gettimeofday(), sleep()
    https://www.gnu.org/software/gawk/manual/html_node/Extension-Samples.html

  • Lệnh một dòng của awk thực sự rất mạnh
    Câu hỏi khó là liệu có đáng đầu tư vào lập trình awk phức tạp hơn hay không
    Khi tác vụ xử lý đòi hỏi logic phức tạp, awk đúng là cung cấp được, nhưng theo kiểu ngắn gọn và khó hiểu từ thời kỳ đầu của điện toán
    Nếu chọn một phương án hiện đại thay thế thì cũng có gánh nặng riêng, đặc biệt có thể không trực quan như pandas, và còn có thể phát sinh vấn đề hiệu năng

    • Vấn đề lớn với tôi là thư viện
      Ngay cả file hàm dùng chung cá nhân cũng không có cảm giác được hỗ trợ tốt, và dường như cũng không có cách phù hợp để đưa thư viện bên thứ ba vào
      Khi bắt đầu cần hàm trợ giúp và phải tách thành nhiều dòng, tôi thường chuyển sang Python
      Rồi chương trình phình to gấp 2–3 lần khiến tôi thở dài
      Ruby là một lựa chọn thay thế awk tuyệt vời, nhưng nếu đồng nghiệp ở công ty không biết Ruby thì khó kỳ vọng họ bảo trì được
    • Trước ChatGPT, tôi không thích các lệnh một dòng awk/sed khó hiểu
      Dạo này, chỉ cần dán những lệnh khó đọc đó vào AI là nó giải thích từng bước khá tốt, nên cũng ổn hơn
      Dù vậy, vì khả năng viết unit test, tôi vẫn thích thêm vài dòng Python hơn, nhưng với các tác vụ xử lý dữ liệu nhanh thì ngay từ đầu cũng có những trường hợp không cần unit test
  • Đã phát hành phiên bản mới của ebook “CLI text processing with GNU awk
    Có thể học lệnh GNU awk từng bước từ cơ bản đến nâng cao qua hàng trăm ví dụ và bài tập, đồng thời đi sâu vào xử lý trường, lọc, xử lý nhiều file, các giải pháp phụ thuộc vào nhiều bản ghi, so sánh bản ghi/trường giữa các file, tìm trùng lặp trong khi vẫn giữ thứ tự đầu vào, biểu thức chính quy, v.v.
    PDF/EPUB: https://learnbyexample.gumroad.com/l/gnu_awk (miễn phí đến ngày 31/8/2023)
    Phiên bản web: https://learnbyexample.github.io/learn_gnuawk/
    Mã nguồn Markdown và các file ví dụ: https://github.com/learnbyexample/learn_gnuawk
    Ứng dụng TUI tương tác để luyện tập: https://github.com/learnbyexample/TUI-apps/blob/main/AwkExercises
    Các gói bundle gồm bộ lệnh một dòng grep, sed, awk, perl, ruby Magical one-liners giá $5: https://learnbyexample.gumroad.com/l/oneliners/new_awk_release, gói toàn bộ 13 ebook giá $12: https://learnbyexample.gumroad.com/l/all-books/new_awk_release
    Mọi phản hồi, từ lỗi chính tả, lỗi code, đến phần nào phù hợp hay chưa phù hợp, đều sẽ rất hữu ích
    Các thảo luận trước đây gồm https://news.ycombinator.com/item?id=15549318https://news.ycombinator.com/item?id=22758217

    • Tôi tò mò không biết có bao nhiêu người thực sự trả tiền cho các tài liệu như Magical one-liners, và liệu đã từng thử nghiệm mô hình “xem xong rồi tùy chọn thanh toán” chưa
      Nó có thể hữu ích đến mức đáng trả $5 hoặc $15, nhưng cũng có thể chẳng khác gì những gì tôi đã lưu sẵn trong file lệnh một dòng cá nhân, nên tôi ngần ngại trả tiền chỉ để kiểm chứng
    • Có thể thấy rõ là đã bỏ rất nhiều công sức, và tôi đặc biệt thích ứng dụng TUI
      Tôi tò mò liệu phiên bản web có tiếp tục được giữ miễn phí sau ngày 31/8 hay không
  • Trước đây tôi từng thử chơi golf vẽ trái tim bằng AWK: https://gist.github.com/auselen/906a53b47a7d616b080dbef85eb8f776

  • 99,9% việc dùng awk của tôi là để tách dòng trong khi bỏ qua các khoảng trắng liên tiếp
    Ví dụ: echo "key: value" | awk '{print $1}'
    Ước gì có một lựa chọn thay thế đơn giản hơn

    • Có thể cân nhắc https://github.com/c-blake/bu/blob/main/doc/cols.md
      Nó viết bằng Nim, nhưng có thể không phải rào cản lớn, và các công cụ khác trong bu/ cũng có thể đáng quan tâm
    • Cũng có thể làm bằng cut: echo "key: value" | cut -wf 2
      Tuy nhiên, liệu nó có thực sự đơn giản hơn hay không thì còn gây tranh luận
      Kiểm tra lại thì GNU cut không có -w, nên đây là tùy chọn chỉ có trên BSD
      Rốt cuộc có lẽ cứ dùng awk là tốt hơn
    • https://github.com/sstadick/hckhttps://github.com/theryangeary/choose cũng đáng xem thử
      Cả hai đều là lựa chọn thay thế cho cut/awk và cũng hỗ trợ tách dựa trên biểu thức chính quy, nhưng theo tôi nhớ thì không loại bỏ khoảng trắng ở đầu/cuối
      Công cụ tôi tự làm https://github.com/learnbyexample/regexp-cut là một công cụ giống cut dùng awk, cung cấp tách dựa trên biểu thức chính quy, chỉ số âm, v.v.; nhờ hành vi mặc định của awk nên cũng xử lý được khoảng trắng ở đầu/cuối
    • Có lẽ ý định là nói echo "key: value" | awk '{print $2}'
  • Trước đây tôi từng viết script diff2html được port từ bash sang awk, và vì lý do hiển nhiên nên nó nhanh hơn rất nhiều
    So với script bash, bản awk dễ đọc hơn hẳn; chỉ trong một đêm tôi đã có thể học ngôn ngữ này, debug, hiểu lỗi và sửa được chúng
    Không biết đó có phải là cách viết awk theo đúng idiom hay không, nhưng tôi cảm thấy đây thật sự là một ngôn ngữ khá ổn
    https://github.com/berry-thawson/diff2html/blob/master/diff2html.sh