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
-
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/shsẽ 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.1Dòng đầu tiên viết
#!/bin/sh, nghĩa là: “chương trình/bin/shhã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/shprogram 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.1Bạ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.shfile, 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:envtì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.1Quyề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 đối700.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ầnx. -
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 -
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 examplegrep r.*t /etc/passwdchạ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.inputvàr.output, shell nởr.*tthành hai tên tệp ấy và tạo ragrep 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/passwdworks most of the time, then mysteriously fails. The answer is probably your current directory: if it containsr.inputandr.output, the shell expandsr.*tinto those names and buildsgrep r.input r.output /etc/passwd.1Danh 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.1Vì 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ã.
-
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.1Nhá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.1Mặ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ầugreptìm chuỗir.*t /etc/passwdtrong đầu vào chuẩn, vìgrepchỉ 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 asksgrepto search standard input for the stringr.*t /etc/passwd, becausegrepreceived only one parameter.1Chuẩ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 clà ba tham số, còna "b c"chỉ là hai.1 -
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$PATHnhưng không thay*.1Nhá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$PATHbut does not substitute for the*.1Khi 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.1Chí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. -
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\'tsẽ 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.1Muố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\'tis a syntax error.1 Oddly enough, you can enclose the single quote inside double quotes, and the output is identical.1Quy 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: \'1Giả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, $2 · shift
Tham số vị trí — và cái băng chuyền
shiftMón thứ nhất, món thứ hai… và cách đẩy hàng Positional parameters — and theshiftconveyor belt$1,$2và 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ẵnshiftdù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:$2thành$1,$3thành$2, và cứ thế.1Khi 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ệnhshiftgiố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,$2and all variables named as positive nonzero integers contain the values of the script's parameters, or arguments.1 The built-inshiftcommand works with them to remove the first argument ($1) and advance the rest, so$2becomes$1,$3becomes$2, and so on.1Ví dụ của tác giảThe author's example Kịch bảnshiftexin tham số, gọishift, in lại, gọishift, 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. Hisshiftexscript prints an argument, shifts, prints again, shifts, prints again — printing all three arguments by only ever reading$1.1Chuẩ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
$#shifthầu như luôn đi cùng$#trong một vòngwhile: bạn lặp chừng nào còn tham số, mỗi vòng xử lý$1rồ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. -
$# · $@
$#đếm,$@chuyển tiếp Đếm số món, và chuyển cả nắm đi tiếp$#counts,$@forwardsBiế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ạyshifttrong vòng lặp để duyệt tham số: khi$#bằng 0, không còn tham số nào, nên$1rỗ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 runningshiftin a loop to pick through them: when$#is 0 no arguments remain, so$1is empty.1$@represents all of a script's arguments and is very useful for passing them to a command inside the script.1Ví 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àogs.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 togs.1Mộ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. -
$0 · standard error
$0và kỷ luật báo lỗi Kịch bản tự xưng tên khi báo lỗi$0and the discipline of error reportingBiến
$0giữ 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ếu2>&1chuyể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ới1>&2, ví dụecho $0: bad option $BADPARM 1>&2.1Khi 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.$0holds 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. Where2>&1redirects standard error to standard output, you reverse the process with1>&2, as inecho $0: bad option $BADPARM 1>&2.1Vì 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.$0theo chuẩnPOSIX định nghĩa tham số đặc biệt
0là “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$0có thể là./pshowhay một đường dẫn tuyệt đối tuỳ cách bạn chạy — điều này lý giải vì saobasename(mục dưới) hay xuất hiện ngay cạnh$0trong kịch bản thật.7 -
$$ · $?
$$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 verdictBiế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/nullrồiecho $?cho 0;ls /asdfasdf > /dev/nullrồiecho $?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/nullthenecho $?gives 0;ls /asdfasdf > /dev/nullthenecho $?gives 1 — the successful command returned 0, the unsuccessful one 1.1Cá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ệnhechothứ 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. -
exit code · exit 1
Dừng đúng cách:
exit 1và các số khác 0 Khi gặp sự cố thì dừng và báo số Stopping properly:exit 1and other nonzero codesKhi 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.1Gặ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 1to 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.1Hệ 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ộtifmà 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 anifwithout reading its source.Theo chuẩn
POSIX mô tả
exitlà 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ấunđược chỉ định.2 -
nonzero ≠ error
Khác 0 không luôn là lỗi —
grepvàdiffCó chương trình trả số khác 0 mà vẫn ổn Nonzero isn't always an error —grepanddiffĐây là ngoại lệ Ward dặn kỹ: một số chương trình như
diffvàgrepdùng mã thoát khác 0 để biểu thị tình trạng bình thường.greptrả 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êngrepvàdiffdùng mã thoát 2 khi gặp trục trặc thật sự.1Vớ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
diffandgrep, use nonzero exit codes to indicate normal conditions.grepreturns 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 — sogrepanddiffuse exit code 2 when they encounter an actual problem.1Cá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ởmancủ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 Trangdiff(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
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
-
[ · 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 itVì phép thử sau
iflà một lệnh, dấu chấm phẩy trongif [ $1 = hi ]; thenchỉ là dấu hiệu kết thúc lệnh bình thường của shell — nó có mặt vì ta đặtthentrên cùng dòng. Không có nó, shell truyềnthenlà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 đặtthenxuống dòng riêng.1Dấu
;chỉ có nghĩa là “hết câu lệnh”. Quên nó thì máy tưởng chữthenlà 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
ifis a command, the semicolon inif [ $1 = hi ]; thenis just the regular shell marker for the end of a command — it's there because we putthenon the same line. Without it, the shell passesthenas a parameter to[, which often results in an error that isn't easy to track.1 You can avoid the semicolon by puttingthenon its own line.1Hệ 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. Becausetestis 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.1Chí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ảntestvà/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.8Vì 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. -
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:
$1có 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.1Nế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:
$1could be empty, because the user might run the script with no parameters. The test then reads[ = hi ], and the[command aborts with an error.1Hai 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ểnif [ x"$1" = x"hi" ]; then.1 Chỉ cần bọc$1trong dấu nháy kép là xong. Enclose the parameter in quotes:if [ "$1" = hi ]; then— or use the classic defensive formif [ 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. -
&& · || · !
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ạycommand1, và nếu mã thoát là 0 thì chạy tiếpcommand2. 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 runscommand1, and if the exit code is 0 it also runscommand2.||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.1Phủ đị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,-ovà một cảnh báo đáng đọcWard cho biết nếu điều kiện của bạn có lệnh
test([), bạn có thể dùng-avà-othay 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-avà-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 Trangtest(1)nói thẳng hơn: “Binary-aand-oare ambiguous. Usetest EXPR1 && test EXPR2ortest EXPR1 || test EXPR2instead.”4 Bản thân Ward cũng đưa ví dụ dạng an toàn hơnif [ "$1" = hi ] || [ "$1" = bye ]; thenngay trước đó.1 -
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.1Bạ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).1Bảng của tác giả, rút gọnThe author's tables, condensed Kiểu tệp:-ftệp thường,-dthư mục,-hliên kết mềm,-bthiết bị khối,-cthiết bị ký tự,-pống có tên,-Ssocket. Quyền:-rđọc được,-wghi được,-xchạy được,-usetuid,-gsetgid,-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:-fregular file,-ddirectory,-hsymbolic link,-bblock device,-ccharacter device,-pnamed pipe,-Ssocket. Permissions:-rreadable,-wwritable,-xexecutable,-usetuid,-gsetgid,-k"sticky". Plus two important tests:-e(file exists) and-s(file is not empty).1Liê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ếulinktrỏ tới một tệp thường thì[ -f link ]trả mã thoát đúng (0).1 Tài liệutest(1)phát biểu y hệt và bổ sung rằng-Lđồng nghĩa với-h.4Dấ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.14Phé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:-ztrả đúng nếu đối số rỗng ([ -z "" ]trả 0),-ntrả đú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ếufile1có ngày sửa mới hơnfile2,-otlàm điều ngược lại, và-efso 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,-otvà-efkhông được đưa vào tiện íchtestchuẩn.9 -
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 codesTừ khoá
casetạ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$1vớ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ớiesac; (3) điều kiện kết thúc ởesac.1casegiố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
casekeyword 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$1against each case value demarcated with); (2) if one matches, the shell executes the commands below it until it hits;;, then skips toesac; (3) the conditional ends atesac.1Viết mẫu thế nàoHow to write the patterns Mỗi giá trịcasecó thể là một chuỗi đơn (bye), nhiều chuỗi nối bằng|(hi|hellođúng nếu$1bằnghihoặchello), 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|hellois true if$1equalshiorhello), 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.1Mẫ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 tronggrepthì*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. -
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:
forvàwhile. Vòngfor— 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 sauin, chạy các lệnh giữadovàdone, rồi quay lại đặt giá trị kế tiếp cho tới khi hết.1 Vòngwhilecủa Bourne shell dùng mã thoát, giống hệt điều kiệnif.1forlà “với mỗi món trong danh sách, làm việc này”.whilelà “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:
forandwhile. Theforloop — really a "for each" loop — is the most common: the shell sets the variable to the first of the space-delimited values followingin, runs the commands betweendoanddone, then goes back and takes the next value until the list is exhausted.1 The Bourne shell'swhileloop uses exit codes, exactly like theifconditional.1Lờ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òngwhilebằngbreak; Bourne shell còn có vònguntilhoạ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ớiwhilevàuntilquá thường xuyên — nếu thấy mình cầnwhile, 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 awhileloop withbreak; the Bourne shell also has anuntilloop that works just likewhile, except it breaks the loop on a zero exit code rather than a nonzero one. But Ward says it plainly: you shouldn't needwhileanduntilvery often — if you find that you needwhile, you should probably be using a language more appropriate to the task, such as Python or awk.1Ví dụ
whilecủa tác giả, đọc kỹKịch bản mười vòng lặp của ông đặt
FILE=/tmp/whiletest.$$, ghifirstlinevào tệp, rồi lặp chừng nàotail -10 $FILE | grep -q firstlinecòn trả mã 0 — mỗi vòng thêm một dòng mới. Ngay khifirstlinekhô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 raTrong 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ý dofor 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 -
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 lineBourne 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ế.1Bì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ớixargsthay 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 fromfind, consider a pipeline toxargsrather than command substitution, or use the-execoption.1Backtick 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ệ.2Hai 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ị
IFSvà 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
-
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ưmktempthường là lựa chọn tốt hơn.1 Đối số củamktemplà một khuôn mẫu: lệnh này chuyểnXXXXXXthành một bộ ký tự duy nhất và tạo một tệp rỗng mang tên đó.1Khi 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
mktempsẽ 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 asmktempis often the better option.1 The argument tomktempis a template: the command converts theXXXXXXinto a unique set of characters and creates an empty file with that name.1Ví dụ của tác giảThe author's example Kịch bản của ông chụp/proc/interruptsvào$(mktemp /tmp/im1.XXXXXX),sleep 2, chụp lần hai, rồidiffhai ả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/interruptsinto$(mktemp /tmp/im1.XXXXXX), sleeps 2 seconds, captures again, thendiffs 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.1Bẫ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
catthứ hai sẽ để lại một tệp trong/tmp. Ward đưa cách xử lý: dùng lệnhtrapđể 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ùngexittrong 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.1Hai 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ế:mktemplà 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 Trangmktemp(1)còn nói rõ khuôn mẫu phải chứa ít nhất ba ký tựXliê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..1traptheo chuẩnPOSIX mô tả
traplà một special built-in: điều kiện có thể làEXIT,0(tương đươngEXIT), 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 choSIGKILLhaySIGSTOPcho kết quả không xác định.2 Đó là lý do handler của Ward viếtINTchứ không phảiSIGINT. -
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ệnhecholiề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 fourechocommands in a row — or when using double quotes for a large amount of text, which is exactly where Ward suggests a here document.1Tắ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 -
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
basenamedù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 .htmllẫnbasename /usr/local/bin/exampleđều trả vềexample.1awkkhô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ùngawknay 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ểuls -l | awk '{print $5}'để in trường thứ năm (kích thước tệp).1sed(“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.1Ba con dao nhỏ:
basenamecắt tên tệp cho gọn;awklấy đúng cột bạn cần trong một bảng;sedthay 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ữ.basenameis for stripping an extension from a filename or getting rid of the directories in a full pathname: bothbasename example.html .htmlandbasename /usr/local/bin/examplereturnexample.1awkis 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 inls -l | awk '{print $5}'to print the fifth field (the file size).1sed("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.1Cú phápsedtối thiểuMinimum viablesedNói chungsednhận một địa chỉ và một thao tác:sed 's/:/%/' /etc/passwdthay dấu hai chấm đầu tiên của mỗi dòng bằng%; thêm bổ từgthành's/:/%/g'để thay tất cả. Địa chỉ cũng có thể là khoảng dòng —sed 3,6d /etc/passwdxoá 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ớpexp. Bỏ địa chỉ thìsedtác động lên mọi dòng. Ward nói hai thao tác phổ biến nhất có lẽ làsvàd.1 Nhớ hai chữ thôi:slà thay,dlà xoá. In generalsedtakes an address and an operation:sed 's/:/%/' /etc/passwdreplaces the first colon on each line with%; add thegmodifier —'s/:/%/g'— to replace them all. The address can be a line range —sed 3,6d /etc/passwddeletes lines three through six — or a regular expression:sed '/exp/d'deletes every line matchingexp. Omit the address andsedoperates on every line. Ward says the two most common operations are probablysandd.1Mẫu kịch bản
basenamecủ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ồib=$(basename $file .gif)trước khi chạygiftopnm $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ả
sedlẫnawk, 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âusednằm ngoài phạm vi sách — đây là chương nhập môn.1Không có tệp thì đọc từ đâu?
Ward ghi: trong mọi ví dụ trên,
sedghi 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 Trangsed(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 -
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úcxargs— when the file list won't fitKhi 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.
xargsgiả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ớifind:find . -name '*.gif' -print | xargs fileđể kiểm tra mọi tệp đuôi.giftrong cây thư mục hiện tại có thật là ảnh GIF không.1Nếu danh sách tệp dài quá, máy không bê hết một lượt được.
xargschia 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.
xargsgets around this by running a command on each filename in its standard input stream.1 The classic pairing is withfind:find . -name '*.gif' -print | xargs file, to verify that every.giffile in the current directory tree really is a GIF image.1Cả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ạngfind . -name '*.gif' -print0 | xargs -0 file, đổi dấu phân cách đầu ra củafindvà dấu phân tách đối số củaxargstừ 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-print0và-0cho 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, usefind . -name '*.gif' -print0 | xargs -0 fileinstead, changing the find output separator and the xargs argument delimiter from a newline to a NULL character.1Tài liệu nói gì về
-0Trang
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-print0của GNUfindsinh ra đúng dạng đầu vào phù hợp.14 Trangfind(1)đi xa hơn một bước và khuyên bạn nên cân nhắc dùng-print0nếu đầu ra sắp được một chương trình khác đọc.15Hai chi tiết nữa của Ward
Thứ nhất:
xargskhở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ệnhxargsnế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ợ.1Lựa chọn thay thế:
-execKhi đang dùng
find, có một cách thay choxargs: 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 -
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
exprgiú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 + 2in 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.1execlà 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ốngexec()đã 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.1exprbiết cộng trừ, nhưng chậm và lóng ngóng — cần tính nhiều thì đổi công cụ.execthì đặ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.exprhelps with arithmetic in shell scripts (and some string operations):expr 1 + 2prints 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.1execis a built-in shell feature that replaces the current shell process with the program you name after it; it carries out theexec()system call described in Chapter 1. The feature is designed for saving system resources — but remember there is no return.1Thí nghiệm của tác giảThe author's experiment Thử chạyexec cattrong một cửa sổ shell. Sau khi bạn bấm Ctrl-D hoặc Ctrl-C để kết thúccat, 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ạyexectrong 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 catrồi thoát — cả cửa sổ đóng luôn. Đó là vì shell cũ đã bị thay mất rồi. Try runningexec catin a shell window. After you press Ctrl-D or Ctrl-C to terminatecat, the window should disappear, because its child process no longer exists.1 Runningexecin a script does the same: the script and the shell running it are gone, replaced by the new command.1Cù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ócommandthì 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ọnexecfailđượ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”:
exprthoá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 - 1thoát với mã 1 dù nó tính đúng. -
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ệnhreadđọ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.1Ba 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ệnhreadhỏ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, thereadcommand reads a line of text from standard input and stores it in a variable:read var.1Phâ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.1Ba ví dụ subshell của tác giả
Thêm một thành phần vào
PATHmà 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ơncp -r.1Cảnh báo của chính tác giả
Với lệnh
tarkiể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ả-eflẫn-ađều nằm ngoài (hoặc bị đánh dấu lỗi thời trong) chuẩn POSIX.9Chuẩ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 theoPATHkhô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
-
Vì sao
echo $100vàecho "$100"cùng in ra00, cònecho '$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.
-
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?
-
$?khác$$ở chỗ nào, và vì sao chạyecho $?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?
-
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 để
thentrên cùng dòng? -
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.
-
casekhácifở đ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ánhcasecó cùng nghĩa với*trong một biểu thức chính quy không? -
Vì sao
find … -print0 | xargs -0an toàn hơnfind … -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?
-
Sau khi chạy
exec cattrong một cửa sổ terminal và thoátcat, 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?
-
Sourcing một tệp bằng
. config.shkhá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?
-
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ế?
Dòng đầu tiên của một tệp kịch bản viết gì, và để làm gì?
Nháy đơn và nháy kép khác nhau chỗ nào?
Số 0 mà chương trình để lại khi chạy xong nghĩa là gì?
$1là gì? Còn$#?Vòng lặp
forlàm gì với một danh sách?Vì sao nên dọn tệp nháp sau khi dùng xong?
Khi nào thì nên ngừng viết kịch bản shell và đổi công cụ?
-
Why do
echo $100andecho "$100"both print00, whileecho '$100'does not? -
Besides the executable bit, which other permission bit does a script file need — and why?
-
How does
$?differ from$$, and why does runningecho $?twice in a row always give 0 the second time? -
In
if [ "$1" = hi ]; then, which parts are shell keywords and which part is a program? -
Why is
[ 01 = 1 ]false while[ 01 -eq 1 ]is true? -
What is the fundamental difference between
caseandifwith respect to exit codes? -
Why is
find … -print0 | xargs -0safer thanfind … -print | xargs? -
After running
exec catin a terminal window and quittingcat, why does the window disappear? -
How does sourcing a file with
. config.shdiffer from running./config.sh? -
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