How Linux Works Chương 11 · Introduction to Shell Scripts

Chương 11 · Nhập môn kịch bản shell Chapter 11 · Introduction to Shell Scripts

Kịch bản shell chỉ là những lệnh bạn vẫn gõ — đóng gói lại trong một tệp Viết sẵn các lệnh vào một tệp, rồi bảo máy chạy tệp đó A shell script is just the commands you already type — packed into a file

Brian Ward mở chương bằng một câu gần như là lời mời: nếu bạn nhập được lệnh vào shell thì bạn viết được kịch bản shell. Một shell script (còn gọi là Bourne shell script) là một chuỗi lệnh viết trong một tệp; shell đọc các lệnh từ tệp đó y như khi bạn gõ chúng vào terminal.1 Không có phép màu nào ở đây — chỉ có một tệp văn bản và một quy ước.

Mỗi ngày bạn gõ vài câu lệnh vào máy. Nếu ngày nào cũng gõ đúng mấy câu đó, sao không viết sẵn ra một tờ giấy rồi đưa cho máy đọc? Tờ giấy ấy chính là kịch bản shell. Máy đọc từng dòng, làm y như bạn đã gõ. Nhớ nhé: kịch bản shell là danh sách việc cần làm, viết sẵn cho máy đọc.

Brian Ward opens the chapter with something close to an invitation: if you can enter commands into the shell, you can write shell scripts. A shell script (also known as a Bourne shell script) is a series of commands written in a file; the shell reads the commands from the file just as it would if you typed them into a terminal.1 There is no magic here — only a text file and a convention.

Điểm tinh tế nằm ở chữ “y như”. Chạy một kịch bản có dòng shebang thì gần giống — nhưng không hoàn toàn giống — chạy một lệnh bằng shell của bạn: chạy một kịch bản tên myscript khiến nhân chạy /bin/sh myscript.1 Ở tầng lời gọi hệ thống, execve(2) quy định rằng đường dẫn được trao cho nó phải là một tệp thực thi nhị phân hoặc một kịch bản bắt đầu bằng dòng dạng #!interpreter [optional-arg].3 Nói cách khác, shebang không phải quy ước của shell — nó là quy ước của nhân.

Luận đề của chương: shell script mạnh ở đúng một việc — thao tác với tệp và lệnh một cách đơn giản; sức mạnh đó đến từ việc hiểu ba cơ chế nền (dấu nháy, biến đặc biệt, mã thoát), và sự khôn ngoan nằm ở chỗ biết khi nào nên dừng lại mà chuyển sang Python, Perl hay awk.1

Ý chính: kịch bản shell rất giỏi việc sắp xếp tệp và gọi lệnh. Nhưng nếu bài toán của bạn dính nhiều phép tính hay xử lý chữ phức tạp, hãy đổi sang công cụ khác. Nhớ nhé: dùng đúng đồ nghề cho đúng việc.

The chapter's thesis: shell scripts are strong at exactly one thing — manipulating simple files and commands; that strength rests on three underlying mechanisms (quoting, special variables, exit codes), and the wisdom lies in knowing when to stop and reach for Python, Perl or awk instead.1

Cụm khái niệm 01 Concept cluster 01

Từ tệp văn bản tới chương trình: shebang, giới hạn, và dấu nháy Tờ giấy lệnh, và dấu nháy để giữ nguyên chữ From text file to program: the shebang, the limits, and quoting

Ward dành phần đầu chương cho hai thứ nghe có vẻ tầm thường nhưng lại quyết định mọi thứ về sau: dòng đầu tiên của tệp, và dấu nháy. Ông gọi dấu nháy là “một trong những yếu tố khó hiểu nhất khi làm việc với shell và kịch bản”.1

Hai thứ nhỏ mà quan trọng nhất: dòng đầu tiên của tệp, và dấu nháy. Tác giả bảo dấu nháy là chỗ gây bối rối nhất khi mới học. Nhớ nhé: chuyện nhỏ mà hay sai thì phải học kỹ.

Ward gives the opening of the chapter to two things that sound trivial but decide everything afterwards: the first line of the file, and quotation marks. He calls quoting "one of the most confusing elements of working with the shell and scripts."1

Định nghĩa. Một literal là một chuỗi mà shell không được phân tích (hay tìm cách thay đổi) trước khi trao cho dòng lệnh. Bạn cần nó mỗi khi muốn đưa nguyên ký tự $, * hay dấu chấm phẩy tới cho lệnh, thay vì để shell diễn giải chúng.1

Nói đơn giản. Literal là “chữ nguyên si”. Bạn bảo máy: đây là chữ, không phải mệnh lệnh, đừng đổi gì cả. Nhớ nhé: muốn giữ nguyên chữ thì phải đóng khung nó lại.

Definition. A literal is a string the shell should not analyse (or try to change) before passing it to the command line. You need one whenever you want a bare $, * or semicolon to reach the command instead of being interpreted by the shell.1

$ echo $100   → 00
$ echo "$100" → 00
$ echo '$100' → $100

nháy đơn '…' → giữ nguyên
nháy kép "…" → biến vẫn nở
không nháy    → shell tự do sửa

$ echo $100   → 00
$ echo "$100" → 00
$ echo '$100' → $100

  1. shebang

    Shebang — hai ký tự quyết định ai thực thi tệp Dòng #! — ai sẽ đọc tờ giấy này The shebang — two characters that decide who runs the file

    Kịch bản Bourne shell thường bắt đầu bằng dòng #!/bin/sh, báo rằng chương trình /bin/sh sẽ thực thi các lệnh trong tệp. Ward nhắc một chi tiết dễ bỏ sót: không được có khoảng trắng ở đầu tệp kịch bản. Phần #! được gọi là shebang.1 Trừ shebang ở đầu tệp, ký tự # ở đầu dòng đánh dấu một chú thích — shell bỏ qua mọi thứ sau nó trên dòng ấy.1

    Dòng đầu tiên viết #!/bin/sh, nghĩa là: “chương trình /bin/sh hãy đọc và làm theo tờ giấy này”. Đừng để dấu cách trước nó. Những dòng khác bắt đầu bằng # chỉ là ghi chú cho người đọc, máy bỏ qua. Nhớ nhé: dòng đầu chỉ người thi hành, dấu thăng là lời nhắn riêng.

    Bourne shell scripts generally start with the line #!/bin/sh, which indicates that the /bin/sh program should execute the commands in the script file. Ward flags a detail that is easy to miss: make sure there is no whitespace at the beginning of the script file. The #! part is called a shebang.1 With the exception of the shebang at the top, a # at the beginning of a line marks a comment — the shell ignores everything after it on that line.1

    Bạn gặp nó ở đâuWhere you meet it Mỗi tệp .sh, mỗi init script, mỗi hook của trình soạn thảo bạn mở ra đều bắt đầu bằng đúng dòng này. Mọi tệp kịch bản bạn từng mở ra đều có dòng đầu như vậy. Every .sh file, every init script, every editor hook you open starts with exactly this line.

    Không bắt buộc phải là /bin/sh

    Ward nói rõ: shebang có thể trỏ tới bất cứ thứ gì trên hệ thống chấp nhận đầu vào dạng kịch bản, chẳng hạn #!/usr/bin/python.1 Bạn cũng sẽ gặp dạng #!/usr/bin/env python: env tìm lệnh cần chạy trong command path hiện tại, nên bạn không cần một vị trí chuẩn hoá cho tệp thực thi. Nhược điểm: tệp khớp đầu tiên trong path có thể không phải thứ bạn muốn.1

    Quyền: cả r lẫn x

    Như mọi chương trình trên hệ Unix, bạn phải bật bit thực thi cho tệp kịch bản — nhưng đồng thời phải bật bit đọc, vì shell cần đọc được nội dung tệp. Cách gọn nhất là chmod +rx script; nếu không muốn người khác đọc và chạy, dùng chế độ tuyệt đối 700.1 Đây là khác biệt thật giữa kịch bản và tệp nhị phân: nhị phân chỉ cần x.

  2. limitations

    Giới hạn — điều tác giả lặp lại suốt cả chương Biết điểm dừng của công cụ Limitations — the refrain of the whole chapter

    Bourne shell thao tác với lệnh và tệp khá dễ dàng, nhưng kịch bản shell chỉ là một công cụ trong lập trình Unix. Ward liệt kê rất cụ thể những việc bạn nên chuyển sang Python, Perl, awk hoặc thậm chí một ngôn ngữ biên dịch như C: tách nhỏ chuỗi, tính toán số học lặp đi lặp lại, truy cập cơ sở dữ liệu phức tạp, hoặc khi bạn cần hàm và cấu trúc điều khiển phức tạp. Ông nói thẳng rằng điều này quan trọng đến mức sẽ còn nhắc lại xuyên suốt chương.1

    Kịch bản shell giỏi việc sắp xếp tệp và gọi lệnh. Nhưng nếu bạn phải cắt ghép chữ nhiều, tính toán nhiều, hay cần chương trình gọn gàng có hàm có lớp, hãy dùng công cụ khác. Nhớ nhé: búa rất tốt, nhưng đừng dùng búa để vặn ốc.

    The Bourne shell manipulates commands and files with relative ease, but a shell script is only one tool for Unix programming. Ward is specific about what belongs elsewhere — Python, Perl, awk, or even a compiled language like C: picking apart strings, performing repeated arithmetic computations, accessing complex databases, or needing functions and complex control structures. He says outright that this matters so much you will see it throughout the chapter.1

    Lời khuyên cụ thểThe concrete advice Hãy để ý kích thước kịch bản. Giữ chúng ngắn: kịch bản Bourne shell không sinh ra để lớn, dù chắc chắn bạn sẽ gặp vài con quái vật.1 Giữ tệp kịch bản ngắn thôi — dài quá là dấu hiệu chọn sai công cụ. Be aware of your script sizes. Keep them short: Bourne shell scripts aren't meant to be big, though you will undoubtedly encounter some monstrosities.1
  3. substitution order

    Hai bước — vì sao lỗi literal lại tinh vi đến thế Máy sửa chữ trước, rồi mới chạy lệnh Two steps — why literal bugs are so subtle

    Ward rút gọn toàn bộ vấn đề thành hai bước: (1) trước khi chạy lệnh, shell tìm biến, glob và các phép thay thế khác rồi thực hiện chúng nếu có; (2) shell trao kết quả của các phép thay thế cho lệnh.1 Lệnh không bao giờ nhìn thấy thứ bạn đã gõ — nó chỉ thấy thứ còn lại sau bước một.

    Máy làm hai bước: trước hết nó sửa các chỗ đặc biệt trong câu bạn gõ, rồi mới đưa câu đã sửa cho chương trình. Chương trình không hề biết bạn gõ gì ban đầu. Nhớ nhé: shell sửa trước, lệnh chạy sau.

    Ward reduces the whole problem to two steps: (1) before running the command, the shell looks for variables, globs and other substitutions and performs them if they appear; (2) the shell passes the results of those substitutions to the command.1 The command never sees what you typed — only what survived step one.

    Ví dụ của tác giảThe author's example grep r.*t /etc/passwd chạy đúng phần lớn thời gian, rồi bỗng hỏng một cách bí ẩn. Nguyên nhân nằm ở thư mục hiện tại: nếu ở đó có r.input và r.output, shell nở r.*t thành hai tên tệp ấy và tạo ra grep r.input r.output /etc/passwd.1 Cùng một câu lệnh, chạy ở thư mục này thì đúng, thư mục kia lại sai — vì máy đã thay dấu * bằng tên các tệp có sẵn quanh đó. grep r.*t /etc/passwd works most of the time, then mysteriously fails. The answer is probably your current directory: if it contains r.input and r.output, the shell expands r.*t into those names and builds grep r.input r.output /etc/passwd.1

    Danh sách ký tự cần cảnh giác

    Chuẩn POSIX liệt kê hẳn các ký tự mà ứng dụng phải đặt trong nháy nếu muốn chúng đại diện cho chính mình: | & ; < > ( ) $ ` \ " ', khoảng trắng, tab và xuống dòng; thêm một nhóm có thể cần nháy tuỳ hoàn cảnh: * ? [ # ~ = %.2 Chìa khoá, như Ward nói, là nhận ra ký tự gây rắc rối trước, rồi mới chọn loại nháy để bảo vệ chúng.1

    Vì sao lỗi này khó bắt

    Vì nó phụ thuộc vào trạng thái bên ngoài kịch bản — thư mục làm việc hiện tại. Cùng một dòng mã, cùng một máy, kết quả khác nhau. Đó là dạng lỗi tệ nhất: không tái hiện được bằng cách đọc mã.

  4. single quotes

    Nháy đơn — lựa chọn mặc định vì nó tuyệt đối Nháy đơn — khoá chặt, không cho sửa gì Single quotes — the default choice because they are absolute

    Cách dễ nhất để tạo một literal và buộc shell để yên một chuỗi là bao toàn bộ chuỗi trong nháy đơn, như grep 'r.*t' /etc/passwd. Với shell, mọi ký tự giữa hai dấu nháy đơn, kể cả khoảng trắng, tạo thành một tham số duy nhất.1 Ward khuyên: khi cần literal, hãy luôn nghĩ đến nháy đơn trước, vì bạn được bảo đảm shell sẽ không thử bất kỳ phép thay thế nào.1

    Nháy đơn giống cái hộp khoá kín: bỏ chữ vào đó thì máy không được phép sửa gì cả, kể cả dấu cách bên trong. Cần giữ nguyên chữ ư? Dùng nháy đơn trước đã. Nhớ nhé: nháy đơn là khoá chặt nhất.

    The easiest way to create a literal and make the shell leave a string alone is to enclose the whole string in single quotes, as in grep 'r.*t' /etc/passwd. As far as the shell is concerned, all characters between two single quotes, including spaces, make up a single parameter.1 Ward's advice: when you need a literal, always turn to single quotes first, because you're guaranteed the shell won't try any substitutions.1

    Mặt trái của “một tham số duy nhất”The flip side of "a single parameter" grep 'r.*t /etc/passwd' không chạy như bạn nghĩ: nó yêu cầu grep tìm chuỗi r.*t /etc/passwd trong đầu vào chuẩn, vì grep chỉ nhận được đúng một tham số.1 Nếu bạn khoá cả tên tệp vào trong nháy, máy tưởng đó là chữ cần tìm chứ không phải tệp cần mở. grep 'r.*t /etc/passwd' does not do what you expect: it asks grep to search standard input for the string r.*t /etc/passwd, because grep received only one parameter.1

    Chuẩn nói gì

    POSIX phát biểu điều này như một quy tắc tuyệt đối: bao các ký tự trong nháy đơn sẽ giữ nguyên giá trị nghĩa đen của từng ký tự bên trong; bản thân dấu nháy đơn không thể xuất hiện bên trong một cặp nháy đơn.2 Đó chính là lý do có cả một thẻ riêng ở dưới cho chuyện đưa một dấu nháy đơn vào chuỗi.

    Đếm tham số

    Ward nhắc lại một hệ quả đáng ghi nhớ: khi bạn đặt một chuỗi trong nháy, shell coi toàn bộ phần bên trong là một tham số. Vì vậy a b c là ba tham số, còn a "b c" chỉ là hai.1

  5. double quotes

    Nháy kép — bảo vệ có chọn lọc Nháy kép — khoá hờ, biến vẫn lọt qua Double quotes — selective protection

    Nháy kép hoạt động giống nháy đơn, ngoại trừ việc shell vẫn nở mọi biến xuất hiện bên trong. Ward đề nghị bạn tự thấy khác biệt bằng cách chạy echo "There is no * in my path: $PATH" rồi đổi sang nháy đơn và chạy lại: với nháy kép, shell thay $PATH nhưng không thay *.1

    Nháy kép giống cái hộp có khe: chữ thường thì ở yên, nhưng những cái tên bắt đầu bằng $ vẫn chui ra và bị thay bằng giá trị. Nhớ nhé: nháy kép giữ chữ, nhưng thả biến.

    Double quotes work just like single quotes, except that the shell expands any variables that appear within them. Ward suggests you see the difference yourself by running echo "There is no * in my path: $PATH" and then rerunning it with single quotes: with double quotes the shell substitutes for $PATH but does not substitute for the *.1

    Khi nào chọn nháy képWhen to pick double quotes Khi bạn cần một phần của chuỗi được thay thế — điển hình là thông điệp có chứa biến. Nếu khối văn bản lớn, Ward gợi ý dùng here document thay vì nháy kép.1 Khi bạn muốn ghép giá trị của biến vào giữa một câu. When you need part of the string substituted — typically a message containing a variable. For large amounts of text, Ward suggests a here document instead.1

    Chính xác thì cái gì vẫn nở?

    Trong nháy kép, POSIX quy định các ký tự $ và ` vẫn giữ nghĩa đặc biệt: nở tham số, thay thế lệnh và nở số học vẫn diễn ra bình thường.2 Nói cách khác, “nháy kép bảo vệ khỏi globbing và tách từ”, chứ không bảo vệ khỏi expansion — một phân biệt mà người mới thường gộp làm một.

  6. literal single quote

    Dấu nháy đơn trong chuỗi — công thức '\'' Làm sao viết được chữ “don't” A literal single quote — the '\'' recipe

    Đây là chỗ Bourne shell trở nên khó chịu nhất. Một cách là đặt dấu gạch chéo ngược trước dấu nháy đơn: echo I don\'t like contractions inside shell scripts. Nhưng cặp gạch chéo–nháy ấy phải nằm ngoài mọi cặp nháy đơn; một chuỗi như 'don\'t sẽ gây lỗi cú pháp.1 Kỳ lạ thay, bạn lại có thể bọc dấu nháy đơn bên trong nháy kép, và kết quả in ra giống hệt.1

    Muốn in chữ có dấu nháy — như “don't” — thì hơi rắc rối. Cách dễ nhất: bọc cả câu trong nháy kép, vì dấu nháy đơn nằm trong nháy kép thì vô hại. Nhớ nhé: nháy nọ bọc nháy kia.

    This is where the Bourne shell is at its most awkward. One way is a backslash before the single quote: echo I don\'t like contractions inside shell scripts. But the backslash-and-quote must appear outside any pair of single quotes; a string such as 'don\'t is a syntax error.1 Oddly enough, you can enclose the single quote inside double quotes, and the output is identical.1

    Quy tắc tổng quát của tác giảThe author's general rule Nếu cần đặt nguyên một chuỗi vào nháy mà không có bất kỳ phép thay thế nào: (1) đổi mọi dấu ' thành '\''; (2) bọc toàn bộ chuỗi trong nháy đơn. Ví dụ của ông: echo 'this isn'\''t a forward slash: \'1 Có một công thức máy móc để khoá bất kỳ câu nào, dù câu đó chứa dấu nháy — cứ theo công thức là xong. If you need to quote an entire string with no substitutions: (1) change every ' to '\''; (2) enclose the whole string in single quotes. His example: echo 'this isn'\''t a forward slash: \'1

    Giải mã công thức

    Chuỗi '\'' không phải phép thuật: nó là đóng cặp nháy đơn đang mở, rồi một dấu nháy đơn đã được thoát bằng gạch chéo ngược, rồi mở lại một cặp nháy đơn mới. Vì POSIX không cho phép một dấu nháy đơn xuất hiện bên trong cặp nháy đơn,2 thoát ra ngoài rồi vào lại là con đường duy nhất.

    Gạch chéo ngược ngoài dấu nháy

    POSIX gọi đây là escape character: một dấu gạch chéo ngược không bị đặt trong nháy sẽ giữ nguyên giá trị nghĩa đen của ký tự ngay sau nó — ngoại lệ duy nhất là ký tự xuống dòng, khi đó shell hiểu là nối dòng.2 Đúng là cơ chế Ward dùng để cắt một dòng lệnh dài thành nhiều dòng dễ đọc.1

Cụm khái niệm 02 Concept cluster 02

Biến đặc biệt và mã thoát: kịch bản tự biết mình Tham số và chuyện thành — bại Special variables and exit codes: how a script knows itself

Ward đóng khung phần này rất rõ: để đưa kịch bản của bạn từ một danh sách lệnh đơn giản lên thành chương trình kịch bản linh hoạt hơn, bạn cần biết dùng các biến đặc biệt của Bourne shell. Chúng giống mọi biến shell khác, ngoại trừ việc bạn không thể thay đổi giá trị của một số biến trong đó.1

Muốn kịch bản “biết điều” hơn, nó phải biết người ta gọi nó với những gì. Shell có sẵn vài cái tên rất ngắn để cho bạn biết điều đó. Nhớ nhé: mấy ký hiệu lạ lạ ấy chỉ là những cái tên có sẵn.

Ward frames this section clearly: to take your scripts from being just a simple list of commands to becoming more flexible shell script programs, you need to know the special Bourne shell variables. They are like any other shell variable, except that you can't change the values of certain ones.1

Định nghĩa. Một mã thoát (exit code, còn gọi là error code hay exit value) là một giá trị số mà một chương trình Unix để lại cho tiến trình cha đã khởi động nó khi chương trình kết thúc. Mã thoát bằng 0 thường nghĩa là chương trình chạy không vấn đề gì; có lỗi thì thường thoát với số khác 0 — nhưng không phải luôn luôn.1

Nói đơn giản. Chạy xong, mỗi chương trình để lại một con số nói “tôi ổn” hay “tôi hỏng”. Số 0 nghĩa là ổn. Nhớ nhé: 0 là thành công — ngược với thói quen đời thường.

Definition. An exit code (also known as an error code or exit value) is a numeric value a Unix program leaves for the parent process that started it. An exit code of 0 typically means the program ran without a problem; on error it usually exits with a number other than 0 — but not always.1

$1 $2 …  tham số thứ n
$#       số tham số
$@       toàn bộ tham số
$0       tên kịch bản
$$       PID của shell
$?       mã thoát lệnh trước

$1 = món thứ nhất
$# = có mấy món
$@ = tất cả các món
$0 = tên tờ giấy
$? = lệnh vừa rồi ổn không

$1 $2 …  nth argument
$#       argument count
$@       all arguments
$0       script name
$$       shell's PID
$?       last command's exit code

  1. $1, $2 · shift

    Tham số vị trí — và cái băng chuyền shift Món thứ nhất, món thứ hai… và cách đẩy hàng Positional parameters — and the shift conveyor belt

    $1, $2 và mọi biến mang tên là số nguyên dương khác 0 chứa giá trị các tham số của kịch bản.1 Lệnh dựng sẵn shift dùng kèm với chúng để loại bỏ tham số đầu tiên ($1) và đẩy phần còn lại lên một bậc: $2 thành $1, $3 thành $2, và cứ thế.1

    Khi gọi kịch bản, bạn đưa thêm vài từ phía sau. Từ thứ nhất là $1, thứ hai là $2. Lệnh shift giống việc lấy món đầu ra khỏi hàng, các món sau nhích lên. Nhớ nhé: shift là đẩy hàng lên một bậc.

    $1, $2 and all variables named as positive nonzero integers contain the values of the script's parameters, or arguments.1 The built-in shift command works with them to remove the first argument ($1) and advance the rest, so $2 becomes $1, $3 becomes $2, and so on.1

    Ví dụ của tác giảThe author's example Kịch bản shiftex in tham số, gọi shift, in lại, gọi shift, in lại — và nhờ vậy in ra cả ba tham số chỉ bằng cách luôn luôn đọc $1.1 Chỉ cần đọc mãi món đầu hàng, rồi đẩy hàng — là duyệt hết mọi món. His shiftex script prints an argument, shifts, prints again, shifts, prints again — printing all three arguments by only ever reading $1.1

    Chuẩn POSIX nói gì

    POSIX gọi chúng là positional parameters: shell gán tên các đối số làm tham số vị trí đánh số từ 1 tới n, còn chính tên lệnh (hoặc tên kịch bản) là tham số vị trí số 0.2 Đây là hành vi POSIX sh chuẩn, không phải phần mở rộng của bash.

    Cặp bài trùng với $#

    shift hầu như luôn đi cùng $# trong một vòng while: bạn lặp chừng nào còn tham số, mỗi vòng xử lý $1 rồi đẩy hàng. Đây chính là khuôn mẫu phân tích tuỳ chọn dòng lệnh viết bằng shell thuần.

  2. $# · $@

    $# đếm, $@ chuyển tiếp Đếm số món, và chuyển cả nắm đi tiếp $# counts, $@ forwards

    Biến $# giữ số lượng tham số truyền vào kịch bản, và đặc biệt quan trọng khi bạn chạy shift trong vòng lặp để duyệt tham số: khi $# bằng 0, không còn tham số nào, nên $1 rỗng.1 Biến $@ đại diện cho toàn bộ tham số của kịch bản, rất hữu ích khi cần chuyển chúng cho một lệnh bên trong.1

    $# cho biết có bao nhiêu món; $@ là cả nắm món đó. Khi đếm về 0 thì hết hàng. Nhớ nhé: đếm bằng $#, chuyển cả nắm bằng $@.

    $# holds the number of arguments passed to a script, and matters most when you're running shift in a loop to pick through them: when $# is 0 no arguments remain, so $1 is empty.1 $@ represents all of a script's arguments and is very useful for passing them to a command inside the script.1

    Ví dụ của tác giảThe author's example Lệnh Ghostscript (gs) thường dài và rối. Ward viết một kịch bản gói sẵn các tuỳ chọn rasterize một tệp PostScript ở 150 dpi qua luồng ra chuẩn, và kết thúc dòng lệnh bằng $@ — để cửa vẫn mở cho việc truyền thêm tuỳ chọn khác vào gs.1 Viết sẵn một câu lệnh dài, rồi thêm $@ ở cuối để lúc nào cần vẫn nhét thêm được tuỳ chọn. Ghostscript (gs) commands are usually long and complicated. Ward writes a script that wraps the options for rasterizing a PostScript file at 150 dpi through standard output, ending the line with $@ — leaving the door open for passing other options to gs.1

    Một mẹo trình bày

    Ward chỉ ra rằng nếu một dòng trong kịch bản dài tới mức khó đọc và khó sửa trong trình soạn thảo, bạn có thể cắt nó bằng dấu gạch chéo ngược \ ở cuối dòng.1 Cơ chế bên dưới là quy tắc nối dòng của POSIX đã nói ở trên: gạch chéo ngược ngay trước ký tự xuống dòng khiến shell hiểu đây là một dòng tiếp diễn.2

    $@ hay "$@"?

    Ví dụ trong sách dùng $@ không nháy.1 Đáng biết thêm: theo POSIX, @ nở thành các tham số vị trí, ban đầu sinh ra một trường cho mỗi tham số; và khi phép nở nằm trong nháy kép thì các trường ban đầu được giữ riêng biệt.2 Đó là lý do "$@" giữ nguyên được tham số có khoảng trắng — một chi tiết ngoài phạm vi ví dụ của chương nhưng cùng một họ vấn đề với mục dấu nháy ở trên.

  3. $0 · standard error

    $0 và kỷ luật báo lỗi Kịch bản tự xưng tên khi báo lỗi $0 and the discipline of error reporting

    Biến $0 giữ tên của kịch bản và hữu ích để sinh thông điệp chẩn đoán.1 Ward nêu một nguyên tắc: mọi thông điệp lỗi chẩn đoán phải đi ra luồng lỗi chuẩn. Nếu 2>&1 chuyển lỗi chuẩn sang đầu ra chuẩn, thì để ghi vào lỗi chuẩn bạn đảo ngược quá trình với 1>&2, ví dụ echo $0: bad option $BADPARM 1>&2.1

    Khi báo lỗi, kịch bản nên nói rõ “tôi là ai” bằng $0, và gửi lời báo lỗi vào đúng kênh lỗi chứ không lẫn vào kết quả. Nhớ nhé: lỗi đi một đường, kết quả đi một nẻo.

    $0 holds the name of the script and is useful for generating diagnostic messages.1 Ward states a rule: all diagnostic error messages should go to standard error. Where 2>&1 redirects standard error to standard output, you reverse the process with 1>&2, as in echo $0: bad option $BADPARM 1>&2.1

    Vì sao đáng giữ kỷ luật nàyWhy the discipline pays Vì kịch bản của bạn rồi sẽ nằm giữa một pipeline. Lỗi lẫn vào đầu ra chuẩn sẽ trôi xuống lệnh kế tiếp và làm hỏng dữ liệu của nó. Vì kết quả của bạn thường được chuyền tiếp cho chương trình khác — lời báo lỗi trộn vào đó sẽ làm hỏng cả dây chuyền. Because your script will end up in the middle of a pipeline. Errors mixed into standard output flow into the next command and corrupt its data.

    $0 theo chuẩn

    POSIX định nghĩa tham số đặc biệt 0 là “nở thành tên của shell hoặc của kịch bản shell”.2 Lưu ý nó là tên được dùng để gọi, nên $0 có thể là ./pshow hay một đường dẫn tuyệt đối tuỳ cách bạn chạy — điều này lý giải vì sao basename (mục dưới) hay xuất hiện ngay cạnh $0 trong kịch bản thật.7

  4. $$ · $?

    $$ là danh tính, $? là phán quyết Số hiệu của mình, và điểm của lệnh vừa rồi $$ is identity, $? is the verdict

    Biến $$ giữ ID tiến trình của shell.1 Biến $? giữ mã thoát của lệnh cuối cùng mà shell đã thực thi; Ward nói rằng mã thoát là thứ tối quan trọng để làm chủ kịch bản shell.1 Hai biến này nhỏ nhưng là bản lề: $$ cho bạn một cái tên không đụng hàng, $? cho bạn toàn bộ logic điều kiện.

    $$ là số hiệu riêng của phiên shell đang chạy — dùng để đặt tên tệp tạm cho khỏi trùng. $? là điểm số của lệnh vừa rồi: 0 là đạt. Nhớ nhé: $$ là tên tôi, $? là kết quả vừa xong.

    $$ holds the process ID of the shell.1 $? holds the exit code of the last command the shell executed; Ward calls exit codes critical to mastering shell scripts.1 Two small variables that are nevertheless hinges: $$ gives you a name nobody else has, $? gives you all of conditional logic.

    Thử ngay ở dấu nhắcTry it at the prompt Ward minh hoạ trực tiếp: ls / > /dev/null rồi echo $? cho 0; ls /asdfasdf > /dev/null rồi echo $? cho 1 — lệnh thành công trả 0, lệnh thất bại trả 1.1 Chạy một lệnh đúng rồi gõ echo $? — bạn thấy số 0. Chạy một lệnh sai rồi gõ lại — bạn thấy số khác. Ward demonstrates it directly: ls / > /dev/null then echo $? gives 0; ls /asdfasdf > /dev/null then echo $? gives 1 — the successful command returned 0, the unsuccessful one 1.1

    Cái bẫy “dùng ngay hoặc mất”

    Nếu bạn định dùng mã thoát của một lệnh, bạn phải dùng hoặc lưu nó ngay sau khi chạy lệnh ấy, vì lệnh kế tiếp sẽ ghi đè mã trước đó. Ward đưa ví dụ sắc gọn: chạy echo $? hai lần liên tiếp thì lần thứ hai luôn in 0, bởi lệnh echo thứ nhất đã hoàn thành thành công.1

    Đúng chuẩn, không phải đặc sản bash

    POSIX định nghĩa ? nở thành mã thoát thập phân của pipeline gần nhất, và $ nở thành ID tiến trình thập phân của shell được gọi — trong một subshell, $ nở thành đúng giá trị của shell hiện hành.2 Chi tiết cuối rất đáng nhớ khi bạn dùng $$ để đặt tên tệp tạm bên trong ngoặc đơn.

  5. exit code · exit 1

    Dừng đúng cách: exit 1 và các số khác 0 Khi gặp sự cố thì dừng và báo số Stopping properly: exit 1 and other nonzero codes

    Khi kịch bản cần dừng vì một lỗi — chẳng hạn tên tệp sai — hãy dùng exit 1 để kết thúc và trả mã thoát 1 về cho bất kỳ tiến trình cha nào đã chạy kịch bản. Ward nói thêm rằng bạn có thể dùng những số khác 0 khác nhau nếu kịch bản có nhiều điều kiện thoát bất thường.1

    Gặp chuyện không ổn thì đừng chạy tiếp — hãy dừng lại và để lại một con số khác 0 để người gọi biết là có lỗi. Nhớ nhé: sai thì dừng, và nói rõ mình dừng vì sao.

    When your script needs to halt because of an error — a bad filename, say — use exit 1 to terminate and pass an exit code of 1 back to whatever parent process ran the script. Ward adds that you can use different nonzero numbers if your script has various abnormal exit conditions.1

    Hệ quả thực dụngThe practical consequence Kịch bản của bạn trở thành một “công dân” đúng nghĩa của shell: người khác có thể ghép nó vào &&, ||, hay một if mà không cần đọc mã nguồn. Nhờ vậy, người khác dùng kịch bản của bạn như dùng một lệnh có sẵn. Your script becomes a proper citizen of the shell: other people can put it into &&, || or an if without reading its source.

    Theo chuẩn

    POSIX mô tả exit là một special built-in: nó khiến shell thoát khỏi môi trường thực thi hiện tại với trạng thái thoát là số nguyên thập phân không dấu n được chỉ định.2

  6. nonzero ≠ error

    Khác 0 không luôn là lỗi — grep và diff Có chương trình trả số khác 0 mà vẫn ổn Nonzero isn't always an error — grep and diff

    Đây là ngoại lệ Ward dặn kỹ: một số chương trình như diff và grep dùng mã thoát khác 0 để biểu thị tình trạng bình thường. grep trả 0 nếu tìm thấy thứ khớp mẫu và 1 nếu không tìm thấy; với chúng, mã thoát 1 không phải lỗi — nên grep và diff dùng mã thoát 2 khi gặp trục trặc thật sự.1

    Với grep, “không tìm thấy” cũng là một câu trả lời hợp lệ chứ không phải hỏng hóc — nên nó trả số 1. Lỗi thật thì nó trả số 2. Nhớ nhé: đọc kỹ sách hướng dẫn trước khi kết luận là lỗi.

    This is the exception Ward warns about: some programs, like diff and grep, use nonzero exit codes to indicate normal conditions. grep returns 0 if it finds something matching a pattern and 1 if it doesn't; for them an exit code of 1 is not an error — so grep and diff use exit code 2 when they encounter an actual problem.1

    Cách tra cứuHow to check Nếu bạn nghĩ một chương trình có thể dùng mã thoát khác 0 để báo thành công, hãy đọc trang hướng dẫn của nó — mã thoát thường được giải thích ở mục EXIT VALUE hoặc DIAGNOSTICS.1 Mở man của chương trình, tìm mục nói về mã thoát. If you think a program might use a nonzero exit code to indicate success, read its manual page — exit codes are usually explained under EXIT VALUE or DIAGNOSTICS.1

    Đối chiếu với tài liệu công khai

    Trang grep(1) của GNU xác nhận: bình thường trạng thái thoát là 0 nếu có dòng được chọn, 1 nếu không dòng nào được chọn, và 2 nếu xảy ra lỗi — với lưu ý rằng khi dùng -q, trạng thái thoát vẫn là 0 nếu có dòng được chọn, ngay cả khi có lỗi xảy ra.5 Trang diff(1) nói ngắn gọn: 0 nếu các đầu vào giống nhau, 1 nếu khác nhau, 2 nếu có trục trặc.6

    Vì sao chi tiết -q lại quan trọng

    Vì grep -q chính là dạng Ward dùng trong ví dụ điều kiện và vòng lặp ở phần sau.1 Khi bạn lấy grep -q làm phép thử, bạn đang chấp nhận rằng “tìm thấy” được ưu tiên hơn “có lỗi” — một sự đánh đổi có chủ ý, được ghi rõ trong tài liệu.5

Cụm khái niệm 03 Concept cluster 03

Điều kiện, vòng lặp, và cách lấy kết quả của một lệnh làm nguyên liệu Rẽ nhánh, lặp lại, và mượn kết quả của lệnh khác Conditionals, loops, and using one command's output as material

Phát hiện quan trọng nhất của phần này không phải cú pháp mà là một sự thật về bản chất: [ là một chương trình thật trên hệ Unix. Ward cảnh báo rằng rất dễ nhầm lẫn điều kiện [ $1 = "hi" ] với cú pháp đặc biệt của shell — nó không phải vậy. Mọi hệ Unix đều có một lệnh tên [ thực hiện các phép thử cho điều kiện kịch bản shell; chương trình này còn được biết với tên test, và trang hướng dẫn của test với [ là một.1

Điều bất ngờ nhất: cái dấu ngoặc vuông [ trong câu điều kiện thật ra là tên một chương trình, không phải ký hiệu của shell. Nó còn có tên khác là test. Nhớ nhé: dấu ngoặc vuông cũng là một lệnh.

The important discovery in this section isn't syntax but a fact about the nature of the thing: [ is an actual program on a Unix system. Ward warns that it's easy to mistake the conditional [ $1 = "hi" ] for special shell syntax — it isn't. All Unix systems have a command called [ that performs tests for shell script conditionals; this program is also known as test, and the manual pages for test and [ are the same.1

Cơ chế của if, bốn bước theo Ward. (1) Shell chạy lệnh đứng sau từ khoá if và thu mã thoát của lệnh đó. (2) Nếu mã thoát là 0, shell thực thi các lệnh sau then, dừng khi gặp else hoặc fi. (3) Nếu mã thoát khác 0 và có mệnh đề else, shell chạy các lệnh sau else. (4) Điều kiện kết thúc ở fi.1

Nói đơn giản. if không so sánh gì cả — nó chỉ chạy một lệnh rồi xem lệnh đó “đạt” (0) hay “không đạt”. Đạt thì làm nhánh trên, không đạt thì làm nhánh dưới. Nhớ nhé: if chỉ hỏi một câu — lệnh vừa rồi có đạt không?

How if works, Ward's four steps. (1) The shell runs the command after the if keyword and collects its exit code. (2) If the exit code is 0, the shell executes the commands following then, stopping at else or fi. (3) If the exit code is not 0 and there's an else clause, the shell runs the commands after else. (4) The conditional ends at fi.1

if [ "$1" = hi ]; then
   echo yes
else
   echo no
fi

if · then · else · fi = từ khoá
[ = chương trình

if <chạy một lệnh>; then
   làm việc A
else
   làm việc B
fi

if [ "$1" = hi ]; then
   echo yes
else
   echo no
fi

if · then · else · fi = keywords
[ = a program

  1. [ · test

    [ là lệnh, dấu chấm phẩy là dấu kết thúc lệnh Ngoặc vuông là lệnh, chấm phẩy là dấu chấm câu [ is a command; the semicolon ends it

    Vì phép thử sau if là một lệnh, dấu chấm phẩy trong if [ $1 = hi ]; then chỉ là dấu hiệu kết thúc lệnh bình thường của shell — nó có mặt vì ta đặt then trên cùng dòng. Không có nó, shell truyền then làm tham số cho lệnh [, thường sinh ra một lỗi khó lần ra.1 Bạn có thể tránh dấu chấm phẩy bằng cách đặt then xuống dòng riêng.1

    Dấu ; chỉ có nghĩa là “hết câu lệnh”. Quên nó thì máy tưởng chữ then là một món hàng đưa cho lệnh ngoặc vuông, và báo lỗi rất khó hiểu. Nhớ nhé: hết một lệnh thì chấm phẩy, hoặc xuống dòng.

    Because the test after if is a command, the semicolon in if [ $1 = hi ]; then is just the regular shell marker for the end of a command — it's there because we put then on the same line. Without it, the shell passes then as a parameter to [, which often results in an error that isn't easy to track.1 You can avoid the semicolon by putting then on its own line.1

    Hệ quả về hiệu năngA performance consequence Vì test được dùng quá rộng rãi trong kịch bản, nó được tích hợp sẵn vào nhiều phiên bản Bourne shell (kể cả bash). Điều này làm kịch bản nhanh hơn, vì shell không phải chạy một lệnh riêng cho mỗi phép thử.1 Vì dùng nhiều quá, shell đã “học thuộc” lệnh này để khỏi phải đi gọi chương trình ngoài mỗi lần. Because test is so widely used in scripts, it's built in to many versions of the Bourne shell (including bash). This speeds scripts up, because the shell doesn't have to run a separate command for each test.1

    Chính tài liệu cũng nói vậy

    Trang test(1) của coreutils kết thúc bằng đúng cảnh báo này: shell của bạn có thể có phiên bản test và/hoặc [ của riêng nó, thường ghi đè phiên bản được mô tả trong trang đó — hãy tra tài liệu của shell để biết nó hỗ trợ những tuỳ chọn nào.4 Tài liệu bash xác nhận phía mình: test expr / [ expr ] trả về trạng thái 0 (đúng) hoặc 1 (sai), và mỗi toán tử, mỗi toán hạng phải là một đối số riêng biệt.8

    Vì sao “mỗi toán hạng là một đối số” lại then chốt

    Vì nó giải thích cả cái bẫy khoảng trắng lẫn cái bẫy tham số rỗng ở thẻ kế bên: [ không phân tích một biểu thức dạng chuỗi — nó đọc một danh sách đối số mà shell đã cắt sẵn. Mất một đối số là mất cả nghĩa.

  2. empty parameter

    Tham số rỗng — cái bẫy hay bị bỏ qua nhất Nếu người dùng quên đưa tham số thì sao? The empty parameter — the most overlooked trap

    Ward chỉ ra một vấn đề tiềm ẩn ngay trong ví dụ đầu tiên của chính ông: $1 có thể rỗng, vì người dùng có thể chạy kịch bản mà không đưa tham số nào. Khi đó phép thử đọc thành [ = hi ], và lệnh [ sẽ bỏ dở với một lỗi.1

    Nếu người dùng chạy kịch bản mà không đưa gì cả, câu điều kiện bị hụt mất một vế và máy báo lỗi. Nhớ nhé: luôn nghĩ tới trường hợp “không có gì cả”.

    Ward points at a latent problem inside his own first example: $1 could be empty, because the user might run the script with no parameters. The test then reads [ = hi ], and the [ command aborts with an error.1

    Hai cách sửa của tác giảThe author's two fixes Bọc tham số trong nháy: if [ "$1" = hi ]; then — hoặc dùng lối viết phòng thủ cổ điển if [ x"$1" = x"hi" ]; then.1 Chỉ cần bọc $1 trong dấu nháy kép là xong. Enclose the parameter in quotes: if [ "$1" = hi ]; then — or use the classic defensive form if [ x"$1" = x"hi" ]; then.1

    Đây chính là mục dấu nháy quay lại

    Không phải trùng hợp. Nháy kép biến một biến rỗng thành một đối số rỗng thay vì không có đối số nào. Vì [ đếm đối số,8 khác biệt ấy quyết định chương trình chạy hay hỏng — và đó là toàn bộ lý do Ward đặt chương dấu nháy trước chương điều kiện.

  3. && · || · !

    Cấu trúc logic — và mã thoát nào được dùng “Và”, “hoặc”, “không” viết gọn trên một dòng Logical constructs — and which exit code wins

    Với command1 && command2, shell chạy command1, và nếu mã thoát là 0 thì chạy tiếp command2. Với || thì ngược lại: nếu lệnh trước trả mã khác 0, shell chạy lệnh thứ hai.1 Ward nêu rõ phần tinh tế: trong cả hai trường hợp, mã thoát của lệnh chạy sau cùng mới là mã quyết định điều kiện — với &&, nếu lệnh đầu thất bại thì shell dùng mã của nó, còn nếu lệnh đầu thành công thì shell dùng mã của lệnh thứ hai; với ||, shell dùng mã của lệnh đầu nếu nó thành công, hoặc mã của lệnh thứ hai nếu lệnh đầu thất bại.1

    && nghĩa là “xong cái này thì làm tiếp cái kia”. || nghĩa là “nếu cái này hỏng thì làm cái kia”. Kết quả cuối cùng tính theo lệnh chạy sau cùng. Nhớ nhé: và thì làm tiếp, hoặc thì làm bù.

    With command1 && command2, the shell runs command1, and if the exit code is 0 it also runs command2. || is the mirror: if the command before it returns a nonzero exit code, the shell runs the second command.1 Ward is explicit about the subtlety: in both cases, the exit code of the last command run determines how the shell processes the conditional — with &&, if the first command fails the shell uses its exit code, but if it succeeds the shell uses the second command's; with ||, the shell uses the first command's code if it succeeded, or the second's if it didn't.1

    Phủ địnhNegation Bạn đảo ngược một phép thử bằng cách đặt toán tử ! trước nó: if [ ! "$1" = hi ]; then. Trong riêng trường hợp so sánh, bạn có thể gặp != như một lựa chọn thay thế, nhưng ! dùng được với mọi phép thử điều kiện.1 Muốn nói “không phải” thì thêm dấu ! phía trước phép thử. You invert a test by placing the ! operator before it: if [ ! "$1" = hi ]; then. In this specific case of comparisons you may see != as an alternative, but ! works with any of the condition tests.1

    -a, -o và một cảnh báo đáng đọc

    Ward cho biết nếu điều kiện của bạn có lệnh test ([), bạn có thể dùng -a và -o thay cho && và ||, ví dụ if [ "$1" = hi -o "$1" = bye ]; then.1 Cần nói rõ để khỏi nhầm: đây không còn là cách được khuyến nghị. POSIX đánh dấu các toán tử nhị phân -a và -o (cùng ( và )) là obsolescent, vì nhiều biểu thức dùng chúng được định nghĩa nhập nhằng bởi văn phạm.9 Trang test(1) nói thẳng hơn: “Binary -a and -o are ambiguous. Use test EXPR1 && test EXPR2 or test EXPR1 || test EXPR2 instead.”4 Bản thân Ward cũng đưa ví dụ dạng an toàn hơn if [ "$1" = hi ] || [ "$1" = bye ]; then ngay trước đó.1

    Độ ưu tiên

    POSIX quy định && và || có độ ưu tiên ngang nhau và được tính từ trái sang phải.2 Vì vậy false && echo foo || echo bar in ra bar — không phải vì có quy tắc “và mạnh hơn hoặc”, mà chỉ vì đọc từ trái sang.2

  4. test operators

    Ba họ phép thử: tệp, chuỗi, số học Ba loại câu hỏi bạn hỏi được Three families of test: file, string, arithmetic

    Ward nói có hàng chục phép thử, tất cả rơi vào ba nhóm chung: phép thử tệp, phép thử chuỗi, và phép thử số học; sách info chứa tài liệu đầy đủ, còn trang test(1) là chỗ tra nhanh.1 Vì kịch bản shell rất hợp với thao tác trên toàn bộ tệp, nhiều phép thử [ hữu ích lại liên quan tới thuộc tính tệp — chẳng hạn [ -f file ] kiểm tra xem đó có phải tệp thường (không phải thư mục hay tệp đặc biệt) hay không.1

    Bạn hỏi được ba loại câu hỏi: về tệp (có tồn tại không, là thư mục hay tệp thường), về chữ (có bằng nhau không, có rỗng không), và về số (lớn hơn, nhỏ hơn). Nhớ nhé: tệp, chữ, số — ba loại câu hỏi.

    Ward says there are dozens of test operations, all falling into three general categories: file tests, string tests and arithmetic tests; the info manual holds the complete documentation, while test(1) is the fast reference.1 Because shell scripts suit operations on entire files, many useful [ tests involve file properties — [ -f file ], for instance, checks whether something is a regular file (not a directory or special file).1

    Bảng của tác giả, rút gọnThe author's tables, condensed Kiểu tệp: -f tệp thường, -d thư mục, -h liên kết mềm, -b thiết bị khối, -c thiết bị ký tự, -p ống có tên, -S socket. Quyền: -r đọc được, -w ghi được, -x chạy được, -u setuid, -g setgid, -k “sticky”. Thêm hai phép thử quan trọng: -e (tệp tồn tại) và -s (tệp không rỗng).1 Có sẵn một bộ câu hỏi ngắn gọn về tệp: có phải thư mục không, có đọc được không, có rỗng không… File types: -f regular file, -d directory, -h symbolic link, -b block device, -c character device, -p named pipe, -S socket. Permissions: -r readable, -w writable, -x executable, -u setuid, -g setgid, -k "sticky". Plus two important tests: -e (file exists) and -s (file is not empty).1

    Liên kết mềm: ngoại lệ duy nhất

    Ward ghi chú rằng nếu test được dùng trên một liên kết mềm, nó thử đối tượng được trỏ tới chứ không phải bản thân liên kết — ngoại trừ phép thử -h. Nghĩa là nếu link trỏ tới một tệp thường thì [ -f link ] trả mã thoát đúng (0).1 Tài liệu test(1) phát biểu y hệt và bổ sung rằng -L đồng nghĩa với -h.4

    Dấu bằng không phải phép so sánh số

    Đây là cái bẫy Ward nhấn mạnh: dấu = tìm sự bằng nhau của chuỗi, không phải của số. Vì vậy [ 1 = 1 ] trả 0 (đúng), nhưng [ 01 = 1 ] trả sai. Làm việc với số thì dùng -eq: [ 01 -eq 1 ] trả đúng.1 Bộ đầy đủ là -eq, -ne, -lt, -gt, -le, -ge.14

    Phép thử chuỗi và hai toán tử nhị phân tệp

    Ngoài = và != còn hai phép thử chuỗi một ngôi: -z trả đúng nếu đối số rỗng ([ -z "" ] trả 0), -n trả đúng nếu đối số không rỗng ([ -n "" ] trả 1).1 Về tệp, ba toán tử nhị phân ít gặp hơn: -nt (“newer than”) trả đúng nếu file1 có ngày sửa mới hơn file2, -ot làm điều ngược lại, và -ef so sánh hai tệp, trả đúng nếu chúng dùng chung số inode và thiết bị — cách phát hiện liên kết cứng trùng nhau.14 Đáng lưu ý cho tính khả chuyển: chính chuẩn POSIX ghi rằng -nt, -ot và -ef không được đưa vào tiện ích test chuẩn.9

  5. case · esac

    case — con đường duy nhất không đi qua mã thoát So khớp mẫu chữ, không cần chạy lệnh thử case — the one path that doesn't go through exit codes

    Từ khoá case tạo nên một cấu trúc điều kiện khác, đặc biệt hữu ích cho việc khớp chuỗi. Ward nêu rõ điểm phân biệt: nó không thực thi bất kỳ lệnh thử nào và do đó không đánh giá mã thoát; bù lại, nó làm được việc khớp mẫu.1 Trình tự: (1) kịch bản khớp $1 với từng giá trị case được đánh dấu bằng ); (2) nếu khớp, shell chạy các lệnh phía dưới cho tới khi gặp ;; rồi nhảy tới esac; (3) điều kiện kết thúc ở esac.1

    case giống bảng phân loại: nếu chữ bạn đưa vào khớp với dòng nào thì làm theo dòng đó. Nó so chữ trực tiếp, không cần chạy lệnh thử. Nhớ nhé: case là bảng tra, if là câu hỏi.

    The case keyword forms another conditional construct, exceptionally useful for matching strings. Ward names the distinction: it does not execute any test commands and therefore does not evaluate exit codes; what it can do instead is pattern matching.1 The sequence: (1) the script matches $1 against each case value demarcated with ); (2) if one matches, the shell executes the commands below it until it hits ;;, then skips to esac; (3) the conditional ends at esac.1

    Viết mẫu thế nàoHow to write the patterns Mỗi giá trị case có thể là một chuỗi đơn (bye), nhiều chuỗi nối bằng | (hi|hello đúng nếu $1 bằng hi hoặc hello), hoặc mẫu dùng * và ? (what*). Muốn có nhánh mặc định bắt mọi giá trị còn lại, dùng một dấu * đơn độc.1 Ward nhắc: kết thúc mỗi nhánh bằng ;; để tránh lỗi cú pháp.1 Một dòng có thể bắt nhiều chữ, và dấu * ở cuối bắt tất cả những chữ còn lại. Each case value can be a single string (bye), several joined with | (hi|hello is true if $1 equals hi or hello), or a pattern using * and ? (what*). For a default case catching everything else, use a single *.1 Ward's reminder: end each case with ;; to avoid a possible syntax error.1

    Mẫu của shell ≠ biểu thức chính quy

    Chuẩn POSIX nói rõ: ký pháp khớp mẫu của shell có liên hệ với nhưng hơi khác ký pháp biểu thức chính quy.2 Đây là chỗ người mới hay trượt: trong case, * nghĩa là “chuỗi bất kỳ”, còn trong grep thì * nghĩa là “không hoặc nhiều lần ký tự trước đó”. Cùng một ký tự, hai hệ thống khác nhau.

    Và elif thì sao?

    Bourne shell có từ khoá elif để nối nhiều điều kiện, và Ward lưu ý rằng luồng điều khiển chỉ đi qua điều kiện thành công đầu tiên — chạy kịch bản với tham số hi bye thì bạn chỉ nhận được xác nhận cho hi.1 Nhưng ông khuyên đừng quá sa đà: cấu trúc case thường phù hợp hơn.1

  6. for · while · until

    Hai loại vòng lặp, và lời khuyên chỉ nên dùng một Lặp theo danh sách, hoặc lặp tới khi hết Two kinds of loop, and advice to use mostly one

    Bourne shell có hai loại vòng lặp: for và while. Vòng for — thực chất là vòng “for each” — là loại phổ biến nhất: shell đặt biến bằng giá trị đầu tiên trong danh sách phân cách bằng khoảng trắng sau in, chạy các lệnh giữa do và done, rồi quay lại đặt giá trị kế tiếp cho tới khi hết.1 Vòng while của Bourne shell dùng mã thoát, giống hệt điều kiện if.1

    for là “với mỗi món trong danh sách, làm việc này”. while là “còn đạt thì còn làm”. Nhớ nhé: for duyệt danh sách, while chờ điều kiện đổi.

    The Bourne shell has two kinds of loop: for and while. The for loop — really a "for each" loop — is the most common: the shell sets the variable to the first of the space-delimited values following in, runs the commands between do and done, then goes back and takes the next value until the list is exhausted.1 The Bourne shell's while loop uses exit codes, exactly like the if conditional.1

    Lời khuyên thẳng thắn của tác giảThe author's blunt advice Bạn có thể thoát khỏi vòng while bằng break; Bourne shell còn có vòng until hoạt động y như while, khác ở chỗ nó phá vòng khi gặp mã thoát bằng 0 thay vì khác 0. Nhưng Ward nói thẳng: bạn không nên cần tới while và until quá thường xuyên — nếu thấy mình cần while, có lẽ bạn nên dùng một ngôn ngữ phù hợp hơn như Python hay awk.1 Nếu bạn thấy mình phải viết vòng lặp phức tạp, có lẽ nên đổi sang công cụ khác. You can break out of a while loop with break; the Bourne shell also has an until loop that works just like while, except it breaks the loop on a zero exit code rather than a nonzero one. But Ward says it plainly: you shouldn't need while and until very often — if you find that you need while, you should probably be using a language more appropriate to the task, such as Python or awk.1

    Ví dụ while của tác giả, đọc kỹ

    Kịch bản mười vòng lặp của ông đặt FILE=/tmp/whiletest.$$, ghi firstline vào tệp, rồi lặp chừng nào tail -10 $FILE | grep -q firstline còn trả mã 0 — mỗi vòng thêm một dòng mới. Ngay khi firstline không còn nằm trong mười dòng cuối, mã thoát khác 0 và vòng lặp dừng.1 Hai chi tiết đáng học: tên tệp tạm dùng $$ để tránh trùng, và phép thử là một pipeline chứ không phải [.

    for đọc từ đâu ra

    Trong danh sách sau in, mỗi giá trị cách nhau bằng khoảng trắng — nghĩa là nó chịu ảnh hưởng của field splitting và của globbing. Đó là lý do for filename in *; do … duyệt được mọi mục trong thư mục hiện tại: dấu * được shell nở ra trước khi vòng lặp bắt đầu, đúng theo mô hình hai bước ở phần 1.1

  7. command substitution

    Thay thế lệnh: $(…) đưa đầu ra ngược về dòng lệnh Lấy kết quả của lệnh này làm nguyên liệu cho lệnh kia Command substitution: $(…) folds output back into the command line

    Bourne shell có thể chuyển hướng đầu ra chuẩn của một lệnh ngược trở lại chính dòng lệnh của shell: bạn dùng đầu ra của một lệnh làm đối số cho lệnh khác, hoặc lưu nó vào một biến shell, bằng cách bọc lệnh trong $().1 Ward minh hoạ bằng một kịch bản đọc cờ của bộ xử lý: FLAGS=$(grep ^flags /proc/cpuinfo | sed 's/.*://' | head -1) — cho thấy bạn dùng được cả nháy đơn lẫn pipeline bên trong phép thay thế.1

    Bình thường lệnh in kết quả ra màn hình. Với $( ), kết quả đó được đem về làm nguyên liệu cho câu lệnh khác, hoặc cất vào một cái hộp có tên. Nhớ nhé: $( ) là cách mượn lời của lệnh khác.

    The Bourne shell can redirect a command's standard output back to the shell's own command line: you use a command's output as an argument to another command, or store it in a shell variable, by enclosing the command in $().1 Ward illustrates it with a script that reads processor flags: FLAGS=$(grep ^flags /proc/cpuinfo | sed 's/.*://' | head -1) — showing that you can use both single quotes and pipelines inside the substitution.1

    Đừng lạm dụngDon't overdo it Ward cảnh báo rất dễ đi quá đà: đừng dùng $(ls) trong kịch bản, vì để shell nở dấu * thì nhanh hơn. Và nếu bạn muốn gọi một lệnh trên nhiều tên tệp lấy từ find, hãy cân nhắc pipeline tới xargs thay vì thay thế lệnh, hoặc dùng tuỳ chọn -exec.1 Đừng dùng nó để liệt kê tệp — shell có cách nhanh hơn. Ward warns it's easy to go overboard: don't use $(ls) in a script, because using the shell to expand * is faster. And if you want to invoke a command on several filenames from find, consider a pipeline to xargs rather than command substitution, or use the -exec option.1

    Backtick và $()

    Ward ghi chú rằng cú pháp truyền thống là bọc lệnh trong dấu huyền (``) và bạn sẽ thấy nó trong rất nhiều kịch bản; dạng $() mới hơn, là một chuẩn POSIX, và nhìn chung dễ đọc dễ viết hơn đối với con người.1 Chuẩn xác nhận cả hai dạng đều là thay thế lệnh hợp lệ.2

    Hai chi tiết chuẩn đáng nhớ

    Thứ nhất: shell thực thi lệnh bên trong trong một môi trường subshell rồi thay cả cụm bằng đầu ra chuẩn của nó, đồng thời xoá các ký tự xuống dòng ở cuối.2 Thứ hai, và hay bị quên: các ký tự xuống dòng ở giữa thì không bị xoá, nhưng có thể bị coi là dấu phân cách trường tuỳ theo giá trị IFS và cách bạn đặt nháy.2

Cụm khái niệm 04 Concept cluster 04

Bộ đồ nghề: tệp tạm, here document, và sáu chương trình nhỏ Đồ nghề đi kèm, và lúc nên buông tay The toolkit: temporary files, here documents, and six small programs

Ward giới thiệu phần này bằng một nhận xét về vai trò: một số tiện ích, như basename, thực ra chỉ thiết thực khi dùng chung với chương trình khác, nên hiếm khi có chỗ đứng ngoài kịch bản shell; nhưng những cái khác, như awk, lại rất hữu dụng ngay trên dòng lệnh.1

Phần này là hộp đồ nghề. Có cái chỉ dùng trong kịch bản, có cái bạn dùng hằng ngày ngay trên dòng lệnh. Nhớ nhé: biết tên đồ nghề rồi thì đọc kịch bản người khác sẽ dễ hơn nhiều.

Ward introduces this part with a remark about roles: certain utilities, such as basename, are really only practical when used with other programs and therefore rarely find a place outside shell scripts; others, such as awk, are quite useful on the command line too.1

Định nghĩa. Một here document là tính năng của shell cho phép bạn nạp một khối văn bản lớn vào đầu vào chuẩn của một lệnh, thay vì dùng nhiều lệnh echo. <<EOF bảo shell chuyển hướng mọi dòng tiếp theo vào đầu vào chuẩn của lệnh đứng ngay trước nó; việc chuyển hướng dừng ngay khi dấu hiệu EOF xuất hiện một mình trên một dòng.1

Nói đơn giản. Here document là cách dán nguyên một đoạn văn dài vào giữa kịch bản, rồi đưa cả đoạn ấy cho một lệnh. Nhớ nhé: mở bằng <<EOF, đóng bằng EOF đứng riêng một dòng.

Definition. A here document is a shell feature that lets you feed a large block of text to a command's standard input instead of using several echo commands. <<EOF tells the shell to redirect all subsequent lines to the standard input of the command preceding it; the redirection stops as soon as the EOF marker occurs on a line by itself.1

DATE=$(date)
cat <<EOF
Date: $DATE
…
EOF

biến vẫn nở bên trong

cat <<EOF
… đoạn văn của bạn …
EOF

DATE=$(date)
cat <<EOF
Date: $DATE
…
EOF

variables still expand inside

  1. mktemp · trap

    Tệp tạm — và cái bẫy Ctrl-C Tệp nháp, và chuyện dọn dẹp sau khi dùng Temporary files — and the Ctrl-C trap

    Khi tạo tệp tạm, hãy chắc rằng tên tệp đủ riêng biệt để không chương trình nào vô tình ghi đè lên. Ward nói dùng PID của shell ($$) trong tên tệp đôi khi là đủ, nhưng khi cần chắc chắn không xung đột thì một tiện ích như mktemp thường là lựa chọn tốt hơn.1 Đối số của mktemp là một khuôn mẫu: lệnh này chuyển XXXXXX thành một bộ ký tự duy nhất và tạo một tệp rỗng mang tên đó.1

    Khi cần một tờ giấy nháp, đừng đặt tên chung chung — dễ đụng hàng với chương trình khác. Lệnh mktemp sẽ nghĩ hộ bạn một cái tên không ai trùng. Nhớ nhé: giấy nháp phải có tên riêng, và dùng xong thì bỏ.

    When creating a temporary file, make sure the filename is distinct enough that no other program will accidentally write to it. Ward says the shell's PID ($$) in a filename sometimes suffices, but when you need certainty about conflicts a utility such as mktemp is often the better option.1 The argument to mktemp is a template: the command converts the XXXXXX into a unique set of characters and creates an empty file with that name.1

    Ví dụ của tác giảThe author's example Kịch bản của ông chụp /proc/interrupts vào $(mktemp /tmp/im1.XXXXXX), sleep 2, chụp lần hai, rồi diff hai ảnh để cho thấy các ngắt thiết bị xảy ra trong hai giây vừa qua — và ông nhấn mạnh việc lưu tên tệp vào biến để chỉ phải sửa một dòng khi muốn đổi tên.1 Chụp hai “bức ảnh” của hệ thống cách nhau hai giây rồi so sánh — thế là thấy được cái gì vừa thay đổi. His script captures /proc/interrupts into $(mktemp /tmp/im1.XXXXXX), sleeps 2 seconds, captures again, then diffs the two snapshots to show the device interrupts of the last two seconds — and he stresses storing filenames in variables so that changing one means editing one line.1

    Bẫy: kịch bản bị ngắt giữa chừng

    Vấn đề thường gặp với kịch bản dùng tệp tạm là nếu kịch bản bị huỷ giữa chừng, tệp tạm có thể bị bỏ lại. Trong ví dụ trên, bấm Ctrl-C trước lệnh cat thứ hai sẽ để lại một tệp trong /tmp. Ward đưa cách xử lý: dùng lệnh trap để tạo một signal handler bắt tín hiệu mà Ctrl-C sinh ra và xoá tệp tạm — trap "rm -f $TMPFILE1 $TMPFILE2; exit 1" INT. Ông cảnh báo bạn phải dùng exit trong handler để kết thúc kịch bản một cách tường minh, nếu không shell sẽ chạy tiếp như thường sau khi handler chạy xong.1

    Hai chú thích về tính khả chuyển

    Ward lưu ý không phải mọi biến thể Unix đều có sẵn mktemp; gặp vấn đề khả chuyển thì tốt nhất cài gói GNU coreutils cho hệ điều hành của bạn.1 Điều này khớp với thực tế: mktemp là một lệnh của coreutils,10 và nó không nằm trong danh mục tiện ích của POSIX.1-2017.11 Trang mktemp(1) còn nói rõ khuôn mẫu phải chứa ít nhất ba ký tự X liên tiếp ở thành phần cuối; nếu không đưa khuôn mẫu, mặc định là tmp.XXXXXXXXXX.10 Ward cũng ghi chú rằng khi không có đối số thì khuôn mẫu bắt đầu bằng tiền tố /tmp/tmp..1

    trap theo chuẩn

    POSIX mô tả trap là một special built-in: điều kiện có thể là EXIT, 0 (tương đương EXIT), hoặc một tín hiệu nêu bằng tên tượng trưng không có tiền tố SIG — ví dụ HUP, INT, QUIT, TERM; đặt trap cho SIGKILL hay SIGSTOP cho kết quả không xác định.2 Đó là lý do handler của Ward viết INT chứ không phải SIGINT.

  2. here document

    Here document — văn bản dài, biến vẫn sống Dán cả đoạn văn vào kịch bản Here documents — long text, live variables

    Dấu hiệu kết thúc có thể là chuỗi bất kỳ, miễn là bạn dùng cùng một dấu hiệu ở đầu và cuối here document; quy ước là viết nó bằng chữ in hoa.1 Ward nhấn mạnh chi tiết làm nên giá trị của nó: shell vẫn nở các biến bên trong here document — đặc biệt hữu ích khi bạn in báo cáo chứa nhiều biến.1

    Bạn chọn một từ làm “dấu đóng cửa” — thường viết hoa như EOF — và mọi dòng giữa hai dấu ấy sẽ được đưa cho lệnh. Tên biến bên trong vẫn được thay bằng giá trị. Nhớ nhé: cùng một từ mở và đóng, và biến vẫn sống bên trong.

    The marker can be any string, as long as you use the same one at the beginning and end of the here document; convention dictates all uppercase.1 Ward stresses the detail that makes it valuable: the shell expands shell variables inside here documents — especially useful when printing reports that contain many variables.1

    Khi nào dùngWhen to use it Khi bạn thấy mình viết ba, bốn lệnh echo liền nhau — hoặc khi dùng nháy kép cho một lượng văn bản lớn, chính là lúc Ward gợi ý chuyển sang here document.1 Khi bạn phải in nhiều dòng liền nhau. When you find yourself writing three or four echo commands in a row — or when using double quotes for a large amount of text, which is exactly where Ward suggests a here document.1

    Tắt phép nở, và biến thể <<-

    Chuẩn POSIX bổ sung hai điều nằm ngoài ví dụ của chương nhưng cùng một cơ chế. Thứ nhất: nếu bất kỳ phần nào của từ đánh dấu được đặt trong nháy, các dòng của here document sẽ không được nở; nếu không phần nào được nháy, mọi dòng đều chịu nở tham số, thay thế lệnh và nở số học.2 Thứ hai: toán tử <<- sẽ cắt bỏ mọi ký tự tab ở đầu các dòng đầu vào và ở dòng chứa dấu hiệu kết thúc — cách giữ thụt lề đẹp trong kịch bản.2

  3. basename · awk · sed

    Ba con dao xử lý chuỗi và tên tệp Cắt tên tệp, lấy cột, thay chữ Three blades for strings and filenames

    basename dùng khi bạn cần bỏ phần mở rộng khỏi tên tệp hoặc bỏ các thư mục khỏi đường dẫn đầy đủ: cả basename example.html .html lẫn basename /usr/local/bin/example đều trả về example.1 awk không phải một lệnh đơn mục đích mà là một ngôn ngữ lập trình đầy đủ; Ward nhận xét rằng nghệ thuật dùng awk nay phần nào đã thất truyền, bị thay bởi các ngôn ngữ lớn hơn như Python — và rất nhiều người chỉ dùng nó cho đúng một việc: rút một trường khỏi luồng đầu vào, kiểu ls -l | awk '{print $5}' để in trường thứ năm (kích thước tệp).1 sed (“stream editor”) là một trình soạn thảo văn bản tự động: nhận một luồng vào, sửa nó theo một biểu thức, rồi in kết quả ra đầu ra chuẩn.1

    Ba con dao nhỏ: basename cắt tên tệp cho gọn; awk lấy đúng cột bạn cần trong một bảng; sed thay chữ này bằng chữ kia trên cả dòng chảy văn bản. Nhớ nhé: cắt tên, lấy cột, thay chữ.

    basename is for stripping an extension from a filename or getting rid of the directories in a full pathname: both basename example.html .html and basename /usr/local/bin/example return example.1 awk is not a simple single-purpose command but a powerful programming language; Ward observes that awk usage is now something of a lost art, replaced by larger languages such as Python — and that many people use it for exactly one thing: picking a single field out of an input stream, as in ls -l | awk '{print $5}' to print the fifth field (the file size).1 sed ("stream editor") is an automatic text editor: it takes an input stream, alters it according to some expression, and prints the results to standard output.1

    Cú pháp sed tối thiểuMinimum viable sed Nói chung sed nhận một địa chỉ và một thao tác: sed 's/:/%/' /etc/passwd thay dấu hai chấm đầu tiên của mỗi dòng bằng %; thêm bổ từ g thành 's/:/%/g' để thay tất cả. Địa chỉ cũng có thể là khoảng dòng — sed 3,6d /etc/passwd xoá dòng ba tới sáu — hoặc một biểu thức chính quy: sed '/exp/d' xoá mọi dòng khớp exp. Bỏ địa chỉ thì sed tác động lên mọi dòng. Ward nói hai thao tác phổ biến nhất có lẽ là s và d.1 Nhớ hai chữ thôi: s là thay, d là xoá. In general sed takes an address and an operation: sed 's/:/%/' /etc/passwd replaces the first colon on each line with %; add the g modifier — 's/:/%/g' — to replace them all. The address can be a line range — sed 3,6d /etc/passwd deletes lines three through six — or a regular expression: sed '/exp/d' deletes every line matching exp. Omit the address and sed operates on every line. Ward says the two most common operations are probably s and d.1

    Mẫu kịch bản basename của tác giả

    Ông chuyển ảnh GIF sang PNG bằng vòng for file in *.gif, thoát sớm nếu không có tệp nào (if [ ! -f $file ]; then exit; fi), rồi b=$(basename $file .gif) trước khi chạy giftopnm $b.gif | pnmtopng > $b.png.1 Đây là bản thu nhỏ của cả chương: glob, phép thử tệp, thay thế lệnh, pipeline — bốn cơ chế trong sáu dòng.

    Sách tham khảo mà tác giả dẫn

    Ward chỉ tới The AWK Programming Language của Alfred V. Aho, Brian W. Kernighan và Peter J. Weinberger (Addison-Wesley, 1988),112 và, cho cả sed lẫn awk, cuốn tra nhanh sed & awk Pocket Reference, ấn bản 2, của Arnold Robbins (O'Reilly, 2002).1 Ông nói thẳng rằng phân tích sâu sed nằm ngoài phạm vi sách — đây là chương nhập môn.1

    Không có tệp thì đọc từ đâu?

    Ward ghi: trong mọi ví dụ trên, sed ghi ra đầu ra chuẩn, và đó là cách dùng phổ biến nhất; không có đối số tệp thì sed đọc từ đầu vào chuẩn — khuôn mẫu bạn sẽ gặp liên tục trong các pipeline của shell.1 Trang sed(1) mô tả đúng hành vi ấy: nếu không nêu tệp đầu vào nào thì đầu vào chuẩn được đọc.13

  4. xargs · find -exec

    xargs — khi danh sách tệp dài quá sức chứa Khi có quá nhiều tệp để đưa cùng lúc xargs — when the file list won't fit

    Khi bạn phải chạy một lệnh trên số lượng tệp khổng lồ, lệnh đó hoặc shell có thể báo rằng không nhét hết đối số vào bộ đệm. xargs giải quyết bằng cách chạy lệnh trên từng tên tệp trong luồng đầu vào chuẩn của nó.1 Cách dùng kinh điển là ghép với find: find . -name '*.gif' -print | xargs file để kiểm tra mọi tệp đuôi .gif trong cây thư mục hiện tại có thật là ảnh GIF không.1

    Nếu danh sách tệp dài quá, máy không bê hết một lượt được. xargs chia nhỏ ra và đưa dần. Nhớ nhé: nhiều quá thì chia ra mà làm.

    When you have to run one command on a huge number of files, the command or the shell may respond that it can't fit all the arguments in its buffer. xargs gets around this by running a command on each filename in its standard input stream.1 The classic pairing is with find: find . -name '*.gif' -print | xargs file, to verify that every .gif file in the current directory tree really is a GIF image.1

    Cảnh báo bảo mật của tác giảThe author's safety warning Cách gọi trên có thể gây lỗi hoặc để hệ thống hở ra vấn đề bảo mật, vì tên tệp có thể chứa khoảng trắng và ký tự xuống dòng. Khi viết kịch bản, hãy dùng dạng find . -name '*.gif' -print0 | xargs -0 file, đổi dấu phân cách đầu ra của find và dấu phân tách đối số của xargs từ ký tự xuống dòng sang ký tự NULL.1 Tên tệp có dấu cách dễ gây rắc rối — dùng cặp -print0 và -0 cho an toàn. That invocation can cause errors or leave your system open to security problems, because filenames can include spaces and newlines. In a script, use find . -name '*.gif' -print0 | xargs -0 file instead, changing the find output separator and the xargs argument delimiter from a newline to a NULL character.1

    Tài liệu nói gì về -0

    Trang xargs(1) mô tả -0/--null: các mục đầu vào được kết thúc bằng ký tự null thay vì khoảng trắng, và dấu nháy cùng gạch chéo ngược không còn đặc biệt — hữu ích khi mục đầu vào có thể chứa khoảng trắng, dấu nháy hay gạch chéo ngược; tuỳ chọn -print0 của GNU find sinh ra đúng dạng đầu vào phù hợp.14 Trang find(1) đi xa hơn một bước và khuyên bạn nên cân nhắc dùng -print0 nếu đầu ra sắp được một chương trình khác đọc.15

    Hai chi tiết nữa của Ward

    Thứ nhất: xargs khởi chạy rất nhiều tiến trình, nên đừng mong hiệu năng tốt với danh sách tệp lớn.1 Thứ hai: bạn có thể cần thêm hai gạch ngang (--) ở cuối lệnh xargs nếu có khả năng tệp đích bắt đầu bằng một dấu gạch ngang; -- báo cho chương trình rằng mọi đối số theo sau là tên tệp chứ không phải tuỳ chọn — nhưng không phải chương trình nào cũng hỗ trợ.1

    Lựa chọn thay thế: -exec

    Khi đang dùng find, có một cách thay cho xargs: tuỳ chọn -exec. Ward thừa nhận cú pháp hơi khó chịu vì bạn phải cấp cặp ngoặc nhọn {} để thay chỗ cho tên tệp và một dấu ; theo nghĩa đen để báo hết lệnh — find . -name '*.gif' -exec file {} \;.1

  5. expr · exec

    Hai công cụ, hai lời cảnh báo khác nhau Làm toán vụng về, và cánh cửa một chiều Two tools, two different warnings

    expr giúp bạn làm phép toán số học trong kịch bản shell (và cả vài thao tác chuỗi): expr 1 + 2 in ra 3. Nhưng Ward gọi nó là một cách làm toán vụng về và chậm; nếu thấy mình dùng nó thường xuyên, có lẽ bạn nên dùng một ngôn ngữ như Python thay vì kịch bản shell.1 exec là một tính năng dựng sẵn của shell, thay thế tiến trình shell hiện tại bằng chương trình bạn nêu sau nó; nó thực hiện lời gọi hệ thống exec() đã mô tả ở Chương 1. Tính năng này được thiết kế để tiết kiệm tài nguyên hệ thống — nhưng hãy nhớ rằng không có đường quay lại.1

    expr biết cộng trừ, nhưng chậm và lóng ngóng — cần tính nhiều thì đổi công cụ. exec thì đặc biệt: nó thay hẳn shell hiện tại bằng chương trình mới, không quay về được nữa. Nhớ nhé: exec là cánh cửa một chiều.

    expr helps with arithmetic in shell scripts (and some string operations): expr 1 + 2 prints 3. But Ward calls it a clumsy, slow way of doing math; if you find yourself using it frequently, you should probably be using a language like Python instead of a shell script.1 exec is a built-in shell feature that replaces the current shell process with the program you name after it; it carries out the exec() system call described in Chapter 1. The feature is designed for saving system resources — but remember there is no return.1

    Thí nghiệm của tác giảThe author's experiment Thử chạy exec cat trong một cửa sổ shell. Sau khi bạn bấm Ctrl-D hoặc Ctrl-C để kết thúc cat, cửa sổ sẽ biến mất, vì tiến trình con của nó không còn tồn tại.1 Chạy exec trong kịch bản cũng vậy: cả kịch bản lẫn shell đang chạy nó đều biến mất, được thay bằng lệnh mới.1 Thử exec cat rồi thoát — cả cửa sổ đóng luôn. Đó là vì shell cũ đã bị thay mất rồi. Try running exec cat in a shell window. After you press Ctrl-D or Ctrl-C to terminate cat, the window should disappear, because its child process no longer exists.1 Running exec in a script does the same: the script and the shell running it are gone, replaced by the new command.1

    Cùng một cơ chế với Chương 1

    Trang execve(2) mô tả chính xác điều Ward nói: lời gọi này khiến chương trình đang chạy bị thay thế bằng chương trình mới, với ngăn xếp, vùng heap và các đoạn dữ liệu được khởi tạo lại.3 Về phía bash, tài liệu ghi rằng nếu có command thì nó thay thế shell mà không tạo tiến trình mới; nếu không thực thi được vì lý do nào đó, một shell không tương tác sẽ thoát, trừ khi tuỳ chọn execfail được bật.8

    Đọc kỹ mã thoát của expr

    Đây là một ví dụ sống động cho mục “khác 0 không luôn là lỗi”: expr thoát với 0 nếu biểu thức không rỗng và không bằng 0, với 1 nếu biểu thức rỗng hoặc bằng 0, với 2 nếu biểu thức sai cú pháp, và với 3 nếu có lỗi xảy ra.16 Nghĩa là expr 1 - 1 thoát với mã 1 dù nó tính đúng.

  6. subshell · sourcing · read

    Ba cách quản lý môi trường và đầu vào Đổi tạm, gộp tệp, và hỏi người dùng Three ways to manage environment and input

    Khi cần thay đổi môi trường một chút mà không muốn thay đổi vĩnh viễn, lựa chọn đơn giản nhất là subshell — một tiến trình shell hoàn toàn mới, tạo ra chỉ để chạy một hai lệnh. Shell mới có một bản sao môi trường của shell gốc; khi nó thoát, mọi thay đổi bạn tạo ra trong môi trường ấy biến mất, để shell ban đầu chạy như thường. Cách dùng: đặt các lệnh vào trong cặp ngoặc đơn, ví dụ (cd uglydir; uglyprogram).1 Muốn gộp mã từ tệp khác, dùng toán tử dấu chấm: . config.sh — còn gọi là sourcing, hữu ích để đọc biến từ tệp cấu hình dùng chung.1 Muốn đọc đầu vào người dùng, lệnh read đọc một dòng văn bản từ đầu vào chuẩn và lưu vào một biến: read var.1

    Ba mẹo nhỏ. Ngoặc đơn ( ) tạo một “phòng riêng”: bạn bày bừa trong đó, ra khỏi phòng là mọi thứ như cũ. Dấu chấm . dán nội dung một tệp khác vào chỗ này. Lệnh read hỏi người dùng một câu rồi cất câu trả lời. Nhớ nhé: ngoặc đơn là phòng riêng, dấu chấm là dán vào, read là hỏi han.

    When you need to alter the environment slightly without a permanent change, the simplest option is a subshell — an entirely new shell process created just to run a command or two. The new shell has a copy of the original's environment, and when it exits any changes you made there disappear, leaving the initial shell to run as normal. To use one, put the commands in parentheses: (cd uglydir; uglyprogram).1 To include code from another file, use the dot operator: . config.sh — also called sourcing, useful for reading variables from a shared configuration file.1 To read user input, the read command reads a line of text from standard input and stores it in a variable: read var.1

    Phân biệt then chốtThe key distinction Sourcing không giống chạy một kịch bản khác: khi bạn chạy một kịch bản như một lệnh, nó khởi động trong một shell mới, và bạn không lấy lại được gì ngoài đầu ra và mã thoát.1 Dán tệp vào thì biến bên trong dùng được ngay; còn chạy tệp như một lệnh thì bạn chỉ nhận lại kết quả in ra. Sourcing is not the same as executing another script: when you run a script as a command it starts in a new shell, and you can't get anything back other than the output and the exit code.1

    Ba ví dụ subshell của tác giả

    Thêm một thành phần vào PATH mà không làm hỏng vĩnh viễn: (PATH=/usr/confusing:$PATH; uglyprogram) — và Ward chỉ ra rằng việc này thông dụng tới mức có sẵn một cú pháp dựng sẵn tránh hẳn subshell: PATH=/usr/confusing:$PATH uglyprogram.1 Ống dẫn và tiến trình nền cũng hoạt động với subshell: tar cf - orig | (cd target; tar xvf -) nhân bản cả cây thư mục, hữu ích vì giữ được quyền sở hữu và quyền hạn, và nhìn chung nhanh hơn cp -r.1

    Cảnh báo của chính tác giả

    Với lệnh tar kiểu trên, Ward dựng hẳn một khối WARNING: hãy kiểm tra kỹ trước khi chạy để chắc rằng thư mục đích tồn tại và hoàn toàn tách biệt với thư mục nguồn; trong kịch bản, bạn kiểm tra điều đó bằng [ -d orig -a ! orig -ef target ].1 Lưu ý phép thử -ef ở đây chính là phép so sánh inode đã nói ở phần trước4 — và hãy nhớ rằng cả -ef lẫn -a đều nằm ngoài (hoặc bị đánh dấu lỗi thời trong) chuẩn POSIX.9

    Chuẩn về dấu chấm và về subshell

    POSIX định nghĩa tiện ích dot: shell thực thi các lệnh từ tệp trong môi trường hiện tại, và khác với tìm lệnh thông thường, tệp được tìm theo PATH không cần có quyền thực thi.2 Về subshell, chuẩn nói môi trường subshell được tạo ra như một bản sao của môi trường shell, các thay đổi trong đó không ảnh hưởng tới môi trường cha; và đáng chú ý, thay thế lệnh cùng các lệnh nhóm bằng ngoặc đơn đều được thực thi trong môi trường subshell.2

Đính chính Corrections

Ba ngộ nhận mà người viết kịch bản lần đầu hay mắc Ba chỗ hay hiểu nhầm Three misconceptions first script writers hit

Phần này thuộc mức đọc Đầy đủ: nó đối chiếu điều người đọc thường nghĩ với điều bản văn và tài liệu chuẩn thật sự nói.

Phần này hơi khó, bạn có thể bỏ qua và quay lại sau. Nhớ nhé: hiểu chậm mà chắc vẫn hơn.

This block belongs to the Đầy đủ (full) reading level: it sets what readers usually assume against what the text and the standards actually say.

“Dấu ngoặc vuông là cú pháp if của shell” "The square bracket is the shell's if syntax"

Ngộ nhận: [ và ] là một phần của cấu trúc điều kiện, giống dấu ngoặc trong C hay Python.

Bản văn nói: [ là một chương trình thật trên hệ Unix; mọi hệ Unix đều có lệnh tên [ thực hiện phép thử, còn gọi là test, và hai trang hướng dẫn là một.1 Hệ quả thực tế: mỗi toán tử và toán hạng phải là một đối số riêng biệt,8 nên thiếu một dấu cách là câu lệnh đổi nghĩa. Ward có ghi chú rằng vì quá thông dụng, test đã được tích hợp sẵn vào nhiều Bourne shell kể cả bash — nhưng điều đó không đổi mô hình: shell vẫn đưa cho nó một danh sách đối số.1

Misconception: [ and ] are part of the conditional construct, like brackets in C or Python.

The text says: [ is an actual program on a Unix system; all Unix systems have a command called [ that performs tests, also known as test, and their manual pages are the same.1 The practical consequence: each operator and operand must be a separate argument,8 so a missing space changes the meaning. Ward does note that because it is so widely used, test is built in to many Bourne shells including bash — but that doesn't change the model: the shell still hands it an argument list.1

“Nháy kép cũng bảo vệ như nháy đơn, chỉ khác hình dạng” "Double quotes protect like single quotes, just a different shape"

Ngộ nhận: đã bọc trong nháy là chuỗi an toàn tuyệt đối.

Bản văn nói: nháy kép hoạt động giống nháy đơn ngoại trừ việc shell vẫn nở mọi biến bên trong — echo "There is no * in my path: $PATH" thay $PATH nhưng không thay *.1 Chính vì vậy mà cả echo $100 lẫn echo "$100" đều in ra 00, và chỉ echo '$100' mới in ra $100.1 POSIX ghi rõ $ và ` giữ nghĩa đặc biệt bên trong nháy kép.2

Misconception: anything inside quotes is absolutely safe.

The text says: double quotes work like single quotes except that the shell still expands any variable inside — echo "There is no * in my path: $PATH" substitutes for $PATH but not for *.1 That is why both echo $100 and echo "$100" print 00, and only echo '$100' prints $100.1 POSIX states that $ and ` keep their special meaning inside double quotes.2

“Mã thoát khác 0 luôn nghĩa là lỗi” "A nonzero exit code always means an error"

Ngộ nhận: thấy $? khác 0 là kết luận chương trình hỏng.

Bản văn nói: một số chương trình như diff và grep dùng mã khác 0 để biểu thị tình trạng bình thường: grep trả 0 khi tìm thấy, 1 khi không tìm thấy, và dành mã 2 cho trục trặc thật.1 Tài liệu công khai xác nhận cả hai: grep(1) ghi 0 / 1 / 2 đúng như vậy,5 còn diff(1) ghi 0 nếu giống nhau, 1 nếu khác, 2 nếu có trục trặc.6 expr còn đi xa hơn với bốn mức 0 / 1 / 2 / 3.16 Cách kiểm chứng, theo Ward: đọc mục EXIT VALUE hoặc DIAGNOSTICS trong trang hướng dẫn.1

Misconception: a nonzero $? means the program broke.

The text says: some programs, like diff and grep, use nonzero codes for normal conditions: grep returns 0 when it finds a match, 1 when it doesn't, and reserves 2 for an actual problem.1 The public documentation confirms both: grep(1) records exactly 0 / 1 / 2,5 and diff(1) records 0 if the inputs are the same, 1 if different, 2 if trouble.6 expr goes further still, with four levels 0 / 1 / 2 / 3.16 How to check, per Ward: read the EXIT VALUE or DIAGNOSTICS section of the manual page.1

Tự kiểm tra Self-check

Bạn đã đọc được một kịch bản shell lạ chưa? Thử trả lời xem nào Can you read a stranger's shell script yet?

Trả lời bằng lời của bạn, không tra sách. Ward có một lời khuyên hợp ở đây: nếu bạn đang cố hiểu một kịch bản và gặp một dòng trông hoàn toàn khó hiểu, hãy tách nó ra từng mảnh một.1

Thử trả lời miệng thôi, không cần giở lại. Câu nào bí thì quay lại đọc phần đó nhé. Nhớ nhé: gặp dòng khó thì tách ra từng mảnh nhỏ mà đọc.

Answer in your own words, without looking back. Ward has advice that fits here: if you're trying to understand a shell script and come across a line that looks completely incomprehensible, pick it apart piece by piece.1

  1. Vì sao echo $100 và echo "$100" cùng in ra 00, còn echo '$100' thì không?

    Gợi ý sâu hơn: hãy trả lời bằng đúng hai bước xử lý lệnh mà Ward liệt kê, thay vì bằng tên các loại dấu nháy.

  2. Ngoài bit thực thi, một tệp kịch bản còn cần bit quyền nào nữa — và vì sao?

    Gợi ý sâu hơn: ai là người thật sự mở tệp ra đọc nội dung?

  3. $? khác $$ ở chỗ nào, và vì sao chạy echo $? hai lần liên tiếp lại luôn cho 0 ở lần thứ hai?

    Gợi ý sâu hơn: điều gì xảy ra với mã thoát cũ khi một lệnh mới chạy xong?

  4. Trong if [ "$1" = hi ]; then, thành phần nào là từ khoá của shell và thành phần nào là một chương trình?

    Gợi ý sâu hơn: điều gì xảy ra nếu bạn bỏ dấu chấm phẩy mà vẫn để then trên cùng dòng?

  5. Vì sao [ 01 = 1 ] sai còn [ 01 -eq 1 ] đúng?

    Gợi ý sâu hơn: hãy kể thêm bốn toán tử so sánh số học còn lại.

  6. case khác if ở điểm căn bản nào liên quan tới mã thoát?

    Gợi ý sâu hơn: dấu * trong một nhánh case có cùng nghĩa với * trong một biểu thức chính quy không?

  7. Vì sao find … -print0 | xargs -0 an toàn hơn find … -print | xargs?

    Gợi ý sâu hơn: ký tự nào được phép xuất hiện trong tên tệp mà lại hay bị dùng làm dấu phân cách?

  8. Sau khi chạy exec cat trong một cửa sổ terminal và thoát cat, vì sao cửa sổ biến mất?

    Gợi ý sâu hơn: lời gọi hệ thống nào ở Chương 1 đứng sau hành vi này?

  9. Sourcing một tệp bằng . config.sh khác chạy ./config.sh ở chỗ nào?

    Gợi ý sâu hơn: cái nào cho bạn lấy lại được biến, và vì sao cái kia thì không?

  10. Nêu ít nhất ba dấu hiệu cho thấy bài toán của bạn đã vượt quá tầm của kịch bản shell.

    Gợi ý sâu hơn: Ward nêu đích danh những ngôn ngữ nào làm lựa chọn thay thế?

  1. Dòng đầu tiên của một tệp kịch bản viết gì, và để làm gì?

  2. Nháy đơn và nháy kép khác nhau chỗ nào?

  3. Số 0 mà chương trình để lại khi chạy xong nghĩa là gì?

  4. $1 là gì? Còn $#?

  5. Vòng lặp for làm gì với một danh sách?

  6. Vì sao nên dọn tệp nháp sau khi dùng xong?

  7. Khi nào thì nên ngừng viết kịch bản shell và đổi công cụ?

  1. Why do echo $100 and echo "$100" both print 00, while echo '$100' does not?

  2. Besides the executable bit, which other permission bit does a script file need — and why?

  3. How does $? differ from $$, and why does running echo $? twice in a row always give 0 the second time?

  4. In if [ "$1" = hi ]; then, which parts are shell keywords and which part is a program?

  5. Why is [ 01 = 1 ] false while [ 01 -eq 1 ] is true?

  6. What is the fundamental difference between case and if with respect to exit codes?

  7. Why is find … -print0 | xargs -0 safer than find … -print | xargs?

  8. After running exec cat in a terminal window and quitting cat, why does the window disappear?

  9. How does sourcing a file with . config.sh differ from running ./config.sh?

  10. Name at least three signs that your problem has outgrown a shell script.

Khép lại chương Closing the chapter

Hãy nhớ kịch bản shell làm gì giỏi nhất: thao tác với những tệp và lệnh đơn giản. Và nếu bạn thấy mình đang viết một thứ trông rối rắm — nhất là khi nó dính tới các phép xử lý chuỗi hay số học phức tạp — đừng ngại tìm đến một ngôn ngữ kịch bản như Python, Perl hay awk.1

Kịch bản shell giỏi nhất ở việc sắp xếp tệp và gọi lệnh. Thấy nó bắt đầu rối rắm thì đổi sang công cụ khác, đừng cố. Nhớ nhé: viết ngắn, viết rõ, và biết lúc nào nên dừng.

Remember what shell scripts do best: manipulate simple files and commands. And if you find yourself writing something that looks convoluted — especially if it involves complicated string or arithmetic operations — don't be afraid to look to a scripting language like Python, Perl, or awk.1