Học
Perl
Randal L. Schwartz
Người dịch: Ngô Trung Việt
Hà Nội 5/1999
Learning
Perl
Randal L. Schwartz
O’Reilly & Associates, Inc., 1993
i
Mục lục
1 Giới thiệu .................................................................. 1
Lịch sử Perl ................................................................. 1
Mục đích của Perl ....................................................... 3
Tính sẵn có .............................................................. 3
Hỗ trợ ...................................................................... 5
Các khái niệm cơ bản .................................................. 6
Dạo qua Perl ................................................................ 9
Chương trình “Xin chào mọi người” .................... 10
Hỏi câu hỏi và nhớ kết quả ................................... 11
Bổ sung chọn lựa .................................................. 12
Đoán từ bí mật ...................................................... 13
Nhiều từ bí mật ..................................................... 14
Cho mỗi người một từ bí mật khác nhau .............. 16
Giải quyết dạng thức cái vào thay đổi .................. 19
Làm cho công bằng với mọi người ....................... 21
Làm cho nó mô đun hơn một chút ........................ 24
Chuyển danh sách từ bí mật vào tệp riêng biệt ..... 28
Đảm bảo một lượng an toàn giản dị ...................... 33
Cảnh báo ai đó khi mọi việc đi sai ........................ 34
Nhiều tệp từ bí mật trong danh mục hiện tại ........ 36
Nhưng chúng ta biết họ là ai! ................................ 38
Liệt kê các từ bí mật .............................................. 40
ii
Làm cho danh sách từ cũ đó đáng lưu ý hơn ........ 44
Duy trì cơ sở dữ liệu đoán đúng cuối cùng ........... 45
Chương trình cuối cùng ........................................ 48
Bài tập ....................................................................... 51
2 Dữ liệu vô hướng .................................................... 53
Dữ liệu vô hướng là gì? ............................................ 53
Số .......................................................................... 54
Tất cả các số đã có cùng dạng thức bên trong ...... 54
Hằng kí hiệu động ................................................. 54
Hằng kí hiệu nguyên ............................................. 55
Xâu ............................................................................ 56
Xâu dấu nháy đơn ................................................. 56
Xâu dấu nháy kép ................................................. 57
Toán tử ...................................................................... 59
Toán tử số ............................................................. 59
Toán tử xâu ........................................................... 61
Thứ tự ưu tiên và luật kết hợp của toán tử ............ 63
Chuyển đổi giữa số và xâu .................................... 66
Các toán tử trên biến vô hướng ............................. 68
Toán tử gán hai ngôi ............................................. 69
Tự tăng và tự giảm ................................................ 71
Toán tử chop() ....................................................... 72
Xen lẫn vô hướng vào trong xâu ........................... 73
<STDIN> xem như một vô hướng ....................... 75
Đưa ra bằng print() ............................................... 76
Giá trị undef ......................................................... 77
Bài tập ....................................................................... 78
3 Dữ liệu mảng và danh sách ................................... 79
iii
Mảng là gì? ............................................................... 79
Biểu diễn hằng kí hiệu .......................................... 80
Biến ....................................................................... 81
Toán tử .................................................................. 82
Phép gán ................................................................ 82
Truy nhập phần tử ................................................. 85
Các toán tử push() và pop() .................................. 88
Các toán tử shift() và unshift() .............................. 89
Toán tử reverse() ................................................... 89
Toán tử sort() ........................................................ 90
Toán tử chop() ....................................................... 90
Hoàn cảnh vô hướng và mảng .............................. 91
<STDIN> như một mảng ...................................... 92
Xen lẫn biến mảng ................................................ 92
Bài tập ....................................................................... 94
4 Cấu trúc điều khiển ................................................ 97
Khối câu lệnh ............................................................ 97
Câu lệnh if/unless .................................................. 98
Câu lệnh while/until ............................................ 101
Câu lệnh for ........................................................ 103
Câu lệnh foreach ................................................. 104
Bài tập ..................................................................... 106
5 Mảng kết hợp........................................................ 109
Mảng kết hợp là gì? ................................................ 109
Biến mảng kết hợp .............................................. 110
Biểu diễn hằng kí hiệu cho mảng kết hợp ........... 111
Các toán tử mảng kết hợp ....................................... 112
iv
Toán tử keys() ..................................................... 112
Toán tử values() .................................................. 113
Toán tử each() ..................................................... 114
Toán tử delete ..................................................... 114
Bài tập ..................................................................... 115
6 Vào / ra cơ bản ..................................................... 117
Vào từ STDIN ......................................................... 117
Đưa vào từ toán tử hình thoi ............................... 119
Đưa ra STDOUT ..................................................... 120
Dùng print cho đưa ra thông thường ................... 120
Dùng printf cho cái ra có dạng thức .................... 121
Bài tập ..................................................................... 122
7 Biểu thức chính qui .............................................. 123
Khái niệm về biểu thức chính qui ........................... 123
Cách dùng đơn giản về biểu thức chính qui........ 124
Khuôn mẫu .............................................................. 126
Khuôn mẫu một kí tự .......................................... 127
Khuôn mẫu nhóm ................................................ 129
Dấu ngoặc tròn như bộ nhớ................................. 132
Thay phiên .......................................................... 134
Khuôn mẫu neo ................................................... 134
Thứ tự ưu tiên.......................................................... 136
Thêm về toán tử đối sánh ........................................ 137
Chọn một mục tiêu khác (toán tử =~) ................. 137
Bỏ qua chữ hoa thường ....................................... 138
Dùng một định biên khác .................................... 139
v
Dùng xen lẫn biến ............................................... 140
Biến chỉ đọc đặc biệt ........................................... 141
Thay thế .................................................................. 142
Các toán tử split() và join() ..................................... 144
Toán tử split() ..................................................... 144
Toán tử join() ...................................................... 146
Bài tập ..................................................................... 146
8 Hàm ....................................................................... 149
Hàm hệ thống và hàm người dùng .......................... 149
Xác định một hàm người dùng ........................... 149
Gọi một hàm người dùng .................................... 151
Giá trị cho lại ...................................................... 152
Đối ...................................................................... 153
Biến cục bộ trong hàm ........................................ 156
Bài tập ..................................................................... 159
9 Các cấu trúc điều khiển khác .............................. 161
Toán tử last ......................................................... 161
Toán tử next ........................................................ 163
Toán tử redo ........................................................ 164
Khối có nhãn ....................................................... 165
Bộ thay đổi biểu thức .......................................... 167
&&, || và ?: xem như các cấu trúc điều khiển ..... 169
Bài tập ..................................................................... 171
10 Tước hiệu tệp và kiểm thử tệp .......................... 173
Tước hiệu tệp là gì?................................................. 173
vi
Mở và đóng một tước hiệu tệp ................................ 174
Một chút tiêu khiển: die() ................................... 175
Dùng tước hiệu tệp .................................................. 177
Kiểm tra tệp -x .................................................... 178
Các toán tử stat() và lstat() .................................. 183
Dùng _Filehandle ................................................ 184
Bài tập ..................................................................... 185
11 Dạng thức ............................................................ 187
Dạng thức là gì? ...................................................... 187
Định nghĩa một dạng thức ................................... 188
Gọi một dạng thức .............................................. 191
Nói thêm về nơi giữ tệp ...................................... 193
Trường văn bản ................................................... 194
Trường số ............................................................ 194
Trường nhiều dòng.............................................. 196
Trường được lấp đầy ........................................... 196
Dạng thức đầu trang ............................................ 200
Thay đổi mặc định cho dạng thức ........................... 201
Dùng select() để thay đổi tước hiệu tệp .............. 201
Thay đổi tên dạng thức ....................................... 203
Đổi tên dạng thức đầu trang ................................ 204
Đổi chiều dài trang .............................................. 205
Thay đổi vị trí trên trang ..................................... 206
Bài tập ..................................................................... 207
12 Truy nhập danh mục ......................................... 209
Chuyển vòng quanh cây danh mục ......................... 209
Globbing ................................................................. 210
vii
Tước hiệu danh mục ........................................... 213
Mở và đóng tước hiệu danh mục ........................ 214
Đọc một tước hiệu danh mục .............................. 215
Bài tập ..................................................................... 216
13 Thao tác tệp và danh mục ................................. 217
Loại bỏ tệp .............................................................. 217
Đổi tên tệp ............................................................... 219
Tạo ra tên thay phiên cho một tệp (móc nối) ...... 220
Về móc nối cứng và mềm ................................... 220
Tạo ra các móc nối cứng và mềm bằng Perl ....... 222
Tạo ra và xoá danh mục ...................................... 224
Thay đổi phép sử dụng ........................................ 225
Thay đổi quyền sở hữu........................................ 226
Thay đổi nhãn thời gian ...................................... 227
Bài tập ..................................................................... 229
14 Quản lí tiến trình ................................................ 230
Dùng system() và exec() ......................................... 230
Dùng dấu nháy đơn ngược .................................. 235
Dùng các tiến trình như tước hiệu tệp ................. 236
Dùng fork ............................................................ 238
Tóm tắt về các phép toán tiến trình ..................... 241
Gửi và nhận tín hiệu ............................................ 243
Bài tập ..................................................................... 246
15 Biến đổi dữ liệu khác ......................................... 249
Tìm một xâu con ..................................................... 249
Trích và thay thế một xâu con............................. 251
viii
Dạng thức dữ liệu bằng sprintf() ......................... 254
Sắp xếp nâng cao .................................................... 254
Chuyển tự ................................................................ 260
Bài tập ..................................................................... 263
16 Truy nhập cơ sở dữ liệu hệ thống ..................... 265
Lấy mật hiệu và thông tin nhóm ............................. 265
Gói và mở dữ liệu nhị phân..................................... 270
Lấy thông tin mạng ................................................. 273
Lấy các thông tin khác ............................................ 275
Bài tập ..................................................................... 276
17 Thao tác cơ sở dữ liệu người dùng .................... 277
Cơ sở dữ liệu DBM và mảng DBM ........................ 277
Mở và đóng mảng DBM ..................................... 278
Dùng mảng DBM ................................................ 280
Cơ sở dữ liệu truy nhập ngẫu nhiên chiều dài cố
định ..................................................................... 281
Cơ sở dữ liệu (văn bản) chiều dài thay đổi ......... 284
Bài tập ..................................................................... 287
18 Chuyển đổi các ngôn ngữ khác sang Perl ......... 289
Chuyển chương trình awk sang Perl ................... 289
Chuyển đổi chương trình sed sang Perl .............. 291
Chuyển đổi chương trình Shell sang Perl ........... 292
Bài tập ..................................................................... 293
Phụ lục A Trả lời các bài tập .................................. 295
ix
Chương 2, Dữ liệu vô hướng .............................. 295
Chương 3, Mảng và dữ liệu danh sách ............... 297
Chương 4, Cấu trúc điều khiển ........................... 299
Chương 5, Mảng kết hợp .................................... 302
Chương 6, Vào/ra cơ sở ...................................... 305
Chương 7, Biểu thức chính qui ........................... 307
Chương 8, Hàm ................................................... 311
Chương 9, Các cấu trúc điều khiển khác ............ 314
Chương 10, Tước hiệu tệp và kiểm thử tệp ........ 315
Chương 11, Dạng thức ........................................ 317
Chương 12, Truy nhập danh mục ....................... 319
Chương 13, Thao tác tệp và danh mục ............... 320
Chương 14, Quản lí tiến trình ............................. 323
Chương 15, Biến đổi dữ liệu khác ...................... 326
Chương 16, Truy nhập cơ sở dữ liệu hệ thống ... 329
Chương 17, Thao tác cơ sở dữ liệu người dùng.. 330
Chương 18, Chuyển các ngôn ngữ khác sang Perl
............................................................................ 332
Phụ lục B Cơ sở về nối mạng .................................. 333
Mô hình khe cắm ................................................ 333
Một khách mẫu ................................................... 335
Bộ phục vụ mẫu .................................................. 335
Phụ lục C Các chủ đề còn chưa nói tới .................. 337
Trình gỡ lỗi ......................................................... 337
Dòng lệnh ............................................................ 338
Các toán tử khác .................................................. 338
Nhiều, nhiều hàm nữa ......................................... 338
Nhiều, nhiều biến định nghĩa sẵn ........................ 338
Xâu ở đây ............................................................ 338
Trở về (từ trình con)............................................ 339
x
Toán tử eval (và s///e) ......................................... 339
Thao tác bảng kí hiệu bằng *FRED .................... 340
Toán tử goto ........................................................ 340
Toán tử require .................................................... 341
Thư viện .............................................................. 341
Tin vui về Perl 5.0 ............................................... 341
1
1
Giới thiệu
Lịch sử Perl
Perl là cách viết tắt cho “Practical Extraction and
Report Language” Ngôn ngữ báo cáo và trích rút thực
hành, mặc dầu nó cũng còn được gọi là “Pathologically
Eclectic Rubbish Lister” - Bộ liệt kê rác điện tử bệnh
hoạn. Chẳng ích gì mà biện minh xem cách gọi nào đúng
hơn, vì cả hai đều được Larry Wall, người sáng tạo và
kiến trúc sư chính, người cài đặt và bảo trì của Perl chấp
nhận. Ông ấy đã tạo ra Perl khi cố gắng sản xuất ra một
số báo cáo từ một cấp bậc các tệp kiểu như thư người
dùng mạng Usenet ở hệ thống báo lỗi, và lệnh awk làm
xì hết hơi. Larry, một người lập trình lười biếng, quyết
định thanh toán vấn đề này bằng một công cụ vạn nóng
mà ông có thể dùng ít nhất cũng ở một nơi khác. Kết quả
là bản đầu tiên của Perl.
Sau khi chơi với bản đầu này của Perl một chút,
thêm chất liệu đây đó, Larry đưa nó cho cộng đồng độc
Trong chương này: Lịch sử Perl Mục đích của Perl Có sẵn Hỗ trợ Các khái niệm cơ bản
Dạo qua về Perl
2
giả Usenet, thường vẫn được gọi là “the Net”. Người
dùng thuộc toán người phù du nghèo khó về hệ thống
trên toàn thế giới (quãng độ chục nghìn người) đưa lại
cho anh ấy phản hồi, hỏi cách làm thế này thế kia, việc
này việc khác, nhiều điểm mà Larry chưa bao giờ mường
tượng ra về việc giải quyết cho Perl nhỏ bé của mình cả.
Nhưng kết quả là Perl trưởng thành, trưởng thành và
trưởng thành thêm nữa, và cũng cùng tỉ lệ như lõi của
UNIX. (với bạn là người mới, toàn bộ lõi UNIX được
dùng chỉ khít vào trong 32K! Và bây giờ chúng ta may
mắn nếu ta có thể có được nó dưới một vài mega.) Nó đã
trưởng thành trong các tính năng. Nó đã trưởng thành
trong tính khả chuyển. Điều mà có thời là một ngôn ngữ
tí tẹo bây giờ đã cả tài liệu sử dụng 80 trang, một cuốn
sách của Nutshell 400 trang, một nhóm tin Usenet với 40
nghìn thuê bao, và bây giờ là đoạn giới thiệu nhẹ nhàng
này.
Larry vẫn là người bảo trì duy nhất, làm việc trên
Perl ngoài giờ khi kết thúc công việc thường ngày của
mình. Và Perl thì vẫn phát triển.
Một cách đại thể lúc mà cuốn sách này đạt tới điểm
dừng của nó, Larry sẽ đưa ra bản Perl mới nhất, bản 5.0,
hứa hẹn có một số tính năng thường hay được yêu cầu,
và được thiết kế lại từ bên trong trở ra. (Larry bảo tôi
rằng không còn mấy dòng lệnh từ lần đưa ra trước, và số
ấy cứ ngày càng ít đi mỗi ngày.) Tuy nhiên, cuốn sách
này đã được thử với Perl bản 4.0 (lần đưa ra gần đây
nhất khi tôi viết điều này). Mọi thứ ở đây đó sẽ làm việc
với bản 5.0 và các bản sau của Perl. Thực ra, chương
trình Perl 1.0 vẫn làm việc tốt với những bản gần đây,
ngoại trừ một vài thay đổi là cần cho sự tiến bộ.
3
Mục đích của Perl
Perl được thiết kế để trợ giúp cho người dùng UNIX
với những nhiệm vụ thông dụng mà có thể rất nặng nề
hay quá nhạy cảm với tính khả chuyển đối với trình vỏ,
và cũng quá kì lạ hay ngắn ngủi hay phức tạp để lập trình
trong C hay một ngôn ngữ công cụ UNIX nào khác.
Một khi bạn trở nên quen thuộc với Perl, bạn có thể
thấy mình mất ít thời gian để lấy được trích dẫn trình vỏ
(hay khai báo C) đúng, và nhiều thời gian hơn để đọc tin
trên Usenet và đi trượt tuyết trên đồi; vì Perl là một công
cụ lớn tựa như chiếc đòn bẩy. Các cấu trúc chặt chẽ của
Perl cho phép bạn tạo ra (với tối thiểu việc làm ầm ĩ) một
số giải pháp có ưu thế rất trầm lặng hay những công cụ
tổng quát. Cũng vậy, bạn có thể lôi những công cụ này
sang công việc tiếp, vì Perl là khả chuyển cao độ và lại
có sẵn, cho nên bạn sẽ có nhiều thời gian hơn để đọc tin
Usenet và trượt tuyết.
Giống như mọi ngôn ngữ, Perl có thể “chỉ viết” - tức
là có thể viết ra chương trình mà không thể nào đọc
được. Nhưng với chú ý đúng đắn, bạn có thể tránh được
kết tội thông thường này. Quả thế, đôi khi Perl trông như
nổi tiếng với những cái không quen thuộc, nhưng với
người lập trình đã thạo Perl, nó tựa như những dòng có
tổng kiểm tra với một sứ mệnh trong cuộc đời. Nếu bạn
tuân theo những hướng dẫn của cuốn sách này thì
chương trình của bạn sẽ dễ đọc và dễ bảo trì, và chúng ta
có lẽ sẽ thắng trong bất kì cuộc tranh luận Perl khó hiểu
nào.
Tính sẵn có
Nếu bạn nhận được
4
Perl: not found
khi bạn thử gọi Perl từ lớp vỏ thì người quản trị hệ thống
của bạn cũng chẳng lên cơn sốt. Nhưng thậm chí nếu nó
không có trên hệ thống của bạn, thì bạn vẫn có thể lấy
được nó không mất tiền (theo nghĩa “ăn trưa không mất
tiền”).
Perl được phân phối theo giấy phép công khai GNU,
nghĩa là thế này, “bạn có thể phân phát chương trình nhị
phân Perl chỉ nếu bạn làm cho chương trình gốc có sẵn
cho mọi người dùng không phải trả tiền gì cả, và nếu bạn
sửa đổi Perl, bạn phải phân phát chương trình gốc của
bạn cho nơi sửa đổi của bạn nữa.” Và đó là bản chất của
cho không. Bạn có thể lấy chương trình gốc của Perl với
giá của một bảng trắng hay vài mêga byte qua đường
dây. Và không ai có thể khoá Perl và bán cho bạn chỉ mã
nhị phân với tư tưởng đặc biệt về “cấu hình phần cứng
được hỗ trợ.”
Thực ra, nó không chỉ là cho không, nhưng nó chạy
còn gọn hơn trên gần như mọi thứ mà có thể gọi là
UNIX hay tựa UNIX và có trình biên dịch C. Đấy là vì
bộ trình này tới với bản viết cấu hình bí quyết được gọi
là Cấu hình, cái sẽ móc và chọc vào các danh mục hệ
thống để tìm những thứ nó cần, và điều chỉnh việc đưa
vào các tệp và các kí hiệu được xác định tương ứng,
chuyển cho bạn việc kiểm chứng phát hiện của nó.
Bên cạnh các hệ thống UNIX hay tựa UNIX, người
đã bị nghiện Perl đem nó sang Amiga, Atari ST, họ
Macintosh, VMS, OS/2, thậm chí MS/DOS - và có lẽ
còn nhiều hơn nữa vào lúc bạn đọc cuốn sách này. vị trí
chính xác và sự có sẵn của những bản Perl này thì biến
5
động, cho nên bạn phải hỏi quanh (trên nhóm tin Usenet
chẳng hạn) để có được thông tin mới nhất. Nếu bạn hoàn
toàn không biết gì, thì một bản cũ của Perl đã có trên đĩa
phần mềm CD-ROM UNIX Power Tools, của Jerry Peek,
Tim O’Reilly và Mike Loukides (O’Reilly & Associates/
Random House Co., 1993).
Hỗ trợ
Perl là con đẻ của Larry Wall, và vẫn đang được anh
ấy nâng niu. Báo cáo lỗi và yêu cầu nâng cao nói chung
đã được sửa chữa trong các lần đưa ra sau, nhưng anh ấy
cũng chẳng có nghĩa vụ nào để làm bất kì cái gì với
chúng cả. Tuy thế Larry thực sự thích thú nghe từ tất cả
chúng ta, và cũng làm việc thực sự để thấy Perl được
dùng trên qui mô thế giới. E-mail trực tiếp cho anh ấy
nói chung đã nhận được trả lời (cho dù đấy chỉ đơn thuần
là máy trả lời email của anh ấy), và đôi khi là sự đáp ứng
con người.
Ích lợi hơn việc viết thư trực tiếp cho Larry là nhóm
hỗ trợ Perl trực tuyến toàn thế giới, liên lạc thông qua
nhóm tin Usenet comp.lang.perl. Nếu bạn có thể gửi
email trên Internet, nhưng chưa vào Usenet, thì bạn có
thể tham gia nhóm này bằng cách gửi một yêu cầu tới
[email protected], yêu cầu sẽ tới một
người có thể nối bạn với cửa khẩu email hai chiều trong
nhóm, và cho bạn những hướng dẫn về cách làm việc.
Khi bạn tham gia một nhóm tin, bạn sẽ thấy đại loại
có khoảng 30 đến 60 “thư” mỗi ngày (vào lúc bản viết
này được soạn thảo) trên đủ mọi chủ đề từ câu hỏi của
người mới bắt đầu cho tới vấn đề chuyển chương trình
6
phức tạp và vấn đề giao diện, và thậm chí cả một hay hai
chương trình khá lớn.
Nhóm tin gần như được những chuyên gia Perl điều
phối. Phần lớn thời gian, câu hỏi của bạn đã có trả lời
trong vòng vài phút khi tin của bạn tới đầu nối Usenet
chính. thử mức độ hỗ trợ từ nhà sản xuất phần mềm
mình ưa chuộng về việc cho không này! Bản thân Larry
cũng đọc về nhóm khi thời gian cho phép, và đôi khi đã
xen các bài viết có thẩm quyền vào để chấm dứt việc cãi
nhau hay làm sáng tỏ một vấn đề. Sau rốt, không có
Usenet, có lẽ không thể có chỗ để dễ dàng công bố Perl
cho cả thế giới.
Bên cạnh nhóm tin, bạn cũng nên đọc tạp chí Perl, đi
cùng việc phân phối Perl. Một nguồn có thẩm quyền
khác là cuốn sách Programming Perl của Larry Wall và
Randal L. Schwatrz (O’Reilly & Associates, 1990).
Programming Perl được xem như “Sách con lạc đà” vì
bìa của nó vẽ con vật này (hệt như cuốn sách này có lẽ sẽ
được biết tới với tên sách lạc đà không bướu). Sách con
lạc đà chứa thông tin tham chiếu đầy đủ về Perl dưới
dạng đóng gọn gàng. Sách con lạc đà cũng bao gồm một
bảng tra tham chiếu bật ra tài tình mà chính là nguồn ưa
chuộng của cá nhân tôi về thông tin Perl.
Các khái niệm cơ bản
Một bản viết vỏ không gì khác hơn là một dãy các
lệnh vỏ nhồi vào trong một tệp văn bản. Tệp này “được
làm cho chạy” bằng cách bật một bit thực hiện (qua
chmod +x filename) và rồi gõ tên của tệp đó vào lời nhắc
7
của vỏ. Bingo, một chương trình vỏ lớn. Chẳng hạn, một
bản viết để chạy chỉ lệnh date theo sau bởi chỉ lệnh who
có thể được tạo ra và thực hiện như thế này:
$ echo date > somecript $ echo who > somecript $ cat somescript date who $ chmod _x somescript $ somescript [output of date followed by who] $
Tương tự như thế, một chương trình Perl là một bộ
các câu lệnh và định nghĩa Perl được ném vào trong một
tệp. Rồi bạn bật bit thực hiện và gõ tên của tệp này tại lời
nhắc của vỏ. Tuy nhiên, tệp này phải chỉ ra rằng đây là
một chương trình Perl và không phải là chương trình vỏ,
nên chúng ta cần một bước phụ.
#! /usr/bin/perl
làm dòng đầu tiên của tệp này. Nhưng nếu Perl của
bạn bị kẹt vào một nơi không chuẩn, hay hệ điều hành
tựa UNIX của bạn không hiểu dòng #!, thì bạn có thêm
việc phải làm. Hỏi người cài đặt Perl về điều này. Các thí
dụ trong sách này giả sử rằng bạn dùng cơ chế thông
thường này.
Perl là một ngôn ngữ phi định dạng kiểu như C -
khoảng trắng giữa các hiệu bài (những phần tử của
chương trình, như print hay +) là tuỳ chọn, trừ phi hai
hiệu bài đi với nhau có thể bị lầm lẫn thành một hiệu bài
khác, trong trường hợp đó thì khoảng trắng thuộc loại
nào đó là bắt buộc. (Khoảng trắng bao gồm dấu cách,
8
dấu tab, dòng mới, về đầu dòng hay kéo giấy.) Có một
vài cấu trúc đòi hỏi một loại khoảng trắng nào đó ở chỗ
nào đó, nhưng chúng sẽ được trỏ ra khi ta nói tới chúng.
Bạn có thể giả thiết rằng loại và khối lượng khoảng trắng
giữa các hiệu bài là tuỳ ý trong các trường hợp khác.
Mặc dầu gần như tất cả các chương trình Perl đã có
thể được viết tất cả trên một dòng, một cách điển hình
chương trình Perl cũng hay được viết tụt lề như chương
trình C, với những phần câu lệnh lồng nhau được viết tụt
vào hơn so với phần bao quanh. Bạn sẽ thấy đầy những
thí dụ chỉ ra phong cách viết tụt lề điển hình trong toàn
bộ cuốn sách này.
Cũng giống như bản viết về vỏ, chương trình Perl
bao gồm tất cả các câu lệnh perl về tệp được lấy tổ hợp
chung như mọt trình lớn cần thực hiện. Không có khái
niệm về trình “chính” main như trong C.
Chú thích của Perl giống như chú thích của lớp vỏ
(hiện đại). Bất kì cái gì nằm giữa một dấu thăng (#) tới
cuối dòng đã là một chú thích. Không có khái niệm về
chú thích trên nhiều dòng như C.
Không giống hầu hết các lớp vỏ (nhưng giống như
awk và sed), bộ thông dịch Perl phân tích và biên dịch
hoàn toàn chương trình trước khi thực hiện nó. Điều này
có nghĩa là bạn không bao giờ nhận được lỗi cú pháp từ
chương trình một khi chương trình đã bắt đầu chạy, và
cũng có nghĩa là khoảng trắng và chú thích biến mất và
sẽ không làm chậm chương trình. Thực ra, giai đoạn biên
dịch này bảo đảm việc thực hiện nhanh chóng của các
thao tác Perl một khi nó được bắt đầu, và nó cung cấp
động cơ phụ để loại bỏ C như một ngôn ngữ tiện ích hệ
9
thống đơn thuần dựa trên nền tảng là C được biên dịch.
Việc biên dịch này không mất thời gian - sẽ là phi
hiệu quả nếu một chương trình Perl cực lớn lại chỉ thực
hiện một nhiệm vụ nhỏ bé chóng vánh (trong số nhiều
nhiệm vụ tiềm năng) và rồi ra, vì thời gian chạy cho
chương trình sẽ nhỏ xíu nếu so với thời gian dịch.
Cho nên Perl giống như một bộ biên dịch và thông
dịch. Nó là biên dịch vì chương trình được đọc và phân
tích hoàn toàn trước khi câu lệnh đầu tiên được thực
hiện. Nó là bộ thông dịch vì không có mã đích ngồi đâu
đó trút đầy không gian đĩa. Theo một cách nào đó, nó là
tốt nhất cho cả hai loại này. Phải thú thực, việc ẩn đi mã
đích đã dịch giữa những lời gọi thì hay, và đó là trong
danh sách mong ước của Larry cho Perl tương lai.
Dạo qua Perl
Chúng ta bắt đầu cuộc hành trình của mình qua Perl
bằng việc đi dạo một chút. Việc đi dạo này sẽ giới thiệu
một số các tính năng khác nhau bằng cách bổ sung vào
một ứng dụng nhỏ. Giải thích ở đây là cực kì ngắn gọn -
mỗi vùng chủ đề đã được thảo luận chi tiết hơn rất nhiều
về sau trong cuốn sách này. Nhưng cuộc đi dạo nhỏ này
sẽ cho bạn kinh nghiệm nhanh chóng về ngôn ngữ, và
bạn có thể quyết định liệu bạn có thực sự muốn kết thúc
cuốn sách này hay đọc thêm các tin Usenet hay chạy đi
chơi trượt tuyết.
10
Chương trình “Xin chào mọi người”
Ta hãy nhìn vào một chương trình nhỏ mà thực tế có
làm điều gì đó. Đây là chương trình “Xin chào mọi
người”:
#!/usr/bin/perl print “Xin chào mọi người\n”;
Dòng đầu tiên là câu thần chú nói rằng đây là
chương trình Perl. Nó cũng là lời chú thích cho Perl -
nhớ rằng lời chú thích là bất kì cái gì nằm sau dấu thăng
cho tới cuối dòng, giống như hầu hết các lớp vỏ hiện đại
hay awk.
Dòng thứ hai là toàn bộ phần thực hiện được của
chương trình này. Tại đây chúng ta thấy câu lệnh print.
Từ khoá print bắt đầu chương trình, và nó có một đối,
một xâu văn bản kiểu C. Bên trong xâu này, tổ hợp kí tự
\n biểu thị cho kí tự dòng mới; hệt như trong C. Câu lệnh
print được kết thúc bởi dấu chấm phẩy (;). Giống như C,
tất cả các câu lệnh đơn giản đã kết thúc bằng chấm
phẩy* .
Khi bạn gọi chương trình này, phần lõi sẽ gọi bộ
thông dịch Perl, phân tích câu toàn bộ chương trình (hai
dòng, kể cả dòng chú thích đầu tiên) và rồi thực hiện
dạng đã dịch. Thao tác đầu tiên và duy nhất là thực hiện
toán tử print, điều này gửi đối của nó ra lối ra. Sau khi
chương trình đã hoàn tất, tiến trình Perl ra, cho lại một
mã ra thành công cho lớp vỏ.
* Dấu chấm phẩy có thể bỏ đi khi câu lệnh này là câu lệnh cuối của
một khối hay tệp hay eval.
11
Hỏi câu hỏi và nhớ kết quả
Thêm một chút phức tạp hơn. Từ Xin chào mọi người
là một đụng chạm lạnh nhạt và cứng rắn. Làm cho
chương trình gọi bạn theo tên bạn. Để làm việc này, cần
một chỗ giữ tên, một cách hỏi tên, và một cách nhận câu
trả lời.
Một loại đặt chỗ giữ giá trị (tựa như một tên) là biến
vô hướng. Với chương trình này, ta sẽ dùng biến vô
hướng $name để giữ tên bạn. Chúng ta sẽ đi chi tiết hơn
trong Chương 2, Dữ liệu vô hướng, về những gì mà biến
này có thể giữ, và những gì bạn có thể làm với chúng.
Hiện tại, giả sử rằng bạn có thể giữ một số hay xâu (dãy
các kí tự) trong biến vô hướng.
Chương trình này cần hỏi về tên. Để làm điều đó,
cần một cách nhắc và một cách nhận cái vào. Chương
trình trước đã chỉ ra cho ta cách nhắc - dùng toán tử print.
Và cách để nhận một dòng từ thiết bị cuối là với toán tử
<STDIN>, mà (như ta sẽ dùng nó ở đây) lấy một dòng
cái vào. Gán cái vào này cho biến $name. Điều này cho
chương trình:
print “Tên bạn là gì? ”; $name = <STDIN> ;
Giá trị của $name tại điểm này có một dấu dòng mới
kết thúc (Randal có trong Randal\n). Để vứt bỏ điều đó,
chúng ta dùng toán tử chop(), toán tử lấy một biến vô
hướng làm đối duy nhất và bỏ đi kí tự cuối từ giá trị xâu
của biến:
chop($name);
Bây giờ tất cả những gì cần làm là nói Xin chào, tiếp
12
đó là giá trị của biến $name, mà có thể thực hiện theo
kiểu vỏ bằng cách nhúng biến này vào bên trong xâu có
ngoặc kép:
print “Xin chào, $name!\n”;
Giống như lớp vỏ, nếu muốn một dấu đô la thay vì
tham chiếu biến vô hướng, thì có thể đặt trước dấu đô la
với một dấu sổ chéo ngược.
Gắn tất cả lại, ta được:
#!/usr/bin/perl print “Tên bạn là gì? ”; $name = <STDIN> ; chop($name); print “Xin chào, $name!\n”;
Bổ sung chọn lựa
Bây giờ ta muốn có một lời chào đặc biệt cho
Randal, nhưng muốn lời chào thông thường cho mọi
người khác. Để làm điều này, cần so sánh tên đã được
đưa vào với xâu Randal, và nếu hai xâu là một, thì làm
điều gì đó đặc biệt. Bổ sung thêm lệnh rẽ nhánh if-then-
else và phép so sánh vào chương trình:
#!/usr/bin/perl print “Tên bạn là gì? ”; $name = <STDIN> ; chop($name); if ($name eq “Randal”) { print “Xin chào Randal! Tốt quá anh lại ở đây!\n”; } else { print “Xin chào, $name!\n”; # chào mừng thông
thường }
13
Toán tử eq so sánh hai xâu. Nếu chúng bằng nhau
(từng kí tự một, và có cùng chiều dài), thì kết quả là
đúng. (Không có toán tử này trong C, và awk phải dùng
cùng toán tử cho xâu và số và tạo ra việc đoán có rèn
luyện.)
Câu lệnh if chọn xem khối câu lệnh nào (giữa các
dấu ngoặc nhọn sánh đúng) là được thực hiện - nếu biểu
thức là đúng, đó là khối thứ nhất, nếu không thì đó là
khối thứ hai.
Đoán từ bí mật
Nào, vì chúng ta đã có một tên nên ta để cho một
người chạy chương trình đoán một từ bí mật. Với mọi
người ngoại trừ Randal, chúng ta sẽ để cho chương trình
cứ hỏi lặp lại để đoán cho đến khi nào người này đoán
được đúng. Trước hết xem chương trình này và rồi xem
giải thích:
#! /usr/bin/perl $secretword = “llama”; # từ bí mật print “Tên bạn là gì?” ; $name = <STDIN> ; chop($name); if ($name eq “Randal”) { print “Xin chào, Randal! May quá anh ở đây!\n”; } else { print “Xin chào, $name!\n”; # chào thông thường print “Từ bí mật là gì?” ; $guess = <STDIN>; chop($guess); while ($guess ne $secrectword) { print “Sai rồi, thử lại đi. Từ bí mật là gì?”; $guess = <STDIN>;
14
chop($guess); } }
Trước hết, định nghĩa từ bí mật bằng việc đặt nó vào
trong biến vô hướng khác, $secretword. Sau khi đón
chào, một người (không phải Randal) sẽ được yêu cầu
(với một câu lệnh print khác) đoán chữ. Lời đoán rồi
được đem ra so sánh với từ bí mật bằng việc dùng toán
tử ne, mà sẽ cho lại đúng nếu các xâu này không bằng
nhau (đây là toán tử logic ngược với toán tử eq). Kết quả
của việc so sánh sẽ kiểm soát cho trình while, chu trình
này thực hiện khối thân cho tới khi việc so sánh vẫn còn
đúng.
Tất nhiên, chương trình này không phải là an toàn
lắm, vì bất kì ai mệt với việc đoán cũng đã có thể đơn
thuần ngắt chương trình và quay trở về lời nhắc, hay
thậm chí còn nhìn qua chương trình gốc để xác định ra
từ. Nhưng, chúng ta hiện tại chưa định viết một hệ thống
an toàn, chỉ xem như một thí dụ cho trang hiện tại của
cuốn sách này.
Nhiều từ bí mật
Ta hãy xem cách thức mình có thể sửa đổi đoạn
chương trình này để cho phép có nhiều từ bí mật. Bằng
việc dùng điều đã thấy, chúng ta có thể so sánh lời đoán
lặp đi lặp lại theo một chuỗi câu trả lời rõ được cất giữ
trong các biến vô hướng tách biệt. Tuy nhiên, một danh
sách như vậy sẽ khó mà thay đổi hay đọc vào từ một tệp
hay máy tính trên cơ sở ngày làm việc thường lệ.
Một giải pháp tốt hơn là cất giữ tất cả các câu trả lời
15
có thể vào trong một cấu trúc dữ liệu gọi là danh sách
hay mảng. Mỗi phần tử của mảng đã là một biến vô
hướng tách biệt mà có thể được đặt giá trị hay truy nhập
độc lập. Toàn bộ mảng cũng có thể được trao cho một
giá trị trong một cú đột nhập. Ta có thể gán một giá trị
cho toàn bộ mảng có tên @words sao cho nó chứa ba mật
hiệu tốt có thể có:
@words = (“camel”, “llama”, “oyster”);
Tên biến mảng bắt đầu với @, cho nên chúng là
khác biệt với các tên biến vô hướng.
Một khi mảng đã được gán thì ta có thể truy nhập
vào từng phần tử bằng việc dùng một tham chiếu chỉ số.
Cho nên $words[0] là camel, $words[1] là llama, $words[2]
là oyster. Chỉ số cũng có thể là một biểu thức, cho nên
nếu ta đặt $i là 2 thì $words[$i] là oyster. (Tham chiếu chỉ
số bắt đầu với $ thay vì @ vì chúng tham chiếu tới một
phần tử riêng của mảng thay vì toàn bộ mảng.) Quay trở
lại với thí dụ trước đây của ta:
#! /usr/bin/perl $words = (“camel”, “llama”, “oyster”); # từ bí mật print “Tên bạn là gì?” ; $name = <STDIN> ; chop($name); if ($name eq “Randal”) { print “Xin chào, Randal! May quá anh ở đây!\n”; } else { print “Xin chào, $name!\n”; # chào thông thường print “Từ bí mật là gì?” ; $guess = <STDIN>; chop($guess); $i = 0; # thử từ này trước hết $correct = “ có thể”; # từ đoán có đúng hay không? while ($correct eq $guess) { # cứ kiểm tra đến khi biết
16
if ($words[$i] eq $guess) { # đúng không $correct = “có”; # có } elsif ($i < 2) { # cần phải xét thêm từ nữa? $i = $i + 1; # nhìn vào từ tiếp lần sau } else # hết rồi, thế là hỏng print “Sai rồi, thử lại đi. Từ bí mật là gì?”; $guess = <STDIN>; chop($guess); $i = 0; # bắt đầu kiểm tra từ đầu lần nữa } } # kết thúc của while not correct } # kết thúc của “not Randal”
Bạn sẽ để ý rằng chúng ta đang dùng biến vô hướng
$correct để chỉ ra rằng chúng ta vẫn đang tìm ra mật hiệu
đúng, hay rằng chúng ta không tìm thấy.
Chương trình này cũng chỉ ra khối elsif của câu lệnh
if-then-else. Không có lệnh nào tương đương như thế
trong C hay awk - đó là việc viết tắt của khối else cùng
với một điều kiện if mới, nhưng không lồng bên trong
cặp dấu ngoặc nhọn khác. Đây chính là cái rất giống Perl
để so sánh một tập các điều kiện trong một dây chuyền
phân tầng if-elsif-elsif-elsif-else.
Cho mỗi người một từ bí mật khác nhau
Trong chương trình trước, bất kì người nào tới cũng
đã có thể đoán bất kì từ nào trong ba từ này và có thể
thành công. Nếu ta muốn từ bí mật là khác nhau cho mỗi
người, thì ta cần một bảng sánh giữa người và từ:
17
Người Từ bí mật
Fred
Barney
Betty
Wilma
camel
llama
oyster
oyster
Chú ý rằng cả Betty và Wilma đã có cùng từ bí mật.
Điều này là được.
Cách dễ nhất để cất giữ một bảng như thế trong Perl
là bằng một mảng kết hợp. Mỗi phần tử của mảng này
giữ một giá trị vô hướng tách biệt (hệt như kiểu mảng
khác), nhưng các mảng lại được tham chiếu tới theo
khoá, mà có thể là bất kì giá trị vô hướng nào (bất kì xâu
hay số, kể cả số không nguyên và giá trị âm). Để tạo ra
một mảng kết hợp được gọi là %words (chú ý % chứ
không phải là @) với khoá và giá trị được cho trong
bảng trên, ta gán một giá trị cho %words (như ta đã làm
nhiều trước đây với mảng khác):
%words = (“fred”, “camel”, “barney”, “llama”,
“betty”, “oyster”, “wilma”, “oyster”) ;
Mỗi cặp giá trị trong danh sách đã biểu thị cho một
khoá và giá trị tương ứng của nó trong mảng kết hợp.
Chú ý rằng ta đã bẻ phép gán này ra hai dòng mà không
có bất kì loại kí tự nối dòng nào, vì khoảng trắng nói
chung là vô nghĩa trong chương trình Perl.
Để tìm ra từ bí mật cho Betty, ta cần dùng Betty như
18
khoá trong một tham chiếu vào mảng kết hợp %words,
qua một biểu thức nào đó như %words{“betty”}. Giá trị của
tham chiếu này là oyster, tương tự như điều ta đã làm
trước đây với mảng khác. Cũng như trước đây, khoá có
thể là bất kì biểu thức nào, cho nên đặt $person với betty
và tính $words{$person} cũng cho oyster.
Gắn tất cả những điều này lại ta được chương trình
như thế này:
#! /usr/bin/perl %words = (“fred”, “camel”, “barney”, “llama”,
“betty”, “oyster”, “wilma”, “oyster”) ; print “Tên bạn là gì?” ; $name = <STDIN> ; chop($name); if ($name eq “Randal”) { print “Xin chào, Randal! May quá anh ở đây!\n”; } else { print “Xin chào, $name!\n”; # chào thông thường $secretword = $words{$name}; # lấy từ bí mật print “Từ bí mật là gì?” ; $guess = <STDIN>; chop($guess); while ($correct ne $secretwords) { print “Sai rồi, thử lại đi. Từ bí mật là gì?”; $guess = <STDIN>; chop($guess); } # kết thúc của while } # kết thúc của “not Randal”
chú ý nhìn vào từ bí mật. Nếu không tìm thấy từ
này thì giá trị của $secretword sẽ là một xâu rỗng*, mà ta
có thể kiểm tra liệu ta có muốn xác định một từ bí mật
* Được, đấy chính là giá trị undef, nhưng nó trông như một xâu
rỗng cho toán tử eq
19
mặc định cho ai đó khác không. Đây là cách xem nó:
[... phần còn lại của chương trình đã bị xoá...] $secretword = $words{$name}; # lấy từ bí mật if ($secretword eq “”) { # ấy, không thấy $secretword = “đồ cáu kỉnh”; # chắc chắn, sao không
là vịt? } print “Từ bí mật là gì?” ;
[... phần còn lại của chương trình đã bị xoá...]
Giải quyết dạng thức cái vào thay đổi
Nếu tôi đưa vào Randal L. Schwartz hay randal thay vì
Randal thì tôi sẽ bị đóng cục lại với phần người dùng còn
lại, vì việc so sánh eq thì lại so sánh đúng sự bằng nhau.
Ta hãy xem một cách giải quyết cho điều đó.
Giả sử tôi muốn tìm bất kì xâu nào bắt đầu với
Randal, thay vì chỉ là một xâu bằng Randal. Tôi có thể
làm điều này trong sed hay awk hoặc grep với một biểu
thức chính qui: một khuôn mẫu sẽ xác định ra một tập
hợp các xâu sánh đúng. Giống như trong sed hay grep,
biểu thức chính qui trong Perl để sánh bất kì xâu nào bắt
đầu với Randal là ^Randal. Để sánh xâu này với xâu trong
$name, chúng ta dùng toán tử sánh như sau:
if ($name =~ /^Randal/) { ## có, sánh đúng } else { ## không, sánh sai }
Chú ý rằng biểu thức chính qui được định biên bởi
dấu sổ chéo. Bên trong các dấu sổ chéo, dấu cách và
20
khoảng trắng là có nghĩa, hệt như chúng ở bên trong xâu.
Điều này gần như thế, nhưng nó lại không giải quyết
việc lựa ra randal hay loại bỏ Randall. Để chấp nhận
randal, chúng ta thêm tuỳ chọn bỏ qua hoa thường, một
chữ i nhỏ được thêm vào sau dấu sổ chéo đóng. Để loại
bỏ Randall, ta thêm một đánh dấu đặc biệt định biên từ
(tương tự với vi và một số bản của grep) dưới dạng của
\b trong biểu thức chính qui. Điều này đảm bảo rằng kí
tự đi sau l đầu tiên trong biểu thức chính qui không phải
là một kí tự khác. Điều này làm thay đổi biểu thức chính
qui thành /^randal\b/i, mà có nghĩa là “randal tại đầu xâu,
không có kí tự hay chữ số nào theo sau, và chấp nhận cả
hai kiểu chữ hoa thường.”
Khi gắn tất cả lại với phần còn lại của chương trình
thì nó sẽ giống như thế này:
#! /usr/bin/perl %words = (“fred”, “camel”, “barney”, “llama”,
“betty”, “oyster”, “wilma”, “oyster”) ; print “Tên bạn là gì?” ; $name = <STDIN> ; chop($name); if ($name =~ /^randal\b/i ) { print “Xin chào, Randal! May quá anh ở đây!\n”; } else { print “Xin chào, $name!\n”; # chào thông thường $secretword = $words{$name}; # lấy từ bí mật if ($secretword eq “”) { # ấy, không thấy $secretword = “đồ cáu kỉnh”; # chắc chắn, sao
không là vịt? } print “Từ bí mật là gì?” ; $guess = <STDIN>; chop($guess); while ($correct ne $secretwords) {
21
print “Sai rồi, thử lại đi. Từ bí mật là gì?”; $guess = <STDIN>; chop($guess); } # kết thúc của while } # kết thúc của “not Randal”
Như bạn có thể thấy, chương trình này khác xa với
chương trình đơn giản Xin chào, mọi người, nhưng nó vẫn
còn rất nhỏ bé và làm việc được, và nó quả làm được tí
chút với cái ngắn xíu vậy. Đây chính là cách thức của
Perl.
Perl đưa ra tính năng về các biểu thức chính qui có
trong mọi trình tiện ích UNIX chuẩn (và thậm chí trong
một số không chuẩn). Không chỉ có thế, nhưng cách thức
Perl giải quyết cho việc đối sánh xâu là cách nhanh nhất
trên hành tinh này, cho nên bạn không bị mất hiệu năng.
(Một chương trình giống như grep được viết trong Perl
thường đánh bại chương trình grep được các nhà cung
cấp viết trong C với hầu hết các cái vào. Điều này có
nghĩa là thậm chí grep không thực hiện việc của nó thật
tốt.)
Làm cho công bằng với mọi người
Vậy bây giờ tôi có thể đưa vào Randal hay randal hay
Randal L. Schwartz, nhưng với những người khác thì sao?
Barney vẫn phải nói đúng barney (thậm chí không được
có barney với một dấu cách theo sau).
Để công bằng cho Barey, chúng ta cần nắm được từ
đầu của bất kì cái gì được đưa vào, và rồi chuyển nó
thành chữ thường trước khi ta tra tên trong bảng. Ta làm
điều này bằng hai toán tử: toán tử substitute, tìm ra một
22
biểu thức chính qui và thay thế nó bằng một xâu, và toán
tử translate, để đặt xâu này thành chữ thường.
Trước hết, toán tử thay thế: chúng ta muốn lấy nội
dung của $name, tìm kí tự đầu tiên không là từ, và loại đi
mọi thứ từ đây cho đến cuối xâu. /\W.*/ là một biểu thức
chính qui mà ta đang tìm kiếm - \W viết tắt cho kí tự
không phải là từ (một cái gì đó không phải là chữ, chữ số
hay gạch thấp) và .* có nghĩa là bất kì kí tự nào từ đấy
tới cuối dòng. Bây giờ, để loại những kí tự này đi, ta cần
lấy bất kì bộ phận nào của xâu sánh đúng với biểu thức
chính qui này và thay nó với cái không có gì:
$name =~ s/\W.*//;
Chúng ta đang dùng cùng toán tử =~ mà ta đã dùng
trước đó, nhưng bây giờ bên vế phải ta có toán tử thay
thế: chữ s được theo sau bởi một biểu thức chính qui và
xâu được định biên bởi dấu sổ chéo. (Xâu trong thí dụ
này là xâu rỗng giữa dấu sổ chéo thứ hai và thứ ba.)
Toán tử này trông giống và hành động rất giống như
phép thay thế của các trình soạn thảo khác nhau.
Bây giờ để có được bất kì cái gì còn lại trở thành
chữ thường thì ta phải dịch xâu này dùng toán tử tr. Nó
trông rất giống chỉ lệnh tr của UNIX, nhận một danh
sách các kí tự để tìm và một danh sách các kí tự để thay
thế chúng. Với thí dụ của ta, để đặt nội dung của $name
thành chữ thường, ta dùng:
$name =~ tr/A-Z/a-z/;
Các dấu sổ chéo phân tách các danh sách kí tự cần
tìm và cần thay thế. Dấu gạch ngang giữa A và Z thay
thế cho tất cả các kí tự nằm giữa, cho nên chúng ta có hai
danh sách, mỗi danh sách có 26 kí tự. Khi toán tử tr tìm
23
thấy một kí tự từ một xâu trong danh sách thứ nhất thì kí
tự đó sẽ được thay thế bằng kí tự tương ứng trong danh
sách thứ hai. Cho nên tất cả các chữ hoa A trở thành chữ
thường a, và cứ như thế* .
Gắn tất cả lại với mọi thứ khác sẽ cho kết quả trong:
#! /usr/bin/perl %words = (“fred”, “camel”, “barney”, “llama”,
“betty”, “oyster”, “wilma”, “oyster”) ; print “Tên bạn là gì?” ; $name = <STDIN> ; chop($name); $original_name = $name; # cất giữ để chào mơng $name =~ s/\W.*//; # bỏ mọi thứ sau từ đầu $name =~ tr/A-Z/a-z/; # mọi thứ thành chữ thường if ($name eq “randal” ) { print “Xin chào, Randal! May quá anh ở đây!\n”; } else { print “Xin chào, $original_name!\n”; # chào thông
thường $secretword = $words{$name}; # lấy từ bí mật if ($secretword eq “”) { # ấy, không thấy $secretword = “đồ cáu kỉnh”; # chắc chắn, sao
không là vịt? } print “Từ bí mật là gì?” ; $guess = <STDIN>; chop($guess); while ($correct ne $secretwords) { print “Sai rồi, thử lại đi. Từ bí mật là gì?”; $guess = <STDIN>; chop($guess); } # kết thúc của while
* Các chuyên gia sẽ lưu ý rằng tôi cũng đã xây dựng một cái gì đó
tựa như s/(\S*).*/\L$1/ để làm tất cả điều này trong một cú đột kích,
nhưng các chuyên gia có lẽ sẽ không đọc mục này.
24
} # kết thúc của “not Randal”
Để ý đến cách thức biểu thức chính qui sánh đúng
với tên tôi Randal đã lại trở thành việc so sánh đơn giản.
Sau rốt, cả Randal L. Schwartz và Randal đã trở thành
randal sau khi việc thay thế và dịch. Và mọi người khác
cũng có được sự công bằng, vì Fred và Fred Flinstone cả
hai đã trở thành fred, Barney Rubble và Barney, the little
guy sẽ trở thành barney, vân vân.
Với chỉ vài câu lệnh, chúng ta đã tạo ra một chương
trình thân thiện người dùng hơn nhiều. Bạn sẽ thấy rằng
việc diễn tả thao tác xâu phức tạp với vài nét là một
trong nhiều điểm mạnh của Perl.
Tuy nhiên, chạm vào tên để cho ta có thể so sánh nó
và tra cứu nó trong bảng thì sẽ phá huỷ mất tên vừa đưa
vào. Cho nên, trước khi chạm vào tên, cần phải cất giữ
nó vào trong @original_name. (Giống như các kí hiệu C,
biến Perl bao gồm các chữ, chữ số và dấu gạch thấp và
có thể có chiều dài gần như không giới hạn.) Vậy có thể
làm tham chiếu tới $original_name về sau.
Perl có nhiều cách để giám sát và chặt cắt xâu. bạn
sẽ thấy phần lớn chúng trong Chương 7, Biểu thức chính
qui và Chương 15, Việc chuyển đổi dữ liệu khác.
Làm cho nó mô đun hơn một chút
Bởi vì chúng ta đã thêm quá nhiều mã nên phải
duyệt qua nhiều dòng chi tiết trước khi ta có thể thu
được toàn bộ luồng chương trình. Điều cần là tách bạch
logic mức cao (hỏi tên, chu trình dựa trên từ bí mật đưa
vào) với các chi tiết (so sánh một từ bí mật với từ đã
25
biết). Chúng ta có thể làm điều này cho rõ ràng, hoặc có
thể bởi vì một người đang viết phần cao cấp còn người
khác thì viết (hay đã viết) phần chi tiết.
Perl cung cấp các chương trình con có tham biến và
giá trị cho lại. Một chương trình con được xác định một
khi nào đó trong chương trình, và rồi có thể được dùng
lặp đi lặp lại bằng việc gọi từ bên trong bất kì biểu thức
nào.
Với chương trình nhỏ nhưng tăng trưởng nhanh của
chúng ta, tạo ra một chương trình con tên là &good_word
(tất cả các tên chương trình con đã bắt đầu với một dấu
và &) mà nhận một tên đã sạch và một từ đoán, rồi cho
lại true nếu từ đó là đúng, và cho lại false nếu không
đúng. Việc xác định chương trình con đó tựa như thế
này:
sub good_word { local($somename, $someguess) = @_; # tên tham
biến $somename =~ s/\W.*//; # bỏ mọi thứ sau từ đầu $somename =~ tr/A-Z/a-z/; # mọi thứ thành chữ
thường if ($somename eq “randal”) { # không nên đoán 1; #giá trị cho lại là true } elsif (($words{$somename} || “đồ cáu kỉnh”) eq
$someguess) { 1; # giá trị cho lại là true } else { 0; # cho lại giá trị false } }
Trước hết, việc định nghĩa ra một chương trình con
bao gồm một từ dành riêng sub đi theo sau là tên chương
trình con (không có dấu và &) tiếp nữa là một khối mã
26
lệnh (được định biên bởi dấu ngoặc nhọn). Định nghĩa
này có thể để vào bất kì đâu trong tệp chương trình,
nhưng phần lớn mọi người thích để chúng vào cuối.
Dòng đầu tiên trong định nghĩa đặc biệt này là một
phép gán làm việc sao các giá trị của hai tham biến của
chương trình này vào hai biến cục bộ có tên $somename
và $someguess. (local() xác định hai biến là cục bộ cho
chương trình con này, và các tham biến ban đầu trong
một mảng cục bộ đặc biệt gọi là @_.)
Hai dòng tiếp làm sạch tên, cũng giống như bản
trước của chương trình.
Câu lệnh if-elsif-else quyết định xem từ được đoán
($someguess) là có đúng cho tên ($somename) hay
không. Randal không nên làm nó thành chương trình con
này, nhưng ngay cả nếu nó có, thì dù từ nào được đoán
cũng đã OK cả.
Biểu thức cuối cùng được tính trong chương trình
con là cho lại giá trị. Chúng ta sẽ thấy cách cho lại giá trị
được dùng sau khi tôi kết thúc việc mô tả định nghĩa về
chương trình con. Phép kiểm tra cho phần elsif trông có
phức tạp hơn một chút - chia nó ra:
($words{$somename} || “đồ cáu kỉnh”) eq $someguess
Vật thứ nhất bên trong dấu ngoặc là mảng kết hợp
quen thuộc của ta, cho một giá trị nào đó từ %words dựa
trên khoá $somename. Toán tử đứng giữa giá trị đó và
xâu đồ cáu kỉnh là toán tử || (phép hoặc logic) như được
dùng trong C và awk và các vỏ khác. Nếu việc tra cứu từ
mảng kết hợp có một giá trị (nghĩa là khoá $somename là
trong mảng), thì giá trị của biểu thức là giá trị đó. Nếu
khoá không tìm được, thì xâu đồ cáu kỉnh sẽ được dùng
27
thay. Đây chính là một vật kiểu Perl thường làm - xác
định một biểu thức nào đó, và rồi đưa ra một giá trị mặc
định bằng cách dùng || nếu biểu thức này có thể trở thành
sai.
Trong mọi trường hợp, dù đó là một giá trị từ mảng
kết hợp, hay giá trị mặc định đồ cáu kỉnh, chúng ta đã so
sánh nó với bất kì cái gì được đoán. Nếu việc so sánh là
đúng thì cho lại 1, nếu không cho lại 0.
Bây giờ tích hợp tất cả những điều này với phần còn
lại của chương trình:
#! /usr/bin/perl %words = (“fred”, “camel”, “barney”, “llama”,
“betty”, “oyster”, “wilma”, “oyster”) ; print “Tên bạn là gì?” ; $name = <STDIN> ; chop($name); if ($name =~ /^randal\b/i ) { # trở lại cách khác print “Xin chào, Randal! May quá anh ở đây!\n”; } else { print “Xin chào, $original_name!\n”; # chào thông
thường print “Từ bí mật là gì?” ; $guess = <STDIN>; chop($guess); while ( ! &good_word($name, $guess)) { print “Sai rồi, thử lại đi. Từ bí mật là gì?”; $guess = <STDIN>; chop($guess); } } [ ... thêm vào định nghĩa của &good_word ở đây ...]
Chú ý rằng chúng ta đã quay trở lại với biểu thức
chính qui để kiểm tra Randal, vì bây giờ không cần kéo
một phần tên thứ nhất và chuyển nó thành chữ thường,
28
chừng nào còn liên quan tới chương trình chính.
Khác biệt lớn là chu trình while có chứa &good_word.
Tại đây, chúng ta thấy một lời gọi tới chương trình con,
truyền cho nó hai đối, $name và $guess. Bên trong
chương trình con này, giá trị của $somename được đặt từ
tham biến thứ nhất, trong trường hợp này là $name.
Giống thế, $someguess được đặt từ tham biến thứ hai,
$guess.
Giá trị do chương trình con cho lại (hoặc 1 hoặc 0,
nhớ lại định nghĩa đã nêu trước đây) là âm với toán tử
tiền tố ! (phép phủ định logic). Như trong C, toán tử này
cho lại đúng nếu biểu thức đi sau là sai, và ngược lại.
Kết quả của phép phủ định này sẽ kiểm soát chu trình
while. Bạn có thể đọc điều này là “trong khi không phải
là từ đúng...”. Nhiều chương trình Perl viết tốt đọc rất
giống tiếng Anh, đưa lại cho bạn một chút tự do với Perl
hay tiếng Anh. (Nhưng bạn chắc chắn không đoạt giải
Pulitzer theo cách đó.)
Chú ý rằng chương trình con này giả thiết rằng giá
trị của mảng %words được chương trình chính đặt. Điều
này không đặc biệt là hay, nhưng chẳng có gì so sánh
được với static của C đối với chương trình con - nói
chung, tất cả các biến nếu không nói khác đi mà được
tạo ra với toán tử local() thì đã là toàn cục đối với toàn bộ
chương trình. Thực ra, đây chỉ là vấn đề nhỏ, và người ta
thì có được tính sáng tạo về cách đặt tên biến dài lâu.
Chuyển danh sách từ bí mật vào tệp riêng biệt
Giả sử ta muốn dùng chung danh sách từ bí mật cho
29
ba chương trình. Nếu cất giữ danh sách từ như đã làm thì
sẽ cần phải thay đổi tất cả ba chương trình này khi Betty
quyết định rằng từ bí mật của cô sẽ là swine thay vì
oyster. Điều này có thể thành phiền phức, đặc biệt khi
xem xét tới việc Betty lại thường xuyên thích thay đổi ý
định.
Cho nên, đặt danh sách từ vào một tệp, và rồi đọc
tệp này để thu được danh sách từ vào trong chương trình.
Để làm điều này, cần tạo ra một kênh vào/ra được gọi là
tước hiệu tệp. Chương trình Perl của bạn sẽ tự động lấy
ba tước hiệu tệp gọi là STDIN, STDOUT và STDERR,
tương ứng với ba kênh vào ra chuẩn cho chương trình
UNIX. Chúng ta cũng đã dùng tước hiệu STDIN để đọc
dữ liệu từ người chạy chương trình. Bây giờ, đấy chỉ là
việc lấy một tước hiệu khác để gắn với một tệp do ta tạo
ra.
Sau đây là một đoạn mã nhỏ để làm điều đó:
sub init_words { open (WORDSLIST, “wordslist”); while ($name = <WORDSLIST>) { chop ($name); $word = <WORDSLIST>; chop($word); $words{$name} = $word; } close (WORDSLIST); }
Tôi đặt nó vào một chương trình con để cho tôi có
thể giữ phần chính của chương trình không bị lộn xộn.
Điều này cũng có nghĩa là vào thời điểm sau (hướng dẫn:
một vài lần ôn lại cuộc đi dạo này), tôi có thể thay đổi
nơi cất giữ danh sách từ, hay thậm chí định dạng của
30
danh sách.
Định dạng được chọn bất kì cho danh sách từ là một
khoản mục trên một dòng, với tên và từ, luân phiên. Cho
nên, với cơ sở dữ liệu hiện tại của chúng ta, chúng ta có
cái tựa như thế này:
fred camel barney llama betty oyster wilma oyster
Toán tử open() tạo ra một tước hiệu tệp có tên
WORDSLIST bằng cách liên kết nó với một tệp mang
tên wordslist trong danh mục hiện tại. Lưu ý rằng tước
hiệu tệp không có kí tự là lạ phía trước nó như ba kiểu
biến vẫn có. Cũng vậy, tước hiệu tệp nói chung là chữ
hoa - mặc dầu chúng không nhất thiết phải là như thế -
bởi những lí do sẽ nêu chi tiết về sau.
Chu trình while đọc các dòng từ tệp wordslist (qua
tước hiệu tệp WORDSLIST) mỗi lần một dòng. Mỗi
dòng đã được cất giữ trong biến $name. Khi đạt đến cuối
tệp thì giá trị cho lại bởi toán tử <WORDSLIST> là xâu
rỗng* , mà sẽ sai cho cho trình while, và kết thúc nó. Đó
là cách chúng ta đi ra ở cuối.
Mặt khác, trường hợp thông thường là ở chỗ chúng
ta đã đọc một dòng (kể cả dấu dòng mới) vào trong
* Về mặt kĩ thuật thì đấy lại là undef, nhưng cũng đủ gần cho thảo
luận này
31
$name. Trước hết, ta bỏ dấu dòng mới bằng việc dùng
toán tử chop(). Rồi, ta phải đọc dòng tiếp để lấy từ bí
mật, giữ nó trong biến $word. Nó nữa cũng phải bỏ dấu
dòng mới đi.
Dòng cuối cùng của chu trình while đặt $word vào
trong %words với khoá $name, cho nên phần còn lại của
chương trình có thể truy nhập vào nó về sau.
Một khi tệp đã được đọc xong thì có thể bỏ tước
hiệu tệp bằng toán tử close(). (Tước hiệu tệp dẫu sao
cũng được tự động đóng lại khi chương trình thoát ra,
nhưng tôi đang định làm cho gọn.)
Định nghĩa chương trình con này có thể đi sau hay
trước chương trình con khác. Và chúng ta gọi tới chương
trình con thay vì đặt %words vào chỗ bắt đầu của chương
trình, cho nên một cách để bao quát tất cả những điều
này có thể giống thế này:
#! /usr/bin/perl &init_words; print “Tên bạn là gì?” ; $name = <STDIN> ; chop($name); if ($name =~ /^randal\b/i ) { # trở lại cách khác print “Xin chào, Randal! May quá anh ở đây!\n”; } else { print “Xin chào, $original_name!\n”; # chào thông
thường print “Từ bí mật là gì?” ; $guess = <STDIN>; chop($guess); while ( ! &good_word($name, $guess)) { print “Sai rồi, thử lại đi. Từ bí mật là gì?”; $guess = <STDIN>; chop($guess);
32
} } ## chương trình con từ đây xuống sub init_words { open (WORDSLIST, “wordslist”); while ($name = <WORDSLIST>) { chop ($name); $word = <WORDSLIST>; chop($word); $words{$name} = $word; } close (WORDSLIST); }
sub good_word { local($somename, $someguess) = @_; # tên tham
biến $somename =~ s/\W.*//; # bỏ mọi thứ sau từ đầu $somename =~ tr/A-Z/a-z/; # mọi thứ thành chữ
thường if ($somename eq “randal”) { # không nên đoán 1; #giá trị cho lại là true } elsif (($words{$somename} || “đồ cáu kỉnh”) eq
$someguess) { 1; # giá trị cho lại là true } else { 0; # cho lại giá trị false } }
Bây giờ nó bắt đầu trông giống một chương trình
trưởng thành hoàn toàn. Chú ý đến dòng thực hiện được
đầu tiên là lời gọi tới &init_words. Không có tham biến
nào được truyền cả, cho nên chúng ta được tự do bỏ đi
dấu ngoặc tròn. Cũng vậy, giá trị cho lại không được
dùng trong tính toán thêm, thì cũng là tốt vì ta đã không
cho lại điều gì đáng để ý. (Giá trị của close() thông
thường là đúng.)
33
Toán tử open() cũng được dùng để mở các tệp đưa
ra, hay mở chương trình như tệp (đã được biểu diễn ngắn
gọn). Tuy thế, việc vét hết về open() sẽ được nêu về sau
trong cuốn sách này, trong Chương 10, Tước hiệu tệp và
kiểm tra tệp.
Đảm bảo một lượng an toàn giản dị
“Danh sách các từ bí mật phải thay đổi ít nhất một
lần mỗi tuần!” ông Trưởng ban danh sách từ bí mật kêu
lên. Thôi được, chúng ta không thể buộc danh sách này
khác đi, nhưng chúng ta có thể ít nhất cũng đưa ra một
cảnh báo nếu danh sách từ bí mật còn chưa được thay
đổi trong hơn một tuần.
Cách tốt nhất để làm điều này là trong chương trình
con &init_words - chúng ta đã nhìn vào tệp ở đó. Toán tử
Perl -M cho lại tuổi tính theo ngày từ một tệp hay tước
hiệu tệp đã được thay đổi từ lần trước, cho nên ta chỉ cần
xem liệu giá trị này có lớn hơn bẩy hay không đối với
tước hiệu tệp WORDSLIST:
sub init_words { open (WORDSLIST, “wordslist”); if (-M WORDSLIST > 7) { # tuân thủ theo đường lối
quan liêu die “Rất tiếc, danh sách từ cũ hơn bẩy ngày
rồi.”; } while ($name = <WORDSLIST>) { chop ($name); $word = <WORDSLIST>; chop($word); $words{$name} = $word; }
34
close (WORDSLIST); }
Giá trị của -M WORDSLIST được so sánh với bẩy,
và nếu lớn hơn, thế thì ta vi phạm vào đường lối rồi. Tại
đây, ta thấy một toán tử mới, toán tử die, mà cho in ra
một thông báo trên thiết bị cuối* , và bỏ chương trình
trong một cú bổ nhào rơi xuống.
Phần còn lại của chương trình vẫn không đổi, cho
nên trong mối quan tâm tới việc tiết kiệm cây cối, tôi sẽ
không lặp lại nó ở đây.
Bên cạnh việc lấy tuổi của tệp, ta cũng có thể tìm ra
người chủ của nó, kích cỡ, thời gian truy nhập, và mọi
thứ khác mà UNIX duy trì về tệp. Nhiều điều hơn được
trình bầy trong Chương 10.
Cảnh báo ai đó khi mọi việc đi sai
Xem có thể làm cho hệ thống bị sa lầy thế nào khi
gửi một mẩu thư điện tử mỗi lần cho một ai đó đoán từ
bí mật của họ không đúng. Cần sửa đổi mỗi chương trình
con &good_word (nhờ có tính mô đun) vì có tất cả thông
tin ngay đây.
Thư sẽ được gửi cho bạn nếu bạn gõ địa chỉ thư của
riêng mình vào chỗ mà chương trình nói “Địa chỉ bạn ở
đây.” Đây là điều phải làm - ngay trước khi trả 0 về từ
chương trình con, tạo ra một tước hiệu tệp mà thực tại là
một tiến trình (mail), giống như:
sub good_word {
* Thực tại là STDERR, nhưng đấy thông thường là màn hình
35
local($somename, $someguess) = @_; # tên tham biến
$somename =~ s/\W.*//; # bỏ mọi thứ sau từ đầu $somename =~ tr/A-Z/a-z/; # mọi thứ thành chữ
thường if ($somename eq “randal”) { # không nên đoán 1; #giá trị cho lại là true } elsif (($words{$somename} || “đồ cáu kỉnh”) eq
$someguess) { 1; # giá trị cho lại là true } else { open(MAIL, “|mail Địa_chỉ_bạn_ở_đây”); print MAIL “tin xấu: $somename đã đoán
$someguess\n”; 0; # cho lại giá trị false } }
Câu lệnh mới thứ nhất ở đây là open(), có một kí
hiệu đường ống (|) trong tên tệp. Đây là một chỉ dẫn đặc
biệt rằng đang mở một chỉ lệnh thay vì một tệp. Vì
đường ống là tại chỗ bắt đầu của chỉ lệnh nên đang mở
một chỉ lệnh để có thể ghi lên nó. (Nếu bạn đặt đường
ống tại cuối thay vì đầu thì bạn có thể đọc cái ra của chỉ
lệnh.)
Câu lệnh tiếp, print, chỉ ra rằng một tước hiệu tệp
giữa từ khoá print và giá trị được in ra chọn tước hiệu tệp
đó làm cái ra, thay vì STDOUT* . Điều này có nghĩa là
thông báo sẽ kết thúc như cái vào cho chỉ lệnh mail.
Cuối cùng, đóng tước hiệu tệp, mà sẽ bắt đầu để
mail gửi dữ liệu của nó theo cách của nó.
* Về mặt kĩ thuật thì tước hiệu tệp này hiện được chọn. Điều đó sẽ
được nói nhiều tới về sau.
36
Để cho đúng, chúng ta có thể gửi câu trả lời đúng
cũng như câu trả lời sai, nhưng rồi ai đó đọc qua vai tôi
(hay núp trong hệ thống thư) trong khi tôi đang đọc thư
mà có thể lấy quá nhiều thông tin có ích.
Perl có thể cũng gọi cả các lệnh với việc kiểm soát
chính xác trên danh sách đối, mở các tước hiệu tệp, hay
thậm chí lôi ra cả bản sao của chương trình hiện tại, và
thực hiện hai (hay nhiều) bản sao song song. Backquotes
(giống như backquotes của vỏ) cho một cách dễ dàng
nắm được cái ra của một chỉ lệnh như dữ liệu. Tất cả
những điều này sẽ được mô tả trong Chương 14, Quản lí
tiến trình, cho nên bạn nhớ đọc.
Nhiều tệp từ bí mật trong danh mục hiện tại
Thay đổi định nghĩa của tên tệp từ bí mật một chút.
Thay vì tệp được đặt tên là wordslist, thì tìm bất kì cái gì
trong danh mục hiện tại mà có tận cùng là .secret. Với
lớp vỏ, nói:
echo *.secret
để thu được một liệt kê ngắn gọn cho tất cả các tên này.
Như lát nữa bạn sẽ thấy, Perl dùng một cú pháp tên
chùm tương tự.
lấy lại định nghĩa &init_words :
sub init_words { while ($filename = <*.secret>) { open (WORDSLIST, $filename); if (-M WORDSLIST > 7) { while ($name = <WORDSLIST>) { chop ($name); $word = <WORDSLIST>;
37
chop($word); $words{$name} = $word; } } close (WORDSLIST); } }
Trước hết, tôi đã bao một chu trình while mới quanh
phần lớn chương trình con của bản cũ. Điều mới ở đây là
toán tử <*.secret>. Điều này được gọi là núm tên tệp, bởi
lí do lịch sử. Nó làm việc rất giống <STDIN>, ở chỗ mỗi
lần được truy nhập tới, nó cho lại giá trị tiếp: tên tệp kế
tiếp sánh với mẫu của vỏ, trong trường hợp này là
*.secret. Khi không có tên tệp thêm nữa được cho lại thì
núm tên tệp cho xâu rỗng* .
Cho nên nếu danh mục hiện tại có chứa fred.secret
và barney.secret, thì $filename là barney.secret ở bước đầu
qua chu trình while (tên tới theo trật tự sắp của bảng chữ).
Ở bước thứ hai, $filename là fred.secret. Và không có tình
huống thứ ba vì núm cho lại xâu rỗng khi lần thứ ba
được gọi tới, làm cho chu trình while thành sai, gây ra
việc ra khỏi chương trình con.
Bên trong chu trình while, chúng ta mở tệp và kiểm
chứng rằng nó đủ gần đây (ít hơn bẩy ngày từ lần sửa đổi
trước). Với những tệp đủ gần, chúng ta duyệt qua như
trước.
Chú ý rằng nếu không có tệp nào sánh với *.secret và
lại ít hơn bẩy ngày thì chương trình con sẽ ra mà không
đặt bất kì từ bí mật nào vào mảng %words. Điều đó có
nghĩa là mọi người sẽ phải dùng từ đồ cáu kỉnh. Thế cũng
* Lại undef lần nữa
38
được. (Với mã thật, tôi sẽ thêm một kiểm tra nào đó về
số các ô trong %words trước khi cho lại, và die nếu nó
không tốt. Xem toán tử keys() khi lấy mảng kết hợp trong
Chương 5, Mảng kết hợp.)
Nhưng chúng ta biết họ là ai!
Được rồi, chúng ta đã hỏi tên người dùng khi thực ra
có thể lấy tên của người dùng hiện tại từ hệ thống, bằng
việc dùng một vài dòng như:
@password = getpwuid($<); # lấy dữ liệu mật hiệu $name = $password[6]; # lấy trường GCOS $name =~ s/,.*//; # vứt đi mọi thứ sau dấu phẩy đầu tiên
Dòng đầu tiên dùng số hiệu người dùng ID của
UNIX (thường được gọi là UID), tự động hiện diện trong
biến Perl $<. Toán tử getpwuid() (được đặt tên giống như
trình thư viện chuẩn) lấy số hiệu UID và cho lại thông
tin từ tệp mật hiệu (hay có thể một số cơ sở dữ liệu khác)
như một danh sách. Chúng đánh dấu thông tin này trong
mảng @password.
Khoản mục thứ bẩy của mảng @password (chỉ số 6)
là trường GCOS, mà thường là một xâu có chứa danh
sách các giá trị các nhau bởi dấu phẩy. Giá trị đầu tiên
của xâu đó thường là tên đầy đủ của người.
Một khi hai câu lệnh đầu này đã hoàn tất thì chúng
có toàn bộ trường GCOS trong $name. Tên đầy đủ chỉ là
phần thứ nhất của xâu trước dấu phẩy đầu tiên, cho nên
câu lệnh thứ ba vứt đi mọi thứ sau dấu phẩy thứ nhất.
Gắn tất cả những điều đó với phần còn lại của
chương trình (như đã được sửa bởi hai thay đổi chương
39
trình con khác)
#! /usr/bin/perl &init_words; @password = getpwuid($<); # lấy dữ liệu mật hiệu $name = $password[6]; # lấy trường GCOS $name =~ s/,.*//; # vứt đi mọi thứ sau dấu phẩy đầu tiên if ($name =~ /^randal\b/i ) { # trở lại cách khác print “Xin chào, Randal! May quá anh ở đây!\n”; } else { print “Xin chào, $name!\n”; # chào thông thường print “Từ bí mật là gì?” ; $guess = <STDIN>; chop($guess); while ( ! &good_word($name, $guess)) { print “Sai rồi, thử lại đi. Từ bí mật là gì?”; $guess = <STDIN>; chop($guess); } }
sub init_words { while ($filename = <*.secret>) { open (WORDSLIST, $filename); if (-M WORDSLIST > 7) { while ($name = <WORDSLIST>) { chop ($name); $word = <WORDSLIST>; chop($word); $words{$name} = $word; } } close (WORDSLIST); } }
sub good_word { local($somename, $someguess) = @_; # tên tham
biến
40
$somename =~ s/\W.*//; # bỏ mọi thứ sau từ đầu $somename =~ tr/A-Z/a-z/; # mọi thứ thành chữ
thường if ($somename eq “randal”) { # không nên đoán 1; #giá trị cho lại là true } elsif (($words{$somename} || “đồ cáu kỉnh”) eq
$someguess) { 1; # giá trị cho lại là true } else { open(MAIL, “|mail Địa_chỉ_bạn_ở_đây”); print MAIL “tin xấu: $somename đã đoán
$someguess\n”; 0; # cho lại giá trị false } }
Chú ý rằng không còn cần hỏi người dùng về tên
người ấy nữa - chúng ta đã biết nó!
Perl cung cấp nhiều trình truy nhập cơ sở dữ liệu hệ
thống để lôi ra những giá trị từ cơ sở dữ liệu mật hiệu,
nhóm, máy chủ, mạng, dịch vụ và giao thức. Cả việc tra
cứu riêng (như được trình bầy ở trên) và việc duyệt số
lớn cũng đã được Perl hỗ trợ.
Liệt kê các từ bí mật
Rồi ông phụ trách danh sách từ bí mật lại muốn có
một báo cáo về tất cả những từ bí mật hiện đang dùng,
và chúng cũ đến đâu. Nếu gạt sang bên chương trình từ
bí mật một lúc, sẽ có thời gian để viết một chương trình
báo cáo cho ông phụ trách.
Trước hết, lấy ra tất cả các từ bí mật, bằng việc ăn
cắp một đoạn mã trong chương trình con &init_words: while ($filename = <*.secret>) {
41
open (WORDSLIST, $filename); if (-M WORDSLIST > 7) { while ($name = <WORDSLIST>) { chop ($name); $word = <WORDSLIST>; chop($word); ### chất liệu mới sẽ đưa vào đây } } close (WORDSLIST); }
Tại điểm có đánh dấu “chất liệu mới sẽ đưa vào
đây,” cần biết ba điều: tên của tệp (trong $filename), tên
một ai đó (trong $name), và rằng từ bí mật của một người
(trong $Word). Sau đây là chỗ để dùng công cụ sinh báo
cáo của Perl. Định nghĩa một dạng thức ở đâu đó trong
chương trình (thông thường gần cuối, giống như chương
trình con):
format STDOUT = @<<<<<<<<<<<<<<< @<<<<<<<<<<
@<<<<<<<<<<<<<< $filename, $name, $word .
Định nghĩa dạng thức này bắt đầu với format
STDOUT =, và kết thúc với một dấu chấm. Hai dòng ở
giữa là chính dạng thức. Dòng đầu của dạng thức này là
dòng định nghĩa trường, xác định số lượng, chiều dài và
kiểu của trường. Với dạng thức này, chúng có ba trường.
Dòng đi sau dòng định nghĩa trường bao giờ cũng là
dòng giá trị trường. Dòng giá trị cho một danh sách các
biểu thức mà sẽ được tính khi dạng thức này được dùng,
và kết quả của những biểu thức đó sẽ được gắn vào trong
các trường đã được xác định trên dòng trước đó.
42
Gọi dạng thức này với toán tử write, như thế này:
#!/usr/bin/perl while ($filename = <*.secret>) { open (WORDSLIST, $filename); if (-M WORDSLIST < 7) { while ($name = <WORDSLIST>) { chop($name); $word = <WORDSLIST> chop($word); write; # gọi dạng thức STDOUT tới STDOUT } } close (WORDSLIST) }
format STDOUT = @<<<<<<<<<<<<<<< @<<<<<<<<<<
@<<<<<<<<<<<<<< $filename, $name, $word .
Khi dạng thức này được gọi tới, Perl sẽ tính các biểu
thức trường và sinh ra một dòng mà nó gửi ra tước hiệu
tệp STDOUT. Vì write là được gọi một lần mỗi khi đi
qua chu trình nên sẽ thu được một loạt các dòng với văn
bản theo cột, mỗi dòng cho một từ bí mật.
Hừm. Chúng còn chưa có nhãn cho các cột. Mà điều
đó thì cũng dễ thôi, chỉ cần thêm vào dạng thức trên đầu
trang, như:
format STDOUT_TOP = Page @<< $% Tên tệp Tên Từ ========== ======== ========== .
43
Dạng thức này mang tên STDOUT_TOP, và sẽ
được dùng đầu tiên ngay khi gọi tới dạng thức STDOUT,
rồi lại sau 60 lần đưa ra STDOUT thì nó lại được sinh ra.
Các tiêu đề cột ở đây thẳng hàng với các cột trong dạng
thức STDOUT, cho nên mọi thứ khớp vào nhau.
Dòng đầu tiên trong dạng thức này chỉ ra một văn
bản hằng nào đó (Page) cùng với việc định nghĩa trường
ba kí tự. Dòng sau là dòng giá trị trường, ở đây với một
biểu thức. Biểu thức này là biến $%, giữ số trang được in
ra - một giá trị rất có ích trong dạng thức đầu trang.
Dòng thứ ba của dạng thức này để trống. Vì dòng
này không chứa bất kì trường nào nên dòng sau nó
không phải là dòng giá trị trường. Dòng trống này được
sao trực tiếp lên cái ra, tạo ra một dòng trống giữa số
trang và tiêu đề cột dưới.
Hai dòng cuối của dạng thức này cũng không chứa
trường nào, cho nên chúng được sao trực tiếp ra cái ra.
Do vậy dạng thức này sinh ra bốn dòng, một trong đó có
một phần bị thay đổi qua mỗi trang.
Chỉ cần thêm định nghĩa này vào chương trình trước
là nó làm việc. Perl để ý đến dạng thức đầu trang tự
động.
Perl cũng có các trường được định tâm hay căn lề
phải, và hỗ trợ cho miền đoạn được rót kín. Sẽ có nhiều
vấn đề về điều này hơn khi đi vào các dạng thức trong
Chương 11, Dạng thức.
44
Làm cho danh sách từ cũ đó đáng lưu ý hơn
Khi đọc qua các tệp *.secret trong danh mục hiện tại,
có thể tìm thấy các tệp quá cũ. Cho tới nay, ta thường
nhảy qua những tệp này. Nay đi thêm một bước nữa - sẽ
đổi tên chúng thành *.secret.old để cho ls của danh mục sẽ
nhanh chóng cho những tệp nào quá cũ, đơn thuần theo
tên.
Sau đây là cách thức thể hiện cho chương trình con
&init_words với sửa đổi này:
sub init_words { while ($filename = <*.secret>) { open (WORDSLIST, $filename); if (-M WORDSLIST > 7) { while ($name = <WORDSLIST>) { chop ($name); $word = <WORDSLIST>; chop($word); $words{$name} = $word; } } else { # đổi tên tệp để nó đáng để ý hơn rename($filename, “$filename.old”); } close (WORDSLIST); } }
Chú đến phần else mới của việc kiểm tra tuổi. Nếu
tệp cũ hơn bẩy ngày, nó sẽ được đổi tên bằng toán tử
rename(). Toán tử này lấy hai tham biến, đổi tệp có tên
trong tham biến thứ nhất thành tên trong tham biến thứ
hai.
Perl có một phạm vi đầy đủ các toán tử thao tác tệp -
gần như bất kì cái gì bạn có thể thực hiện cho một tệp
45
trong chương trình C, bạn cũng có thể làm từ Perl.
Duy trì cơ sở dữ liệu đoán đúng cuối cùng
Cần giữ lại dấu vết khi nào việc đoán đúng gần nhất
đã được thực hiện cho mỗi người dùng. Một cấu trúc dữ
liệu dường như mới thoáng nhìn thì có vẻ được là mảng
kết hợp. Chẳng hạn, câu lệnh:
$last_good{$name} = time ;
gán thời gian UNIX hiện tại (một số nguyên lớn
quãng 700 triệu, chỉ ra số giây) cho một phần tử
của %last_good có tên với khoá đó. Qua thời gian, điều
này sẽ dường như cho một cơ sở dữ liệu chỉ ra thời điểm
gần nhất mà từ bí mật đã được đoán đúng cho từng
người dùng đã gọi tới chương trình này.
Nhưng, mảng lại không tồn tại giữa những lần gọi
chương trình. Mỗi lần chương trình này được gọi thì một
mảng mới lại được hình thành, cho nên nhiều nhất thì tạo
ra được mảng một phần tử và rồi lập tức lại quên mất nó
khi chương trình ra.
Toán tử dbmopen() ánh xạ một mảng kết hợp vào
một tệp đĩa (thực tế là một cặp tệp đĩa) được xem như
một DBM. Nó được dùng như thế này:
dbmopen(%last_good, “lastdb”, 066); $last_good($name) = time; dbmclose(%last_good);
Câu lệnh đầu tiên thực hiện việc ánh xạ này, dùng
các tên tệp đĩa của lastdb.dir và lastdb.pag (các tên này là
tên thông thường cho DBM được gọi là lastdb). Các phép
về tệp UNIX được dùng cho hai tệp này nếu các tệp đó
46
phải được tạo ra (khi chúng lần đầu tiên được gặp tới) là
0666. Mốt này có nghĩa là bất kì ai cũng có thể đọc hay
ghi lên tệp. (Các bit phép tệp được mô tả trong manpage
chmod(2).)
Câu lệnh thứ hai chỉ ra rằng chúng dùng mảng kết
hợp đã được ánh xạ này hệt như mảng kết hợp thông
thường. Tuy nhiên, việc tạo ra hay cập nhật một phần tử
của mảng sẽ tự động cập nhật tệp đĩa tạo nên DBM. Và,
khi mảng được truy nhập tới lần cuối thì giá trị bên trong
mảng sẽ tới trực tiếp từ hình ảnh đĩa. Điều này cho mảng
kết hợp cuộc sống trải bên ngoài lời gọi hiện thời của
chương trình - một sự bền lâu của riêng nó.
Câu lệnh thứ ba ngắt mảng kết hợp ra khỏi DBM,
giống hệt thao tác đóng tệp close().
Bạn có thể chèn thêm ba câu lệnh này vào ngay đầu
các định nghĩa chương trình con.
Mặc dầu các câu lệnh được chèn thêm này duy trì cơ
sở dữ liệu là tốt (và thậm chí còn tạo ra nó trong lần
đầu), chúng vẫn không có cách nào để xem xét thông tin
trong đó. Để làm điều đó, có thể tạo ra một chương trình
nhỏ tách biệt trông đại thể như thế này:
#!/usr/bin/perl dbmopen(%last_good, “lastdb”, 0666); foreach $name (sort keys(%last_good) { $when = $last_good{$name}; $hours = (time - $when) / 3600; # tính giờ đã qua write; } format STDOUT = User @<<<<<<<<<<: lần đoán đúng cuối cùng là @<<<
giờ đã qua. $name, $hour
47
.
Chúng có vài toán tử mới ở đây: chu trình foreach,
sắp xếp một danh sách, và lấy khoá của mảng.
Trước hết, toán tử keys() lấy một tên mảng kết hợp
như một đối và cho lại một danh sách tất cả các khoá của
mảng đó theo một thứ tự không xác định nào đó. (Điều
này hệt như toán tử keys trong awk.) Với mảng %words
được xác định trước đây, kết quả là một cái gì đó tựa như
fred, barney, betty, wilma, theo một thứ tự không xác định.
Với mảng %last_good, kết quả sẽ là một danh sách của
tất cả người dùng đã đoán thành công từ bí mật của riêng
mình.
Toán tử sort sẵp xếp theo thứ tự bảng chữ (hệt như
việc truyền một tệp văn bản qua chỉ lệnh sort). Điều này
bảo đảm rằng danh sách được xử lí bởi câu lệnh foreach
bao giờ cũng theo thứ tự bảng chữ.
Thân của chu trình foreach nạp vào hai biến được
dùng trong dạng thức STDOUT, và rồi gọi tới dạng thức
đó. Chú ý rằng chúng đoán ra tuổi của một phần tử bằng
cách trừ thời gian UNIX đã cất giữ (trong mảng) từ thời
gian hiện tại (như kết quả của time) và rồi chia cho 3600
(để chuyển từ giây sang giờ).
Perl cũng cung cấp những cách thức dễ dàng để tạo
ra và duy trì các cơ sở dữ liệu hướng văn bản (như tệp
mật hiệu) và cơ sở dữ liệu bản ghi chiều dài cố định (như
cơ sở dữ liệu “đăng nhập lần cuối” do chương trình login
duy trì). Những cơ sở dữ liệu này sẽ được mô tả trong
Chương 17, Thao tác cơ sở dữ liệu người dùng.
48
Chương trình cuối cùng
Sau đây là chương trình mà cuộc đi dạo này đã đưa
tới dưới dạng cuối cùng để bạn có thể chơi với chúng.
Trước hết là chương trình “nói lời chào”:
#! /usr/bin/perl &init_words; @password = getpwuid($<); # lấy dữ liệu mật hiệu $name = $password[6]; # lấy trường GCOS $name =~ s/,.*//; # vứt đi mọi thứ sau dấu phẩy đầu tiên if ($name =~ /^randal\b/i ) { # trở lại cách khác print “Xin chào, Randal! May quá anh ở đây!\n”; } else { print “Xin chào, $name!\n”; # chào thông thường print “Từ bí mật là gì?” ; $guess = <STDIN>; chop($guess); while ( ! &good_word($name, $guess)) { print “Sai rồi, thử lại đi. Từ bí mật là gì?”; $guess = <STDIN>; chop($guess); } }
dbmopen(%last_good, “lastdb”, 066); $last_good($name) = time; dbmclose(%last_good);
sub init_words { while ($filename = <*.secret>) { open (WORDSLIST, $filename); if (-M WORDSLIST > 7) { while ($name = <WORDSLIST>) { chop ($name); $word = <WORDSLIST>; chop($word); $words{$name} = $word;
49
} } close (WORDSLIST); } }
sub good_word { local($somename, $someguess) = @_; # tên tham
biến $somename =~ s/\W.*//; # bỏ mọi thứ sau từ đầu $somename =~ tr/A-Z/a-z/; # mọi thứ thành chữ
thường if ($somename eq “randal”) { # không nên đoán 1; #giá trị cho lại là true } elsif (($words{$somename} || “đồ cáu kỉnh”) eq
$someguess) { 1; # giá trị cho lại là true } else { open(MAIL, “|mail Địa_chỉ_bạn_ở_đây”); print MAIL “tin xấu: $somename đã đoán
$someguess\n”; 0; # cho lại giá trị false } }
Tiếp đó, có bộ in từ bí mật:
#! /usr/bin/perl while ($filename = <*.secret>) { open (WORDSLIST, $filename); if (-M WORDSLIST > 7) { while ($name = <WORDSLIST>) { chop ($name); $word = <WORDSLIST>; chop($word); write; # gọi dạng thức STDOUT cho
STDOUT } } close(WORDSLIST);
50
}
format STDOUT = @<<<<<<<<<<<<<<< @<<<<<<<<<<
@<<<<<<<<<<<<<< $filename, $name, $word .
format STDOUT_TOP = Page @<< $% Tên tệp Tên Từ ========== ======== ========== .
Và cuối cùng, là chương trình hiển thị từ đã được
dùng lần cuối cùng:
#!/usr/bin/perl dbmopen(%last_good, “lastdb”, 0666); foreach $name (sort keys(%last_good) { $when = $last_good{$name}; $hours = (time - $when) / 3600; # tính giờ đã qua write; } format STDOUT = User @<<<<<<<<<<: lần đoán đúng cuối cùng là @<<<
giờ đã qua. $name, $hour .
Cùng với danh sách từ bí mật (các tệp có tên
something.secret trong danh mục hiện tại) và cơ sở dữ
liệu lastdb.dir và lastdb.pag, bạn đã có tất cả những gì
mình cần.
51
Bài tập
Thông thường, mỗi chương sẽ kết thúc với một số
bài tập, lời giải cho chúng sẽ có trong Phụ lục A, Trả lời
bài tập. Với chuyến đi dạo này, lời giải đã được cho ở
trên.
1. Gõ chương trình thí dụ trên vào máy rồi cho nó chạy.
(Bạn cần tạo ra danh sách từ bí mật nữa.) Hỏi thầy
Perl của bạn nếu bạn cần trợ giúp.
52
53
2
Dữ liệu
vô hướng
Dữ liệu vô hướng là gì?
Vô hướng là loại dữ liệu đơn giản nhất mà Perl thao
tác. Một vô hướng thì hoặc là một số (giống 4 hay
3.25e20) hay một xâu các kí tự (giống Xin chào hay
Gettysburg Address). Mặc dầu bạn có thể nghĩ về số và
xâu như những vật rất khác nhau, Perl dùng chúng gần
như đổi lẫn cho nhau, cho nên tôi sẽ mô tả chúng với
nhau.
Một giá trị vô hướng có thể được tác động bởi các
toán tử (giống như phép cộng hay ghép tiếp), nói chung
cho lại một kết quả vô hướng. Một giá trị vô hướng có
thể được cất giữ vào trong một biến vô hướng. Các vô
hướng có thể được đọc từ tệp và thiết bị, và được ghi ra.
Trong chương này:
Dữ liệu vô hướng là gì?
Số
Xâu
Toán tử
Biến vô hướng
Toán tử trên biến vô hướng
<STDIN> xem như giá trị vô hướng
In ra với print()
Giá trị undef
54
Số
Mặc dầu vô hướng hoặc là một số hay một xâu, điều
cũng có ích là nhìn vào các số và xâu tách biệt nhau
trong một chốc. Ta sẽ xét số trước rồi đến xâu...
Tất cả các số đã có cùng dạng thức bên trong
Như bạn sẽ thấy trong vài đoạn tiếp đây, bạn có thể
xác định cả số nguyên (toàn bộ số, giống như 14 hay
342) và số dấu phẩy động (số thực với dấu chấm thập
phân, như 3.14, hay 1.35 lần 1025
). Nhưng bên trong,
Perl chỉ tính với các giá trị dấu phẩy động độ chính xác
gấp đôi. Điều này có nghĩa là không có giá trị nguyên
bên trong Perl - một hằng nguyên trong chương trình
được xử lí như giá trị dấu phẩy động tương đương. Bạn
có lẽ không để ý (hay quan tâm nhiều) đến việc chuyển
đổi, nhưng bạn nên dùng tìm kiếm phép toán nguyên
(xem như ngược với các phép toán dấu phẩy động), vì
không có tẹo nào.
Hằng kí hiệu động
Hằng kí hiệu là một cách để biểu diễn một giá trị
trong văn bản chương trình Perl - bạn cũng có thể gọi
điều này là một hằng trong chương trình mình, nhưng tôi
sẽ dùng thuật ngữ hằng kí hiệu. Hằng kí hiệu là cách
thức biểu diễn dữ liệu trong mã chương trình gốc của
chương trình bạn như cái vào cho trình biên dịch Perl.
(Dữ liệu được đọc từ hay ghi lên các tệp đã được xử lí
55
tương tự, nhưng không đồng nhất.)
Perl chấp nhận tập hợp đầy đủ các hằng kí hiệu dấu
phẩy động có sẵn cho người lập trình C. Số có hay
không có dấu chấm thập phân đã được phép (kể cả tiền
tố cộng hay trừ tuỳ chọn), cũng như phần chỉ số mũ phụ
thêm (kí pháp luỹ thừa) với cách viết E. Chẳng hạn:
1.25 # một phần tư 7.25e45 # 7.25 lần 10 mũ 45 (một số lớn) -6.5e24 # âm 6.5 lần 10 mũ 24 (một số âm lớn) -12e-24 # âm 12 lần 10 mũ -24 (một số âm rất nhỏ) -1.2E-23 # một cách khác để nói điều đó.
Hằng kí hiệu nguyên
Các hằng kí hiệu nguyên cũng là trực tiếp, như
trong:
12 15 -2004 3485
Bạn đừng bắt đầu một số bằng 0, vì Perl hỗ trợ cho
hằng kí hiệu hệ tám và hệ mười sáu (hệt như kiểu C). Số
hệ tám bắt đầu bằng số 0 đứng đầu, còn số hệ mười sáu
bắt đầu bằng 0x hay 0X* . Các chữ số hệ mười sáu A đến
F (trong cả hai kiểu chữ hoa thường) đã biểu thị cho các
giá trị số qui ước từ 10 đến 15. Chẳng hạn:
0377 # 377 hệ tám, giống như 255 thập phân
* Chỉ báo “số không đứng đầu” chỉ có tác dụng với các hằng kí hiệu
- không có tác dụng cho việc chuyển đổi tự động xâu sang số. bạn
có thể chuyển đổi một xâu dữ liệu giống như một giá trị hệ tám và
hệ mười sáu thành một số với oct() hay hex().
56
-0xff # FF hệ mười sáu âm, hệt như -255 thập phân
Xâu
Xâu là các dẫy kí tự (như Xin chào). Mỗi kí tự đã là
một giá trị 8-bit trong toàn bộ tập 256 kí tự (không có gì
đặc biệt về kí tự NUL như trong C).
Xâu ngắn nhất có thể được thì không có kí tự nào.
Xâu dài nhất chiếm trọn bộ nhớ của bạn (mặc dầu bạn sẽ
chẳng thể nào làm gì nhiều với nó cả). Điều này phù hợp
với nguyên lí “không có giới hạn sẵn” mà Perl cho phép
mọi cơ hội. Các xâu điển hình là các dãy in được gồm
các chữ và số và dấu ngắt trong phạm vi ASCII 32 tới
ASCII 126. Tuy nhiên, khả năng để có bất kì kí tự nào từ
0 tới 255 trong một xâu có nghĩa là bạn có thể tạo ra,
quét qua, và thao tác dữ liệu nhị phân thô như các xâu -
một cái gì đó mà phần lớn các trình tiện ích UNIX khác
sẽ gặp khó khăn lớn. (Chẳng hạn, bạn có thể vá víu lõi
UNIX bằng việc đọc nó vào trong xâu Perl, tiến hành
thay đổi, và ghi kết quả lại.)
Giống như số, xâu có biểu diễn hằng kí hiệu (cách
thức bạn biểu diễn xâu trong chương trình Perl). Các xâu
hằng kí hiệu có theo hai hương vị: xâu dấu nháy đơn và
xâu dấu nháy kép.
Xâu dấu nháy đơn
Xâu dấu nháy đơn là một dãy các kí tự được bao
trong dấu nháy đơn. Dấu nháy đơn không phải là một
phần của bản thân xâu - chúng chỉ có đó để Perl xác định
57
chỗ bắt đầu và kết thúc của xâu. Bất kì kí tự nào nằm
giữa các dấu nháy (kể cả dấu dòng mới, nếu xâu vẫn còn
tiếp tục sang dòng sau) đều là hợp pháp bên trong xâu.
Hai ngoại lệ: để lấy được một dấu nháy đơn trong một
xâu có nháy đơn, đặt trước nó một dấu sổ chéo ngược.
Và để lấy được dấu sổ chéo ngược trong một xâu có
nháy đơn, đặt trước dấu sổ chéo ngược một dấu sổ chéo
ngược nữa. Dưới dạng hình ảnh:
‘hello’ # năm kí tự: h, e, l, l, o ‘dont\’t’ # năm kí tự: d, o, n, nháy đơn, t ‘’ # xâu không (không kí tự) ‘silly\\me’ # silly, theo sau là một sổ chéo ngược, sau là
me ‘hello\n’ # hello theo sau là sổ chéo ngược và n ‘hello there’ # hello, dòng mới, there (toàn bộ 11 kí tự)
Chú ý rằng \n bên trong một xâu có nháy đơn thì
không được hiểu là dòng mới, nhưng là hai kí tự sổ chéo
ngược và n. (Chỉ khi sổ chéo ngược đi theo sau bởi một
sổ chéo ngược khác hay một dấu nháy đơn thì mới mang
nghĩa đặc biệt.)
Xâu dấu nháy kép
Xâu dấu nháy kép hành động hệt như xâu trong C.
Một lần nữa, nó lại là dãy các kí tự, mặc dầu lần này
được bao bởi dấu ngoặc kép. Nhưng bây giờ dấu sổ chéo
ngược lấy toàn bộ sức mạnh của nó để xác định các kí tự
điều khiển nào đó, hay thậm chí bất kì kí tự nào qua các
biểu diễn hệ tám hay hệ mười sáu. Đây là một số xâu dấu
nháy kép:
“hello world\n” # hello world, và dòng mới
58
“new \177” # new, dấu cách và kí tự xoá (177 hệ tám) “coke\tsprite” # coke, dấu tab, và sprite
Dấu sổ chéo có thể đứng trước nhiều kí tự khác nhau
để hàm ý những điều khác nhau (về điển hình nó được
gọi là lối thoát sổ chéo). Danh sách đầy đủ của các lối
thoát xâu nháy kép được cho trong Bảng 2-1.
Bảng 2-1 Lối thoát sổ chéo ngược xâu nháy kép
Kết cấu Ý nghĩa
\n dòng mới
\r quay lại
\t tab
\f kéo giấy
\b Xoá lùi
\v tab chiều đứng
\a Chuông
\e lối thoát
\007 bất kì giá trị ASCII hệ tám (ở
đây, 007 = chuông)
\x7f giá trị ASCII hệ mười sáu (ở
đây, 7f = xoá)
\cC bất kì kí tự “điều khiển” nào (ở
đây, control C)
\\ sổ chéo ngược
\” dấu nháy kép
\l chữ tiếp là chữ thường
\L tất cả các chữ đi sau cho tới \E
đã là chữ thường
\u Chữ tiếp là chữ hoa
\U tất cả các chữ đi sau cho tới \E
đã là chữ hoa
\E Kết thúc \L hay \U
59
Một tính năng khác của xâu nháy kép là ở chỗ chúng
cho phép chen lẫn các biến, nghĩa là một số tên biến nào
đó bên trong xâu được thay thế bởi giá trị hiện tại của
chúng khi xâu được dùng. Chúng đã không được giới
thiệu một cách chính thức là các biến trông như thế nào
(ngoại trừ trong cuộc đi dạo), cho nên tôi sẽ quay lại vấn
đề này sau.
Toán tử
Một toán tử tạo ra một giá trị mới (kết quả) từ một
hay nhiều giá trị khác (các toán hạng). Chẳng hạn, + là
một toán tử vì nó nhận hai số (toán hạng, như 5 và 6), và
tạo ra một giá trị mới (11, kết quả).
Các toán tử và biểu thức của Perl nói chung đã là
siêu tập của các toán tử đã có trong hầu hết các ngôn ngữ
lập trình tựa ALGOL/Pascal, như C. Một toán tử bao giờ
cũng trông đợi các toán hạng số hay xâu (hay có thể là tổ
hợp của cả hai). Nếu bạn cung cấp một toán hạng xâu ở
chỗ đang cần tới một số, hay ngược lại, thì Perl sẽ tự
động chuyển toán hạng đó bằng việc dùng các qui tắc
khá trực giác, mà sẽ được nêu chi tiết trong mục
“Chuyển đổi giữa số và xâu,” dưới đây.
Toán tử số
Perl cung cấp các toán tử cộng, trừ, nhân, chia điển
hình thông thường, vân vân. Chẳng hạn:
2 + 3 # 2 cộng 3, hay 5
60
5.1 - 2.4 # 5.1 trừ đi 2.4, hay 2.7 3 * 12 # 3 lần 12 = 36 14 / 2 # 14 chia cho 2, hay 7 10.2 / 0.3 # 10.2 chia cho 0.3, hay 34 10 / 3 # bao giờ là phép chia dấu phẩy động, nên
3.333...
Bên cạnh đó, Perl cung cấp toán tử luỹ thừa kiểu
FORTRAN, mà nhiều người đã từng mong mỏi cho
Pascal và C. Toán tử này được biểu diễn bằng hai dấu
sao, như 2**3, chính là hai luỹ thừa ba, hay tám. (Nếu
kết quả không thể khớp trong số dấu phẩy động độ chính
xác gấp đôi, như một số âm mà lại luỹ thừa theo số
không nguyên, hay một số lớn lấy luỹ thừa theo số lớn,
thì bạn sẽ nhận được lỗi định mệnh.)
Perl cũng hỗ trợ cho toán tử lấy đồng dư modulus,
như trong C. Giá trị của biểu thức 10 % 3 là số dư khi
lấy mười chia cho ba, chính là một. Cả hai giá trị đã
trước hết được đưa về giá trị nguyên, cho nên 10.5 % 3.2
được tính là 10 % 3.
Các toán tử so sánh logic là hệt như các toán tử có
trong C (< <= == >= > !=), và việc so sánh hai giá trị về
mặt số sẽ cho lại một giá trị đúng hay sai. Chẳng hạn,
3>2 cho lại đúng vì ba lớn hơn hai, trong khi 5 != 5 cho
lại sai vì không đúng là năm lại không bằng năm. Các
định nghĩa về đúng và sai được nói tới về sau, nhưng với
hiện tại, nghĩ về giá trị cho lại giống như chúng ở trong
C - một là đúng, còn không là sai. (Các toán tử này sẽ
được thăm lại trong Bảng 2-2.)
61
Toán tử xâu
Các giá trị xâu có thể được ghép với toán tử chấm
(.). (Quả thế, đó là dấu chấm đơn.) Điều này không làm
thay đổi xâu, cũng như 2+3 không làm thay đổi 2 hay 3.
Xâu kết quả (dài hơn) vậy là có sẵn cho tính toán thêm
hay được cất giữ trong một biến.
“hello” . “world” # hệt như “helloworld” ‘hello wordl’ . “\n” # hệt như “hello world\n” “fred” . “ “ . “barney” # hệt như “fred barney”
Chú ý rằng việc ghép nối phải được gọi tường minh
tới toán tử ., không giống awk mà bạn đơn thuần phải
đánh dấu hai giá trị gần lẫn nhau.
Một tập các toán tử cho xâu khác là toán tử so sánh
xâu. Các toán tử này đã tựa FORTRAN, như lt thay cho
bé hơn, vân vân. Các toán tử so sánh các giá trị ASCII
của các kí tự của xâu theo cách thông thường. Tập đầy
đủ các toán tử so sánh (cho cả số và xâu) được nêu trong
Bảng 2-2.
Bảng 2-2. Các toán tử so sánh số và xâu
Phép so sánh Số Xâu
Bằng == eq
Không bằng != ne
BĐ hơn < lt
Lớn hơn > gt
BĐ hơn hay bằng <= le
Lớn hơn hay bằng >= ge
62
Bạn có thể tự hỏi tại sao có các toán tử phân tách
cho số và xâu vậy, nếu số và xâu được tự động chuyển
đổi lẫn cho nhau. Xét hai giá trị 7 và 30. Nếu được so
sánh như số thì 7 hiển nhiên bé hơn 30, nhưng nếu được
so sánh theo xâu, thì xâu “30” sẽ đứng trước xâu “7” (vì
giá trị ASCII của 3 thì bé hơn giá trị ASCII của 7), và do
đó là bé hơn. Cho nên, không giống awk, Perl đòi hỏi
bạn xác định đúng kiểu so sánh, liệu đó là số hay xâu.
Chú ý rằng các phép so sánh số và xâu về đại thể
ngược với những điều xảy ra cho chỉ lệnh test của
UNIX, mà thường dùng kí hiệu -eq để so sánh số còn =
để so sánh xâu.
Vẫn còn một toán tử xâu khác là toán tử lặp lại xâu,
bao gồm một kí tự chữ thường đơn giản x. Toán tử này
lấy toán hạng trái của nó (một xâu), và thực hiện nhiều
việc ghép bản sao của xâu đó theo số lần do toán hạng
bên phải chỉ ra (một số). Chẳng hạn:
“fred” x 3 # là “fredfredfred” “barney” x (4+1) # là “barney” x 5 hay # “barneybarneybarneybarneybarney” (3+2) x 4 # là 5 x 4, hay thực sự “5” x 4, là
“5555”
Thí dụ cuối cùng đáng để xem xét chậm rãi. Các dấu
ngoặc trên (3+2) buộc cho phần này của biểu thức cần
phải được tính trước, cho năm. (Các dấu ngoặc ở đây
làm việc giống như trong C, hay trong toán học chuẩn.)
Nhưng toán tử lặp lại xâu cần một xâu cho toán hạng bên
trái, cho nên số 5 được chuyển thành xâu “5” (dùng các
qui tắc sẽ được mô tả chi tiết về sau), thành xâu một kí
tự. Xâu mới này rồi được sao lên bốn lần, cho xâu bốn kí
tự 5555. Chú ý rằng nếu đảo ngược trật tự các toán hạng,
63
thì sẽ làm năm bản sao của xâu 4, cho 44444. Điều này
chỉ ra rằng việc lặp lại xâu là không giao hoán.
Số đếm bản sao (toán hạng bên phải) trước hết sẽ bị
chặt cụt đi để cho giá trị nguyên (4.8 trở thành 4) trước
khi được sử dụng. Số đếm bản sao bé hơn một sẽ gây ra
kết quả là xâu rỗng (chiều dài không).
Thứ tự ưu tiên và luật kết hợp của toán tử
Thứ tự ưu tiên của toán tử xác định ra cách giải
quyết trường hợp không rõ ràng khi nào dùng toán tử
nào trên ba toán hạng. Chẳng hạn, trong biểu thức
2+3*4, sẽ thực hiện phép cộng trước hay phép nhân
trước? Nếu làm phép cộng trước thì sẽ được 5*4, hay 20.
Nhưng nếu làm phép nhân trước (như vẫn được dạy
trong giờ toán) thì được 2+12, hay 14. May mắn là Perl
chọn định nghĩa toán học thông thường, thực hiện nhân
trước. Bởi điều này, nói nhân có số ưu tiên cao hơn
cộng.
Bạn có thể phá rào trật tự ưu tiên bằng việc dùng
dấu ngoặc. Bất kì cái gì trong dấu ngoặc đã được tính hết
trước khi toán tử bên ngoài dấu ngoặc được áp dụng (hệt
như bạn đã học trong giờ toán). Cho nên nếu tôi thực sự
muốn cộng trước khi nhân, thì tôi có thể viết (2+3)*4,
cho 20. Cũng vậy, nếu tôi muốn biểu thị rằng phép nhân
được thực hiện trước phép cộng, tôi có thể trang điểm
thêm nhưng chẳng để làm gì, một cặp dấu ngoặc trong
2+(3*4).
Trong khi ưu tiên là trực giác cho phép cộng và
nhân thì ta bắt đầu lao vào vấn đề thường hay phải
64
đương đầu với, chẳng hạn, phân biệt thế nào đối với
phép ghép xâu và nâng lên luỹ thừa. Cách đúng đắn để
giải quyết điều này là tra cứu sơ đồ số thứ tự ưu tiên toán
tử của Perl, được nêu trong Bảng 2-3. (Chú ý rằng một
số các toán tử còn chưa được mô tả, và thực ra, thậm chí
không thể xuất hiện ở bất kì đâu trong cuốn sách này,
nhưng chớ có làm điều đó làm bạn hoảng sợ về việc đọc
chúng.) Với những toán tử cũng có trong C, thì những
toán tử đó có cùng số thứ tự ưu tiên như chúng có trong
C (mà tôi có thể chẳng bao giờ nhớ được).
Bảng 2-3: Luật kết hợp và số ưu tiên của các toán tử
(thấp nhất đến cao nhất)
Luật
kết hợp
Toán tử
không toán tử “danh sách”
trái , (phẩy)
phải += và các toán tử khác (toán tử “gán”)
phải ? : (toán tử if/then/else ba ngôi)
không .. (toán tử phạm vi, cấu tử danh sách)
trái || (hoặc logic)
trái && (và logic)
trái | ^ (hoặc bit, hoặc bit loại trừ)
trái & (và bit)
không == != <=> eq ne cmp (toán tử “bằng”)
65
Luật
kết hợp
Toán tử
không < <= > >= lt le gt ge (toán tử “không
bằng”)
không Toán tử một ngôi có tên
không -r và (các toán tử kiểm tra tệp)* khác
trái << >> (dịch chuyển bit)
trái + - . (cộng, trừ, ghép xâu)
trái * / % x (nhân, chia, lấy dư, lặp xâu)
trái =~ !~ (sánh, không sánh)
phải ** (luỹ thừa)
phải ! ~ - (phủ định logic, phủ định bit, phủ
định số)
không ++ -- (tự tăng, tự giảm)
Trong sơ đồ này, bất kì toán tử đã cho nào đã có số
ưu tiên lớn hơn các toán tử được liệt kê trên nó, và có số
ưu tiên thấp hơn các toán tử được liệt kê dưới nó. (Điều
này ngược lại điều có lẽ bạn đang trông đợi, nhưng nó
hệt như trong sách con lạc đà, và chúng tôi cũng đã dựng
ngược nó xuống ở đó nữa.) Các toán tử tại cùng mức ưu
tiên được giải quyết theo luật kết hợp.
Giống như với số ưu tiên, luật kết hợp giải quyết trật
* Perl 5.0 tổ hợp các toán tử kiểm tra tệp và toán tử một ngôi có tên
vào cùng mức số ưu tiên
66
tự của các phép toán khi hai toán tử có cùng mức ưu tiên
cùng tác động trên ba toán hạng:
2 ** 3 ** 4 # 2 ** (3 ** 4), hay 2 ** 81, hay xấp xỉ 2.41e24 72 / 12 / 3 # (72 / 12) / 3, hay 6 / 3, hay 2 30 / 6 * 3 # (30/6)*3, hay 15
Trong trường hợp thứ nhất, toán tử ** có luật kết
hợp phải, cho nên các dấu ngoặc được áp dụng từ bên
phải. So sánh với nó, các toán tử * và / có luật kết hợp
trái, cho tập các dấu ngoặc bên trái.
Chuyển đổi giữa số và xâu
Nếu một giá trị xâu được dùng như một toán hạng
cho một toán tử số (chẳng hạn, +), thì Perl sẽ tự động
chuyển xâu thành giá trị số tương đương, dường như nó
đã được đưa vào như một giá trị dấu phẩy động* . Những
chất liệu phi số đằng đuôi và khoảng trắng đằng đầu đã
bị bỏ qua một cách yên lặng và lễ phép, cho nên
“ 123.45fred” (với dấu cách đứng trước) chuyển thành
123.45 với lời cảnh báo* . Tại một cực điểm của điều
này, một cái gì đó không phải là số tẹo nào chuyển thành
không mà không có báo trước (như xâu fred được dùng
như số).
Giống vậy, nếu một giá trị số được cho khi đang cần
tới một giá trị xâu (cho phép ghép xâu chẳng hạn), thì
giá trị số sẽ được mở rộng thành bất kì xâu nào sẽ được
in ra cho số đó. Chẳng hạn, nếu bạn muốn ghép nối X và
* Các giá trị hệ tám và hệ mười sáu không được hỗ trợ trong chuyển
đổi tự động này. dùng hex() và oct() để diễn giải các giá trị hệ mười
sáu và tám. * Trừ phi bạn bật tuỳ chọn -w trên dòng lệnh
67
theo sau là kết quả của 4 nhân với 5 thì bạn có thể làm
đơn giản là:
“X”.(4*5) # hệt như “X”.20, hay “X20”
(Nhớ rằng các dấu ngoặc này buộc 4*5 phải được
tính trước khi xem xét toán tử ghép nối xâu.)
Nói cách khác, bạn không thực sự phải lo lắng gì về
liệu bạn có một số hay một xâu (phần lớn thời gian). Perl
thực hiện mọi chuyển đổi cho bạn.
Biến vô hướng
Một biến là một tên gọi cho một chỗ chứa giữ được
một hay nhiều giá trị. Tên của biến là không đổi trong
toàn bộ chương trình, nhưng giá trị hay các giá trị được
chứa trong biến đó về cơ bản thì lại thay đổi đi thay đổi
lại trong suốt thực hiện chương trình.
Một biến vô hướng giữ một giá trị vô hướng riêng
(biểu thị cho một số, hay một xâu, hay cả hai). Các tên
biến vô hướng bắt đầu với dấu đô la và tiếp theo sau là
một chữ, rồi thì có thể là nhiều chữ, số hay dấu gạch
thấp. Chữ hoa và chữ thường là phân biệt: biến $A là
khác biến $a. Và tất cả các chữ, số và gạch thấp đã có
nghĩa, cho nên:
$a_very_long_variable_that_ends_in_1
là khác với
$a_very_long_variable_that_ends_in_2
Nói chung nên chọn tên biến mang nghĩa nào đó có
liên quan tới giá trị của biến đó. Chẳng hạn, $xyz123 có
lẽ không mang tính mô tả nhiều lắm nhưng $line_length
thì lại có nghĩa.
68
Các toán tử trên biến vô hướng
Phép toán thông dụng nhất trên biến vô hướng là
phép gán, chính là cách đặt một giá trị cho một biến.
Toán tử gán của Perl là dấu bằng (giống như C hay
FORTRAN), để tên biến bên vế trái và cho giá trị của
biểu thức bên vế phải, kiểu như:
$a = 17; # cho $a giá trị 17 $b = $a + 3; # cho $b giá trị hiện tại của $a cộng với 3
(20) $b = $b * 2; # cho $b giá trị của $b được nhân với 2 (40)
Chú ý rằng dòng cuối dùng biến $b hai lần: khi lấy
được giá trị của nó (ở vế phải dấu =), và khi xác định
xem phải đặt biểu thức tính được vào đâu (ở vế trái của
dấu =). Điều này là hợp lệ, an toàn và thực ra, khá thông
dụng. Thực ra, nó thông dụng đến mức chúng sẽ thấy
trong vài phút đây là có thể viết điều này bằng việc dùng
cách viết tắt qui ước.
Bạn có thể đã chú ý rằng các biến vô hướng bao giờ
cũng được tham chiếu bằng dấu $ đứng trước. Trong lớp
vỏ, bạn dùng $ để lấy một giá trị, nhưng để $ đứng một
mình để gán một giá trị mới. Trong awk hay C, bạn để
cho $ riêng hoàn toàn. Nếu bạn phải viết đi viết lại các
biến rất nhiều thì bạn sẽ thấy mình ngẫu nhiên bị gõ sai.
Thường hay bị vậy. (Giải pháp của tôi là chấm dứt việc
viết chương trình vỏ, awk và C, nhưng điều đó lại có thể
không có tác dụng cho bạn.)
Việc gán vô hướng có thể được dùng như một giá trị
cũng như một phép toán, như trong C. Nói cách khác, $a
= 3 có một giá trị, cũng như $a+3 có một giá trị. Giá trị
69
chính là số được gán, cho nên giá trị của $a = 3 là 3. Mặc
dầu điều này dường như có vẻ kì lạ lúc thoáng nhìn, việc
dùng một phép gán như một giá trị lại có ích nếu bạn
muốn gán một giá trị trung gian trong một biểu thức cho
một biến, hay nếu bạn muốn đơn giản sao cùng một giá
trị cho một hay nhiều biến. Chẳng hạn:
$b = 4 + ($a = 3); # gán 3 cho $a, rồi cộng kết quả đó với 4 đặt vào $b, được 7
$d = ($c = 5); # sao 5 vào $c, và rồi sao vào $d
$d = $c = 5; # cũng điều ấy nhưng không có dấu ngoặc
Thí dụ cuối làm việc tốt vì phép gán có tính kết hợp
bên phải.
Toán tử gán hai ngôi
Các biểu thức như $a = $a + 5 (trong đó cùng một
biến lại xuất hiện ở cả hai vế của phép gán) thường xuất
hiện đến mức Perl (giống như C) có cách viết tắt cho
phép toán làm thay đổi biến - toán tử gán hai ngôi. Gần
như tất cả các toán tử hai ngôi tính một giá trị đã có dạng
phép gán hai ngôi tương ứng với dấu bằng có bổ sung
thêm phần tử. Chẳng hạn, hai dòng sau đây là tương
đương:
$a = $a + 5 ; # không có toán tử gán hai ngôi $a += 5 ; # có toán tử gán hai ngôi
Và tương tự như thế:
$b = $b * 3; $b *= 3;
Trong từng trường hợp, toán tử này làm cho giá trị
hiện tại của biến được thay đổi theo một cách nào đó,
70
thay vì đơn giản ghi đè lên giá trị này bằng kết quả của
một biểu thức mới nào đó.
Toán tử gán thông dụng khác là toán tử ghép nối
xâu:
$str = $str . “ ”; # thêm dấucách vào $str $str .= “ ”; # cũng điều ấy với toán tử gán
Gần như tất cả các toán tử hai ngôi đã hợp lệ theo
cách này. Chẳng hạn, toán tử nâng lên luỹ thừa của sẽ
được viết là **=. Cho nên, $a **= 3 có nghĩa là “nâng một
số trong $a lên luỹ thừa ba, rồi đặt kết quả trở lại $a”.
Giống như toán tử gán đơn, các toán tử này cũng có
một giá trị : giá trị mới của biến. Chẳng hạn:
$a = 3; $b = ($a += 4); # $a và $b cả hai bây giờ đã là 7
Nhưng không may là trật tự tính toán của các toán
hạng của toán tử hai ngôi lại không được xác định, cho
nên một số biểu thức không thể nào được xác định hoàn
toàn:
$a = 3; $b = ($a += 2) * ($a -= 2); # Chương trình tồi: $b có thể
là 15 hay 3
Nếu toán hạng bên phải của phép nhân được tính
đầu tiên thì kết quả sẽ là 3 lần 1, hay 3. Tuy nhiên, nếu
toán hạng bên trái được tính trước toán hạng bên phải,
thì nó là 5 lần 3, hay 15. Bạn chớ có làm điều này, chừng
nào bạn còn chưa vào Cuộc tranh luận Perl rối rắm.
71
Tự tăng và tự giảm
Dùng cũng đã đủ dễ dàng để thêm một vào $a bằng
việc nói $a += 1. Perl còn đi xa hơn và thậm chí lại còn
làm ngắn hơn cho điều này nữa. Toán tử ++ (được gọi là
toán tử tự tăng) cộng thêm một vào toán hạng của nó, và
cho lại giá trị đã được tăng, giống như:
$a += 1 ; # có toán tử gán ++$a; # với tự tăng tiền tố $d = 17; $e = ++$d; # $e và $d bây giờ đã là 18
Tại đây, toán tử ++ được dùng như toán tử tiền tố -
tức là, toán tử xuất hiện ở bên trái toán hạng của nó.
Phép tự tăng cũng có thể được dùng trong dạng hậu tố
(nằm ở bên phải toán hạng của nó). Trong trường hợp
này, kết quả của biểu thức này là giá trị của biến trước
khi biến được tăng lên. Chẳng hạn,
$c = 17; $d = $c++; # $d là 17, nhưng $c bây giờ là 18
Vì giá trị của toán hạng thay đổi nên toán hạng này
phải là một biến vô hướng, không phải là biểu thức. Bạn
không thể nói ++16 để có được 17, mà cũng không thể
nói ++($a+$b) là cách nào đó để có được giá trị lớn hơn
tổng của $a và $b một đơn vị.
Toán tử tự giảm (--) cũng tương tự như toán tử tự
tăng, nhưng trừ đi một thay vì cộng với một. Giống như
toán tử tự tăng, toán tử tự giảm cũng có dạng tiền tố và
hậu tố. Chẳng hạn:
$x = 12; --$x ; # $x bây giờ là 11 $y = $x-- ; # $y là 11, còn $x bây giờ là 10
72
Không giống C, các toán tử tự tăng và tự giảm làm
việc trên số dấu phẩy động. Cho nên việc tăng một biến
với giá trị 4.2 sẽ cho 5.2 như dự kiến.
Toán tử chop()
Một toán tử có ích khác là chop(). Toán tử tiền tố này
nhận một đối bên trong các dấu ngoặc của nó - tên của
một biến vô hướng - và bỏ đi kí tự cuối cùng từ giá trị
xâu của biến đó. Chẳng hạn:
$x = “Xin chào mọi người”; chop($x); # $x bây giờ là “Xin chào mọi người”
Lưu ý rằng giá trị của đối bị thay đổi ở đây, do đó
cần phải có một biến vô hướng, thay vì chỉ đơn giản là
giá trị vô hướng. Sẽ là vô nghĩa, chẳng hạn, để viết
chop(‘suey’) để biến nó thành ‘sue’, vì không có chỗ nào
để cất giữ giá trị này. Bên cạnh đó, bạn có thể chỉ viết
‘sue’ cũng đủ.
Toán tử này trông giống như một lời gọi hàm, và
quả thực cho lại một giá trị (mà bạn sẽ trông đợi nếu bạn
quen thuộc với lời gọi hàm từ các ngôn ngữ). Giá trị
được cho lại chính là kí tự đã bị loại bỏ (chữ i trong
người ở trên). Điều này có nghĩa là đoạn mã sau đây có
lẽ sai:
$x = chop($x); # SAI: thay thế $x bằng kí tự cuối cùng của nó
chop($x); # Đúng: như trên, loại bỏ kí tự cuối
Nếu chop() được cho một xâu rỗng, thì nó chẳng làm
gì cả, và chẳng cho lại gì, mà cũng không đưa ra lỗi hay
than vãn gì. Phần lớn các phép toán trong Perl đã có
73
những điều kiện nhạy cảm - nói cách khác, bạn có thể
dùng chúng ngay sát cạnh (và vượt ra ngoài) mà thường
không có lời phàn nàn nào. Một số người biện minh rằng
đây là một trong những nhược điểm nền tảng của Perl,
trong khi số còn lại trong chúng ta thì vẫn viết ra những
chương trình tức cười mà chẳng phải lo lắng gì về phần
rỏm bên. Bạn quyết định xem mình sẽ theo vào phía nào.
Xen lẫn vô hướng vào trong xâu
Khi một hằng kí tự xâu là được đặt trong nháy kép
thì nó là chủ đề cho việc xen lẫn biến (bên cạnh việc
được kiểm tra cho lối thoát sổ chéo ngược). Điều này có
nghĩa là xâu này được duyệt qua để tìm các tên biến* vô
hướng có thể - có nghĩa là dấu đô la đi theo sau một chữ,
số hay dấu gạch thấp. Khi tìm thấy một tham chiếu biến
thì nó được thay thế bằng giá trị hiện tại (hay bất kì xâu
rỗng nào nếu biến vô hướng còn chưa được gán giá trị
nào). Chẳng hạn:
$a = “fred”; $b = “some text $a”; # $b bây giờ là “some text fred” $c = “no such variable $what”; # $c là “no such variable ”
Để ngăn cản việc thay thế một biến bằng giá trị của
nó, bạn phải hoặc làm thay đổi phần đó của xâu để cho
nó xuất hiện trong ngoặc đến, hoặc đặt trước dấu đô la
một dấu sổ chéo ngược, mà sẽ tắt ý nghĩa đặc biệt của
dấu đô la:
$fred = ‘hi’; $barney = “a test of “.’$fred’; # hằng kí hiệu: ‘a test of
* Và cả biến mảng nữa, nhưng chúng ta vẫn còn chưa biết đến chúng
chừng nào chưa tới Chương 3, biến mảng
74
$fred’ $barney2 = “a test of \$fred”; # cũng như vậy
Tên biến sẽ là tên biến dài nhất có thể mà tạo nên
nghĩa tại phần đó của xâu. Điều này có thể là vấn đề nếu
bạn muốn đặt sau ngay giá trị được thay thế với một văn
bản hằng mà bắt đầu bằng một chữ, số hay dấu gạch
thấp. Vì Perl duyệt qua các tên biến nên nó sẽ xét những
kí tự là các kí tự tên phụ, mà không phải là điều bạn
muốn. Perl cung cấp một định biên cho tên biến theo các
hệ thống tương tự như lớp vỏ. Đơn thuần bao tên của
biến đó trong một cặp dấu ngoặc nhọn. Hay có thể kết
thúc phần đó của xâu và bắt đầu một phần khác của xâu
bằng toán tử ghép nối:
$fred = ‘pay’; $fredday = “wrong!”; $barney = “It’s ’$fredday”; # không phải payday, mà là
“It’s wrong!” $barney = “It’s ’${fred}day”; # bây giờ, $barney là “It’s
payday!” $barney2 = “It’s $fred”; # cách khác để làm việc đó $barney3 = “It’s “ . $fred . “day”; và một cách khác
Toán tử sổ chéo ngược chuyển hoa thường có thể
được dùng để làm thay đổi chữ hoa thường được đem
theo cùng việc xen lẫn biến. Chẳng hạn:
$bigfred = “\ufred”; # $bigfred là FRED $fred = “fred”; $bigfred = “\Ufred”; # cùng điều đó $capfred = “\u$fred”; # $capfred là “Fred” $barney = “\LBARNEY”; # $barney bây giờ là “barney” $capbarney = “\u\LBARNEY”; #capbarney bây giờ là
“Barney” $bigbarney = “BARNEY”; $capbarney = “\u\L$bigbarney”;
thế
Như bạn có thể thấy, các toán tử dịch chuyển hoa
thường được ghi nhớ bên trong xâu chừng nào chúng
75
còn chưa được dùng tới, cho nên ngay kí tự đầu tiên của
BARNEY không tuân theo \u, nó vẫn còn là chữ hoa vì \u.
Thuật ngữ xen lẫn biến thường được dùng lẫn với
xen lẫn nháy kép, vì các xâu có nháy kép là chủ đề cho
việc xen lẫn biến.
<STDIN> xem như một vô hướng
Tại điểm này, nếu bạn là một người chuyên nghiệp
lập trình thì bạn có thể tự hỏi làm sao lấy được một giá
trị vào trong chương trình Perl. Sau đây là cách đơn giản
nhất. Mỗi lần bạn dùng <STDIN> ở chỗ đang trông đợi
một giá trị vô hướng, thì Perl sẽ đọc toàn bộ dòng văn
bản tiếp từ lối vào chuẩn (cho tới dấu dòng mới đầu
tiên), và dùng xâu đó như giá trị cho <STDIN>. Đầu vào
chuẩn có thể mang nhiều nghĩa, nhưng chừng nào bạn
còn chưa làm điều gì đó kì lạ, thì nó vẫn còn mang nghĩa
là thiết bị cuối của người dùng, người đã gọi chương
trình của bạn (có thể là bạn). Nếu không có gì chờ đợi để
đọc cả (trường hợp điển hình, chừng nào bạn còn chưa
gõ xong toàn bộ dòng), thì chương trình Perl sẽ dùng và
đợi cho bạn đưa vào một số kí tự theo sau bằng một dấu
dòng mới (xuống dòng).
Giá trị xâu của <STDIN> về điển hình có một dấu
dòng mới ở cuối của nó. Thông thường nhất là bạn muốn
gỡ bỏ cái dấu dòng mới đó đi (có sự khác biệt lớn giữa
hello và hello\n). Đây là chỗ mà anh bạn chúng ta, toán tử
chop(), tới giúp. Một dãy cái vào điển hình đưa tới một
cái gì đó tựa như thế này:
$a = <STDIN>; # nhận văn bản chop($a); # gỡ bỏ dấu dòng mới khó chịu
76
Cách viết tắt thông dụng cho hai dòng này là:
chop($a = <STDIN>) ;
Phép gán bên trong các dấu ngoặc tròn tiếp tục là
một tham chiếu tới $a, thậm chí sau khi nó đã được trao
cho một giá trị với toán tử <STDIN>. Vậy, toán tử chop()
làm việc trên $a. (Điều này là đúng nói chung đối với
toán tử gán - một biểu thức gán có thể được dùng bất kì
khi nào một biến là cần tới, và những hành động tham
chiếu tới biến đó ở bên trái của dấu bằng.)
Đưa ra bằng print()
Vậy thu được mọi thứ với <STDIN>. làm sao đưa ra
mọi thứ đây? Bằng toán tử print() đấy. Toán tử tiền tố này
nhận một giá trị vô hướng bên trong các dấu ngoặc của
nó và đưa ra mà không cần bất kì trang điểm nào lên lối
ra chuẩn. Một lần nữa, chừng nào bạn còn chưa làm điều
gì kì lạ, thì lối ra này vẫn cứ là thiết bị cuối của bạn.
Chẳng hạn:
print (“Xin chào mọi người\n”); # nói chào mọi người, tiếp là dấu dòng mới
print “Xin chào mọi người\n”; # cũng cùng điều đó
Lưu ý rằng thí dụ thứ hai chỉ ra dạng của print()
không có dấu ngoặc. Thực ra, nhiều toán tử trông như
các hàm cũng có dạng cú pháp làm việc không cần dấu
ngoặc. Dù có dùng hay không, dấu ngoặc cũng gần như
là vấn đề về kiểu cách và sự nhanh nhẩu trong cách gõ,
mặc dầu có vài trường hợp bạn sẽ cần các dấu ngoặc để
loại bỏ bớt mập mờ.
Chúng ta sẽ thấy rằng bạn thực sự có thể cho print
77
một danh sách các giá trị, trong mục “Dùng print để đưa
ra thông thường” ở Chương 6, Cơ sở về vào/ra, nhưng
chúng ta vẫn còn chưa nói về danh sách, cho nên chúng
ta sẽ hoãn nó về sau.
Giá trị undef
Điều gì sẽ xảy ra nếu bạn dùng một biến vô hướng
trước khi bạn cho nó một giá trị? Chẳng có gì nghiêm
trọng cả, và chẳng có gì dứt khoát sẽ gây định mệnh cả.
Các biến đã có giá trị undef trước khi chúng được gán lần
đầu tiên. Giá trị này trông như số không khi được dùng
như một số, hay xâu rỗng chiều dài không nếu được
dùng như một xâu.
Nhiều toán tử cho lại undef khi các đối vượt ra ngoài
phạm vi và thành vô nghĩa. Nếu bạn không làm điều gì
đặc biệt thì bạn sẽ nhận được không hay xâu không mà
không có hậu quả gì lớn. Trong thực hành, điều này gần
như không gây ra vấn đề gì.
Một toán tử mà chúng ta đã thấy có cho lại undef
trong hoàn cảnh nào đó là toán tử <STDIN>. Thông
thường toán tử này cho lại một xâu của dòng tiếp vừa
được đọc, tuy nhiên (như khi bạn gõ control-D tại thiết
bị cuối, hay khi một tệp không còn dữ liệu nữa), thì toán
tử này cho lại undef như một giá trị. Chúng sẽ thấy trong
chương 6 cách kiểm tra điều này và chọn hành động đặc
biệt khi không còn dữ liệu nào có sẵn để đọc nữa.
78
Bài tập
Xem Phụ lục A về lời giải.
1. Viết chương trình tính chu vi đường tròn với bán
kính 12.5. Chu vi bằng 2 lần bán kính, hay khoảng
2 lần 3.141592654.
2. Sửa chương trình từ bài tập trước để nhắc việc nhận
bán kính từ người chạy chương trình.
3. Viết một chương trình nhắc và đọc vào hai số, rồi in
ra kết quả của việc nhân hai số đó.
4. Viết một chương trình đọc một xâu và một số rồi in
ra xâu số lần được chỉ ra bởi số các dòng tách biệt.
(Hướng dẫn: dùng toán tử “x”.)
79
3
Dữ liệu mảng
và danh sách
Mảng là gì?
Mảng là một danh sách có thứ tự các dữ liệu vô
hướng. Mỗi phần tử của mảng đã là một biến vô hướng
tách biệt với một giá trị vô hướng độc lập. Các giá trị
này là được sắp thứ tự - tức là chúng có một trình tự đặc
biệt từ phần tử thấp nhất đến cao nhất.
Mảng có thể có bất kì số phần tử nào. Mảng nhỏ
nhất không có phần tử nào, trong khi mảng lớn nhất thì
có thể lấp kín toàn bộ bộ nhớ có sẵn. Một lần nữa, điều
này lại được giữ hợp với triết lí của Perl về “không có
giới hạn không cần thiết nào.”
Trong chương này:
Mảng là gì?
Biểu diễn hằng kí hiệu
Biến
Toán tử
Ngữ cảnh vô hướng và mảng
<STDIN> xem như mảng
Biến
Xen lẫn mảng
80
Biểu diễn hằng kí hiệu
Một hằng kí hiệu mảng (cách thức bạn biểu diễn giá
trị của một mảng bên trong chương trình mình) là một
danh sách các giá trị tách nhau bằng dấu phẩy và được
bao trong dấu ngoặc tròn. Những giá trị này tạo nên các
phần tử của danh sách. Chẳng hạn:
(1,2,3) # mảng gồm ba giá trị 1, 2 và 3 (“fred”, 4.5) # hai giá trị, “fred” và 4.5
Các phần tử của mảng không nhất thiết là hằng -
chúng có thể là biểu thức mà sẽ được tính mới lại mỗi
lần hằng được sử dụng. Chẳng hạn:
($a, 17) # hai giá trị: giá trị hiện tại của $a, và 17 ($b+$c,$d+$e) # hai giá trị
Mảng rỗng (mảng không có phần tử nào) được biểu
diễn bằng một cặp dấu ngoặc rỗng:
() # mảng rỗng (không phần tử)
Một phần tử của mảng có thể bao gồm toán tử cấu
tử mảng, được chỉ ra bởi hai giá trị vô hướng tách nhau
bởi hai dấu chấm liên tiếp. Toán tử này tạo ra một danh
sách các giá trị bắt đầu tại giá trị vô hướng bên trái kéo
cho tới giá trị vô hướng bên phải, mỗi lần tăng lên một.
Chẳng hạn:
(1..5) # giống như (1, 2, ,3 ,4, 5) (1.2..5.2) # giống như (1.2, 2.2, 3.2, 4.2, 5.2) (2..6,10,12) # giống như (2,3,4,5,6,10,12) ($a..$b) # phạm vi được xác định bởi giá trị hiện tại
của $a và $b
Nếu giá trị vô hướng bên phải bé hơn vô hướng bên
trái thì sẽ tạo ra danh sách rỗng - bạn không thể đếm
ngược trật tự của các giá trị. Nếu giá trị cuối cùng không
81
phải là toàn bộ số bước trên giá trị ban đầu thì danh sách
sẽ dùng chỉ ngay trước giá trị tiếp mà sẽ vượt ra ngoài
phạm vi:
(1.3..6.1) # giống như (1.3, 2.3, 3.3, 4.3, 5.3)
Một cách dùng của hằng kí hiệu mảng là như đối
của toán tử print() đã được giới thiệu trước đây. Các phần
tử của danh sách này được in ra mà không có bất kì
khoảng trống xen thêm vào:
print (“Câu trả lời là ”, $a, “\n”) ; # ba phần tử mảng hằng kí hiệu
Câu lệnh này in ra “Câu trả lời là”, theo sau bởi một
dấu cách, giá trị của $a, và dấu dòng mới. Chuyển sang
cách dùng khác cho hằng kí hiệu mảng.
Biến
Một biến mảng giữ một giá trị mảng riêng (không
hay nhiều giá trị vô hướng). Các tên biến mảng là tương
tự với các tên biến vô hướng, chỉ khác kí tự khởi đầu, là
một dấu a còng @ chứ không phải là dấu đô la $. Chẳng
hạn:
@fred # biến mảng @fred @A_Very_Long_Array_Variable_Name @A_Very_Long_Array_Variable_Name_that_is_different
Lưu ý rằng biến mảng @fred là không có quan hệ gì
theo bất kì cách nào với biến vô hướng $fred. Perl duy trì
không gian tên tách biệt cho các kiểu đối tượng khác
nhau.
Giá trị của một biến mảng mà chưa được gán là (),
danh sách rỗng.
82
Một biểu thức có thể tham chiếu tới các biến mảng
như một tổng thể, hoặc nó có thể xem xét và thay đổi
từng phần tử của mảng đó.
Toán tử
Các toán tử mảng hành động trên các mảng như một
tổng thể. Một số toán tử mảng có thể cho lại một giá trị
mảng, mà có thể hoặc được dùng như một giá trị cho
toán tử mảng khác, hoặc được gán vào một biến mảng
khác.
Phép gán
Có lẽ toán tử mảng quan trọng nhất là toán tử gán
mảng, cho mảng một giá trị. Nó là dấu bằng, giống như
toán tử gán vô hướng. Perl xác định liệu phép gán có là
phép gán vô hướng hay phép gán mảng bằng việc để ý
xem liệu phép gán là cho biến vô hướng hay mảng* .
Chẳng hạn:
@fred = (1,2,3); # mảng fred nhận ba phần tử hằng kí hiệu
@barney = @fred; # bây giờ được sao sang @barney
Nếu một giá trị vô hướng được gán vào trong một
biến mảng thì giá trị vô hướng trở thành phần tử duy
nhất của mảng:
@huh = 1; # 1 được đặt cho danh sách (1) một cách tự động
* Điều này áp dụng cho “lvalue” vô hướng hay mảng cũng như các
biến đến
83
Tên biến mảng có thể xuất hiện trong danh sách
hằng kí hiệu mảng. Khi giá trị của danh sách được tính
thì Perl thay thế tên biến mảng bằng giá trị hiện tại của
mảng đó, giống vậy:
@fred = (“một”, “hai”); @barney = (4,5,@fred, 6, 7); @barney trở thành (4,5,”một”,”hai”,6,7) @barney = (8, @barney); # đặt 8 vào trước @barney @barney = (@barney, “cuối”); # và “cuối” là ở cuối # @barney bây giờ là (8,4,5,”một”,”hai”,6,7,”cuối”)
Lưu ý rằng các phần tử mảng được thêm vào đã ở
cùng mức như phần còn lại của hằng kí hiệu - một danh
sách không thể chứa một danh sách khác như một phần
tử* .
Nếu một mảng hằng kí hiệu chỉ chứa các tham chiếu
biến (không phải là biểu thức) thì mảng hằng kí hiệu ấy
cũng có thể được xử lí như một biến. Nói cách khác, một
mảng hằng kí hiệu như thế có thể được dùng ở vế bên
trái của phép gán. Mỗi biến vô hướng trong mảng kí hiệu
nhận một giá trị tương ứng từ danh sách ở vế phải của
phép gán. Chẳng hạn:
($a, $b, $c) = (1, 2, 3); # đặt 1 cho $a, 2 cho $b, 3 cho $c ($a, $b) = ($b, $a); # tráo đổi $a và $b ($d, @fred) = ($a, $b, $c); # đặt $a cho $d, và ($b,$c)
cho @fred ($e,@fred) = @fred; # loại bỏ phần tử thứ nhất của
@fred là $e # điều này làm cho @fred = ($c) và $e = $b
* Perl 5.0 cho phép một tham chiếu danh sách là một phần tử danh
sách, nhưng đấy vẫn không phải là danh sách như một phần tử danh
sách
84
Nếu số phần tử được gán không sánh đúng với số
các biến để giữ các giá trị thì mọi giá trị vượt quá (ở vế
phải của dấu bằng) đã im lặng bị loại bỏ, và bất kì biến
vượt quá nào (ở vế trái của dấu bằng) đã được cho giá trị
undef.
Một biến mảng xuất hiện trong danh sách mảng
hằng kí hiệu đã phải ở cuối, vì biến mảng là “tham lam”,
và nó tiêu thụ tất cả các giá trị còn lại. (Này, bạn có thể
đặt các biến khác sau nó, nhưng chúng sẽ chỉ nhận giá trị
undef mà thôi.)
Nếu một biến mảng được gán cho một biến vô
hướng thì số được gán là chiều dài của mảng, như trong:
@fred = (4, 5, 6); # khởi đầu @fred
$a = @fred; # $a nhận phần tử đầu tiên của @fred
Chiều dài cũng được cho lại nếu một tên biến mảng
được dùng trong hầu hết mọi chỗ mà một giá trị vô
hướng đang được cần tới. (Trong mục dưới đây có tên
“Hoàn cảnh vô hướng mảng”, chúng ta sẽ thấy rằng điều
này quả là được gọi như vậy với việc dùng tên mảng
trong hoàn cảnh vô hướng.) Chẳng hạn, để lấy giá trị bé
hơn chiều dài mảng một đơn vị, bạn có thể dùng @fred-1,
vì toán tử trừ vô hướng cần các vô hướng cho cả hai toán
hạng của nó. Chú ý điều sau:
$a = @fred; # $a nhận chiều dài của @fred ($a) = @fred; # $a nhận phần tử đầu tiên của @fred
Phép gán đầu tiên là phép gán vô hướng, và do vậy
@fred được đối xử như một vô hướng, cho lại chiều dài
của nó. Phép gán thứ hai là phép gán mảng (cho dù chỉ
một giá trị là cần tới), và do vậy cho phần tử đầu tiên của
@fred, im lặng bỏ đi tất cả phần còn lại.
85
Giá trị của phép gán mảng là chính bản thân giá trị
mảng, và có thể được xếp tầng như bạn có thể làm với
các phép gán vô hướng. Chẳng hạn:
@fred = (@barney = (2,3,4)); # @fred và @barney nhận (2,3,4)
@fred = @barney = (2,3,4); # cùng điều ấy
Truy nhập phần tử
Cho tới nay, chúng ta vẫn xử lí mảng như một tổng
thể, thêm vào và bỏ bớt các giá trị bằng việc thực hiện
gán mảng. Nhiều chương trình có ích đã được xây dựng
dùng mảng mà thậm chí chẳng truy nhập vào phần tử
mảng nào. Tuy nhiên, Perl cung cấp toán tử chỉ số truyền
thống để tham chiếu tới một phần tử mảng theo chỉ số.
Với toán tử chỉ số mảng, các phần tử mảng đã được
đánh số bằng việc dùng số nguyên tuần tự, bắt đầu từ
không* và tăng lên một cho mỗi phần tử. Phần tử đầu
tiên của mảng @fred mà được truy nhập tới là $fred[0].
Chú ý rằng @ trên tên mảng trở thành $ trên tham chiếu
phần tử. Đó là vì việc tham chiếu tới một phần tử của
mảng xác định ra một biến vô hướng (một phần của
mảng), mà có thể hoặc được gán cho, hoặc có giá trị hiện
tại của nó được dùng trong một biểu thức, kiểu như:
@fred = (7,8,9);
* Cũng có thể thay đổi giá trị chỉ số của phần tử đầ utiên thành một
số nào đó khác (như một) bằng việc đặt giá trị cho biến $[. Tuy
nhiên, làm như vậy có ảnh hưởng toàn cục, mà có thể gây lẫn lộn
người sẽ bảo trì chương trình của bạn, và có thể làm tan vỡ chương
trình bạn nhận được từ người khác. Do vậy, chúng tôi khuyên bạn
nên coi đây là một tính năng không nên dùng.
86
$b = $fred[0]; # đặt 7 vào $b (phần tử đầu tiên của @fred)
$fred[0] = 5; # bây giờ @fred = (5,8,9)
Cũng có thể truy nhập tới các phần tử khác dễ tương
tự, như trong:
$c = $fred[1]; $ đặt 8 cho $c $fred[2]++; # tăng phần tử thứ ba của @fred $fred[1] += 4; # cộng 4 vào phần tử thứ hai ($fred[0], $fred[1]) = ($fred[1], $fred[0]); # tráo đổi hai
phần tử đầu
Việc truy nhập vào một danh sách các phần tử từ
cùng mảng (như trong thí dụ cuối) được gọi là lát cắt, và
thường xuất hiện đến mức có một cách biểu diễn đặc biệt
cho nó:
@fred[0,1] # hệt như ($fred[0], $fred[1])
@fred[0,1] = @fred[1,0] # tráo đổi hai phần tử đầu
@fred[0,1,2] = @fred[1,1,1] # làm cho cả 3 phần tử giống phần tử thứ hai
@fred[1,2] = (9,10); # đổi hai giá trị cuối thành 9 và 10
Chú ý rằng lát cắt này dùng tiền tố @ chứ không là
$. Điều này là vì bạn đang tạo ra một biến mảng bằng
việc chọn một phần của mảng chứ không phải là biến vô
hướng chỉ truy nhập vào một phần tử.
Lát cắt cũng làm việc trên danh sách hằng kí hiệu,
hay bất kì toán tử nào cho lại một giá trị danh sách:
@who = (“fred”,”barney”,”betty”,”wilma”)[2,3] ;
# giống như @x = (“fred”,”barney”,”betty”,”wilma”); @who = @x[2,3]
Các giá trị chỉ số trong những thí dụ này là các số
87
nguyên hằng kí hiệu, nhưng chỉ số cũng có thể là bất kì
biểu thức nào cho lại một số, mà rồi được dùng để chọn
phần tử thích hợp:
@fred = (7,8,9); $a = 2; $b = $fred[$a]; # giống $fred[2], hay giá trị 9 $c = $fred[$a-1]; # $c nhận $fred[1], hay 8 ($c) = (7,8,9) [$a-1]; # cũng điều đó nhưng dùng lát cắt
Vậy chương trình Perl có thể có việc truy nhập
mảng tương tự như các ngôn ngữ lập trình truyền thống.
Ý tưởng này về việc dùng một biểu thức cho chỉ số
cũng có tác dụng cho các lát cắt. Tuy nhiên nhớ rằng chỉ
số cho lát cắt là một danh sách các giá trị, cho nên biểu
thức này là một biểu thức mảng, thay vì là một biểu thức
vô hướng.
@fred = (7,8,9); # như trong thí dụ trước @barney = (2,1,0); @backfred = @fred[@barney]; # giống như @fred[2,1,0], hay ($fred[2],$fred[1],$fred[0]), # hay (9,8,7)
Nếu bạn truy nhập vào một phần tử mảng bên ngoài
hai đầu của mảng hiện tại (tức là một chỉ số bé hơn
không hay lớn hơn chỉ số của phần tử cuối cùng), thì giá
trị undef sẽ được cho lại mà không có lời cảnh báo.
Chẳng hạn:
@fred = (1,2,3); $barney = $fred[7]; # $barney bây giờ là undef
Việc gán một giá trị bên ngoài đầu của mảng hiện tại
sẽ tự động mở rộng mảng (cho một giá trị undef cho tất
cả các giá trị trung gian, nếu có). Chẳng hạn:
@fred = (1,2,3);
88
$fred[3] = “hi”; # @fred bây giờ là (1,2,3,”hi”) $fred[6] = “ho”; # @fred bây giờ là (1,2,3,”hi”,undef,”ho”)
Phép gán cho một phần tử mảng có chỉ số bé hơn
không là lỗi định mệnh, vì nó có thể làm phát sinh kiểu
cách lập trình rất xấu.
Bạn có thể dùng $#fred để lấy giá trị chỉ số của phần
tử cuối của @fred. (Điều này giống như tham chiếu vỏ
C). Bạn thậm chí còn có thể gán vào trong giá trị này để
làm thay đổi chiều dài hiển nhiên của @fred, làm cho nó
to lên hay co lại, nhưng điều đó nói chung là không cần
thiết, vì mảng thường to lên hay co lại một cách tự động.
Các toán tử push() và pop()
Một cách dùng thông dụng của mảng là như một
chồng thông tin, nơi những giá trị mới được thêm vào và
lấy đi từ phía bên phải của danh sách. Những phép toán
này thường xuất hiện đến mức chúng có các hàm đặc
biệt của riêng chúng:
push(@mylist,$newvalue); # giống @mylist = (@mylist, $newvalue)
$oldvalue = pop(@mylist); # lấy ra phần tử cuối của @mylist
Toán tử pop() cho lại undef nếu đối của nó là danh
sách rỗng, thay vì làm điều gì đó khác kiểu Perl như
phàn nàn hay sinh ra thông báo lỗi.
Toán tử push() cũng chấp nhận một danh sách các
giá trị cần được đẩy vào danh sách. Các giá trị được đẩy
vào cuối của danh sách. Chẳng hạn:
@mylist = (1,2,3);
89
push(@mylist,4,5,6); # @mylist = (1,2,3,4,5,6)
Chú ý rằng đối thứ nhất phải là một tên biến mảng* -
đẩy vào và lấy ra sẽ không có nghĩa với danh sách hằng
kí hiệu.
Các toán tử shift() và unshift()
Các toán tử push() và pop() làm mọi điều ở bên
“phải” của danh sách (phần với chỉ số cao nhất). Tương
tự thế, các toán tử unshift() và shift() thực hiện những hành
động tương ứng về bên “trái” của một danh sách (phần
với chỉ số thấp nhất). Sau đây là vài thí dụ:
unshift(@fred,$a); # như @fred = ($a,@fred); unshift(@fred,$a,$b,$c); # như @fred = ($a, $b, $c,
@fred); $x = shift(@fred); # như ($x,@fred) = @fred; # với một số giá trị thực @fred = (5,6,7); unshift(@fred,2,3,4); # @fred bây giờ là (2,3,4,5,6,7) $x = shift(@fred); # $x nhận 2, $fred nhận bây giờ là
(3,4,5,6,7)
Như với pop(), shift() cho lại undef nếu biến mảng là
rỗng.
Toán tử reverse()
Toán tử reverse() đảo ngược trật tự các phần tử của
đối của nó, cho lại danh sách kết quả. Chẳng hạn:
@a = (7,8,9);
* Trong thực tế, bạn có thể bỏ @ mặc dầu tôi nghe nói rằng Perl 5.0
lại đòi hỏi nó.
90
@b = reverse(@a); # đặt $b giá trị (9,8,7) $b = reverse(7,8,9); # cũng việc ấy
Chú ý rằng danh sách đối là không bị thay đổi - toán
tử reverse() chỉ làm việc trên bản sao. Nếu bạn muốn đảo
ngược một mảng “tại chỗ”, thì bạn cần gán nó ngược trở
lại cho cùng biến:
@b = reverse(@b); # đóth @b là đảo ngược của chính nó
Toán tử sort()
Toán tử sort() lấy đối của nó và sắp xếp chúng dường
như chúng tất cả đã là các xâu theo trật tự ASCII tăng
dần. Nó cho lại danh sách đã sắp xếp, không làm thay
đổi danh sách gốc. Chẳng hạn:
@x = sort(“small”, “medium”, “large”); # @x nhận “large”, “medium”, “small” @y = (1,2,4,8,16,32,64); @y = sort(@y); # @y nhận 1, 16, 2, 32, 4, 64, 8
Chú ý rằng các số sắp xếp không xuất hiện theo thứ
tự số, nhưng theo giá trị xâu của từng số (1, 16, 2, 32,
vân vân). Trong mục “Sắp xếp nâng cao”, ở Chương 15,
Việc biến đổi dữ liệu khác, bạn sẽ học cách sắp xếp theo
số, hoặc theo thứ tự giảm, hay theo kí tự thứ ba của từng
xâu, hay bất kì phương pháp nào khác mà bạn chọn.
Toán tử chop()
Toán tử chop() làm việc trên biến mảng cũng như
biến vô hướng. Mỗi phần tử của mảng đã có kí tự cuối bị
bỏ đi. Điều này có thể là thuận tiện khi bạn đọc một danh
91
sách các dòng như các phần tử mảng tách bạch, và bạn
muốn bỏ đi dấu dòng mới trong tất cả các dòng ngay lập
tức. Chẳng hạn:
@stuff = (“hello\n”, “world\n”, “happy day”);
chop(@stuff); # @stuff bây giờ là (“hello”, “world”, happy day”)
Hoàn cảnh vô hướng và mảng
Như bạn có thể thấy, từng toán tử đã được thiết kế
để hoạt động trên một số tổ hợp xác định các vô hướng
hay mảng, và cho lại một vô hướng hay mảng. Nếu một
toán tử trông đợi một toán hạng là vô hướng thì nói rằng
toán hạng đó là được tính trong hoàn cảnh vô hướng.
Tương tự, nếu một toán hạng đang trông đợi một giá trị
mảng thì nói rằng toán hạng đó là được tính trong hoàn
cảnh mảng.
Thông thường, điều này là khá thông thường. Nhưng
đôi khi bạn nhận được một thao tác hoàn toàn khác tuỳ
theo liệu bạn đang trong hoàn cảnh vô hướng hay mảng.
Chẳng hạn, @fred cho lại nội dung của mảng @fred trong
hoàn cảnh mảng, nhưng cho lại chiều dài của mảng đó
trong hoàn cảnh vô hướng. Nhưng sự tinh vi này sẽ được
nhắc tới khi các toán tử đó được mô tả.
Nếu bạn muốn buộc một biểu thức phải được tính
trong hoàn cảnh vô hướng thì bạn có thể ghép nối một
xâu không vào cho nó*. Chẳng hạn:
* Bạn cũng có thể dùng toán tử scalar() nhưng chúng ta sẽ không
nói về điều đó ở đây.
92
@a = (“x”,”y”,”z”); print (“Tôi thấy ”, @a, “ phần tử\n”); # sai, sẽ in là “xyz”
cho @a print (“Tôi thấy ”,””.@a, “ phần tưt\n”); # đúng, in 3 cho
@a
Tại đây, chúng đã nối xâu không “” vào @a, làm
nảy sinh xâu “3”, mà rồi trở thành một phần của danh
sách cho print.
Một giá trị vô hướng được dùng bên trong một hoàn
cảnh mảng thì sẽ được coi như mảng một phần tử.
<STDIN> như một mảng
Một toán tử đã thấy trước đây cũng cho giá trị khác
trong hoàn cảnh mảng là <STDIN>. Như đã mô tả trước
đây, <STDIN> cho dòng tiếp của cái vào trong hoàn cảnh
vô hướng. Bây giờ, trong hoàn cảnh mảng, toán tử này
cho lại tất cả phần dòng còn lại cho tới cuối tệp. Mỗi
dòng đã được cho lại như một phần tử tách bạch của
danh sách. Chẳng hạn:
@a = <STDIN>; # đọc cái vào chuẩn trong hoàn cảnh mảng
Nếu một người chạy chương trình này gõ vào ba
dòng, rồi nhấn Control-D (để chỉ ra “cuối tệp”), thì mảng
kết thúc với ba phần tử. Mỗi phần tử sẽ là một xâu mà
kết thúc bằng một dấu dòng mới, tương ứng với ba dòng
có kết thúc là dấu dòng mới đã gõ vào.
Xen lẫn biến mảng
Giống như các vô hướng, các giá trị mảng có thể
93
được để xen lẫn trong xâu có nháy kép. Một phần tử
riêng của một mảng sẽ được thay thế bởi giá trị của nó,
giống như:
@fred = (“hello”, “dolly”); $y = 2; $x = “This is $fred[1]’s place”; # “This is dolly’s place” $x = “This is $fred[$y-1]’s place”; # cũng câu ấy
Chú ý rằng biểu thức chỉ số được tính như một biểu
thức thông thường, dường như nó ở bên ngoài xâu. Nó
không phải là biến được xen lẫn trước hết. Nói cách
khác, nếu $y chứa xâu 2*4 thì vẫn nói về phần tử 1, chứ
không phải là 7, vì 2*4 xem như một số (giá trị của $y
được dùng trong biểu thức số) chỉ là 2 rõ.
Nếu bạn muốn đặt sau một tham chiếu biến vô
hướng đến dấu ngoặc vuông trái thì bạn cần định biên
cho dấu ngoặc vuông để cho nó không được coi như một
phần của một tham chiếu mảng, như sau:
@fred = (“hello”, “dolly”); # đặt giá trị cho @fred để kiểm thử
$fred = “right”; # chúng đang định nói “this is right[1]”... $x = “this is $fred[1]”; # sai, cho “this is dolly” $x = “this is ${fred}[1]” ; # đúng (được bảo vệ bởi dấu
ngoặc nhọn) $x = “this is $fred.”.”[1]”; # đúng (xâu khác) $x = “this is $fred\[1]”; # đúng (sổ chéo ngược che dấu
nó)
Tương tự, một danh sách các giá trị từ một biến
mảng cũng có thể được xen lẫn. Việc xen lẫn đơn giản
nhất là toàn bộ mảng, được chỉ ra bằng việc cho tên
mảng (kể cả kí tự @ đứng đầu của nó). Trong trường
hợp này, các phần tử được xen lẫn theo trình tự với một
94
dấu cách giữa chúng, như trong:
@fred = (“a”, “bb”, “ccc”, 1, 2, 3); $all = “Now for @fred here!”; # $all cho “Now for a bb ccc 1 2 3 here!”
Bạn cũng có thể chọn ra một phần của mảng với lát
cắt:
@fred = (“a”, “bb”, “ccc”, 1,2,3); $all = “Now for @fred[2,3] here!”; # $all cho “Now for ccc 1 here!” $all = “Now for @fred[@fred[4,5]] here!”; # cũng thế
Một lần nữa, bạn có thể dùng bất kì cơ chế nháy kép
nào đã được mô tả trước đây nếu bạn muốn đặt sau một
tham chiếu tên mảng bằng một hằng kí hiệu dấu ngoặc
nhọn trái thay vì một biểu thức chỉ số.
Bài tập
Xem Phụ lục A về lời giải
1. Viết một chương trình đọc một danh sách các xâu và
in ra danh sách theo thứ tự đảo ngược.
2. Viết một chương trình đọc một số rồi một danh sách
các xâu (tất cả đã trên các dòng tách biệt), rồi in một
trong các dòng đó từ danh sách như được lựa chọn
bởi con số đó.
3. Viết một chương trình đọc một danh sách các xâu rồi
cọn ra và in xâu ngẫu nhiên trong danh sách đó.
Chọn một phần tử ngẫu nhiên của @somearray , đặt
srand;
95
tại đầu chương trình của bạn (điều này sẽ khởi đầu
bộ sinh số ngẫu nhiên), và rồi dùng
rand (@somearray)
khi bạn cần một giá trị ngẫu nhiên giữa 0 và một số
bé hơn chiều dài của @somearray.
96
97
4
Cấu trúc
điều khiển
Khối câu lệnh
Khối câu lệnh là một dẫy các câu lệnh, được bao
trong cặp dấu ngoặc nhọn. Nó trông tựa như thế này:
{ câu lệnh thứ nhất; câu lệnh thứ hai; câu lệnh thứ ba; ... câu lệnh cuối; }
Perl thực hiện từng câu lệnh theo trình tự, từ đầu đến
cuối. (Về sau, tôi sẽ chỉ cho bạn cách thay đổi trình tự
thực hiện này bên trong khối, nhưng hiện tại thì thế là
đủ.)
Về mặt cú pháp, một khối các câu lệnh được chấp
Trong chương này:
Khối câu lệnh
Câu lệnh if / unless
Câu lệnh while / until
Câu lệnh for
Câu lệnh foreach
98
nhận ở mọi vị trí của một câu lệnh.
Câu lệnh if/unless
Độ phức tạp tiếp theo trong các câu lệnh là câu lệnh
if. Kết cấu này trông rất giống kết cấu trong C: một biểu
thức điều khiển (được tính theo tính đúng đắn của nó),
và hai khối. Nói cách khác, nó trông tựa như thế này:
if (biểu thức nào đó) { câu lệnh 1 trong trường hợp đúng ; câu lệnh 2 trong trường hợp đúng ; câu lệnh 3 trong trường hợp đúng ; } else { câu lệnh 1 trong trường hợp sai ; câu lệnh 2 trong trường hợp sai ; câu lệnh 3 trong trường hợp sai ; }
(Nếu bạn thành thạo về C thì bạn sẽ chú ý rằng các
dấu ngoặc nhọn là cần thiết. Điều này khử bỏ nhu cầu về
qui tắc “else lòng thòng”.)
Trong khi thực hiện, Perl sẽ tính biểu thức điều
khiển. Nếu biểu thức này là đúng thì khối thứ nhất (các
câu lệnh trong trường hợp đúng trên) sẽ được thục hiện.
Nếu biểu thức là sai thì khối thứ hai (các câu lệnh trong
trường hợp sai trên) sẽ được thực hiện.
Nhưng đúng sai là như thế nào? Trong Perl, các qui
tắc có đôi chút hơi huyền ảo, nhưng chúng cho bạn kết
quả như dự kiến. Biểu thức điều khiển được tính cho một
giá trị xâu (nếu nó đã là xâu, thì chẳng có thay đổi gì,
99
nhưng nếu nó là số thì nó sẽ được chuyển thành xâu* ).
Nếu xâu này hoặc là xâu rỗng (chiều dài không), hoặc là
một xâu có chứa một kí tự “0” (không), thì giá trị của
biểu thức là sai. Mọi thứ khác đã được tự động coi như là
đúng. Tại sao có cái qui tắc buồn cười này vậy? Vì điều
ấy làm cho dễ dàng nhảy theo cái rỗng* so với một xâu
khác rỗng, cũng như số không so với số khác không,
không cần phải tạo ra hai cách hiểu về các giá trị đúng và
sai. Sau đây là những thí dụ về cách hiểu đúng và sai:
0 # chuyển thành “0”, cho nên là sai 1-1 # chuyển thành 0, rồi chuyển thành “0”, cho nên là
sai 1 # chuyển thành “1”, nên là đúng “” # xâu rỗng, cho nên là sai “1” # khong phải là “” hay “0”, cho nên đúng “00” # không phải là “” hay “0”, cho nên là đúng (trường
hợp này có huyền ảo, xem mà xem) “0.000” # cũng đúng với cùng lí do và cảnh báo undef # tính thành “”, cho nên sai
Về mặt thực hành mà nói, cách hiểu các giá trị đúng
sai thì khá trực giác. Đừng để tôi làm bạn sợ.
Sau đây là một thí dụ về câu lệnh if đầy đủ:
print “Bạn bao nhiêu tuổi rồi?” $a = <STDIN>; chop($a); if ($a < 18) { print “Này, bạn chưa đủ tuổi bầu cử đâu nhé?\n”; } else { print “Đủ tuổi rồi! Hãy bình thản! Vậy đi bầu cử đi!\n”;
* Bên trong, điều này không hoàn toàn đúng. Nhưng nó hành động
giống như đây là điều nó thực hiện. * Này, rỗng là ngoại trừ cho trường hợp bệnh hoạn của một kí tự
không đấy
100
$voter++; # đếm số cử tri về sau }
Bạn có thể cắt bỏ khối else, chỉ để lại phần then, như
trong:
print “Bạn bao nhiêu tuổi rồi?” $a = <STDIN>; chop($a); if ($a < 18) { print “Này, bạn chưa đủ tuổi bầu cử đâu nhé?\n”; }
Đôi khi, bạn muốn bỏ đi phần then mà chỉ có phần
else, vì sẽ tự nhiên hơn để nói “làm điều đó nếu điều này
sai,” so với “làm điều đó nếu điều phủ định của điều này
là đúng.” Perl giải quyết điều này với biến thể unless:
print “Bạn bao nhiêu tuổi rồi?” $a = <STDIN>; chop($a); unless ($a < 18) { print “Đủ tuổi rồi! Hãy bình thản! Vậy đi bầu cử đi!\n”; $voter++; }
Việc thay thế if bằng unless là có hiệu quả khi nói
“Nếu biểu thức điều khiển là không đúng thì làm...” (một
unless cũng có thể có một else, như if.)
Nếu bạn có nhiều hơn hai chọn lựa thì bạn có thể
thêm một nhánh elsif vào câu lệnh if , giống như:
if (biểu thức một nào đó) { câu lệnh 1 trong trường hợp đúng một; câu lệnh 2 trong trường hợp đúng một; câu lệnh 3 trong trường hợp đúng một; } elsif (biểu thức hai nào đó ) { câu lệnh 1 trong trường hợp đúng hai; câu lệnh 2 trong trường hợp đúng hai;
101
câu lệnh 3 trong trường hợp đúng hai; } elsif (biểu thức ba nào đó ){ câu lệnh 1 trong trường hợp đúng ba; câu lệnh 2 trong trường hợp đúng ba; câu lệnh 3 trong trường hợp đúng ba; } else { câu lệnh 1 trong trường hợp sai tất cả ; câu lệnh 2 trong trường hợp sai tất cả; câu lệnh 3 trong trường hợp sai tất cả; }
Mỗi biểu thức (ở đây, biểu thức một nào đó, biểu thức
hai nào đó, và biểu thức ba nào đó) đã được tính lần lượt.
Nếu một biểu thức là đúng thì nhánh tương ứng sẽ được
thực hiện, và tất cả phần còn lại của biểu thức điều khiển
cũng các nhánh câu lệnh sẽ bị bỏ qua. Nếu tất cả các
biểu thức này đã sai thì nhánh else sẽ được thực hiện
(nếu có). Bạn có thể có nhiều nhánh elsif tuỳ ý.
Câu lệnh while/until
Không một ngôn ngữ thuật toán nào hoàn chỉnh mà
không có một dạng lặp nào đó (thực hiện lặp lại một
khối các câu lệnh). Perl có thể lặp bằng việc dùng câu
lệnh while:
while (biểu thức nào đó) { câu lệnh 1; câu lệnh 2; câu lệnh 3; }
Để thực hiện câu lệnh while này, Perl tính biểu thức
điều khiển (biểu thức nào đó trong thí dụ này). Nếu giá trị
này là đúng (bằng việc dùng ý tưởng về cái đúng của câu
lệnh if), thì thân của câu lệnh while sẽ được tính một lần.
102
Điều này được lặp lại cho tới khi biểu thức điều khiển
trở thành sai, tại điểm đó Perl chuyển sang câu lệnh tiếp
sau while. Chẳng hạn:
print “Bạn bao nhiêu tuổi rồi?” $a = <STDIN>; chop($a); while ($a > 0) { print “Vào lúc này bạn mới $a tuổi.\n”; $a--; }
Đôi khi nói “làm việc đó trong khi điều này sai” lại
dễ hơn là nói “làm việc đó trong khi phủ định điều này là
đúng.” Một lần nữa, Perl lại có câu trả lời. Thay cho
while là until, cũng cho kết quả mong muốn:
until (biểu thức nào đó) { câu lệnh 1; câu lệnh 2; câu lệnh 3; }
Chú ý rằng trong cả hai dạng while và until, các câu
lệnh thân sẽ bị bỏ qua hoàn toàn nếu biểu thức điều
khiển là giá trị kết thúc ngay từ lúc bắt đầu. Chẳng hạn,
nếu người dùng đưa vào một độ tuổi bé hơn không cho
đoạn chương trình trên thì Perl sẽ bỏ qua thân chu trình.
Có thể là biểu thức điều khiển sẽ chẳng bao giờ để
cho chu trình ra được. Điều này hoàn toàn hợp pháp, và
đôi khi cũng là mong muốn nữa, và do vậy không bị coi
như lỗi. Chẳng hạn, bạn có thể muốn một chu trình cứ
lặp lại mãi chừng nào bạn còn chưa phạm phải lỗi, và rồi
có một đoạn trình giải quyết lỗi đi theo sau chu trình.
Bạn có thể dùng điều này cho một việc quái quỉ cứ thế
chạy hoài cho tới khi hệ thống sập.
103
Câu lệnh for
Một kết cấu lặp khác của Perl là câu lệnh for, trông
giống như câu lệnh for của C, và làm việc thì đại thể
cũng giống thế. Sau đây là nó:
for (biểu thức khởi đầu; biểu thức kiểm tra; biểu thức tăng) {
câu lệnh 1; câu lệnh 2; câu lệnh 3; }
Gỡ ra theo dạng đã thấy trước đây, điều này trở
thành
biểu thức khởi đầu while (biểu thức kiểm tra) { câu lệnh 1; câu lệnh 2; câu lệnh 3; biểu thức tăng }
Trong cả hai trường hợp, biểu thức khởi đầu đã được
tính trước. Biểu thức này về điển hình chỉ gán giá trị ban
đầu cho một biến lặp, nhưng cũng chẳng có hạn chế nào
về việc nó có thể chứa cái gì - thực ra nó có thể rỗng
(chẳng làm gì cả). Rồi biểu thức kiểm tra sẽ được tính để
xác định đúng sai. Nếu giá trị tính được là đúng thì thân
chu trình sẽ được tính, tiếp theo đó là tính biểu thức tăng
(mà điển hình là được dùng để tăng bộ lặp). Perl tiếp đó
sẽ tính lại biểu thức kiểm tra, lặp lại khi còn cần.
Thí dụ này in ra các số từ 1 đến 10, mỗi số đã có sau
nó một dấu cách:
104
for ($i = 1; $i <= 10; $i++) print “$i “; }
Ban đầu, biến $i được đặt là 1. Rồi, biến này được so
sánh với 10, mà thực sự nó đang bé hơn hay bằng. Thân
của chu trình (mỗi câu lệnh print) được thực hiện, và rồi
biểu thức tăng (biểu thức tự tăng $i++) sẽ được thực
hiện, thay đổi giá trị trong $i thành 2. Vì điều này vẫn
còn bé hơn 10 nên lặp lại tiến trình, cho tới khi lần lặp
cuối mà giá trị 10 của $i đổi thành 11. Rồi điều này
không còn bé hơn hay bằng 10 nữa, cho nên chu trình đi
ra (với $i có giá trị 11).
Câu lệnh foreach
Vẫn còn một kết cấu lặp khác là câu lệnh foreach.
Câu lệnh này rất giống như câu lệnh foreach của vỏ C: nó
nhận một danh sách các giá trị và mỗi lần lại gán chúng
cho một biến vô hướng, rồi thực hiện một khối mã cùng
với việc gán đó. Nó trông tựa như thế này:
foreach $i (@danh sách nào đó) { câu lệnh 1; câu lệnh 2; câu lệnh 3; }
Không giống lớp vỏ C, giá trị nguyên gốc của biến
vô hướng được tự động khôi phục khi chu trình đi ra -
một cách khác để nói điều này là ở chỗ biến vô hướng là
cục bộ cho chu trình.
Sau đây là một thí dụ về foreach: @a = (1,2,3,4,5); foreach $b (reverse @a) {
105
print $b; }
Mẩu chương trình này in ra 54321. Chú ý rằng danh
sách được foreach sử dụng có thể là một biểu thức danh
sách bất kì, không chỉ là một biến mảng. (Đây là điển
hình cho phần lớn các kết cấu Perl có yêu cầu một danh
sách.)
Có thể bỏ tên của biến vô hướng, trong trường hợp
đó Perl giả thiết rằng bạn đã xác định dùng tên biến $_.
Bạn sẽ thấy rằng biến $_ được dùng như mặc định cho
nhiều phép toán Perl, cho nên bạn có thể coi nó như một
vùng nháp. (Tất cả các phép toán có dùng $_ theo mặc
định cũng đã có thể dùng một biến vô hướng thông
thường.) Chẳng hạn, toán tử print in ra giá trị của $_ nếu
không có giá trị nào khác được xác định, cho nên thí dụ
sau đây sẽ làm việc như thí dụ trước:
@a = (1,2,3,4,5); foreach (reverse @a) { print ; }
Xem việc dùng biến $_ làm đơn giản hơn bao nhiêu
không? (Hay ít nhất thì cũng ngắn hơn.)
Nếu danh sách mà bạn đang lặp được tạo nên từ một
tham chiếu biến mảng đến, thay vì một toán tử nào đó
mà cho lại một giá trị danh sách, thì biến vô hướng đang
được dùng cho lặp thực ra lại là tham chiếu tới từng phần
tử của mảng đó, thay vì là bản sao của các giá trị đó.
Điều này ngụ ý gì theo nghĩa thông thường? Nó có nghĩa
là nếu thay đổi biến vô hướng thì cũng thay đổi phần tử
đặc biệt trong mảng mà biến đó đang đại diện cho.
Chẳng hạn:
106
@a = (3,5,7,9); foreach $one ($a) { $one *= 3; } # @a bây giờ là (9, 15,21,27)
Chú ý đến việc thay đổi $one thực ra làm thay đổi
từng phần tử của @a.
Bài tập
Xem Phụ lục A về lời giải.
1. Viết một chương trình hỏi về nhiệt độ bên ngoài, rồi
in “quá nóng” nếu nhiệt độ là trên 72 F, và “quá
lạnh” trong các trường hợp khác.
2. Sửa đổi chương trình trong bài tập trước để cho nó in
ra “quá nóng” nếu nhiệt độ là trên 75F, “quá lạnh”
nếu nhiệt độ là dưới 68F, và “vừa phải” nếu nhiệt độ
trong khoảng 68 và 75.
3. Viết một chương trình đọc một danh sách các số
(mỗi số một hàng) cho tới khi đọc tới số 999, rồi in
ra toàn bộ tất cả các số đã cộng lại với nhau. (Phải
chắc đừng có cộng cả 999 vào!) Chẳng hạn, nếu bạn
đưa vào 1,2,3 và 999 thì chương trình sẽ đáp ứng với
câu trả lời 6 (1+2+3).
4. Viết một chương trình đọc một danh sách các xâu rồi
in chúng ra thành danh sách các xâu theo thứ tự đảo
ngược (không dùng toán tử reverse cho danh sách).
(Nhớ rằng toán tử <STDIN> sẽ đọc một danh sách các
xâu trên từng dòng tách biệt khi được dùng trong
hoàn cảnh mảng.)
107
5. Viết một chương trình in ra bảng các số và bình
phương của chúng từ không đến 32. Thử đưa ra một
cách mà bạn không cần phải có tất cả các số từ 0 đến
32 trong danh sách, rồi thử một cách bạn phải có các
số đó. (Để trông cho đẹp,
printf “%5g %8g\n”, $a, $b
sẽ in ra $a và $b như một số có năm cột, còn $b như
một số có tám cột.
108
109
5
Mảng kết hợp
Mảng kết hợp là gì?
Mảng kết hợp cũng tựa như mảng (kiểu danh sách)
đã thảo luận trước đây, trong đó nó là một tuyển tập các
dữ liệu vô hướng, với các phần tử riêng được chọn ra
bằng một giá trị chỉ số nào đó. Không giống mảng danh
sách, giá trị chỉ số của mảng kết hợp không phải là số
nguyên không âm nhỏ, mà thay vào đó là vô hướng tuỳ
ý. Những vô hướng này (còn gọi là khoá) được dùng về
sau để tìm kiếm các giá trị từ mảng này.
Các phần tử của mảng kết hợp không có thứ tự đặc
biệt. Xem chúng tựa như bàn đầy những quân bài. Nửa
trên của các con bài là khoá, còn nửa dưới là giá trị của
chúng. Mỗi lần bạn đặt một giá trị vào trong mảng kết
hợp thì một con bài mới lại được tạo ra. Về sau khi bạn
Trong chương này:
Mảng kết hợp là gì?
Biến mảng kết hợp
Biểu diễn hằng cho mảng kết hợp
Các toán tử mảng kết
hợp
110
muốn sửa đổi giá trị này, bạn cho khoá, còn Perl tìm ra
đúng con bài. Cho nên, thực sự, trật tự của các con bài là
không quan trọng. Thực ra, Perl cất giữ các con bài (cặp
khoá-giá trị) theo thứ tự bên trong đặc biệt để dễ dàng
tìm ra một con bài đặc biệt, cho nên Perl không phải
duyệt qua tất cả các cặp để tìm ra đúng con bài. Bạn
không thể kiểm soát được trật tự này, cho nên đừng thử.
Biến mảng kết hợp
Tên biến mảng kết hợp là dấu phần trăm (%) theo
sau bởi một chữ, theo sau nữa là không hay nhiều chữ,
chữ số và dấu gạch thấp. Nói cách khác, phần đi sau dấu
phần trăm giống hệt cái mà chúng có cho tên biến vô
hướng và biến mảng. Và giống như chẳng có quan hệ gì
giữa $fred và @fred, biến mảng kết hợp %fred cũng
chẳng liên quan gì tới hai loại biến kia cả.
Thay vì tham chiếu tới toàn bộ mảng kết hợp, thông
dụng hơn cả là tạo ra một mảng kết hợp và truy nhập vào
nó bằng cách tham chiếu tới các phần tử của nó. Mỗi
phần tử của mảng đã là một vô hướng tách biệt, được
truy nhập tới bởi một chỉ mục vô hướng, gọi là khoá.
Các phần tử của mảng kết hợp %fred vậy được tham
chiếu đến bằng $fred{$key} với $key là bất kì biểu thức
vô hướng nào. Lại chú ý rằng việc truy nhập vào một
phần tử của mảng không bắt đầu bằng cùng chỗ ngắt như
tên của toàn bộ mảng.
Giống như với mảng danh sách, có thể tạo ra những
phần tử mới bằng việc gán cho mảng một phần tử:
$fred{“aaa”} = “bbb” ; # tạo ra khoá “aaa”, giá trị “bbb” $fred{234.5} = 456.7; # tạo ra khoá “234.5”, giá trị 456.7
111
Hai câu lệnh này tạo ra hai phần tử trong mảng.
Những tham chiếu về sau tới cùng những phần tử này
(dùng cùng khoá) sẽ cho lại giá trị được cất giữ.
print $fred{“aaa”}; # in “bbb” $fred{234.5} += 3; # làm cho nó thành 459.7
Việc tham chiếu tới một phần tử không có sẵn sẽ
cho lại giá trị undef, giống như với mảng danh sách hay
biến vô hướng không xác định.
Biểu diễn hằng kí hiệu cho mảng kết hợp
Bạn có thể muốn truy nhập vào mảng kết hợp như
một toàn thể, để hoặc khởi đầu nó hay sao chép nó sang
mảng kết hợp khác. Perl không thực sự có biểu diễn
hằng kí hiệu cho mảng kết hợp, cho nên thay vì thế nó
tháo rời mảng ra như một danh sách. Mỗi cặp phần tử
trong danh sách (mà bao giờ cũng phải có một số chẵn
phần tử) đã xác định ra một khoá và giá trị tương ứng
của nó. Biểu diễn tháo rời này có thể được gán vào trong
mảng kết hợp khác, mà rồi sẽ tái tạo lại cùng mảng kết
hợp đó. Nói cách khác:
@fred_list = %fred; # @fred_list nhận (“aaa”, “bbb”, “234.5”, 456.7) %barney = @fred_list; # tạo ra %barney giống %fred %barney = %fred; # cách nhanh hơn để làm cùng việc
đó %smooth = (“aaa”, “bbb”, “234.5”, 456.7); # tạo ra %smooth giống như %fred, từ các giá trị hằng kí
hiệu
Trật tự của các cặp khoá-giá trị là tuỳ ý trong cách
biểu diễn tháo rời này, và không thể kiểm soát được. Cho
dù bạn có tráo đổi một số giá trị và tạo ra một mảng như
112
một toàn thể thì danh sách tháo rời thu được vẫn cứ theo
bất kì trật tự nào mà Perl đã tạo ra để truy nhập hiệu quả
vào các phần tử riêng. Bạn đừng bao giờ nên dựa trên bất
kì trật tự đặc biệt nào.
Các toán tử mảng kết hợp
Sau đây là một số toán tử cho mảng kết hợp.
Toán tử keys()
Toán tử keys(%tên mảng) cho lại danh sách các tất
cả các khoá hiện có trong mảng kết hợp %tên mảng . Nói
cách khác, nó tựa như các phần tử được đánh số lẻ (một,
ba năm vân vân) của danh sách được việc tháo rời %tên
mảng cho lại trong ngữ cảnh mảng, và thực ra, cho lại
chúng theo trật tự đó. Nếu không có phần tử nào trong
mảng kết hợp thì keys() cho lại một danh sách rỗng.
Chẳng hạn, bằng việc dùng mảng kết hợp từ thí dụ
trước:
$fred{“aaa”} = “bbb”; $fred{234.5} = 456.7; @list = keys(%fred) ; # @list nhận được (“aaa”, 234.5)
hay (234.5, “aaa”)
Các dấu ngoặc tròn là tuỳ chọn: keys %fred cũng
giống như keys(%fred).
foreach $key (key %fred) {# một lần cho mỗi khoá của %fred
print “tại $key chúng có $fred{$key}\n”; # in khoá và giá trị }
113
Thí dụ này cũng chỉ ra rằng các phần tử mảng kết
hợp có thể chen lẫn nhau trong các xâu nháy kép. Tuy
nhiên bạn không thể xen lẫn toàn bộ mảng* .
Trong ngữ cảnh vô hướng, toán tử keys() cho lại số
các phần tử (cặp khoá-giá trị) trong mảng kết hợp.
Chẳng hạn, bạn có thể tìm ra liệu mảng kết hợp có rỗng
hay không:
if (keys(%mảng nào đó)) { # nếu keys() khác không: ...; # mảng là khác rỗng }
# ... hoặc ...
while (keys(%mảng nào đó) < 10) { ... ; # cứ lặp chu trình khi có ít hơn 10 phần tử }
Toán tử values()
Toán tử values(%tên mảng) cho lại một danh sách
tất cả các giá trị hiện tại của %tên mảng, theo cùng trật
tự như các khoá được toán tử keys(%tên mảng) cho lại.
Như với keys(), các dấu ngoặc tròn là tuỳ chọn. Chẳng
hạn:
%lastname = (); # buộc %lastname là rỗng $lastname(“fred”} = “flintstore”; $lastname{“barney”} = “rubble”; @lastname = values(%lastname); # lấy các giá trị
Tại điểm này @lastname chứa hoặc (“flintstore”,
“rubble”) hay đảo ngược của nó.
* Này, bạn có thể đấy, bằng cách dùng lát cắt, nhưng chúng ta không
nói về lát cắt ở đây.
114
Toán tử each()
Nếu bạn muốn lặp trên (tức là xem xét mọi phần tử
của) toàn bộ mảng kết hợp, bạn có thể dùng keys(), duyệt
xét từng khoá được cho lại và nhận giá trị tương ứng.
Trong khi phương pháp này thường hay được dùng, một
cách hiệu quả hơn là dùng each(%tên mảng), toán tử sẽ
cho lại cặp khoá-giá trị như một danh sách hai phần tử.
Với mỗi lần thực hiện toán tử này cho cùng mảng, cặp
khoá-giá trị kế tiếp sẽ được cho lại, cho tới khi tất cả các
phần tử đã đã được truy nhập tới. Khi không còn cặp nào
nữa thì each() cho lạimột danh sách rỗng.
Vậy chẳng hạn, để đi qua mảng %lastname trong thí
dụ trước, làm điều gì đó tựa như thế này:
while (($first, $last) = each(%lastname)) { print “Tên cuối cùng của $first là $last\n”; }
Việc gán một giá trị mới cho toàn bộ mảng sẽ đặt lại
từng toán tử each() cho từ đầu. Việc bổ sung hay loại bỏ
các phần tử của mảng rất có thể gây ra lẫn lộn each() (và
có thể gây lẫn lộn cho cả bạn nữa).
Toán tử delete
Cho đến giờ, với điều bạn biết được, bạn có thể
thêm phần tử vào mảng kết hợp, nhưng bạn không thể
loại bỏ chúng (một việc khác hơn là gán giá trị mới cho
toàn bộ mảng). Perl cung cấp toán tử delete để loại bỏ
các phần tử. Toán hạng của delete là một tham chiếu
mảng kết hợp, hệt như nếu bạn chỉ nhìn vào một giá trị
115
đặc biệt. Perl loại bỏ cặp khoá-giá trị khỏi mảng kết hợp,
và cho lại giá trị của phần tử bị xoá. Chẳng hạn:
%fred = (“aaa”, “bbb”, 234.5, 34.56) ; # cho %fred 2 phần tử
delete $fred{“aaa”}; # %fred bây giờ chỉ còn một cặp khoá-giá trị
Bài tập
Xem phụ lục A về lời giải.
1. Viết một chương trình đọc và in một xâu và giá trị
ánh xạ của nó tương ứng với ánh xạ được trình bầy
trong bảng sau:
Cái vào Cái ra
đỏ
lục
xanh
táo
lá
đại dương
2. Viết một chương trình đọc một chuỗi các từ với một
từ trên dòng cho tới cuối tệp, rồi in ra một tóm tắt có
bao nhiêu lần mỗi từ đã gặp. (Thêm một thách thức,
sắp xếp các từ theo thứ tự giảm dần mã ASCII khi
đưa ra.)
116
117
6
Vào / ra cơ bản
Vào từ STDIN
Việc đọc từ lối vào chuẩn (qua bộ điều khiển Perl
được gọi là STDIN) thì thật dễ dàng. Chúng đã làm việc
này với toán tử <STDIN>. Việc tính toán tử này trong ngữ
cảnh vô hướng cho bạn một dòng tiếp của cái vào, hay
undef nếu không còn dòng nào nữa, giống như:
$a = <STDIN>; # đọc dòng tiếp
Việc tính toán tử này trong ngữ cảnh mảng sẽ cho
bạn tất cả các dòng còn lại như một danh sách - mỗi
phần tử của danh sách này là một dòng, bao gồm các kết
thúc dòng mới của nó. Chúng đã thấy điều này trước
đây, nhưng xem như việc làm mới lại, nó có thể trông
như một cái gì đó tựa như thế này:
@a = <STDIN>;
Trong chương này:
Vào từ STDIN
Vào từ toán tử Diamond
Đưa ra STDOUT
118
Một cách điển hình, một trong những điều bạn muốn
làm là đọc tất cả các dòng một lúc, và làm điều gì đó trên
mỗi dòng. Một cách chung để làm điều này là:
while ($_ = <STDIN>) { # xử lí $_ tại đây (cho từng dòng) }
Cứ mỗi khi một dòng được đọc vào, <STDIN> lại cho
một giá trị đúng* , cho nên chu trình tiếp tục thực hiện.
Khi <STDIN> không còn dòng nào để đọc nữa thì nó cho
lại undef , cho giá trị sai, kết thúc chu trình.
Việc đọc một giá trị vô hướng cho <STDIN> và dùng
giá trị đó làm biểu thức điều khiển cho chu trình (như
trong thí dụ trước) thường hay xuất hiện đến mức Perl có
hẳn một cách viết tắt cho nó. Bất kì khi nào việc kiểm
thử chu trình chỉ bao gồm một toán tử đưa vào (cái gì đó
tựa như <...>), Perl tự động sao dòng được đọc vào trong
biến $_.
while ($_ = <STDIN>) { # giống “while ($_ = <STDIN>)” chop; # giống “chop($_)” # các phép toán khác với $_ ở đây }
Vì biến $_ là mặc định cho nhiều phép toán nên bạn
có thể tiết kiệm khá nhiều về việc gõ theo cách này.
* Nếu dòng cuối cùng của tệp chỉ có một kí tự “0” thì <STDIN> cho
lại undef tại dòng đó; thay vì tại cuối tệp. Nếu bạn tạo ra một tệp
giống như thế thì người lập trình Perl trên toàn thế giới sẽ gửi cho
bạn thư giận dỗi. Nhưng đó là một tệp bệnh hoạn làm phá vỡ việc
dùng loại chu trình này.
119
Đưa vào từ toán tử hình thoi
Một cách khác để đọc cái vào là dùng toán tử hình
thoi: <>. Toán tử này giống như <STDIN> ở chỗ nó cho
lại một dòng riêng lẻ trong ngữ cảnh vô hướng (với undef
nếu tất cả các dòng này đã được đọc), hay tất cả các
dòng còn lại nếu được dùng trong ngữ cảnh mảng. Tuy
nhiên, khác với <STDIN>, toán tử hình thoi lấy dữ liệu
từ tệp hay các tệp được xác định trên dòng lệnh được gọi
trong chương trình Perl. Chẳng hạn, nếu bạn có một
chương trình mang tên kitty, bao gồm:
#! /usr/bin/perl while (<>) { print $_; }
và nếu bạn gọi kitty với:
kitty file1 file2 file3
thì toán tử hình thoi sẽ đọc từng dòng của file1 theo sau
bởi từng dòng của file2 và file3 lần lượt, cho lại undef khi
khi tất cả các dòng đã được đọc hết. Như bạn có thể
thấy, kitty làm việc giống như cat, gửi tất cả các dòng
của tệp có tên ra lối ra chuẩn theo tuần tự. Nếu, giống
cat, bạn không xác định bất kì tên tệp nào trên dòng lệnh
thì toán tử hình thoi sẽ tự động đọc từ lối vào chuẩn.
Về mặt kĩ thuật, toán tử hình thoi không nhìn y
nguyên vào các đối dòng lệnh - nó làm việc từ mảng
@ARGV. Mảng này là một mảng đặc biệt được bộ thông
dịch Perl đặt sẵn là một danh sách các đối dòng lệnh.
Mỗi đối dòng lệnh lại bị bỏ đi sau khi Perl đã lấy các
chuyển mạch dòng lệnh của nó để đưa vào một phần tử
tách biệt của mảng @ARGV. Bạn có thể hiểu danh sách
120
này theo bất kì cách nào bạn muốn*. Bạn thậm chí có thể
đặt mảng này bên trong chương trình của mình, và có
toán tử hình thoi làm việc trên danh sách mới thay vì các
đối dòng lệnh, như thế này:
@ARGV = (“aaa”, “bbb”, “ccc”); while (<>) { # xử lí tệp aaa, bbb và ccc print “Dòng này là: $_”; }
Trong Chương 10, Giải quyết tệp và kiểm thử tệp,
chúng ta sẽ thấy cách mở và đóng các tệp xác định vào
thời điểm xác định, nhưng kĩ thuật này đã được dùng cho
một số chương trình nhanh-và-bẩn của tôi.
Đưa ra STDOUT
Perl dùng các toán tử print và printf để ghi lên lối ra
chuẩn. Xem cách chúng được dùng.
Dùng print cho đưa ra thông thường
Chúng ta đã dùng print để hiển thị văn bản lên lối ra
chuẩn. Mở rộng thêm một chút.
Toán tử print nhận một danh sách các xâu, và gửi lần
lượt từng xâu ra lối ra chuẩn, không can thiệp hay thêm
các kí tự vào đuôi. Điều có thể không hiển nhiên là ở chỗ
print thực sự chỉ là toán tử danh sách, và cho lại một giá
trị giống như bất kì toán tử danh sách nào khác. Nói cách
* Thư viện chuẩn của Perl chứa các trình cho việc phân tích kiểu như
getop cho các đối dòng lệnh của chương trình Perl. Xam Sách con
lừa để biết thêm thông tin về thư việc này.
121
khác:
$a = print (“xin chào”, “mọi người”, “\n”) ;
sẽ là một cách khác để nói xin chào mọi người. Giá trị cho
lại của print là một giá trị đúng hay sai, chỉ ra sự thành
công của việc in. Nó gần như bao giờ cũng thành công,
trừ phi bạn gặp lỗi vào/ra nào đó, cho nên $a trong
trường hợp này sẽ gần như bao giờ cũng là 1.
Đôi khi, bạn sẽ cần bổ sung thêm các dấu ngoặc vào
print như được nêu trong thí dụ này, đặc biệt nếu điều
đầu tiên bạn muốn in bắt đầu với một dấu mở ngoặc
tròn, như trong:
print (2+3), “xin chào”; # sai! in 5, bỏ qua “xin chào’ print ((2+3), “xin chào”); # đúng! in 5xin chào print 2+3, “xin chào”; # cũng đúng! in 5xin chào
Dùng printf cho cái ra có dạng thức
Bạn có thể muốn có một chút ít kiểm soát với cái ra
hơn là khả năng print cung cấp. Thực ra, bạn có thể quen
với cái ra có dạng thức của hàm printf của C. Chớ có sợ -
Perl cung cấp một phép toán tương ứng với cùng tên.
Toán tử printf nhận một danh sách đối (được bao
trong dấu ngoặc tròn tuỳ chọn, như toán tử print). Đối thứ
nhất là một xâu kiểm soát dạng thức, xác định cách in
các đối còn lại. Nếu bạn còn chưa quen thuộc với hàm
printf chuẩn, thì bạn nên kiểm tra xem lại hàm printf. Tuy
nhiên, xem như một thí dụ:
printf “%15s %5d %10.2f\n”, $s, $n, $r;
sẽ in ra $s trong một trường 15 kí tự, rồi đến dấu
122
cách, rồi đến $n xem như một số nguyên trong trường 5
kí tự, rồi đến một dấu cách khác, đến $r như giá trị dấu
phẩy động với 2 vị trí thập phân trong một trường 10 kí
tự, và cuối cùng là một dấu dòng mới.
Bài tập
Xem Phụ lục A về lời giải.
1. Viết một chương trình hành động như cat, nhưng đảo
ngược thứ tự các dòng. (Một số hệ thống có tiện ích
kiểu như vậy mang tên tac.)
2. Viết một chương trình đọc một danh sách các xâu rồi
in ra xâu trong một cột có căn lề phải 20 kí tự. Chẳng
hạn, đưa vào xin chào, tạm biệt in ra xin chào và tạm
biệt được căn lề phải trong cột 20 kí tự.
3. Sửa đổi chương trình của bài tập trước để cho phép
người dùng chọn lấy chiều rộng cột. Chẳng hạn, đưa
vào 20, xin chào và tạm biệt cũng làm cùng việc như
chương trình trước đã làm, nhưng đưa vào 30, xin
chào và tạm biệt thì phải căn lề xin chào và tạm biệt
theo cột 30 kí tự.
123
7
Biểu thức
chính qui
Khái niệm về biểu thức chính qui
Biểu thức chính qui là một khuôn mẫu - một khuôn
mẫu - để được sánh với một xâu. Việc sánh một biểu
thức chính qui với một xâu thì hoặc thành công hoặc thất
bại. Đôi khi, sự thành công hay thất bại này có thể là tất
cả những gì bạn quan tâm tới. Vào lúc khác, bạn sẽ
muốn lấy một khuôn mẫu đã sánh đúng và thay thế nó
bằng một xâu khác, một phần trong đó có thể phụ thuộc
đích xác vào cách thức và nơi chốn mà biểu thức chính
qui được sánh đúng.
Biểu thức chính qui thường được nhiều chương trình
UNIX dùng tới, như grep, sed, awk, ed, vi, emacs và
Trong chương này:
Khái niệm về biểu thức chính qui
Cách dùng đơn giản về biểu thức chính qui
Khuôn mẫu
Thêm về toán tử đối sánh
Phép thế
Toán tử split() và join()
124
thậm chí cả nhiều vỏ nữa. mỗi chương trình đã có một
tập các kí tự khuôn mẫu khác nhau (phần lớn là chờm
lên nhau). Perl là một siêu tệp ngữ nghĩa cho tất cả
những công cụ này - bất kì biểu thức chính qui nào mà
có thể được viết trong một trong những công cụ UNIX
này thì cũng đã có thể được viết trong Perl, nhưng không
nhất thiết dùng hệt các kí tự đó.
Cách dùng đơn giản về biểu thức chính qui
Nếu chúng tìm tất cả các dòng của một tệp có chứa
xâu abc, thì có thể dùng chỉ lệnh grep:
grep abc sonefile > result
Trong trường hợp này, abc là biểu thức chính qui mà
chỉ lệnh grep lấy để kiểm tra cho từng dòng đưa vào.
Những dòng sánh đúng sẽ được chuyển ra lối ra chuẩn (ở
đây, kết thúc với tệp result vì việc chuyển hướng của vỏ).
Trong Perl, có thể nói về xâu abc như biểu thức
chính qui bằng việc bao xâu này trong hai dấu sổ chéo:
if (/abc/) { print “$_”; }
Nhưng cái gì được kiểm tra so với biểu thức chính
qui abc trong trường hợp này? Tại sao, anh bạn cũ của
chúng ta, biến $_ lại có mặt ở đây? Khi một biểu thức
chính qui được bao trong hai dấu sổ chéo (như trên), thì
biến $_ sẽ được kiểm tra theo biểu thức chính qui đó.
Nếu biểu thức chính qui sánh đúng, thì toán tử sánh sẽ
cho lại giá trị đúng. Ngoài ra, nó cho lại giá trị sai.
Trong thí dụ này, biến $_ được giả sử có chứa một
125
dòng văn bản nào đó, và được in ra nếu dòng này có
chứa các kí tự abc đâu đó bên trong dòng - tương tự như
chỉ lệnh grep ở trên. Không giống như chỉ lệnh grep, vận
hành trên tất cả các dòng của tệp, đoạn chương trình Perl
này chỉ nhìn vào có một dòng thôi. Để làm việc trên tất
cả các dòng, cần thêm vào một chu trình, như trong:
while (<>) { if (/abc/) { print “$_”; } }
Điều gì sẽ xảy ra nêu như không biết được số của
các b giữa a và c? Tức là, điều gì sẽ xảy ra nếu muốn in
dòng có chứa một a và theo sau nó là không hay nhiều b,
rồi theo sau nữa là một c? Với grep, phải nói:
grep “ab*c” somefile > result
(Đối này có chứa dấu sao trong ngoặc kép bởi vì
chúng không muốn lớp vỏ trải rộng đối đó cứ như là một
mẫu tên tệp. Nó phải được truyền qua grep để có hiệu
quả.) Thế mà trong Perl, chúng ta có thể nói đích xác
cùng điều đó:
while (<>) { if (/ab*c/) { print “$_”; } }
Cũng hệt như grep, điều này có nghĩa là một a theo
sau bởi không hay nhiều b, theo sau là c.
Chúng ta sẽ xem xét nhiều tuỳ chọn khác về toán tử
đối sánh trong mục “Nói thêm về toán tử đối sánh”, ở
cuối chương này, sau khi đã nói về tất cả các loại biểu
126
thức chính qui.
Một toán tử biểu thức chính qui nữa là toán tử thay
thế, làm việc thay thế một phần của xâu mà sánh đúng
biểu thức chính qui bằng một xâu khác. Toán tử thay thế
giống như chỉ lệnh s trong sed, bao gồm một chữ s, một
sổ chéo, một biểu thức chính qui, một sổ chéo, một xâu
thay thế, và một sổ chéo cuối cùng, trông tựa như thế
này:
s/ab*c/def/;
Xâu (trong trường hợp này là biến $_) được đem ra
đối sánh với biểu thức chính qui (ab*c). Nếu việc đối
sánh thành công, thì phần của xâu sánh đúng sẽ bị loại ra
và được thay thế bằng xâu thay thế (def). Nếu việc đối
sánh không thành công thì chẳng có gì xảy ra cả.
Như với toán tử đối sánh, sẽ còn xem xét lại vô số
các tuỳ chọn về toán tử thay thế dưới đây, trong mục
“Thay thế”.
Khuôn mẫu
Một biểu thức chính qui là một khuôn mẫu. Một số
phần của khuôn mẫu sánh đúng chỉ các kí tự trong xâu
thuộc kiểu đặc biệt. Những phần khác của khuôn mẫu
sánh đúng cho đa kí tự, hay đa đa kí tự. Trước hết, sẽ
xem các khuôn mẫu một kí tự, rồi đến các khuôn mẫu đa
kí tự.
127
Khuôn mẫu một kí tự
Kí tự sánh mẫu đơn giản nhất và thông dụng nhất
trong các biểu thức chính qui là một kí tự sánh với chính
nó. Nói cách khác, đặt một chữ a vào trong biểu thức
chính qui đòi hỏi một chữ tương ứng a trong xâu.
Kí tự sánh mẫu thông dụng nhất tiếp đó là dấu chấm
“.”. Dấu chấm đối sánh bất kì kí tự riêng lẻ nào ngoại trừ
dấu dòng mới (\n). Chẳng hạn, khuôn mẫu /a./ đối sánh
bất kì dãy hai kí tự nào bắt đầu bằng a và không phải là
“a\n”.
Lớp kí tự sánh mẫu được biểu diễn bởi cặp dấu
ngoặc vuông mở và đóng, và một danh sách các kí tự
nằm giữa hai dấu ngoặc này. Một và chỉ một trong các kí
tự này phải hiện diện tại phần tương ứng của xâu cần
sánh mẫu. Chẳng hạn,
/[abcde]/
sánh với bất kì một trong năm chữ đầu tiên của bảng
chữ thường, trong khi
/[aeiouAEIOU]/
lại sánh với bất kì năm nguyên âm hoặc chữ thường
hoặc chữ hoa. Nếu bạn muốn đặt dấu ngoặc vuông phải
(]) vào danh sách thì đặt một sổ chéo ngược ở trước nó,
hay đặt nó như kí tự đầu tiên bên trong danh sách. Phạm
vi của các kí tự (như a tới z có thể được viết tắt bằng
việc chỉ ra những điểm cuối của phạm vi được tách biệt
bởi dấu gạch ngang (-); để có được hằng kí hiệu gạch
ngang, đặt trước dấu gạch ngang một sổ chéo ngược. Sau
đây là một số thí dụ khác:
[0123456789] # sánh với mọi chữ số
128
[0-9] # cũng thế [0-9\-] # sánh 0-9 hay dấu trừ [a-z0-9] # sánh bất kì chữ thường hay số nào [a-zA-Z0-9_] # sánh bất kì chữ, số hay dấu gạch thấp
Cũng có lớp kí tự bị phủ định, cũng là cùng lớp kí
tự, nhưng có thêm dấu mũi tên ngược (hay dấu mũ ^)
đằng trước, đi ngay sau dấu ngoặc trái. Lớp kí tự này đối
sánh với bất kì kí tự đơn nào không trong danh sách.
Chẳng hạn:
[^0-9] # sánh với bất kì kí tự phi số nào [^aeiouyAEIOUY] # sánh với bất kì kí tự nào không
nguyên âm [^\^] # sánh với một kí tự đơn trừ mũi tên ngược
Để tiện cho bạn, đã có định nghĩa sẵn một số lớp kí
tự chung, như được mô tả trong Bảng 7-1.
Bảng 7-1: Viết tắt cho lớp kí tự định sẵn
Kết cấu Lớp tương
đương
Kết cấu
phủ định
Lớp phủ
định tương
đương
\d (số) [0-9] \D (số,
không!)
[^0-9]
\w (từ) [a-zA-Z0-
9_]
\W (từ,
không!)
[^a-zA-Z0-
9_]
\s (cách) [ \r\t\n\f] \S (cách,
không!)
[^ \r\t\n\f]
Khuôn mẫu \d sánh với “số”. Khuôn mẫu \w sánh
129
với “kí tự từ”, mặc dầu điều thực sự sánh đúng là bất kì
cái gì hợp lệ trong tên biến Perl. Khuôn mẫu \s sánh với
“dấu cách” (khoảng trắng), ở đây được xác định như dấu
cách, về đầu dòng (không hay dùng mấy trong UNIX),
tab, xuống dòng (dấu dòng mới của UNIX), và kéo giấy.
Các bản chữ hoa sánh đúng với cái đối lập cho những
lớp này.
Khuôn mẫu nhóm
Sức mạnh thực sự của biểu thức chính qui là khi bạn
có thể nói “một hay nhiều những thứ này” hay “cho tới
năm thứ này”. Ta nói về cách thực hiện điều này.
Dãy
Khuôn mẫu nhóm đầu tiên (và có lẽ kém hiển nhiên
nhất) là dãy. Điều này có nghĩa là abc sánh đúng với một
a theo sau là b, theo sau là c. Nó dường như đơn giản,
nhưng tôi cứ đặt tên cho nó để tôi có thể nói về nó sau
này.
Bội
Chúng đã thấy dấu sao (*) như một khuôn mẫu
nhóm. Dấu sao chỉ ra rằng “không hay nhiều” kí tự (hay
lớp kí tự) đứng ngay trước nó.
Hai khuôn mẫu nhóm khác làm việc giống thế là dấu
cộng (+), nghĩa là “một hay nhiều” kí tự đứng ngay
trước, và dấu hỏi (?), nghĩa là “không hay một” kí tự
ngay trước. Chẳng hạn, biểu thức chính qui /fo+ba?r/
sánh đúng cho một f theo sau là một hay nhiều o, theo
130
sau là a, b và tuỳ chọn a, theo sau là một r.
Trong tất cả ba khuôn mẫu nhóm này, các khuôn
mẫu đã tham lam. Nếu một khuôn mẫu như vậy có cơ
hội sánh đúng giữa năm và mười kí tự thì nó sẽ lọc ra
xâu mười kí tự mỗi lúc. Chẳng hạn:
$_ = “fred xxxxxxxxxx barney”; s/x*/boom/;
bao giờ cũng thay tất cả các x liên tiếp bằng boom
(kết quả là fred boom barney), thay vì chỉ thay thế cho
một hay hai x, cho dù một tập x ngắn hơn cũng sánh
được cho cùng biểu thức chính qui
Nếu bạn cần nói “năm tới mười” x, thì bạn có thể
xoay xở bằng cách đặt năm x theo sau bởi năm x nữa đi
liền sau dấu chấm hỏi. Nhưng làm thế trông xấu, mà
cũng chẳng làm việc tốt lắm. Thay vì vậy, có một cách
dễ hơn: số bội tổng quát. Số bội tổng quát bao gồm một
cặp dấu ngoặc nhọn với một hay hai số bên trong, như
trong /x{5,10}/. Giống như ba số bội khác, kí tự đứng
ngay trước (trong trường hợp này là chữ “x”) phải được
tìm thấy bên trong số lần lặp đã chỉ ra (năm đến mười ở
đây).
Nếu bạn bỏ đi con số thứ hai, như trong /x{5,}/, thì
điều này có nghĩa là “nhiều hay hơn nữa” (năm hay
nhiều hơn trong trường hợp này), và nếu bạn bỏ nốt dấu
phẩy, như trong /x{5}/, thì điều đó có nghĩa là “đúng con
số này” (năm x). Để được 5 x hay ít hơn, bạn phải đặt số
không vào, như trong /x{0,5}/.
Vậy, biểu thức chính qui /a.{5}b/ sánh đúng cho kí
tự a được tách với kí tự b bởi bất kì năm kí tự khác kí tự
dòng mới. (Nhớ lại rằng dấu chấm sánh với bất kì kí tự
131
khác dấu dòng mới, và chúng sánh với năm kí tự như thế
ở đây.) Năm kí tự này không cần phải như nhau. (Chúng
ta sẽ biết cách để buộc chúng là như nhau trong mục
tiếp.)
Có thể miễn trừ hoàn toàn bằng *, +, và ?, vì chúng
hoàn toàn tương đương với {0,}, {1,}, và {0,1}. Nhưng
dễ dàng hơn vẫn là gõ một kí tự ngắt tương đương, mà
cũng quen thuộc hơn.
Nếu có hai số bội trong một biểu thức, thì qui tắc
tham lam được tăng lên với “bên trái nhất là tham nhất”.
Chẳng hạn:
$_ = “a xxx c xxxxxxx d”;
/a.*c.*d/;
Trong trường hợp này, “.*” thứ nhất trong biểu thức
chính qui sánh với tất cả các kí tự cho tới c thứ hai, cho
dù việc sánh đúng chỉ với các kí tự cho tới c đầu tiên vẫn
cho phép toàn bộ biểu thức chính qui được sánh. Điều
này không tạo ra khác biệt gì (khuôn mẫu sẽ sánh theo cả
hai cách), nhưng sau này khi chúng có thể nhìn vào các
bộ phận của biểu thức chính qui mà được sánh, thì sẽ có
đôi chút vấn đề.
Điều gì xảy ra nếu biểu thức xâu và chính qui hơi bị
thay đổi đi, chẳng hạn như:
$_ = “a xxx ce xxxxxxx ci xxx d”;
/a.*ce.*d/;
Trong trường hợp này, nếu .* sánh với phần lớn các
kí tự có thể trước c tiếp, thì kí tự biểu thức chính qui tiếp
(e) sẽ không sánh với kí tự tiếp của xâu (i). Trong trường
hợp này, thu được việc lần ngược tự động - số bội bị
132
tháo ra và thử lại, dùng lại tại chỗ nào đó phía trước
(trong trường hợp này, tại c trước, tiếp sau là (e)* . Một
biểu thức chính qui phức tạp có thể bao gồm nhiều mức
lần ngược như vậy, dẫn tới thời gian thực hiện lâu.
Dấu ngoặc tròn như bộ nhớ
Một toán tử nhóm khác là cặp mở và đóng ngoặc
tròn quanh bất kì phần khuôn mẫu nào. Điều này không
làm thay đổi liệu khuôn mẫu có sánh đúng hay không,
nhưng thay vì thế lại làm cho một phần của xâu được
khuôn mẫu sánh đúng sẽ được ghi nhớ, để cho nó có thể
được tham chiếu tới về sau. Vậy chẳng hạn, (a) vẫn sánh
với a, còn ([a-z]) thì vẫn sánh với bất kì chữ thường nào.
Để nhớ lại một phần đã ghi nhớ của một xâu, bạn
phải đưa vào một dấu sổ chéo ngược theo sau bởi một số
nguyên. Kết cấu khuôn mẫu này biểu thị cho cùng dãy
các kí tự được sánh trước đây trong cặp dấu ngoặc tròn
cùng số (đếm từ một) . Chẳng hạn:
/fred(.)barney\1/;
sánh một xâu có chứa fred, tiếp theo là một kí hiệu
khác dấu dòng mới, tiếp nữa là barney, rồi tiếp bởi cùng
một kí tự đó. Vậy, nó sánh với fredxbarneyx, nhưng
không sánh với fredxbarneyy. So sánh điều đó với:
/fred.barney./’
trong đó hai kí tự không xác định có thể là một, hay
* Về mặt kĩ thuật, có nhiều cách lần ngược của toán tử * để tìm ra c
ở vị trí đầu tiên. Nhưng phải hơi thủ thuật hơn để mô tả nó, mà nó
vẫn hoạt động theo cùng nguyên lí.
133
khác nhau - cũng chẳng thành vấn đề gì.
Số 1 đến từ đâu vậy? Nó có nghĩa là phần biểu thức
chính qui nằm trong dấu ngoặc đầu tiên. Nếu có nhiều
phần như thế, thì phần thứ hai (đếm các dấu ngặc trái từ
trái sang phải) sẽ được tham chiếu tới là \2, phần thứ ba
là \3, và cứ thế. Chẳng hạn:
/a(.)b(.)c\2d\1/;
sẽ sánh với một a, một kí tự (gọi nó là #1), một b,
một kí tự khác (gọi nó là #2), một c, kí tự #2, một d, và
kí tự #1. Cho nên nó sánh với axbycydx, chẳng hạn.
Phần được tham chiếu tới có thể nhiều hơn một kí
tự. Chẳng hạn:
/a(.*)b\1c/;
sánh với một a, theo sau bởi một số bất kì kí tự nào
(thậm chí không), theo sau bởi b, theo sau bởi cùng dãy
kí tự đó, theo sau bởi c. Vậy, nó sẽ sánh với
aFREDnFREDc, hay thậm chí abc, nhưng không
aXXbXXXc.
Một cách dùng khác của phần được nhớ của biểu
thức chính qui là trong xâu thay thế của chỉ lệnh thay
thế. Kết cấu kiểu \1 vẫn giữ giá trị của chúng trong xâu
thay thế, và có thể được tham chiếu tới để xây dựng xâu,
như trong:
$_ = “a xxx b yyy c zzz d”;
s/b(.*)c/d\1e/;
mà sẽ thay thế b và c bằng d và e, vẫn giữ lại phần ở
giữa.
134
Thay phiên
Một kết cấu nhóm khác là thay phiên, như trong
a|b|c. Điều này có nghĩa là sánh đúng một trong các khả
năng (a hay b hay c trong trường hợp này). Điều này vẫn
có tác dụng ngay cả khi các thay phiên có nhiều kí tự,
như trong /song|blue/, sẽ sánh hoặc song hoặc blue. (Với
những thay phiên đơn giản, tốt hơn cả là bạn có thể bỏ
lớp kí tự như /[abc]/.)
Điều gì xảy ra nếu muốn sánh songbird hay bluebird?
có thể viết /songbird|bluebird/, nhưng phần bird đó không
nên có đó hai lần. Thực ra, cũng có cách ra, nhưng phải
nói tới thứ tự ưu tiên cho các khuôn mẫu nhóm, sẽ được
đề cập tới trong mục “Thứ tự ưu tiên” dưới đây.
Khuôn mẫu neo
Bốn kí pháp đặc biệt đóng neo cho một khuôn mẫu.
Thông thường, khi một khuôn mẫu được sánh với xâu thì
sự bắt đầu của khuôn mẫu đó được rê đi trong toàn bộ
xâu từ trái sang phải, sánh với cơ hội có thể đầu tiên.
Neo cũng cho phép bạn đảm bảo rằng các phần của dòng
khuôn mẫu sắp thẳng với những phần đặc biệt của xâu.
Cặp neo thứ nhất đòi hỏi rằng một phần đặc biệt của
việc đối sánh phải được định vị tại biên giới từ hay
không tại biên giới từ. Neo \b yêu cầu một biên giới từ
tại điểm đã chỉ ra cho khuôn mẫu đối sánh. Biên giới từ
là nơi ở giữa các kí tự sánh với \w và \W, hay giữa các kí
tự sánh với \w và chỗ bắt đầu hay kết thúc của xâu. Chú
ý rằng điều này ít phải xử lí đối với tiếng Anh và phải
làm nhiều đối với các kí hiệu C, nhưng điều đó cũng gần
135
thôi khi đạt tới. Chẳng hạn:
/fred\b/; # sánh fred, nhưng không Frederick /\bwiz/; # sánh wiz và wizard, nhưng không qwiz /\bFred\b/; # sánh Fred nhưng không Frederick hay
alFred /abc\bdef/; # không bao giờ sánh (không thể có cận ở
đây)
Giống thế, \B yêu cầu không có biên giới từ tại vị trí
đã chỉ ra. Chẳng hạn:
/\bFred\B/; # sánh “Frederick” nhưng không “Fred Flintstonee”
Hai neo nữa yêu cầu rằng một phần đặc biệt của
khuôn mẫu phải đi ngay sau cuối xâu. Dấu mũ (^) sánh
với điểm bắt đầu của xâu nếu nó đang ở một vị trí tạo ra
nghĩa để đối sánh tại chỗ bắt đầu của xâu. Chẳng hạn, ^a
sánh một a nếu và chỉ nếu a là kí tự đầu tiên của xâu.
Tuy nhiên, ^a cũng sánh với hai kí tự a và ^ ở bất kì đâu
trong xâu. Nói cách khác, dấu mũ đã mất ý nghĩa đặc
biệt của nó. Nếu bạn cần dấu mũ là một hằng kí hiệu dấu
mũ ngay tại chỗ bắt đầu, đặt một dấu sổ chéo ngược phía
trước nó.
Dấu $ cũng giống như ^, neo lại khuôn mẫu, nhưng
tại cuối của xâu, không phải bắt đầu. Nói cách khác,
c$ sánh với một c chỉ nếu nó xuất hiện tại cuối xâu. Dấu
đô la ở bất kì nơi đâu khác trong khuôn mẫu có lẽ vẫn cứ
được diễn giải như cách hiểu giá trị vô hướng, cho nên
bạn gần như bao giờ cũng phải dùng dấu sổ chéo ngược
để đối sánh một dấu hiệu đô là hàng kí hiệu trong xâu.
136
Thứ tự ưu tiên
Vậy điều gì xảy ra khi lấy a|b* cùng nhau? Liệu đây
là a hay b một số lần bất kì hay chỉ một a hay nhiều b?
Được rồi, cũng giống như các toán tử có số ưu tiên,
các khuôn mẫu bỏ neo và gộp nhóm cũng có số ưu tiên.
Số ưu tiên của khuôn mẫu từ cao xuống thấp nhất được
cho trong Bảng 7-2.
Bảng 7-2: Số ưu tiên toán tử gộp nhóm biểu thức
chính qui (cao nhất xuống thấp nhất)
Tên Biểu diễn
Dấu ngoặc tròn ( )
Số bội + * ? {m,n}
Tuần tự và bỏ neo abc^$\b\B
Thay phiên |
Theo bảng này, * có số ưu tiên cao hơn |. Cho nên
/a|b*/ được diễn giải như một a, hay số bất kì b.
Điều gì xảy ra nếu muốn một nghĩa khác, như trong
“bất kì số a hay b nào”? Chúng đơn thuần chỉ ném vào
một cặp dấu ngoặc. Trong trường hợp này, bao phần của
biểu thức mà toán tử * cần áp dụng, vào bên trong các
dấu ngoặc, và sẽ được nó, như (a|b)*. Nếu bạn muốn làm
rõ ràng biểu thức thứ nhất, thì bạn có thể đóng dấu ngoặc
dư thừa nó với a|(b*).
Khi bạn dùng dấu ngoặc để tác động tới số ưu tiên
chúng cũng đặt lẫy bộ nhớ, như đã chỉ ra trước đây trong
chương này. Tức là tập các dấu ngoặc này sẽ cần được
đếm khi bạn muốn nói tới một cái gì đó là \2, \3 hay bất
137
kì cái gì. Một ngày nào đó có thể có một loại dấu ngoặc
mà không phải đếm, nhưng chưa có trong lần đưa ra Perl
4.036.
Sau đây là một số thí dụ khác về biểu thức chính
qui, và tác động của dấu ngoặc:
abc* $ sánh với ab, abc, abcc, abccc, abcccc vân vân (abc)* # sánh với “”, abc, abcabc, abcabcabc vân vân ^x|y # sánh x tại đầu dòng, hay y ở bất kì đâu ^(x|y) # sánh hoặc với x hoặc với y tại đầu dòng a|bc|d # a hoặc bc hoặc d (a|b)(c|d) # ac, ad, bc hoặc bd (song|blue)bird # songbird hay bluebird
Thêm về toán tử đối sánh
Chúng ta đã nhìn vào cách dùng đơn giản nhất của
toán tử đối sánh (một biểu thức chính qui được bao trong
sổ chéo). Bây giờ nhìn vào vô vàn cách làm cho toán tử
này làm được điều gì đó hơi khác hơn.
Chọn một mục tiêu khác (toán tử =~)
Đôi khi xâu bạn muốn sánh với khuôn mẫu lại
không ở bên trong biến $_, và đó sẽ là sắc thái để đặt nó
ở đó (có lẽ bạn đã có một giá trị trong $_ mà bạn rất
thích). Không hề gì. Toán tử =~ sẽ giúp ở đây. Toán tử
này nhận một toán tử biểu thức chính qui ở vế bên phải,
rồi thay đổi đối tượng của toán tử này thành một cái gì
đó bên cạnh biến $_ - có nghĩa là một giá trị nào đó có
tên bên vế trái của toán tử này. Nó trông tựa như thế này:
$a = “hello world”;
138
$a =~ /^he/; # đúng $a =~ /(.)\l/; # cũng đúng (sánh với hai l) if ($a =~ /(.)\1/) { # đúng, cho nên có ... # một số chất liệu khác }
Mục tiêu của toán tử =~ có thể là bất kì biểu thức
nào cho một giá trị xâu vô hướng nào đó. Chẳng hạn,
<STDIN> cho một giá trị xâu vô hướng khi được dùng
trong hoàn cảnh vô hướng, cho nên chúng có thể tổ hợp
điều này với toán tử =~ và một toán tử sánh biểu thức
chính qui để được một kiểm tra gọn gàng về cái vào đặc
biệt, như trong:
print “còn yêu cầu nào nữa không?”; if (<STDIN> =~ /^[yY]/) { # cái vào có bắt đầu bằng một y
không? print “Vậy yêu cầu đó có thể là gì? ”; <STDIN>; # bỏ một dòng cái vào chuẩn print “Rất tiếc, tôi không thể làm được điều đó.\n”;
Trong trường hợp này, toán tử <STDIN> cho dòng
tiếp từ cái vào chuẩn, mà rồi ngay lập tức được dùng như
xâu đem sánh với khuôn mẫu ^[yY]. Lưu ý rằng bạn
chưa bao giờ cất giữ cái vào vào một biến, cho nên nếu
bạn muốn sánh cái vào với mẫu khác, hay có thể cho
hiện lại dữ liệu trong một thông báo lỗi thì bạn không
gặp may rồi. Nhưng dạng này thường hay đến đúng lúc.
Bỏ qua chữ hoa thường
Trong thí dụ trước, tôi đã dùng [yY] để đối sánh
hoặc chữ Y hoa hoặc y thường. Với những xâu rất ngắn
như y hay fred thì điều này là dễ dàng, như [fF]
[oO][oO]. Nhưng điều gì xẩy ra nếu xâu tôi muốn sánh
139
lại là từ “procedure” trong hoặc chữ thường hoặc chữ
hoa?
Trong một số bản của grep, cờ -i chỉ ra “bỏ qua hoa
thường”. Perl cũng có tuỳ chọn như vậy. Bạn chỉ ra tuỳ
chọn bỏ qua hoa thường bằng cách thêm vào chữ i
thường vào sau sổ chéo đóng, như trong /somepattern/i.
Điều này nói lên rằng các chữ của khuôn mẫu này sẽ
sánh với các chữ trong xâu trong cả chữ hoa lẫn thường.
Chẳng hạn, để sánh từ “procedure” trong cả hoa lẫn
thường tại đầu dòng, dùng /^procedure/i.
Bây giờ thí dụ trước của trong giống thế này:
print “còn yêu cầu nào nữa không?”; if (<STDIN> =~ /^y/i) { # cái vào có bắt đầu bằng một y
không? # có! xử lí cho nó ... }
Dùng một định biên khác
Nếu bạn đang tìm kiếm một xâu với một biểu thức
chính qui có chứa kí tự sổ chéo (/), thì bạn phải đặt trước
mỗi sổ chéo một sổ chéo ngược (\). Chẳng hạn, bạn có
thể tìm một xâu bắt đầu bằng /usr/etc tựa như thế này:
$path = <STDIN>; # đọc một tên đường dẫn (có lẽ từ “find”?)
if ($path =~ /^\/usr\/etc/) { # bắt đầu với /usr/etc ... }
Như bạn có thể thấy, tổ hợp sổ chéo ngược-sổ chéo
làm cho nó trông giống như có một thung lũng nhỏ giữa
hai mẩu văn bản. Làm điều này cho nhiều sổ chéo có thể
140
gây cồng kềnh, cho nên Perl cho phép bạn xác định một
kí tự định biên khác. Chỉ cần đặt trước bất kì kí tự phi
chữ-số* nào (định biên do bạn chọn) với một m, rồi liệt
kê khuôn mẫu của bạn theo sau bởi một kí tự định biên y
hệt thế nữa, là bạn đã hoàn thành, như trong:
/^\/usr/etc/ # dùng định biên sổ chéo chuẩn m@^/usr/etc@ # dùng @ làm định biên m#^/usr/etc# # dùng # làm định biên (sở thích của tôi)
Bạn có thể thậm chí dùng cả sổ chéo lần nữa nếu
bạn cứ muốn, như trong m/fred/, cho nên toán tử sánh
biểu thức chính qui thông thường thực sự là toán tử m,
tuy nhiên, m là tuỳ chọn, nếu bạn chọn sổ chéo làm định
biên.
Dùng xen lẫn biến
Một biểu thức chính qui là được xen lẫn biến trước
khi nó được xem xét cho các kí tự đặc biệt khác. Do đó,
bạn có thể xây dựng một biểu thức chính qui từ các xâu
được tính toán thay vì chỉ là hằng kí hiệu. Chẳng hạn:
$what = “bird”; $sentence = “Mọi con chim tốt đã bay.”; if ($sentence =~ /\b$what\b/) { print “Câu này chứa từ $what!\n”; }
Tại đây chúng đã xây dựng một cách có hiệu quả
toán tử biểu thức chính qui /\bbird\b/ bằng việc dùng một
* Nếu dấu định biên ngẫu nhiên là kí tự bên trái hay cặp trái-phải
(ngoặc tròn, ngoặc nhọn hay ngoặc vuông), thì dấu định biên đóng
là kí tự bên phải của cùng cặp đó. Nhưng ngoài ra, các kí tự là hệt
nhau cho bắt đầu và kết thúc.
141
tham chiếu biến.
Sau đây là một thí dụ có hơi phức tạp hơn:
$sentence = “Mọi con chim tốt đã bay.”; print “Tôi phải tìm cái gì đây?”; $what = <STDIN>; chop ($what) ; if ($sentence =~ /$what/) { # tìm thấy nó! print “Tôi đã thấy $what trong $sentence.\n”; } else { print “không... chẳng thấy cóc gì cả.\n”; }
Nếu bạn đưa vào bird, thì nó được tìm ra. Nếu bạn
đưa vào scream nó sẽ không tìm thấy. Nếu bạn đưa vào
[bw]ird, điều ấy cũng được tìm ra, chỉ ra rằng các kí tự đối
sánh khuôn mẫu biểu thức chính qui quả thực là vẫn có ý
nghĩa. Tôi sẽ chỉ ra cho bạn trong phần “Thay thế” dưới
đây về cách thay đổi xâu để cho các kí tự đối sánh khuôn
mẫu chính qui được tắt đi.
Biến chỉ đọc đặc biệt
Sau khi đối sánh khuôn mẫu thành công, các biến
$1, $2, $3 vân vân sẽ được đặt cho cùng giá trị là \1, \2,
\3 vân vân. Bạn có thể dùng điều này để nhìn vào một
phần của việc đối sánh trong đoạn chương trình sau.
Chẳng hạn:
$_ = “đây là phép kiểm tra”; /(\W+)\W+(\W+)/; # đối sánh hai từ đầu # $1 bây giờ là “đây” còn $2 bây giờ là “là”
Bạn cũng có thể thu được cùng các giá trị ($1, $2,
$3 vân vân) bằng việc đặt đối sánh trong hoàn cảnh
142
mảng. Kết quả là một danh sách các giá trị mà sẽ được
đặt cho $1 cho tới số các vật được ghi nhớ, nhưng chỉ
nếu biểu thức chính qui sánh đúng. Lấy lại thí dụ trước
theo cách khác
$_ = “đây là phép kiểm tra”; ($first, $second) = /(\W+)\W+(\W+)/; # đối sánh hai từ
đầu # $first bây giờ là “đây” còn $second bây giờ là “là”
Lưu ý rằng các biến $1 và $2 vẫn không bị thay đổi.
Các biến chỉ đọc được xác định trước còn bao gồm
$&, mà là một phần của xâu sánh đúng với biểu thức
chính qui; $`, là một phần của xâu trước phần sánh đúng;
còn $’ là phần của xâu sau phần sánh đúng. Chẳng hạn:
$_ = “đây là xâu mẫu”; /xâ.*u/; # sánh “xâu” bên trong xâu # $` bây giờ là “đây là” # $& bây giờ là “xâu” # $’ bây giờ là “mẫu”
Vì tất cả những biến này đã được đặt lại cho từng
lần sánh thành công cho nên bạn nên cất giữ các giá trị
trong các biến vô hướng khác nếu bạn cần các giá trị đó
về sau trong chương trình.
Thay thế
Chúng ta đã nói về dạng đơn giản nhất của toán tử
thay thế: s/old-regex/new-string/. Bây giờ là lúc nói tới vài
biến thể của toán tử này.
Nếu bạn muốn việc thay thế vận hành trên tất cả các
đối sánh có thể thay vì chỉ việc đối sánh đầu tiên thì viết
143
thêm g vào toán tử này, như trong:
$_ = “foot fool buffon”;
s/foo/bar/g; # $_ bây giờ là “bart barl bufbarn”
Xâu thay thế có biến xen vào, cho phép bạn xác định
xâu thay thế vào lúc chạy:
$_ = “hello, world” ;
$new = “goodbye”;
s/hello/$new/; # thay thế hello bằng goodbye
Các kí tự khuôn mẫu trong biểu thức chính qui cho
phép các khuôn mẫu được đối sánh, thay vì chỉ là các kí
tự cố định:
$_ = “đây là phép kiểm tra”;
s/(\w+)/<$1>/g; # $_ bây giờ là “<đây> <là> <phép> <kiểm> <tra>”
Nhớ lại rằng $1 được đặt là dữ liệu bên trong việc
đối sánh đúng mẫu trong dấu ngoặc.
Hậu tố i (hoặc trước hoặc sau g nếu có) làm cho biểu
thức chính qui trong toán tử thay thế bỏ qua chữ hoa
thường, giống như cùng tuỳ chọn trên toán tử đối sánh
đã mô tả trước đây.
Cũng vậy, giống như toán tử đối sánh, một dấu định
biên khác cũng có thể được tuyển lựa nếu sổ chéo là
không tiện. Chỉ cần dùng cùng kí tự đó ba lần:*
s#fred#barney#; # thay fred bằng barney, giống s/fred/barney/
* hoặc hai cặp sánh nhau nếu kí tự trái - phải được dùng.
144
Cũng vậy, giống toán tử đối sánh, bạn có thể xác
định một mục tiêu thay phiên bằng toán tử =~. Trong
trường hợp này, mục tiêu được chọn phải là một cái gì
đó mà bạn có thể gán cho một giá trị vô hướng vào, như
một biến vô hướng hay một phần tử của mảng. Sau đây
là một thí dụ:
$which = “đây là phép thử”;
$which =~ s/thử/đố/; # $which bây giờ là “đây là phép đố”
$someplace[$here] =~ s/left/right/; # đổi một phần tử mảng
$d{”t”} =~ s/^/x/; # thay bất kì kí tự nào bằng “x “ cho phần tử mảng kết hợp
Các toán tử split() và join()
Biểu thức chính qui có thể được dùng để chặt một
xâu thành các trường. Toán tử split() thực hiện điều này
còn toán tử join() lại có thể dính các mẩu lại với nhau.
Toán tử split()
Toán tử split() nhận một biểu thức chính qui và một
xâu rồi tìm tất cả mọi sự xuất hiện của biểu thức chính
qui bên trong xâu này (dường như bạn đã thực hiện toán
tử s///g). Các bộ phận của xâu không sánh với biểu thức
chính qui sẽ được cho lại lần lượt như một danh sách các
giá trị. Chẳng hạn, sau đây là một cách phân tích các
thành tố /etc/passwd:
$line =
145
“merlyn::118:10:Randal:/home/merlyn:/usr/bin/perl”; @fields = split(/:/,$line); # chặt $line ra, dùng : làm dấu
định biên # bây giờ @field là (“merlyn”, “”, “118”, “10”, “Randal”, #
“/home/merlyn”,”/usr/bin/perl”)
Lưu ý rằng trường thứ hai rỗng trở thành một xâu
rỗng. Nếu bạn không muốn điều này, đối sánh tất cả các
hai chấm trong một lần phân tách:
@fields = split(/:+/, $line);
Điều này sẽ sánh cả hai dấu hai chấm đi kèm, cho
nên sẽ không có trường thứ hai rỗng nữa.
Một xâu thông dụng để chặt biến $_, và biến thành
mặc định là:
$_ = “xâu nào đó”; @words = split(/ /); # hệt như @words = split(/ /, $_);
Lưu ý rằng đối với việc chặt này, các khoảng cách
liên tiếp trong xâu cần chặt sẽ gây ra các trường không
(xâu rỗng) trong kết quả. Một khuôn mẫu tốt hơn sẽ là /
+/, hay một cách lí tưởng /\s+/, mà sẽ đối sánh một hay
nhiều kí tự khoảng trắng. Thực ra, khuôn mẫu này là
khuôn mẫu mặc định, cho nên nếu bạn định chặt biến $_
theo các khoảng trắng, thì bạn có thể dùng tất cả các mặc
định và đơn thuần nói:
@words = split; # hệt như @words = split(/\s+/, $_);
Các trường theo sau rỗng không trở thành một phần
của danh sách. Điều này nói chung không cần quan tâm -
một giải pháp giống thế này:
$line = “merlyn::118:10:Randal:/home/merlyn:/usr/bin/perl”;
146
($name, $password, $uid,$gid,$gcos,$home,$shell) = split(/:/, $line); # chặt $line ra bằng cách dùng : làm dấu
định biên
sẽ đơn thuần cho $shell một giá trị không (undef) nếu
dòng này không đủ dài, hay nếu nó chứa các giá trị rỗng
trong trường cuối. (Các trường phụ thì im lặng bị bỏ qua,
vì việc gán danh sách làm việc theo cách đó.)
Toán tử join()
Toán tử join() nhận một danh sách các giá trị và gắn
chúng lại với nhau dùng xâu gắn giữa từng phần tử danh
sách. Nó trông tựa như thế này:
$bigstring = join($glue, @list);
Chẳng hạn, để xây dựng lại dòng mật hiệu, thử một
cách kiểu như:
$outline = join(“:”, @fields);
Lưu ý rằng xâu gắn không phải là biểu thức chính
qui - chỉ là một xâu bình thường gồm không hay nhiều kí
tự.
Bài tập
Xem Phụ lục A về lời giải.
1. Xây dựng một biểu thức chính qui sánh cho:
(a) ít nhất một a theo sau bởi một số bất kì b
(b) một số bất kì dấu sổ chéo ngược theo sau bởi một
số bất kì dấu sao
147
(c) ba bản sao liên tiếp của bất kì cái gì có chứa
trong $whatever
(d) bất kì năm kí tự nào, kể cả dấu dòng mới
(e) cùng một từ được viết hai hay nhiều lần trong
một hàng, với ‘từ” được xác định như dãy các kí
tự khác khoảng trắng không rỗng.
2. (a) Viết một chương trình nhận một danh sách các từ
trên STDIN và tìm một dòng có chứa tất cả năm
nguyên âm (a, e, i, o , u). Chạy chương trình này trên
/usr/dict/words và xem nó cho ra cái gì. Nói cách
khác, đưa vào:
$ myprogram < /usr/dict/words
(Điều này giả sử bạn đặt tên chương trình của mình
là myprogram)
(b) Sửa đổi chương trình này để cho năm nguyên âm
này được sắp thứ tự
3. Viết một chương trình tìm trong /etc/passwd (trên
STDIN), in ra tên đăng nhập và tên thật của từng
người dùng. (Hướng dẫn: dùng split để chặt dòng
này thành các trường, rồi s/// để bỏ các phần trường
comment đi sau dấu phẩy thứ nhất.)
4. Viết một chương trình tìm trong /etc/passwd (trên
STDIN) hai người dùng có cùng họ, rồi in ra tên của
họ. (Hướng dẫn: sau khi trích ra tên, tạo ra một mảng
kết hợp với tên đó làm khoá và số lần gặp nó là giá
trị. Khi dòng cuối của STDIN đã được đọc vào thì
duyệt qua mảng kết hợp để đếm các số lớn hơn 1.)
5. Lặp lại bài tập trước, nhưng báo cáo về tên đăng
148
nhập của tất cả người dùng với cùng tên của họ.
(Hướng dẫn: thay vì cất giữ một số đếm, cất giữ một
danh sách các tên đăng nhập cách nhau bằng dấu
cách. Khi kết thúc, duyệt qua các giá trị có chứa một
dấu cách.)
149
8
Hàm
Hàm hệ thống và hàm người dùng
Chúng ta đã đã thấy và đã dùng các hàm hệ thống,
như chop, print vân vân. Bây giờ nhìn vào các hàm bạn
định nghĩa ra, tạo nên các lệnh chương trình Perl.
Xác định một hàm người dùng
Một hàm người dùng, thường hay được gọi là
chương trình con hay trình con, được xác định trong
chương trình Perl của bạn bằng việc dùng một kết cấu
như:
sub subname { câu lệnh 1; câu lệnh 2; câu lệnh 3;
Trong chương này:
Các hàm hệ thống và người dùng
Định nghĩa một hàm người dùng
Cho lại giá trị
Đối
Biến cục bộ trong hàm
150
}
subname là tên của chương trình con, là bất kì tên
nào giống như tên đã đặt cho biến vô hướng, mảng và
mảng kết hợp. Một lần nữa, những tên này lại đến từ một
không gian tên khác, cho nên bạn có thể có một biến vô
hướng $fred, một mảng @fred, một mảng kết hợp %fred,
và bây giờ một trình con fred.
Khối các câu lệnh đi sau tên trình con trở thành định
nghĩa của trình con. Khi trình con được gọi tới (được mô
tả ngắn gọn), thì khối các câu lệnh tạo nên trình con này
sẽ được thực hiện, và bất kì giá trị cho lại nào (được mô
tả sau đây) đã được trả về cho nơi gọi.
Chẳng hạn sau đây là một trình con cho hiển thị câu
nói nổi tiếng:
sub say_hello {
print “Xin chào, mọi người!\n”;
}
Định nghĩa trình con có thể ở bất kì đâu trong văn
bản chương trình của bạn (chúng bị bỏ qua khi thực
hiện), nhưng tôi thì thích đặt tất cả các trình con của tôi
vào cuối tệp, để cho phần còn lại của chương trình có vẻ
như là ở đầu tệp. (Nếu bạn thích nghĩ theo kiểu Pascal
thì bạn có thể đặt các trình con của mình vào đầu và các
câu lệnh thực hiện vào cuối. Điều đấy thì tuỳ bạn.)
Các định nghĩa trình con là toàn cục; không có trình
con cục bộ. Nếu bạn có hai định nghĩa trình con với cùng
tên thì trình sau sẽ đè lấp trình trước mà không có cảnh
báo gì cả.
Bên trong thân trình con, bạn có thể truy nhập hay
151
đặt các giá trị cho các biến được dùng chung với phần
còn lại của chương trình (biến toàn cục). Thực ra, theo
mặc định, mọi tham chiếu biến bên trong thân trình con
đã tham chiếu tới biến toàn cục. Tôi mách bạn về các
ngoại lệ trong mục “Biến cục bộ trong hàm” ở dưới đây.
Trong thí dụ sau:
sub say_what {
print “Xin chào, $what\n”;
}
$what tham chiếu tới giá trị toàn cục cho $what mà
được dùng chung với phần còn lại của chương trình.
Gọi một hàm người dùng
Bạn gọi một trình con từ bên trong bất kì biểu thức
nào bằng việc đặt trước tên trình con này một dấu và &,
như trong:
&say_hello; # một biểu thức đơn giản $a = 3 + &say_hello; # phần của biểu thức lớn hơn for ($x = &start_value; $x < &end_value; $x +=
&increment) { ... } # gọi ba trình con để xác định các giá trị
Một trình con có thể gọi một trình con khác, và trình
con khác này đến lượt nó lại có thể gọi trình con khác
nữa, và cứ như thế, cho tới khi tất cả bộ nhớ có sẵn đã bị
chất đầy bằng địa chỉ quay về và các biểu thức được tính
toán bộ phận. (Không có tám hay 32 mức nào có thể thoả
mãn được cho người mê Perl.)
152
Giá trị cho lại
Giống như trong C, một trình con bao giờ cũng là
một phần của một biểu thức nào đó (không có cái tương
đương trong lời gọi thủ tục tựa Pascal). Giá trị của việc
gọi trình con được gọi là giá trị cho lại. Giá trị cho lại
của một trình con là giá trị của biểu thức cuối cùng được
tính bên trong thân của trình con cho mỗi lần gọi.
Chẳng hạn, định nghĩa trình con này:
sub sum_of_a_and_b { $a + $b; }
Biểu thức cuối cùng được tính trong thân của trình
con này (thực ra, đó là biểu thức duy nhất được tính) là
tổng của $a và $b, cho nên tổng của $a và $b sẽ là giá trị
cho lại. Sau đây là điều đó trong hành động:
$a = 3; $b = 4; $c = &sum_of_a_and_b; #c nhận 7 $d = 3*sum_of_a_and_b; # $d nhận 21
Một trình con cũng có thể cho lại một danh sách các
giá trị khi được tính trong hoàn cảnh mảng. Xét trình con
này và lời gọi:
sub list_of_a_and_b { ($a, $b); } $a = 5; $b = 6; $c = &list_of_a_and_b; # @c nhận (5, 6)
Biểu thức cuối được tính thực sự nghĩa là biểu thức
cuối cùng được tính, thay vì là biểu thức cuối cùng được
xác định trong thân của trình con. Chẳng hạn, trình con
này cho lại $a nếu $a > 0, ngoài ra nó cho $b:
153
sub gime_a_or_b { if ($a > 0) { print “chọn a ($a)\n”; $a; } else { print “chọn b ($b)\n”; $b; } }
Lưu ý rằng trình con này cũng cho hiển thị một
thông báo. Biểu thức cuối cùng được tính là $a hay $b,
mà trở thành giá trị cho lại. Nếu bạn đảo ngược các dòng
có chứa $a và print ngay trước nó, thì bạn sẽ nhận được
một giá trị cho lại là 1 (giá trị được cho lại bởi hàm print
thành công) thay vì giá trị của $a.
Tất cả chúng đã là các thí dụ khá tầm thường. Tốt
hơn cả là nên truyền các giá trị khác nhau cho mỗi lần
gọi tới một trình con thay vì phải dựa vào các biến toàn
cục. Thực ra, điều đó cũng đúng đến chỗ cần nói.
Đối
Mặc dầu các trình con có chức năng đặc biệt là có
ích, toàn bộ mức độ có ích mới trở thành sẵn có cho bạn
khi bạn có thể truyền các đối cho trình con. Trong Perl
nếu lời gọi trình con (với dấu và @ cùng tên trình con)
có theo sau nó một danh sách nằm trong ngoặc tròn, thì
danh sách này sẽ được tự động gán cho một biến đặc biệt
có tên @_ trong suốt thời gian hoạt động của trình con.
Trình con có thể truy nhập vào biến này để xác định số
các đối và giá trị của các đối đó. Chẳng hạn:
sub say_hello_to {
154
print “Hello, $_[0]!\n”; # tham biến đầu là mục tiêu
}
Tại đây thấy một tham chiếu tới $_[0], chính là phần
tử đầu tiên của mảng @_. Lưu ý đặc biệt: tương tự như
dáng vẻ của chúng, giá trị $_[0] (phần tử đầu tiên của
mảng @_) chẳng có bất kì liên quan gì với biến $_ (một
biến vô hướng của riêng nó). Bạn đừng lầm lẫn chúng!
Từ chương trình này, rõ ràng nó nói hello với bất kì cái
gì chúng truyền cho nó như tham biến đầu tiên. Điều đó
có nghĩa là chúng có thể gọi nó giống thế này:
&say_hello_to (“world”); # sẽcho hello, world! $x = “somebody”; &say_hello_to ($x); # cho hello, somebody! &say_hello_to (“me”) + &say_hello_to (“you”) # và me và
you
Lưu ý rằng trong dòng cuối, giá trị cho lại không
thực sự được dùng. Nhưng trong khi tính tổng Perl phải
tính tất cả các bộ phận của nó, cho nên trình con này
được gọi hai lần.
Sau đây là một thí dụ về việc dùng nhiều hơn một
tham biến:
sub say { print “$_[0], $_[1]!\n”; }
&say (“hello”, “world”); # hello world, lần nữa &say (“goodbye”, “cruel world”) # im lặng
Các tham biến vượt quá đã bị bỏ qua - nếu bạn chưa
bao giờ nhòm ngó tới $_[3], thì Perl cũng chẳng bận
tâm. Các tham số không đủ cũng bị bỏ qua - bạn đơn
thuần nhận được undef nếu bạn nhìn vượt ra cuối của
155
mảng @_, như với mọi mảng khác.
Biến @_ là cục bộ cho trình con này; nếu có một
biến toàn cục cho @_, nó sẽ được cất giữ trước khi trình
con được gọi và được khôi phục lại giá trị trước của nó
khi trở về từ chương trình con. Điều này cũng có nghĩa
là một trình con có thể truyền các đối cho một trình con
khác mà không sợ mất biến @_ riêng của nó - việc gọi
trình con lồng nhau đã nhận được @_ riêng của nó theo
cùng cách.
Xem xét lại trình “cộng a và b” của mục trước. Tại
đây một trình con thực hiện việc cộng hai giá trị bất kì,
đặc biệt, hai giá trị được truyền cho trình con này như
tham biến.
sub add_two { $_[0] + $_[1]; } print &add_two (3, 4) ; # in 7 $c = &add_two (5, 6) ; $c được 11
Bây giờ tổng quát hoá chương trình này. Nếu chúng
ta có ba, bốn hay hàng trăm giá trị cần phải cộng lại thì
sao? Chúng ta có thể làm việc đó bằng một chu trình, tựa
như:
sub add { $sum = 0 ; # khởi đầu giá trị cho sum foreach $_ (@_ ) { $sum += $_ ; # cộng từng phần tử } $sum ; # biểu thức cuối được tính: tổng của tất cả
các phần tử } $a = &add(4,5,6) ; # cộng 4+5+6 = 15, và gán cho $a print &add(1,2,3,4,5) ; # ina ra 15
156
print &add(1..5); # cũng in ra 15, vì 1..5 được mở rộng
Điều gì xảy ra nếu có một biến mang tên $sum khi
gọi &add_list? Chúng ta đã đánh trúng vào nó. Trong mục
tiếp chúng ta sẽ xem cách thức tránh điều này.
Biến cục bộ trong hàm
Chúng ta đã nói tới biến @_ và cách thức việc sao
chép cục bộ được tạo ra cho từng trình con có gọi tới
tham biến. Bạn có thể tạo ra các biến vô hướng, mảng
hay mảng kết hợp của riêng mình làm việc theo cùng
cách. Bạn làm điều này với toán tử local(), nhận một danh
sách các tên biến và tạo ra các bản cục bộ của chúng
(hay các thể nghiệm, nếu bạn thích từ đao to búa lớn).
Sau đây lại là hàm cộng đó, lần này dùng local():
sub add { local ($sum) ; # làm cho $sum thành biến cục bộ $sum = 0 ; # khởi đầu giá trị cho sum foreach $_ (@_ ) { $sum += $_ ; # cộng từng phần tử } $sum ; # biểu thức cuối được tính: tổng của tất cả
các phần tử }
Khi câu lệnh thân đầu tiên được thực hiện, thì bất kì
giá trị hiện tại nào của biến toàn cục $sum cũng đã được
cất giữ và một biến mới mang tên $sum sẽ được tạo ra
(với giá trị undef). Khi trình con này đi ra, thì Perl bỏ qua
biến cục bộ và khôi phục giá trị trước (toàn cục). Điều
này vận hành cả khi biến $sum hiện là biến cục bộ của
một trình con khác (một trình con mà gọi tới trình con
này, hay một trình con gọi tới một trình mà gọi tới trình
157
con này, vân vân). Các biến có thể có nhiều bản cục bộ
lồng nhau, mặc dầu bạn có thể truy nhập mỗi lúc chỉ vào
một biến.
Sau đây là cách để tạo ra một danh sách tất cả các
phần tử hơn 100 của một mảng lớn:
sub bigger_than_100 { local ($result) ; # tạm thời giữ giá trị trở về foreach $_ (@_ ) { # đi qua danh sách đối if ($_ > 100) { # có đủ tư cách không? push (@result, $_) ; # cộng nó } $result ; # cho lại danh sách cuối }
Điều gì xảy ra nếu chúng ta muốn tất cả các phần tử
này lớn hơn 50 thay vì 100? Chúng ta phải sửa chương
trình này, đổi tất cả các số 100 thành 50. Nhưng điều gì
xảy ra nếu chúng ta lại cần cả hai? Được, chúng ta có thể
thay thế 50 hay 100 bằng một biến tham chiếu. Điều này
làm chương trình trông giống thế này:
sub bigger_than { local ($n, @values) ; # tạo ra các biến cục bộ nào đó ($n, @values) = @_ ; # chặt arg thành giới hạn và giá
trị local (@result) ; # tạm thời giữ giá trị trả lại foreach $_ (@values) { # đi qua danh sách đối arg if ($_ > $n) { # có đủ tư cách không? push (@result, $_) ; # cộng nó } $result ; # cho lại danh sách cuối } # một số lời gọi @new = &bigger_than (100, @list); # @new nhận tất cả
@list > 100 @this = &bigger_than(5,1,5,15,30); # @this nhận
158
(15,30)
Lưư ý rằng lần này tôi đã dùng hai biến cục bộ phụ
để đặt tên cho các đối. Điều này khá thông dụng trong
thực hành - dễ nói về $n và @values hơn nói về $_[0] và
@_[1..$#_] nhiều lắm.
Kết quả của local() là một danh sách gán được, nghĩa
là nó có thể được dùng ở vế bên trái của toán tử gán
mảng. Danh sách này có thể được đặt giá trị khởi đầu
cho từng biến mới được tạo ra. (Nếu bạn không đặt giá
trị cho danh sách này, thì các biến mới bắt đầu với một
giá trị của undef, giống như bất kì biến mới nào khác.)
Điều này có nghĩa là chúng ta có thể tổ hợp hai câu lệnh
đầu của trình con này, bằng cách thay thế:
local ($n, @values) ; ($n, @values) = @_ ; # chặt arg thành giới hạn và giá
trị
bằng local ($n, @values) = @_ ;
Thực ra, đây là một thứ rất đặc thù thông dụng Perl
cũng hệt như khai báo vậy, local() thực sự là một toán tử
thực hiện được. Nếu bạn đặt nó vào bên trong chu trình,
thì bạn sẽ thu được một biến mới cho mỗi lần lặp chu
trình, mà gần như là vô dụng trừ phi bạn thích lãng phí
bộ nhớ và quên mất mình đã tính gì trong lần lặp trước.
Chiến lược lập trình Perl tốt gợi ý rằng bạn nên nhóm tất
cả các toán tử local() của mình vào phần đầu định nghĩa
trình con, trước khi bạn chui vào phần thịt của trình này.
159
Bài tập
1. Viết một trình con nhận một giá trị số từ 1 tới 9 và
cho lại tên tiếng Anh (như một, hai, hay chín). Nếu
giá trị đưa vào ở ngoài phạm vi này, thì cho lại số
ban đầu thay vì cho tên. Thử nó với một số dữ liệu
vào - có lẽ bạn sẽ phải viết ra một loại khiển trình
nào đó.
2. Lấy chương trình trong bài tập trước, viết một
chương trình nhận hai số và cộng chúng lại, hiển thị
kết quả kiểu “hai cộng hai là bốn.” (Chớ quên viết
hoa từ đầu!)
3. Mở rộng trình con này để cho lại âm chín qua âm
một và không. Thử chương trình này.
160
161
9
Các cấu trúc
điều khiển khác
Toán tử last
Trong một số bài tập trước đây bạn có thể đã nghĩ,
“Nếu tôi có được một câu lệnh break của C ở đây, thì đã
xong rồi.” Cho dù bạn không nghĩ như thế, thì hãy cứ để
tôi nói cho bạn về sự tương đương của Perl để thoát sớm
khỏi chu trình : toán tử last.
Toán tử last ngắt khối chu trình bao quanh ở bên
trong nhất, gây ra việc thực hiện tiếp tục với câu lệnh đi
ngay sau khối đó. Chẳng hạn:
while (cái gì đó) { cái gì đó ; cái gì đó ; cái gì đó ;
Trong chương này:
Toán tử last
Toán tử next
Toán tử redo
Khối có nhãn
Bộ thay đổi biểu thức
&&, || và ?: xem như các cấu trúc
điều khiển
162
if (điều kiện nào đó) { cái gì đó khác ; cái gì đó khác ; last ; # nhảy ra khỏi chu trình while } thêm nữa ; thêm nữa ;
} # last nhảy tới đây
Nếu điều kiện nào đó là đúng, thì cái gì đó khác sẽ
được thực hiện, và thế rồi toán tử last buộc chu trình
while phải kết thúc.
Toán tử last chỉ tính tới khối chu trình, không tính
khối cần để tạo nên kết cấu cú pháp nào đó. Điều này có
nghĩa là khối tạo nên nhánh then của câu lệnh if không
được tính tới - chỉ khối tạo nên for, foreach, while và các
khối ‘trần” mới được tính. (Khối trần là khối không
thuộc phần khác của một kết cấu lớn hơn, như một chu
trình, hay một trình con, hay một câu lệnh if/then/else).
Giả sử tôi muốn xem liệu thông báo thư đã được cất
giữ trong một tệp có là từ tôi hay không. Một thông báo
như vậy có thể giống như là:
From: [email protected] (Randal L. Schwartz) To: [email protected] Date: 01-SEP-93 08:16:24 PM PDT - 0700 Subject: A sample mail message Here’s the body of the mail message. And here is some
more.
Tôi phải duyệt qua thông báo này từ dòng bắt đầu
với From: và rồi để ý liệu dòng này có chứa tên đăng
nhập của tôi hay không, merlyn.
163
Tôi có thể làm điều đó như thế này:
while (<STDIN>) { # đọc dòng vào if (/^From:/) { #có bắt đầu với From: không? Nếu có... if (/merlyn/) { # nó là từ tôi! print “Email from Randal! It’s about
time!\n”; } last ; # không cần tìm From: nữa, cho nên ra } # kết thúc “if from:” if (/^$/) { # dòng trống ? last ; # nếu đúng, đừng kiểm tra thêm nữa } } # kết thúc while
Lưu ý rằng một khi dòng có chứa From: được tìm
thấy thì chúng ta đi ra khỏi chu trình chính bởi vì tôi
muốn xem chỉ dòng From: đầu tiên. Cũng lưu ý rằng một
đầu đề thư kết thúc tại dòng trống đầu tiên, cho nên
chúng có thể ra khỏi chu trình chính nữa.
Toán tử next
Giống như last, next cũng làm thay đổi luồng thực
hiện theo trình tự thông thường. Tuy nhiên, toán tử next
làm cho việc thực hiện bỏ qua phần còn lại của khối chu
trình được bao bên trong nhất mà không kết thúc khối
này* . Nó được dùng như thế này:
while (cái gì đó) { phần thứ nhất ; phần thứ nhất ;
* Nếu có một khối continue cho chu trình này, mà chúng ta thì chưa
thảo luận tới, thì toán tử next đi tới chỗ bắt đầu của khối continue
thay vì tới cuối khối này. Khá gần.
164
phần thứ nhất ; if (điều kiện nào đó) { phần nào đó ; phần nào đó ; next ; # nhảy ra khỏi chu trình while } phần khác ; phần khác ; # next tới đây
}
Nếu điều kiện nào đó là đúng, thì phần nào đó được
thực hiện, và phần khác bị bỏ qua.
Lần nữa, khối của một câu lệnh if không được tính
tới như khối chu trình.
Toán tử redo
Cách thứ ba mà bạn có thể nhảy qua trong một khối
chu trình là bằng redo. Toán tử này nhảy tới chỗ bắt đầu
của khối hiện tại (không tính lại biểu thức điều kiện),
kiểu như:
while (cái gì đó) { # redo tới đây
cái gì đó ; cái gì đó ; cái gì đó ;
if (điều kiện nào đó) { phần nào đó ; phần nào đó ; redo ; } phần khác ; phần khác ; phần khác ;
165
}
Một lần nữa, khối if không được tính tới. Chỉ tính
các khối chu trình.
Lưư ý rằng với redo và last và khối trần, bạn có thể
tạo nên chu trình vô hạn mà đi ra từ giữa, kiểu như:
{ phần bắt đầu ; phần bắt đầu ; phần bắt đầu ;
if (điều kiện nào đó) { last ; } phần sau ; phần sau ; phần sau ; redo ;
}
Điều này sẽ phù hợp cho một chu trình kiểu while mà
cần tới việc có một phần nào đó của chu trình này được
thực hiện như việc khởi đầu trước phép kiểm thử thứ
nhất. (Trong mục “Bộ thay đổi biểu thức”, dưới đây, tôi
sẽ chỉ ra cho bạn cách viết câu lệnh if với ít kí tự ngắt
hơn.)
Khối có nhãn
Điều gì xảy ra nếu bạn muốn nhảy ra khỏi một khối
có chứa khối bên trong nhất, hay nói theo cách khác, ra
khỏi hai khối lồng nhau ngay một lúc? Trong C, bạn phải
viện tới toán tử goto để đi ra. Không cần phải làm như
vậy trong Perl - bạn có thể dùng last, next và redo tại bất
kì khối kết nào bằng việc cho khối một cái tên có nhãn.
166
Nhãn là một kiểu tên khác từ một không gian tên
khác mà tuân theo cùng qui tắc như vô hướng, mảng,
mảng kết hợp và trình con. Tuy nhiên, như chúng ta
thấy, một nhãn không có kí tự ngắt đi đầu đặc biệt (như
$ cho vô hướng, & cho trình con, vân vân), cho nên một
nhãn có tên print sẽ xung đột với từ dành riêng print và sẽ
không được phép. Bởi lí do này, Larry gợi ý chọn các
nhãn bao gồm toàn chữ hoa và số, mà anh ấy đảm bảo sẽ
không bao giờ bị chọn nhầm thành một từ dành riêng
trong tương lai. Bên cạnh đó, tất cả các chữ hoa cho
phép dễ nhìn thấy hơn trong một văn bản chương trình
mà phần lớn là chữ thường.
Một khi bạn đã chọn cẩn thận nhãn, nó sẽ đứng ngay
trước câu lệnh có chứa khối, theo sau dấu hai chấm, kiểu
như thế này:
SOMELABEL: while (điều kiện) { câu lệnh ; câu lệnh ; câu lệnh ; if (điều kiện khác) { last SOMELABEL ; } }
Lưư ý rằng tôi đã thêm SOMELABEL, như một
tham biến vào câu lệnh last. Tham biến này bảo cho Perl
ra khỏi khối có tên SOMELABEL, thay vì ra khỏi khối
bên trong nhất. Trong trường hợp này, chúng ta không
có cái gì khác ngoài khối bên trong nhất. Nhưng giả sử
tôi có các chu trình lồng nhau:
OUTER: for ($i = 1; $i <= 10 ; $i++) { INNER: for ($j = 1 ; $j >= 10 ; $j++) { if ($i + $j == 63) {
167
print “$i lần $j là 63!\n” ; last OUTER; } if ($j >= $i) { next OUTER ; } } }
Tập hợp các câu lệnh này thử tất cả các giá trị kế
tiếp của hai số nhỏ nhất được nhân với nhau cho tới khi
nó tìm ra một cặp có tích là 63 (7 và 9). Lưu ý rằng một
khi đã tìm được một cặp thì không cần phải kiểm tra các
số khác nữa, cho nên câu lệnh if thứ nhất ra khỏi cả hai
chu trình for bằng việc dùng last với nhãn. Câu lệnh if thứ
hai cố gắng đảm bảo rằng số lớn hơn trong hai số bao
giờ cũng là số thứ nhất bằng việc bỏ qua việc lặp tiếp
của chu trình bên ngoài ngay khi điều kiện này không
còn xảy ra nữa. Điều này có nghĩa là các số sẽ được
kiểm thử với ($i, $j) là (1,1), (2,1), (2,2), (3,1), (3,2),
(3,3), (4,1) vân vân.
Cho dù khối bên trong nhất được gắn nhãn, thì các
toán tử last, next, và redo không có tham biến tuỳ chọn
(nhãn) vẫn vận hành tôn trọng khối bên trong nhất. Cũng
vậy, bạn không thể dùng nhãn để nhảy vào trong một
khối - chỉ để nhảy ra khối. Các toán tử last, next hay redo
phải ở bên trong khối.
Bộ thay đổi biểu thức
Xem như một cách khác để chỉ ra “nếu thế này, thì
thế kia,” Perl cho phép bạn gắn nhãn cho một bộ sửa đổi
if lên một biểu thức vốn là một biểu thức đứng riêng.
168
Kiểu như:
biểu thức nào đó if biểu thức điều khiển ;
Trong trường hợp này, biểu thức điều khiển được tính
trước để xét giá trị chân lí của nó (bằng việc dùng cùng
qui tắc như thường lệ), và nếu đúng, thì biểu thức nào đó
sẽ được tính tiếp. Điều này đại thể tương đương với:
if (biểu thức điều khiển nào đó) { biểu thức nào đó ; }
ngoại trừ rằng bạn không cần thêm dấu ngắt phụ,
câu lệnh này đọc ngược lại, và biểu thức phải là một biểu
thức đơn (không phải là một khối câu lệnh). Tuy nhiên,
nhiều lần cách mô tả ngược này biến thành cách tự nhiên
nhất để phát biểu vấn đề, trong khi cũng tiết kiệm được
vài lần gõ. Chẳng hạn, sau đây là cách bạn có thể ra khỏi
chu trình khi một điều kiện nào đó nảy sinh:
LINE: while (<STDIN>) { last LINE if /^From: / ; }
Bạn xem dễ viết làm sao. Và bạn thậm chí còn có
thể đọc nó theo kiểu tiếng Anh: “dòng cuối nếu nó bắt
đầu với From.”
Các dạng song song khác bao gồm những dạng sau:
exp2 unless exp1; # giống: unless (exp1) { exp2 ; } exp2 while exp1; # giống: while (exp1) { exp2 ; } exp2 until exp1; # giống: util (exp1) { exp2 ; }
Lưư ý rằng tất cả các dạng này đã tính exp1 trước rồi
dựa trên đó, tính hay không tính cái gì đó với exp2.
Chẳng hạn, sau đây là cách tìm ra luỹ thừa đầu tiên
169
của hai số lớn hơn một số đã cho:
chop ($n = <STDIN>) ; $i = 1; # khởi đầu $i *= 2 until $i > $n ; # lặp cho tới khi tìm ra nó.
Các dạng này không lồng nhau - bạn không thể nói
được exp3 while exp2 if exp1. Điều này là vì dạng exp2
if exp1 không còn là một biểu thức, mà là một câu lệnh
hoàn toàn, và bạn không thể gắn thêm một trong các bộ
sửa đổi này vào sau câu lệnh.
&&, || và ?: xem như các cấu trúc điều khiển
Những cấu trúc này trông tựa như các kí tự ngắt, hay
một phần của biểu thức. Liệu chúng có thể thực sự được
coi là các cấu trúc điều khiển không? Thế này, theo cách
nghĩ Perl, gần như bất kì cái gì cũng đều có thể cả, cho
nên xem điều tôi nói ở đây.
Thông thường, bạn bắt gặp “nếu cái này, thì cái nọ.”
Trước đây chúng ta đã thấy hai dạng này:
if (cái này) { cái nọ ; } # một cách cái nọ if cái này ; # cách khác
Đây là cách thứ ba (và tin tôi đi, vẫn còn nữa đấy):
cái này && cái nọ ;
Tại sao nó lại làm việc? Nó chẳng phải là toán tử
logic và sao? kiểm tra xem cái gì xảy ra khi cái này lấy
giá trị đúng hay sai:
Nếu cái này là đúng, thế thì giá trị của toàn bộ biểu
thức vẫn còn chưa được biết tới, vì nó phụ thuộc vào
giá trị của cái nọ. Cho nên cái nọ phải được tính.
170
Nếu cái này là sai, thế thì chẳng cần gì mà nhìn vào
cái nọ nữa, bởi vì giá trị của toàn bộ biểu thức phải là
sai rồi. Vì chẳng cần gì phải tính cái nọ nên chúng ta
có thể bỏ qua.
Và thực ra, đây là điều mà Perl làm. Perl tính cái nọ
chỉ khi cái này là đúng, làm cho nó thành tương đương
với hai dạng trước.
Giống thế, toán tử logic hoặc giống như câu lệnh
unless (hay bộ sửa đổi unless). Cho nên bạn có thể thay
thế:
unless (cái này) { cái nọ ; } bằng cái này || cái nọ ;
Nếu bạn quen thuộc với việc dùng các toán tử này
trong lớp vỏ để kiểm soát các chỉ lệnh thực hiện điều
kiện, thì bạn sẽ thấy rằng chúng vận hành tương tự trong
Perl.
Cuối cùng toán tử ba ngôi kiểu C:
exp1 ? exp2 : exp3 ;
tính exp2 nếu exp1 đúng, ngược lại tính exp3. Cũng
dường như là chúng nói:
if (exp1) { exp2 ; } else { exp3 ; }
nhưng một lần nữa không có tất cả các dấu ngắt đó.
Chẳng hạn, bạn có thể viết:
($a < 10) ? $b = $a : $a = $b ;
nên dùng cái nào đây? Tuỳ vào tâm trạng bạn thôi,
đôi khi, hay tuỳ theo từng phần biểu thức lớn đến đâu,
hay liệu cần thêm đóng mở ngoặc nào bởi vì xung đột
171
thứ tự ưu tiên. Nhìn vào chương trình của người khác và
xem chúng làm gì. Có lẽ bạn sẽ thấy đôi điều ở đó. Larry
gợi ý rằng nên đặt phần quan trọng nhất của biểu thức
lên trước, để cho nó nổi bật ra.
Bài tập
1. Mở rộng bài toán của chương trước để lặp lại phép
toán đó cho tới khi từ end được đưa vào cho một
trong các giá trị. (Hướng dẫn: dùng một chu trình vô
hạn, và rồi thực hiện last nếu giá trị đưa vào là end.)
172
173
10
Tước hiệu tệp
và kiểm thử tệp
Tước hiệu tệp là gì?
Tước hiệu tệp là tên trong một chương trình Perl
dành cho việc nối giữa tiến trình Perl của bạn và thế giới
bên ngoài. Chúng ta đã thấy và dùng tước hiệu tệp một
cách không tường minh: STDIN là một tước hiệu tệp, đặt
tên cho việc nối giữa tiến trình Perl và lối vào chuẩn của
UNIX. Giống như vậy, Perl cung cấp STDOUT (cho lối
ra chuẩn) và STDERR (cho lối ra chuẩn cho lỗi). Những
tên này là trùng với các tên được dùng trong bộ trình thư
viện “vào/ra chuẩn” của UNIX, Perl dùng chúng cho hầu
Trong chương này:
Tước hiệu tệp là gì?
Mở và đóng tước hiệu tệp
die()
Dùng tước hiệu
Kiểm thử tệp -x
Các toán tử stat() và lstat()
Dùng tước hiệu tệp
174
hết việc vào/ra.
Tên tước hiệu tệp cũng giống như tên dành cho các
khối có nhãn, nhưng chúng đến từ một không gian tên
khác (cho nên bạn có thể có một vô hướng $fred, một
mảng @fred, một mảng kết hợp %fred, một chương trình
con &fred, một nhãn fred, và bây giờ một tước hiệu tệp
fred). Giống như nhãn khối, tước hiệu tệp được dùng
không cần một kí tự đặc biệt đứng trước, và do vậy có
thể bị lẫn lộn với các từ dành riêng hiện có hay trong
tương lai. Một lần nữa, khuyến cáo của Larry là dùng
TẤT CẢ CÁC CHỮ HOA trong tước hiệu tệp của mình
- không chỉ nó biểu thị tốt hơn, mà nó cũng sẽ đảm bảo
rằng chương trình của bạn sẽ không hỏng khi các từ
dành riêng tương lai được đưa vào.
Mở và đóng một tước hiệu tệp
Perl cung cấp ba tước hiệu tệp, STDIN, STDOUT,
STDERR, tự động mở cho các tệp hay thiết bị do tiến
trình cha mẹ của chương trình này đã thiết lập (có thể là
lớp vỏ). Bạn dùng toán tử open() để mở các tước hiệu tệp
phụ, hệt như bạn làm trong chương trình được viết trong
C. Cú pháp của nó giống thế này:
open (FILEHANDLE, “tên nào đó”);
với FILEHANDLE là tước hiệu tệp mới, còn tên
nào đó là tên tệp UNIX ngoài (như một tệp hay thiết bị)
mà sẽ được liên kết với tước hiệu tệp mới. Việc gọi này
mở tước hiệu tệp để đọc. Việc mở một tệp để ghi cũng
dùng cùng toán tử open, nhưng phần tiền tố của tên tệp
có một dấu lớn hơn (như trong vỏ):
175
open (OUT, “>outfile”);
Chúng ta sẽ thấy trong mục “Dùng tước hiệu tệp,”
dưới đây cách sử dụng tước hiệu tệp này. Cũng vậy, như
trong vỏ, bạn có thể mở một tệp để thêm vào sau bằng
việc dùng hai dấu lớn hơn làm tiền tố, như trong:
open (LOGFILE, “>>mylogfile”);
Tất cả ba dạng này của open đã cho lại đúng nếu
việc mở thành công và sai nếu thất bại. (Việc mở một tệp
đưa vào sẽ sai, chẳng hạn, nếu tệp đó không có hay
không thể truy nhập tới được bởi không được phép; việc
mở tệp đưa ra sẽ sai nếu danh mục không cho ghi hay
không cho truy nhập tới.)
Khi bạn kết thúc với một tước hiệu tệp, bạn có thể
đóng nó bằng toán tử close, tựa như:
close(LOGFILE);
Việc mở lại một tước hiệu tệp cũng làm đóng tệp
mở trước đó một cách tự động, cũng như khi ra khỏi
chương trình. Vì điều này, phần lớn các chương trình
Perl không bận tâm với close(). Nhưng nó vẫn có đó nếu
bạn muốn được chặt chẽ hay chắc chắn rằng tất cả dữ
liệu đã được đẩy ra hết đôi khi sớm hơn việc kết thúc của
chương trình.
Một chút tiêu khiển: die()
Coi đây như là một chú thích cuối trang lớn, nhưng
lại nằm ở giữa trang.
Một tước hiệu tệp mà không được mở thành công thì
có thể vẫn được dùng mà thậm chí không gây ra cảnh
176
báo gì nhiều lắm trong toàn bộ chương trình. Nếu bạn
đọc từ tước hiệu tệp, bạn sẽ nhận được ngay cuối tệp.
Nếu bạn ghi lên tước hiệu tệp, dữ liệu cứ im ỉm bị bỏ đi
(giống như lời hứa hẹn vận động bầu cử năm trước).
Thường bạn muốn kiểm tra lại kết quả của việc mở
tệp và báo cáo lại lỗi nếu kết quả không phải là điều bạn
dự kiến. Chắc chắn, bạn có thể rải rác trong chương trình
của mình với những thứ kiểu như:
unless (open(DATAPLACE, “>/tmp/dataplace”)) { print “Rất tiếc, tôi không thể tạo được
/tmp/dataplace\n”; } else { # phần còn lại chương trình của bạn }
Nhưng đấy là cả đống việc. Và điều thường xẩy ra
với Perl là đưa ra một lối tắt. Toán tử die() lấy một danh
sách bên trong dấu ngoặc tròn tuỳ chọn, phun ra danh
sách đó (giống như print) trên lối ra lỗi chuẩn, và rồi kết
thúc tiến trình Perl (tiến trình đang chạy chương trình
Perl) với một trạng thái ra khác không của UNIX (nói
chung chỉ ra một cái gì đó bất thường xẩy ra). Cho nên,
viết lại đoạn mã trên thì sẽ thấy nó giống như thế này:
unless (open(DATAPLACE, “>/tmp/dataplace”)) { die “Rất tiếc, tôi không thể tạo được
/tmp/dataplace\n”; } # phần còn lại chương trình của bạn
Nhưng chúng ta thậm chí còn có thể đi thêm một
bước nữa. Nhớ rằng có thể dùng toán tử (logic hoặc) || để
làm ngắn thêm điều này, như trong
unless (open(DATAPLACE, “>/tmp/dataplace”)) || die “Rất tiếc, tôi không thể tạo được
177
/tmp/dataplace\n”;
Vậy die sẽ được thực hiện chỉ khi kết quả của open
là sai. Cách thông dụng để đọc điều này là “mở tệp đó ra
nếu không thì chết quách đi cho rồi!” và đó là cách dễ
dàng để nhớ bất kì khi nào dùng toán tử logic và hay
logic hoặc.
Thông báo vào lúc chết (được xây dựng từ đối của
die) có tên chương trình Perl và số dòng được gắn tự
động vào, cho nên bạn có thể dễ dàng xác định được die
nào trong chương trình của bạn chịu trách nhiệm cho
việc ra không đúng lúc này. Nếu bạn không thích số
dòng hay tệp bị lộ ra, thì phải chắc chắn rằng văn bản
chết có một dấu dòng mới ở cuối. Chẳng hạn:
die “bạn nấu nước xốt - lợn sữa”;
sẽ in ra tên tệp và số dòng, trong khi
die “bạn nấu nước xốt - lợn sữa\n”;
thì không in ra tên tệp và số dòng.
Dùng tước hiệu tệp
Một khi tước hiệu tệp được mở ra để đọc, bạn có thể
đọc các dòng từ nó hệt như bạn có thể đọc từ lối vào
chuẩn với STDIN. Vậy, chẳng hạn, để đọc các dòng từ
tệp mật hiệu:
open (EP, “/etc/passwd”); while (<EP>) { chop; print “Tôi thấy $_ trong tệp mật hiệu!\n”; }
178
Lưu ý rằng tước hiệu tệp mới mở được dùng bên
trong dấu ngoặc nhọn hệt như đã dùng STDIN trước đây.
Một tước hiệu tệp mở ra để ghi hay hiệu đính đã
phải được cho như một đối của toán tử print, xuất hiện
ngay sau từ khoá print nhưng trước danh sách đối:
print LOGFILE “Khoản mục kết thúc của $max\n”; print STDOUT “Xin chào, mọi người!\n”; # giống như in
“xin chào mọi người!\n”
Trong trường hợp này, thông báo bắt đầu với Khoản
mục kết thúc sẽ ghi lên tước hiệu tệp LOGFILE, mà giả
thiết là đã mở trước đây trong chương trình. Và xin chào
mọi người sẽ đi ra lối ra chuẩn, hệt như khi bạn không
xác định tước hiệu tệp. Chúng ta nói rằng STDOUT là
tước hiệu xử lí tệp ngầm định cho câu lệnh print.
Vậy, tóm lại, sau đây là cách để sao chép tất cả văn
bản từ một tệp được xác định trong $a vào một tệp được
xác định trong $b. Nó minh hoạ gần như mọi thứ mà đã
học trong vài trang vừa qua:
open (IN,$a) || die “không thể mở được $a để đọc”; open (OUT, “>$b”) || die “không thể tạo dược $b”; while (<IN>) { # đọc một dòng từ tệp $a vào $_ print OUT $_; # in dòng đó vào tệp $b } close(IN); close(OUT);
Kiểm tra tệp -x
Bây giờ bạn đã biết cách để mở một tước hiệu tệp để
ghi ra, viết đè lên bất kì tệp hiện có nào với cùng tên.
Giả sử bạn muốn chắc chắn rằng không có một tệp nào
179
với tên đó (để giữ cho bạn khỏi ngẫu nhiên làm mất tiêu
dữ liệu bảng tính hay lịch ngày sinh quan trọng). Nếu
bạn định viết một bản ghi vỏ thì bạn nên dùng cái gì đó
tựa như -e tên tệp để kiểm tra liệu tệp đó có tồn tại hay
không. Tương tự thế, Perl dùng -e $filevar để kiểm tra sự
tồn tại của tệp mang tên bởi giá trị vô hướng $filevar. Nếu
tệp này tồn tại thì kết quả là đúng; ngược lại nó là sai.
Chẳng hạn:
$x = “/etc/passwd”; if (-e $x) { # liệu /etc/passwd có tồn tại không? # tốt } else { print “how in the world did you get logged in?\n”; }
Toán hạng của toán tử -e thực sự là bất kì biểu thức
vô hướng nào tính một xâu nào đó, kể cả một xâu hằng.
Sau đây là một thí dụ kiểm tra cho cả mật hiệu hệ thống
và tệp nhóm:
if (-e “/etc/passwd && -e “/etc/group”) { print “looks like you have a normal system\n”;
Các toán tử khác cũng được xác định rõ. Chẳng hạn,
-r $filevar cho lại giá trị đúng nếu tệp đã có tên trong
$filevar đang tồn tại và đọc được. Tương tự, -w $filevar
kiểm tra xem liệu có ghi được được không. Sau đây là
một thí dụ kiểm tra tên tệp do người dùng xác định cho
cả tính đọc được và ghi được:
print “ở đâu? “; $filename = <STDIN>; chop($filename); # quẳng cái dấu dòng mới khó chịu đi if (-r $filename && -w $filename) { # tệp đã có, và tôi có thể đọc và ghi nó ...
180
}
Nhiều việc kiểm tra tệp đã có sẵn. Xin xem Bảng
10-1 để biết danh sách đầy đủ.
Bảng 10.1: Kiểm tra tệp và ý nghĩa của chúng
Kiểm
tra tệp
Ý nghĩa
-r Tệp hay danh mục đọc được
-w Tệp hay danh mục ghi được
-x Tệp hay danh mục thực hiện được
-o Tệp hay danh mục do người dùng sở
hữu
-R Tệp hay danh mục đọc được bởi
người dùng thực, không phải người
dùng hiệu quả (khác -r với chương
trình setuid)
-W Tệp hay danh mục ghi được bởi người
dùng thực, không phải người dùng
hiệu quả (khác với -w cho chương
trình setuid)
-X Tệp hay danh mục thực hiện được bởi
người dùng thực, không phải người
dùng hiệu quả (khác với -x cho
chương trình setuid)
-O Tệp hay danh mục được sở hữu bởi
người dùng thực, không phải người
dùng hiệu quả (khác với -o cho
chương trình setuid)
-e Tệp hay danh mục đã có
-z Tệp đã có và có kích thước không
181
Kiểm
tra tệp
Ý nghĩa
(danh mục thì không bao giờ rỗng)
-s Tệp hay danh mục đã có và có kích
thước khác không (giá trị được tính
theo byte)
-f Khoản mục là tệp rõ
-d Khoản mục là danh mục
-l Khoản mục là symlink
-S Khoản mục là chỗ cắm
-p Khoản mục là đường ống có tên (một
“fifo”)
-b Khoản mục là tệp khối đặc biệt (giống
như đĩa tháo lắp được)
-c Khoản mục là tệp kí tự đặc biệt (như
thiết bị vào/ra)
-u Tệp hay danh mục là setuid
-g Tệp hay danh mục là setgid
-k Tệp hay danh mục có tập bit dính
-t isatty() trên tước hiệu tệp là đúng
-T Tệp là văn bản
-B Tệp là “nhị phân”
-M sửa tuổi theo ngày
-A Tuổi truy nhập theo ngày
-C Tuổi thay đổi inode theo ngày
Phần lớn trong những phép kiểm tra này đã cho lại
một điều kiện đúng-sai đơn giản. Số ít thì không, cho
nên hãy nói về chúng.
Toán tử -s không cho lại giá trị đúng nếu tệp là khác
rỗng, nhưng giá trị cho lại là một loại đúng đặc biệt. đó
182
là chiều dài theo byte của tệp, vẫn được coi là đúng đối
với một số khác không.
Toán tử tuổi -M, -A và -C (đúng, chúng đã là chữ
hoa cả) cho lại số ngày kể từ tệp được sửa đổi, truy nhập
hay có thay đổi inode* lần cuối. (inode chứa tất cả các
thông tin về tệp ngoại trừ nội dung của nó - xem chi tiết
trong lời gọi hệ thống stat). Giá trị tuổi này là phân số
với độ phân giải một giây - 36 giờ được cho lại là 1.5
ngày. Nếu bạn so sánh tuổi này với toàn bộ số (chẳng
hạn ba), bạn sẽ thu được chỉ các tệp đã bị thay đổi đúng
nhiều ngày trước đây, không nhiều hay ít hơn một giây.
Điều này có nghĩa là có lẽ bạn sẽ muốn có việc so sánh
theo phạm vi (hay toán tử int()) hơn là so sánh chính xác
để được các tệp nằm giữa ba và bốn ngày lẻ.
Tất cả những toán tử này có thể vận hành trên tước
hiệu tệp cũng như tên tệp. Với tước hiệu tệp làm toán
hạng là tất cả những gì toán tử đó cần. Vậy để kiểm tra
xem liệu tệp có được mở như SOMEFILE có là thực
hiện được hay không, bạn có thể dùng:
if (-x SOMEFILE) { # mở tệp trên SOMEFILE là thực hiện được }
Nếu bạn để tham biến tên tệp hay tước hiệu tệp bỏ
không (tức là, bạn chỉ có -r hay -s) thì toán hạng mặc
định là tệp có tên trong biến $_ (nó vẫn có đấy!). Cho
nên, để kiểm thử một danh sách các tên tệp xem tệp nào
* Tuổi được đo tương đối theo thời gian chương trình bắt đầu, như
được lấy theo thời gian UNIX trong biến S^T. Có thể lấy được số
âm cho những tuổi này nếu giá trị hỏi tham chiếu tới một sự kiện đã
xẩy ra sau khi chương trình bắt đầu.
183
đọc được, thì chỉ cần đơn giản là:
foreach (@some_list_of_filenames) { print “”$_ là đọc được\n” if -r; # cũng như -r $_ }
Các toán tử stat() và lstat()
Trong khi các phép kiểm tra tệp này là tốt cho việc
kiểm tra nhiều thuộc tính liên quan tới một tệp hay danh
mục đặc biệt, thì chúng lại không nói được toàn bộ câu
chuyện. Chẳng hạn, không có việc kiểm tra tệp nào cho
lại số các liên kết với một tệp. Để thu được thông tin còn
lại về tệp, đơn thuần gọi tới toán tử stat(), toán tử cho lại
đủ mọi thứ mà lời gọi hàm hệ thống UNIX stat() cho (hi
vọng nhiều thứ hơn điều bạn muốn biết).
Toán hạng của stat() là tước hiệu tệp, hay một biểu
thức tính cho tên tệp. Giá trị cho lại hoặc là undef, chỉ ra
rằng stat không sinh được, hay một mảng 13 giá trị, phần
lớn đã dễ mô tả bằng việc dùng danh sách sau đây các
biến vô hướng:
($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,
$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat(...)
Các tên ở đây đã trỏ tới các bộ phận của cấu trúc
stat, được mô tả chi tiết trong stat(2) của bạn. Có lẽ bạn
nên nhìn vào đó để xem các mô tả chi tiết.
Chẳng hạn để lấy ID (số hiệu) người dùng và ID
nhóm của tệp mật hiệu, thử:
($uid,$gid) = (stat (“/etc/passwd”)) [4, 5];
Và đó là cách làm.
184
Gọi toán tử stat() trên tên của một liên kết kí hiệu sẽ
cho lại thông tin về liên kết kí hiệu này đang trỏ tới cái
gì, không phải thông tin về bản thân liên kết kí hiệu (trừ
phi liên kết này xẩy ra để không trỏ vào cái gì hiện thời
truy nhập được). Nếu bạn cần thông tin (phần lớn vô
dụng) về bản thân liên kết kí hiệu, dùng lstat() thay vì
stat() (cho cùng thông tin theo cùng thứ tự). Toán tử
lstat() làm việc tựa như stat() trên những điều không phải
là liên kết kí hiệu.
Giống như việc kiểm tra tệp, toán hạng của stat hay
lstat mặc định là $_, nghĩa là stat sẽ được thực hiện trên
tệp có tên trong biến vô hướng $_.
Dùng _Filehandle
Mọi lần bạn nói stat(), -r, -w hay bất kì cái gì trong
chương trình, Perl đã phải trở ra hệ thống để hỏi bộ đệm
stat trên tệp (bộ đệm cho lại từ lời gọi hệ thống stat).
Điều đó có nghĩa là nếu bạn muốn biết liệu tệp có vừa
đọc được và ghi được không, bạn về bản chất đã hỏi hệ
thống hai lần cho cùng một thông tin (điều này không
thể thay đổi được trong một môi trường khá không thân
thiện)
Điều này có vẻ như lãng phí, và thực ra, có thể tránh
được. Thực hiện việc kiểm tra tệp, stat, hay lstat trên
_filehandle (một dấu gạch thấp) xem như toán hạng sẽ
bảo cho Perl dùng bất kì cái gì ngẫu nhiên có trong bộ
nhớ từ lần kiểm tra tệp trước đó. Đôi khi điều này là
nguy hiểm: một chương trình con có thể gọi stat một
cách không chủ định, ném tiêu bộ đệm của bạn đi.
Nhưng nếu bạn cẩn thận, bạn có thể tiết kiệm một vài lời
185
gọi hệ thống không cần thiết. Sau đây là thí dụ nữa về
việc kiểm tra tính ghi được và đọc được $filevar, dùng
mẹo mới:
if (-r $filevar && -w _) { print “$filevar là vừa đọc được và ghi được\n”; }
Lưu ý rằng tôi đã dùng $filevar cho phép kiểm tra
lần thứ nhất - điều này lấy dữ liệu từ hệ điều hành. Lần
kiểm tra thứ hai dùng _filehandle ảo thuật; với phép
kiểm tra này, bản thân dữ liệu bị bỏ lại từ phép kiểm tra
$filevar về tính đọc được nay lại được dùng, đúng là điều
mong muốn.
Chú ý rằng việc kiểm thử _filehandle không hệt như
việc cho phép toán hạng của việc kiểm tra tệp, stat, hay
lstat được mặc định kiểm tra $_; điều này sẽ là việc kiểm
tra mới mỗi lần trên tệp hiện tại mang tên theo nội dung
của $_. Đây lại là một trường hợp khác khi các tên tương
tự đuợc chọn cho các chức năng khá khác nhau. Hiện tại,
bạn có lẽ đã quen với nó.
Bài tập
Xem phụ lục A về lời giải.
1. Viết một chương trình để đọc vào một tên tệp từ
STDIN, rồi mở tệp đó và hiển thị nội dung của nó có
đứng trước bởi tên tệp và một dấu hai chấm. Chẳng
hạn, nếu fred được đọc vào, và tệp fred bao gồm ba
dòng aaa, bbb và ccc, bạn sẽ thấy fred: aaa, fred: bbb
và fred: ccc.
186
2. Viết một chương trình nhắc đưa vào một tên tệp vào,
một tên tệp ra, một mẫu tìm kiếm, và một xâu thay
thế, rồi thay thế tất cả mọi lần xuất hiện của mẫu tìm
kiếm bằng xâu thay thế trong khi sao tệp vào sang
tệp ra. Thử nó trên các tệp. Bạn có thể ghi đè một tệp
hiện có (đừng thử nó với những tệp quan trọng!)
không? Bạn có thể dùng các kí tự biểu thức chính qui
trong xâu tìm kiếm không? Bạn có thể dùng \1 trong
xâu thay thế không?
3. Viết một chương trình để đọc vào một danh sách các
tên tệp và rồi cho hiển thị từ đó các tệp nào là đọc
được, ghi được, và/hoặc thực hiện được, và tệp nào
không tồn tại. (Bạn có thể thực hiện từng phép kiểm
thử cho từng tên tệp khi bạn đọc chúng; hay trên toàn
bộ tập các tên khi bạn đã đọc tất cả chúng. Đừng
quên loại bỏ dấu dòng mới tại cuối mỗi tên tệp mà
bạn đã đọc vào.
4. Viết một chương trình để đọc vào một danh sách các
tên tệp, và tìm tệp cũ nhất trong chúng. In ra tên của
tệp đó, tuổi của nó theo số ngày.
187
11
Dạng thức
Dạng thức là gì?
Trong số nhiều việc làm được, Perl thường được
dùng làm “ngôn ngữ trích rút và báo cáo thực hành.”
Đây là lúc biết về việc “ngôn ngữ báo cáo đó”.
Perl cung cấp một khái niệm về khuôn mẫu viết báo
cáo đơn giản, được gọi là dạng thức. Dạng thức xác định
ra phần không đổi (tiêu đề cột, nhãn, văn bản cố định
hay bất kì cái gì) và phần biến đổi (dữ liệu hiện tại mà
bạn báo cáo). Hình dạng của dạng thức, rất gần với hình
dạng của cái ra, tương tự như cái ra đã được dạng thức
trong COBL hay mệnh đề print using của một số ngôn
ngữ BASIC.
Việc dùng dạng thức bao gồm ba điều sau:
1. Định nghĩa dạng thức
Trong chương này:
Dạng thức là gì?
Gọi một dạng thức
Nói thêm về Fieldholder
Dạng thức đầu trang
Đổi mặc định cho dạng thức
188
2. Nạp dữ liệu cần in vào phần biến đổi của dạng
thức (trường)
3. Gọi tới dạng thức
Thường, bước thứ nhất được thực hiện ngay (trong
văn bản chương trình sao cho nó được xác định vào lúc
dịch), và hai bước sau được thực hiện lặp đi lặp lại.
Định nghĩa một dạng thức
Dạng thức được định nghĩa bằng việc dùng định
nghĩa dạng thức. Định nghĩa dạng thức này có thể xuất
hiện ở bất kì đâu trong văn bản chương trình của bạn,
giống như chương trình con. Định nghĩa dạng thức trông
tựa như thế này:
format têndạngthức =
dòngtrường
giá_trị_một, giá _trị_hai, giá _trị_ba
dòngtrường
giá_trị_một, giá _trị_hai, giá _trị_ba
dòngtrường
giá_trị_một, giá _trị_hai, giá _trị_ba
.
Dòng thứ nhất có chứa từ dành riêng format, tiếp đó
là tên dạng thức và rồi đến dấu bằng (=). Tên dạng thức
được chọn từ một không gian tên khác, và tuân theo
cùng qui tắc như mọi thứ khác. Vì tên dạng thức không
bao giờ được dùng bên trong thân chương trình (ngoại
trừ bên trong giá trị xâu), nên bạn có thể an toàn dùng
các tên trùng với với các từ dàng riêng. Như bạn sẽ thấy
trong mục sau, “Gọi dạng thức,” phần lớn các tên dạng
189
thức của bạn có lẽ sẽ là một như tên tước hiệu tệp (mà
thế, làm cho chúng không phải là một như các từ dành
riêng)
Tiếp theo sau dòng thứ nhất là bản thân khuôn mẫu,
mở rộng từ không đến nhiều dòng văn bản. Phần cuối
của khuôn mẫu được chỉ ra bằng một dấu chấm. Khuôn
mẫu là nhạy cảm với khoảng trắng - đây là một trong vài
chỗ mà một số khoảng trắng (dấu cách, xuống dòng, hay
tab) gây ra vấn đề trong văn bản chương trình Perl.
Định nghĩa khuôn mẫu có chứa một chuỗi các dòng
trường. Mỗi dòng trường có thể chứa văn bản cố định -
văn bản sẽ được in ra theo từng kí tự khi dạng thức này
được gọi tới. Sau đây là một thí dụ về dòng trường có
văn bản cố định:
Hello, my name is Fred Flintstone.
Tên trường có thể chứa cả nơi giữ trường cho văn
bản biến đổi. Nếu một dòng có chứa nơi giữ trường,
dòng tiếp sau của khuôn mẫu (được gọi là dòng giá trị)
sẽ mô tả cho một loạt các giá trị vô hướng - mỗi giá trị
ứng với một nơi giữ trường - mà cung cấp ra giá trị sẽ
được gắn vào trong trường. Sau đây là một thí dụ về
dòng trường với một nơi giữ trường, và dòng giá trị đi
theo:
Hello, my name is @<<<<<<<<<<<.
$name
Nơi giữ trường là @<<<<<<<<<<<, sẽ xác định ra
trường văn bản được dồn trái bởi 11 kí tự. Các chi tiết
đầy đủ hơn về nơi giữ trường sẽ được nêu trong mục có
tên “Nói thêm về nơi giữ trường” dưới đây.
190
Nếu dòng trường có nhiều nơi giữ trường, nó cần
nhiều giá trị, cho nên các giá trị được tách nhau bởi dấu
phẩy:
Hello, my name is @<<<<<<<<<<< and I’m @<< years old..
$name, $age
Gắn tất cả những điều này lại chúng ta có thể tạo ra
một dạng thức đơn giản cho một nhãn địa chỉ:
format ADDRESSLABEL = ======================== | @<<<<<<<<<<<<<<<<<<<< | $name | @<<<<<<<<<<<<<<<<<<<< | $address | @<<<<<<<<<<<, @< @<<<< | $city, $state, $zip ======================== .
Lưu ý rằng các dòng có dấu bằng trên đỉnh và dưới
đáy của dạng thức không có trường, và do vậy không có
dòng giá trị theo sau. (Nếu bạn đặt một dòng giá trị đi
theo sau dòng trường như vậy, nó sẽ được diễn giải như
một dòng trường khác, có thể không làm điều bạn
muốn.)
Khoảng trống bên trong dòng giá trị bị bỏ qua. Một
số người chọn việc dùng khoảng trống phụ trong dòng
giá trị để nối dòng biến với nơi giữ trường trên dòng
trước đó (như đặt $zip ở dưới trường thứ ba của dòng
trước đó trong thí dụ này), nhưng thế chỉ để mà trông
thôi. Perl không quan tâm tới điều đó, và nó không ảnh
hưởng tới cái ra của bạn.
191
Các giá trị được tính toán cho các giá trị vô hướng
của chúng, cho nên tất cả các biểu thức đã được diễn giải
theo ngữ cảnh vô hướng* . Văn bản theo sau dấu xuống
dòng thứ nhất trong một giá trị bị bỏ qua (ngoại trừ trong
trường hợp đặc biệt nhiều nơi giữ trường, sẽ được mô tả
về sau).
Định nghĩa dạng thức cũng giống như định nghĩa
chương trình con. Nó không chứa chương trình thực hiện
ngay lập tức, và do đó có thể được đặt ở bất kì đâu trong
tệp với phần còn lại của chương trình - tôi có khuynh
hướng đặt những dạng thức của mình vào cuối tệp, trước
các định nghĩa chương trình con.
Gọi một dạng thức
Bạn gọi tới một dạng thức bằng toán tử write. Toán
tử này lấy tên của tước hiệu tệp, và sinh ra văn bản cho
tước hiệu tệp đó bằng việc dùng dạng thức hiện thời cho
tước hiệu tệp đó. Theo ngầm định, dạng thức hiện thời
cho một tước hiệu tệp là dạng thức với cùng tên (cho nên
với tước hiệu tệp STDOUT, dạng thức STDOUT sẽ
được dùng), nhưng chúng ta sẽ thấy ngay rằng bạn có thể
thay đổi nó.
Lấy một thí dụ khác bằng việc xét dạng thức nhãn
địa chỉ, và tạo ra một tệp chứa các nhãn địa chỉ. Sau đây
là một đoạn chương trình:
format ADDRESSLABEL =
* Trong Perl 5.0, tôi được biết rằng toàn bộ dòng bây giờ được tính
theo ngữ cảnh mảng, cho nên phát biểu này là không đúng. Đáng
phải nói khác đi nêu bạn có điều gì đó như @a là một giá trị.
192
======================== | @<<<<<<<<<<<<<<<<<<<< | $name | @<<<<<<<<<<<<<<<<<<<< | $address | @<<<<<<<<<<<, @< @<<<< | $city, $state, $zip ======================== . open (ADDRESSLABEL, “>labels-to-print”) || die “can’t
create”; open (ADDRESSLABEL, “addresses”) || die “can not
open addresses”; while ( <ADDRESSES> ) { chop; # remove newline ($name, $address, $city, $state, $zip) = split (/:/) ; # load up the global variables write ADDRESSLABEL; # send the output }
Tại đây chúng ta thấy định nghĩa dạng thức trước,
nhưng bây giờ chúng ta cũng còn có thêm cả chương
trình thực hiện nữa. Trước hết, chúng ta mở một tước
hiệu tệp lên một tệp ra được gọi là labels-to-print. Lưu ý
rằng tên tước hiệu tệp (ADDRESSLABEL) là cùng tên
của dạng thức. Điều này là quan trọng. Tiếp đó, mở tước
hiệu tệp trên danh sách địa chỉ. Dạng thức của danh sách
địa chỉ được giả sử là một cái gì đó tựa như:
Stonehenge:4470 SW Hall Suite 107: Beaverton:OR:97005
Fred Flintstone:3737 Hard Rock Lane:Bedrock:OZ:999bc
Nói cách khác, năm trường tách biệt, mà chương
trình sẽ phân tích như mô tả dưới đây.
Chu trình while trong chương trình này đọc từng
193
dòng của tệp địa chỉ mỗi lúc, bỏ đi dấu xuống dòng, rồi
chẻ dòng này (không có dấu xuống dòng) vào năm biến.
Lưu ý rằng các tên biến cũng là tên mà đã dùng khi định
nghĩa dạng thức. Điều này nữa cũng là quan trọng.
Một khi có tất cả các biến được nạp vào (để cho các
giá trị được dạng thức sử dụng là đúng đắn), toán tử write
gọi tới dạng thức này. Lưu ý rằng tham biến cho write là
tước hiệu tệp cần ghi ra và theo mặc định dạng thức cho
cùng tên cũng được dùng.
Mỗi trường trong dạng thức đã được thay thế bởi
một giá trị tương ứng từ dòng tiếp của dạng thức. Sau
khi hai bản ghi mẫu được nêu ở trên đã được xử lí, tệp
labels-to-print có chứa:
===================== | Stonehege | | 4470 SW Hall Suite 107 | | Beaverton , OR 97005| ===================== ===================== | Fred Flintstone | | 3737 Hard Rock Lane | | Bedrock , OZ 999bc | =====================
Nói thêm về nơi giữ tệp
Cho đến giờ, qua thí dụ, bạn đã biết rằng nơi giữ
trường @<<<< có nghĩa là một trường được dồn trái với
năm kí tự và rằng @<<<<<<<<<<< nghĩa là một trường
được dồn trái với 11 kí tự. Sau đây là toàn bộ phạm vi,
như đã hứa trước đây.
194
Trường văn bản
Phần lớn những nơi giữ trường đã bắt đầu bằng @.
Các kí tự đi sau @ chỉ ra kiểu của trường, trong khi số
các kí tự (kể cả @) chỉ ra chiều rộng của trường.
Nếu các kí tự đi sau @ là dấu mở ngoặc góc trái
(<<<<), bạn nhận được một trường được dồn trái - tức
là, giá trị sẽ được gắn thêm bên phải bằng dấu cách nếu
giá trị này ngắn hơn chiều rộng trường. (Nếu một giá trị
quá dài, nó sẽ bị chặt cụt tự động - dạng của dạng thức
bao giờ cũng được bảo tồn.)
Nếu các kí tự đi sau @ là dấu đóng ngoặc góc phải
(>>>>), bạn nhận được một trường được dồn phải - tức
là nếu giá trị quá ngắn, nó sẽ được bổ sung dấu cách vào
bên trái.
Cuối cùng, nếu các kí tự đi sau @ là dấu sổ đứng (| |
| |), bạn nhận được một trường định tâm: nếu giá trị quá
ngắn, nó được bổ sung thêm dấu cách vào cả hai bên, đủ
cho từng bên làm cho giá trị thành định tâm nhất bên
trong trường.
Trường số
Một loại nơi giữ trường khác là trường số độ chính
xác tĩnh, có ích cho những báo cáo tài chính lớn. Trường
này cũng bắt đầu với @, và được theo sau bởi một hay
nhiều dấu # với một dấu chấm tuỳ chọn (chỉ ra dấu chấm
thập phân). Một lần nữa, @ lại được đếm như một trong
các kí tự của trường. Chẳng hạn:
format MONEY Assets: @#####.## Liabilities: @#####.## Net:
195
@#####.## $assets, $liabilities, $assets-$liabilities .
Ba trường số cho phép sáu vị trí bên trái dấu chấm
thập phân và hai vị trí bên phải (có ích cho đô la Mĩ và
phần xu). Lưu ý tới việc dùng một biểu thức theo dạng
thức - hoàn toàn hợp lệ và thường hay được dùng.
Perl không đưa ra điều gì cho người thành thạo khác
hơn điều này: bạn không thể nào lấy kí hiệu tiền trôi nổi
hay dấu ngoặc nhọn quanh giá trị âm hay bất kì cái gì
khác. Để làm điều đó, bạn phải viết chương trình con của
riêng mình, kiểu như:
format MONEYCOOL = Assets: @#####.## Liabilities: @#####.## Net:
@#####.## &cool ($assets, 10), &cool ($liabilities, 9) , &cool
($assets-$liabilities, 10) . sub cool { local ($n, $width) = @_ ; $width -= 2 ; # back off for negative stuff $n = sprintf (“%.2f”, $n) ; # sprintf is in later chapter if ($n < 0) { sprintf (“[%$width.2f]”, - $n) ; # negative numbers get spaces
instead } } ## body of program: $assets = 32125.12; $liab = 45212.15; write
MONEYCOOL;
196
Trường nhiều dòng
Như đã nói trước đây, Perl thông thường dùng tại
dấu xuống dòng đầu tiên của một giá trị khi đặt kết quả
vào đầu ra. Một loại nơi chứa trường, nơi chứa trường
nhiều dòng, cho phép bạn đưa vào một giá trị mà có thể
có nhiều dòng thông tin. Nơi chứa trường này được kí
hiệu bởi @* trên một dòng bởi chính nó - bao giờ cũng
vậy, dòng đi theo sau xác định ra giá trị mà sẽ được thế
vào trong trường này, mà trong trường hợp này có thể là
một biểu thức cho kết quả có chứa trên nhiều dòng.
Giá trị được thế vào sẽ trông hệt như văn bản gốc:
bốn dòng của giá trị trở thành bốn dòng của cái ra.
Chẳng hạn:
format STDOUT = Text Before. @* $long_string Text After. . $long_string = “Fred\nBaney\nBetty\nWilma\n”; write ;
sinh ra cái ra:
Text Before. Fred Baney Betty Wilma Text After.
Trường được lấp đầy
Một loại nơi chứa trường khác là trường được lấp
197
đầy. Nơi chứa trường này cho phép bạn tạo ra một đoạn
được lấp đầy, bẻ văn bản thành các dòng có kích cỡ qui
ước tại biên giới từ, bao bọc dòng nếu cần. Có vài phần
cùng làm việc ở đây, nhưng chúng ta hãy xét chúng một
cách tách biệt.
Trước hết, một trường được lấp đầy được kí hiệu
bằng việc thay thế dấu hiệu @ trong nơi chứa trường văn
bản bởi dấu mũ (vậy bạn nhận được ^<<<<, chẳng hạn).
Giá trị tương ứng cho trường được lấp đầy (trên dòng
tiếp của dạng thức) phải là một biến vô hướng có chứa
văn bản, thay vì một biểu thức cho lại một giá trị vô
hướng. Lí do cho điều này là ở chỗ Perl sẽ thay đổi biến
này trong khi rót đầy trường được lấp, và cũng hơi khó
để mà thay đổi một biểu thức.
Khi Perl rót đầy trường được lấp, nó lấy giá trị của
biến và vét lấy nhiều từ (bằng việc dùng một định nghĩa
hợp lí về “từ”)* đủ khít vào trong trường. Những từ này
thực tế vượt ra ngoài biến - giá trị của biến này sau khi
rót đầy trường này là bất kì cái gì bị bỏ lại sau khi loại
bỏ từ. Bạn sẽ thấy tại sao ngay sau đây.
Cho đến đây, điều này dường như không khác nhiều
lắm với cách thức trường văn bản làm việc - chúng ta chỉ
in ra vừa đủ trường (ngoại trừ rằng chúng ta vẫn tôn
trọng biên giới từ thay vì cắt bỏ nó theo chiều rộng từ).
Cái đẹp của trường được rót này xuất hiện khi bạn có
nhiều tham chiếu tới cùng biến theo cùng dạng thức.
Nhìn vào điều này:
format PEOPLE =
Name: @<<<<<<<<<<< Comment: ^<<<<<<<<<<<<<<<<<<
* Kí tự tách từ được định nghĩa bởi $:biến
198
$name, $comment Comment: ^<<<<<<<<<<<<<<<<<< $comment Comment: ^<<<<<<<<<<<<<<<<<< $comment Comment: ^<<<<<<<<<<<<<<<<<< $comment .
Lưu ý rằng biến $comment xuất hiện bốn lần. Dòng
thứ nhất (dòng với trường tên) in ra tên người và vài từ
đầu của giá trị trong $comment. Nhưng trong tiến trình
tính dòng này, $comment bị thay đổi để cho các từ biến
mất. Dòng thứ hai lại tham chiếu đến cùng biến này
($comment), và do vậy sẽ lấy đi vài từ mới nữa từ cùng
biến này. Điều này cũng đúng cho dòng thứ ba và thứ tư.
Một cách có hiệu quả, điều tôi đã tạo ra là một hình chữ
nhật trong cái ra mà sẽ được rót đầy với các từ trong
$comment trải qua bốn dòng.
Điều gì xảy nếu toàn bộ văn bản chiếm ít hơn bốn
dòng? Được, bạn sẽ được một hay hai dòng trống. Điều
này có lẽ là được nếu bạn định in ra các nhãn và cần
đúng cùng số dòng cho mỗi mục sánh đúng với nhãn đó.
Nhưng nếu bạn định in ra một báo cáo, nhiều dòng trống
sẽ làm tốn giấy máy in của bạn.
Để giải quyết vấn đề này, chúng ta có thể dùng một
chỉ báo cắt bỏ. Bất kì dòng nào có chứa dấu ngã (~) đã bị
cắt bỏ (không in ra) nếu dòng chỉ có dấu cách. Bản thân
dấu ngã bao giờ cũng in ra như dấu trống, và có thể được
đặt ở bất kì đâu mà dấu cách có thể được đặt trong dòng.
Viết lại thí dụ vừa rồi:
format PEOPLE =
Name: @<<<<<<<<<<< Comment: ^<<<<<<<<<<<<<<<<<<
199
$name, $comment ~ Comment: ^<<<<<<<<<<<<<<<<<< $comment ~ Comment: ^<<<<<<<<<<<<<<<<<< $comment ~ Comment: ^<<<<<<<<<<<<<<<<<< $comment .
Bây giờ, nếu lời bình luận chỉ chiếm hai dòng, các
dòng thứ ba và thứ tư tự động bị cắt bỏ.
Điều gì xảy ra nếu lời bình luận dài hơn bốn dòng?
Được, chúng ta có thể tạo 20 bản sao cho hai dòng cuối
của dạng thức đó, hi vọng rằng 20 dòng sẽ đủ cho nó.
Nhưng điều đó đi ngược với ý tưởng rằng Perl giúp bạn
lười biếng, cho nên có một cách lười biếng để thực hiện
điều đó. Bất kì dòng nào có chứa hai dấu ngã liên tiếp sẽ
được lặp lại một cách tự động cho tới khi kết quả là một
dòng trống hoàn toàn. (Dòng trống bị cắt bỏ.) Điều này
làm thay đổi dạng thức của chúng trông giống thế này:
format PEOPLE =
Name: @<<<<<<<<<<< Comment: ^<<<<<<<<<<<<<<<<<< $name, $comment ~~ Comment: ^<<<<<<<<<<<<<<<<<< $comment .
Cách này, nếu lời bình luận chiếm một, hai hay 20
dòng, chúng vẫn giải quyết ổn thoả.
Lưu ý rằng tiêu chuẩn để chấm dứt dòng lặp lại đòi
hỏi dòng phải trống tại điểm nào đó. Điều đó nghĩa là
bạn có lẽ không muốn bất kì văn bản hằng nào (khác hơn
dấu trống hay dấu ngã) trên dòng này, hay nếu không nó
sẽ chẳng bao giờ trở thành trống. (Có đấy, Perl sẽ lặp
200
mãi trong trường hợp này. Điều này nghe đồn đã được
giải quyết trong Perl 5.0)
Dạng thức đầu trang
Nhiều báo cáo kết thúc trên một thiết bị in ra máy in
nào đó. Giấy máy in thông thường được cắt ra thành
chùm các trang, bởi vì phần lớn chúng đã thôi là cuộn
giấy từ lâu rồi. Cho nên văn bản được nạp vào máy in về
cơ bản phải tính tới biên giới trang để đưa vào các dòng
trống hay kí tự kéo trang để nhảy qua chỗ dư. Bây giờ
bạn có thể lấy một cái ra của chương trình Perl và nạp nó
vào một trình tiện ích nào đó (có thể thậm chí là được
viết trong Perl) mà làm việc phân trang này, nhưng vẫn
còn cách dễ hơn.
Perl cho phép bạn xác định dạng thức đầu trang để
cài bẫy xử lí trang. Perl đếm từng dòng được sinh ra và
gọi tới dạng thức cho một tước hiệu tệp đặc biệt. Khi
dạng thức cái ra tiếp không còn khít vào phần còn lại của
trang, Perl phun ra một dấu kéo trang tiếp sau đó tự động
gọi tới dạng thức đầu trang, và cuối cùng là in ra văn bản
theo dạng thức đã gọi. Theo cách đó, kết quả của một lần
gọi write sẽ không bao giờ cắt ngang qua biên giới trang
(tất nhiên trừ phi nó quá lớn mà không thể khít trên
chính bản thân trang).
Dạng thức đầu trang được xác định giống như các
dạng thức khác. Tên mặc định cho dạng thức đầu trang
đối với một tước hiệu tệp đặc biệt là giống như tên của
tước hiệu tệp có theo sau bởi _TOP (xin viết chữ hoa).
Perl định nghĩa biến $% là số dòng của dạng thức
201
đầu trang mà đã được gọi cho một tước hiệu tệp đặc biệt,
cho nên bạn có thể dùng biến này trong dạng thức đầu
trang của mình để đánh số trang cho đúng. Chẳng hạn,
việc thêm định nghĩa dạng thức sau cho đoạn chương
trình trước ngăn cản các nhãn không bị xé lẻ qua biên
giới trang, và cũng đánh số trang liên tục:
format ADDRESSLABEL_TOP = My Address -- Page @< $% .
Chiều dài trang mặc định là 60 dòng. Bạn có thể
thay đổi điều này bằng việc đặt một biến đặc biệt, được
mô tả tóm tắt.
Perl không để ý liệu bạn có dùng print để in lên cùng
tước hiệu tệp hay không cho nên nó có thể ném đi số
dòng trên trang. Bạn có thể hoặc là viết lại chương trình
của mình để dùng các dạng thức để gửi đi mọi thứ, hay
tránh né biến “số dòng trên trang hiện tại” sau khi bạn
thực hiện lệnh print. Chút nữa chúng ta sẽ thấy cách thay
đổi giá trị này.
Thay đổi mặc định cho dạng thức
Tôi thường nói tới “mặc định” cho điều này điều nọ.
Được, Perl cung cấp một cách để vượt qua các mặc định
cho mọi bước. Ta nói về điều này.
Dùng select() để thay đổi tước hiệu tệp
Quay trở lại khi nói về print, trong chương 6, Cơ sở
202
về vào/ra, tôi đã nói rằng print và print STDOUT là đồng
nhất, bởi vì STDOUT là mặc định cho print. Không hẳn
hoàn toàn thế. Mặc định thực cho print (và write cùng một
vài phép toán khác mà sẽ gặp ngay sau đây) là một khái
niệm kì cục được gọi là tước hiệu tệp hiện đang được
lựa.
Tước hiệu tệp hiện đang được lựa viết tắt là
STDOUT - để làm cho nó dễ in mọi thứ trên đầu ra
chuẩn. Tuy nhiên, bạn có thể thay thế tước hiệu tệp hiện
đang được lựa bằng toán tử select(). Toán tử này nhận
một tước hiệu tệp (hay biến vô hướng có chứa tên của
tước hiệu tệp) như một đối. Một khi tước hiệu tệp hiện
được lựa mà thay đổi, nó ảnh hưởng tới tất cả các phép
toán tương lai phụ thuộc vào tước hiệu tệp hiện được
lựa. Chẳng hạn:
print “hello world\n”; # giống như print STDOUT “hello world\n”;
select(LOGFILE) ; # chọn một tước hiệu tệp mới print “howdy, world\n”; giống như print LOGFILE “howdy
world\n”; print “more for the log\n”; # thêm về LOGFILE select (STDOUT); # chọn lại STDOUT print “back to stdout\n”; # lại trở về với đầu ra chuẩn
Lưu ý rằng phép toán select là khó tính - một khi bạn
đã lựa một tước hiệu mới, nó vẫn còn có hiệu quả cho tới
select tiếp.
Cho nên, một định nghĩa tốt hơn cho STDOUT vẫn
tôn trọng print và write là ở chỗ STDOUT là tước hiệu
hiện được lựa mặc định, hay tước hiệu “mặc định mặc
định”
Các chương trình con có thể thấy nhu cầu thay đổi
203
tước hiệu tệp hiện được lựa. Tuy nhiên, sẽ bất ngờ nếu
bạn gọi một chương trình con rồi phát hiện ra là tất cả
các dòng văn bản bạn đã soạn công phu lại dồn thành
một bộ các bit nào đó bởi vì chương trình con đã thay
đổi tước hiệu tệp hiện được lựa mà không khôi phục lại
nó. Cho nên một chương trình con hành xử tốt cần phải
làm gì? Nếu chương trình con này biết rằng tước hiệu
hiện thời là STDOUT, chương trình con đó có thể khôi
phục tước hiệu đã lựa với chương trình tương tự như
trên. Tuy nhiên, điều gì xẩy ra nếu nơi gọi chương trình
con này đã thay đổi tước hiệu tệp đã lựa?
Vậy vấn đề trở thành giá trị cho lại từ select là một
xâu có chứa tên của tước hiệu đã lựa trước đó. Bạn có
thể nắm lấy giá trị này để khôi phục tước hiệu tệp đã lựa
trước đó, bằng việc dùng đoạn chương trình như thế này:
$oldhandle = select(LOGFILE) ; print “this goes to LOGFILE\n”; select($oldhandle); # khôi phục tước hiệu trước đó
Đấy, để làm thí dụ, dễ dàng hơn nhiều là chỉ cần đặt
LOGFILE một cách tường minh như một tước hiệu tệp
cho print, nhưng có một số thao tác đòi hỏi tước hiệu tệp
hiện được lựa phải thay đổi, như sẽ thấy ngay sau đây.
Thay đổi tên dạng thức
Tên dạng thức ngầm định cho một tước hiệu tệp là
giống như tước hiệu tệp. Tuy nhiên, bạn có thể cthay đổi
điều này cho tước hiệu tệp hiện được lựa bằng việc thiết
đặt tên dạng thức mới trong một biến đặc biệt được gọi
là $~. Bạn có thể cũng xem xét lại giá trị của biến này để
xem dạng thức hiện thời là gì đối với tước hiệu tệp hiện
204
được lựa.
Chẳng hạn, để dùng dạng thức ADDRESSLABEL
trên STDOUT, cũng dễ như:
$_ = “ADDRESSLABEL” ;
Nhưng điều gì xảy ra nếu bạn muốn đặt dạng thức
cho tước hiệu tệp REPORT là SUMMARY? Chỉ cần vài
bước để làm điều đó ở đây:
$oldhandle = select (REPORT) ; $~ = “SUMMARY” ; select ($oldhandle) ;
Lần tiếp chúng ta nói:
write REPORT ;
thu được văn bản trên tước hiệu tệp REPORT,
nhưng dùng dạng thức SUMMARY.
Lưu ý rằng chúng ta đã cất giữ tước hiệu trước đó
vào biến vô hướng và rồi khôi phục lại nó sau này. Đây
là một thực hành lập trình tốt. Thực ra, khi viết chương
trình tôi có lẽ đã giải quyết sự tương tự điển hình trên
một dòng trước đó, và không giả thiết rằng STDOUT là
tước hiệu ngầm định.
Bằng việc đặt dạng thức hiện thời cho một tước hiệu
tệp đặc biệt, bạn có thể xen lẫn nhiều dạng thức khác
nhau trong một báo cáo.
Đổi tên dạng thức đầu trang
Cũng như chúng ta có thể thay đổi tên của dạng thức
cho một tước hiệu tệp đặc biệt bằng việc đặt biến $~,
205
chúng ta cũng có thể thay đổi dạng thức đầu trang bằng
việc đặt biến $^. Biến này giữ tên của dạng thức đầu
trang cho tước hiệu tệp hiện đang được lựa và là đọc/ghi,
có nghĩa là bạn có thể kiểm tra lại giá trị của nó để xem
tên dạng thức hiện thời và bạn có thể thay đổi nó qua
việc gán cho nó.
Đổi chiều dài trang
Nếu dạng thức đầu trang đã được xác định, chiều dài
trang trở thành quan trọng. Theo mặc định, chiều dài
trang là 60 dòng - tức là, khi lệnh write khớp với cuối
dòng 60, dạng thức đầu trang sẽ được tự động gọi tới
trước khi in văn bản.
Đôi khi 60 dòng lại không khớp. Bạn có thể thay đổi
điều này bằng việc đặt biến $=. Biến này giữ chiều dài
trang hiện thời cho tước hiệu tệp đang được lựa. Một lần
nữa, để thay đổi nó sang một tước hiệu tệp khác hơn
STDOUT (tước hiệu tệp hiện đang được lựa ngầm định),
bạn sẽ cần dùng toán tử select(). Sau đây là cách thay đổi
tước hiệu tệp LOGFILE để có trang 30 dòng:
$old = select(LOGFILE) ; # lựa LOGFILE và cất
giữ tước hiệu cũ
$= = 30 ;
select($old) ;
Việc thay đổi chiều dài trang sẽ không có hiệu lực
cho tới lần tiếp gọi tới dạng thức đầu trang. Nếu bạn đặt
nó trước khi bất kì văn bản nào được đưa ra qua tước
hiệu tệp này qua một dạng thức, nó sẽ làm việc tốt vì
206
dạng thức đầu trang được gọi ngay lập tức tại văn bản
đầu tiên được dạng thức.
Thay đổi vị trí trên trang
Nếu bạn dùng print để in một văn bản của mình lên
một tước hiệu tệp, nó sẽ trộn lẫn số đếm dòng vị trí trang
vì Perl không đếm số dòng qua bất kì cái gì khác ngoài
write. Nếu bạn muốn để cho Perl biết rằng bạn đang đưa
ra một vài dòng phụ, bạn có thể điều chỉnh số dòng được
đếm bởi việc thay đổi biến $-. Biến này chứa số dòng
còn lại trên trang trên tước hiệu tệp hiện được lựa. Mỗi
write sẽ làm giảm giá trị của số dòng còn lại theo số dòng
thực tế đưa ra; khi số đếm này đạt tới không, dạng thức
đầu trang sẽ được gọi tới và giá trị của $- được sao từ
biến $= (chiều dài trang).
Chẳng hạn, để bảo Perl rằng bạn đã gửi một dòng
phụ ra STDOUT, làm điều như thế này:
write ; # gọi dạng thức STDOUT trên STDOUT ... ; print “Một dòng phụ ... đây rồi!\n” ; # dòng này đi ra
STDOUT $- -- ; # giảm $- để chỉ ra dòng không ghi đã gửi tới
STDOUT ... ; write ; # điều này vẫn làm việc, tính cả dòng phụ
Tại đầu chương trình, $- được đặt là không cho mỗi
tước hiệu tệp. Điều này đảm bảo rằng dạng thức đầu
trang sẽ là cái đầu tiên được gọi tới cho từng tước hiệu
tệp tuỳ theo write đầu tiên.
207
Bài tập
1. Viết một chương trình để mở tệp /etc/passwd và in ra
tên người dùng, (số hiệu) ID người dùng, và tên thực
theo cột có dạng thức. Dùng format và write.
2. Thêm dạng thức đầu trang vào chương trình trước.
(Nếu tệp mật hiệu còn ít, bạn có thể cần đặt chiều dài
trang thành số nào đó như 10 dòng chẳng hạn để cho
bạn có thể thu được nhiều thể nghiệm của đầu trang.)
3. Thêm số trang tăng tuần tự vào đầu trang, để cho bạn
thu được trang 1, trang 2 vân vân trên cái ra.
208
209
12
Truy nhập
danh mục
Chuyển vòng quanh cây danh mục
Cho tới nay, có lẽ bạn đã quen thuộc với khái niệm
về danh mục hiện thời và dùng chỉ lệnh cd của vỏ. Trong
lập trình UNIX, bạn vẫn gọi lời gọi hệ thống chdir() để
thay đổi danh mục hiện thời của tiến trình, và đây là tên
mà Perl cũng dùng.
Toán tử chdir() trong Perl nhận một đối - một biểu
thức tính ra một tên danh mục để sẽ đặt làm danh mục
hiện thời. Như với hầu hết các toán tử khác, chdir() cho
lại đúng khi bạn đã đổi được sang danh mục yêu cầu và
cho lại sai nếu bạn không thể làm được điều này. Sau
Trong chương này:
Đi quanh cây danh mục
Globbing
Tước hiệu danh mục
Mở và đóng tước hiệu danh mục
. Đọc tước hiệu danh mục
210
đây là một thí dụ:
chdir(“/etc”) || die “không thể chuyển sang /etc (kì thật!)” ;
Các dấu ngoặc tròn là tuỳ chọn, cho nên bạn cũng có
thể viết cách khác bằng chương trình như:
print “Bạn muốn đi đâu?” ; chop ($where) { if (chdir $where) { # tới đó } else { # không tới đó }
Bạn không thể tìm ra được bạn ở đâu mà không đưa
ra chỉ lệnh pwd. Chúng ta sẽ học về việc đưa ra các chỉ
lệnh trong Chương 14, Quản lí tiến trình.
Mọi tiến trình UNIX đã có danh mục riêng của nó.
Khi một tiến trình mới được đưa ra, nó kế thừa danh mục
hiện thời của cha mẹ, nhưng đó là chấm hết mối ghép
nối. Nếu chương trình Perl của bạn đổi danh mục của nó,
điều ấy sẽ ảnh hưởng tới lớp vỏ (hay bất kì cái gì) đã
khởi động tiến trình Perl. Giống vậy, các tiến trình mà
Perl tạo ra không thể nào ảnh hưởng tới danh mục hiện
thời của chương trình Perl. Các danh mục hiện thời cho
những tiến trình mới này được kế thừa từ danh mục hiện
thời của chương trình Perl.
Globbing
Lớp vỏ thường nhận đối dòng lệnh có dấu sao riêng
biệt (*) và chuyển nó thành một danh sách tất cả các tên
tệp trong danh mục hiện thời. Vậy, khi bạn nói rm *, bạn
211
sẽ loại bỏ đi tất cả các tệp khỏi danh mục hiện thời. (Chớ
thử điều này nếu bạn không muốn chọc tức người quản
trị hệ thống của mình khi bạn yêu cầu các tệp đó phải
được cất giữ.) Tương tự, [a-m]*.c xem như đối dòng lệnh
sẽ biến thành một danh sách tất cả các tên tệp trong danh
mục hiện thời mà có chữ bắt đầu thuộc vào nửa trước
của bảng chữ cái, và kết thúc với .c, và /etc/host* là danh
sách tất cả các tên tệp bắt đầu với host trong danh mục
/etc. (nếu điều này là mới đối với bạn, có lẽ bạn muốn
đọc thêm nhiều nữa về kịch đoạn lớp vỏ ở đâu đó khác
trước khi xử lí tiếp.)
Việc mở rộng đối dòng lệnh như * hay /etc/host*
thành một danh sách các tên tệp sánh đúng được gọi là
globbing. Perl hỗ trợ cho globbing qua một cơ chế rất
đơn giản - chỉ đặt mẫu globbing vào giữa hai ngoặc
nhọn, như:
@a = < /etc/host*> ;
Trong hoàn cảnh mảng, như trình bầy ở đây, glob
cho lại một danh sách tất cả các tên sánh đúng với mẫu
(cũng như lớp vỏ đã mở rộng các đối glob), hay một
danh sách rỗng nếu không sánh đúng. Trong hoàn cảnh
vô hướng, sẽ cho lại tên tiếp đó sánh đúng, hay undef nếu
không còn tên nào sánh đúng: điều này rất giống với việc
đọc từ một tước hiệu tệp. Chẳng hạn, mỗi lúc nhìn vào
một tên:
while ($nextname = </etc/host*> ) { print “một trong các tệp là $nextname\n” ; }
Tại đây tên tệp được cho lại bắt đầu với /etc/host,
cho nên nếu bạn muốn chỉ phần cuối cùng của tên, bạn
212
sẽ phải tự mình đẽo gọt nó, giống như:
while ($nextname = </etc/host*> ) {
$nextname =~ s#.* / ## ; # bỏ phần trước dấu sổ chéo
print “một trong các tệp là $nextname\n” ; }
Nhiều khuôn mẫu được phép đặt vào bên trong đối
glob - các danh sách được xây dựng tách biệt và rồi được
nối lại dường như chúng là một danh sách lớn:
@fred_barney_files = <fred* barney*> ;
Nói cách khác, glob cho lại cùng các giá trị tương
đương với chỉ lệnh echo với cùng các tham biến sẽ cho
lại* .
Mặc dầu globbing và hàm sánh biểu thức chính qui
là tương tự nhau, ý nghĩa của các kí tự đặc biệt ở đây lại
hoàn toàn khác nhau. Bạn đừng lẫn lộn giữa hai cách kí
hiệu này, nếu không bạn sẽ khó hiểu tại sao <\.c$> lại
không tìm thấy tất cả các tệp có tận cùng là .c!
Đối cho glob là một biến được chen vào trước khi
mở rộng. Bạn có thể có các tham chiếu biến Perl để lựa
ra một dấu đại diện dựa trên một xâu được tính vào lúc
chạy:
if (-d “/usr/etc”) { $where = “/usr/etc” ; } else { $where = “/etc” ; }
* Điều này thực tại không đáng ngạc nhiên khi bạn hiểu rằng để thực
hiện glob, Perl đơn thuần bắn ra lớp vỏ C để lấy danh sách đặc biệt
rồi phân tích cái nó nhận được.
213
@files = <$where/*> ;
Tại đây tôi đặt $where là một trong hai tên danh mục
khác, dựa trên liệu danh mục /usr/etc có tồn tại hay
không. Rồi tôi lấy một danh sách các tệp trong danh mục
đã được chọn. Lưu ý rằng biến $where được mở rộng, có
nghĩa là khuôn mẫu được glob hoặc là /etc/* hay /usr/etc/*.
Có một ngoại lệ cho qui tắc này: khuôn mẫu <$var>
(nghĩa là dùng biến $var như một khuôn mẫu glob toàn
bộ) phải được viết là <${var}> bởi lí do tôi không muốn
đưa vào tại điểm này**
.
Tước hiệu danh mục
Nếu bạn muốn có hương vị UNIX đặc biệt như hàm
thư viện readdir, Perl cũng cung cấp việc truy nhập vào
trình con đó (và phép so sánh của nó) bằng việc dùng
tước hiệu danh mục. Tước hiệu danh mục là một tên lấy
từ một không gian tên khác, và những thận trọng cùng
lời khuyên áp dụng cho tước hiệu tệp, cũng áp dụng cho
tước hiệu danh mục (bạn không thể dùng một từ dành
riêng, và chữ hoa được khuyên nên dùng). Tước hiệu tệp
FRED và tước hiệu danh mục FRED là không có quan
hệ.
Tước hiệu danh mục biểu thị cho một ghép nối tới
một danh mục đặc biệt. Thay vì đọc dữ liệu (như từ tước
**
Kết cấu <$fred> đọc một dòng từ tước hiệu tệp có tên theo nội
dung của biến vô hướng $fred. Cùng với một số tính năng khác
không được nói tới trong cuốn sách này, kết cấu này cho phép bạn
dùng “tước hiệu tệp gián tiếp”, chỗ mà tên của tước hiệu được
truyền quanh và thao tác dường như là một dữ liệu
214
hiệu tệp), bạn dùng tước hiệu danh mục để đọc một danh
sách các tên tệp bên trong danh mục đó. Tước hiệu danh
mục bao giờ cũng được mở chỉ đọc - bạn không thể nào
dùng một tước hiệu danh mục để thay đổi tên của tệp hay
xoá tệp.
Nếu thư viện của bạn không cung cấp readdir (và
bạn không cung cấp một trong trong những cài đặt thay
thế sao chép được trong khi xây dựng Perl), việc dùng
bất kì một trong những trình con này sẽ là lỗi định mệnh,
và chương trình của bạn sẽ không qua được trình biên
dịch - nó sẽ bị bỏ trước khi dòng chương trình đầu tiên
được thực hiện. Perl cố gắng rất vất vả để cô lập bạn với
môi trường, nhưng nó không phải là một công nhân mầu
nhiệm.
Mở và đóng tước hiệu danh mục
Hàm opendir() làm việc giống như lời gọi thư viện
của cùng tên. Bạn trao cho nó tên của một tước hiệu
danh mục mới và một giá trị xâu kí hiệu cho tên này của
danh mục sẽ được mở ra. Giá trị cho lại từ opendir() là
đúng nếu danh mục có thể được mở, và là sai trong các
trường hợp khác. Sau đây là một thí dụ:
opendir(ETC, “/etc”) || die “Không thể mở opendir /etc” ;
Thông thường, tại điểm này, chúng ta đang chơi với
tước hiệu danh mục ETC, nhưng có thể hay hơn nếu biết
cách đóng tước hiệu danh mục này trước. Điều này được
thực hiện với closedir(), theo cách tương tự như việc dùng
close(), giống như:
closedir(ETC) ;
215
Giống như close(), closedir() thường cũng chẳng cần,
vì tất cả các tước hiệu danh mục đã tự động được đóng
lại trước khi chúng được mở lại hay tại cuối chương
trình.
Đọc một tước hiệu danh mục
Một khi chúng ta có việc mở một tước hiệu danh
mục, chúng ta có thể đọc danh sách các tên bằng
readdir(), nhận một tham biến: tên của tước hiệu danh
mục. Mỗi lần gọi tới readdir() trong hoàn cảnh vô hướng
đã cho lại tên tệp tiếp (hệt như basename - bạn sẽ không
bao giờ nhận được bất kì dấu sổ chéo nào trong giá trị
cho lại) theo một trật tự dường như ngẫu nhiên*. Nếu
không còn tên nào nữa, readdir() cho lại undef. Việc gọi
readdir() trong hoàn cảnh mảng cho lại tất cả các tên còn
lại như một danh sách với mỗi tên cho một phần tử. Sau
đây là một thí dụ về danh sách tất cả các tên lấy từ danh
mục /etc:
opendir(ETC, “/etc”) || die “Không có etc?” ; while ($name = readdir(ETC)) { # hoàn cảnh vô hướng print “$name\n” ; # in ., .., passwd, group vân vân } closedir(ETC) ;
Và sau đây là cách lấy chúng tất cả theo trình tự
bảng chữ với sự hỗ trợ của sort:
opendir(ETC, “/etc”) || die “Không có etc?” ; foreach ($name (sort readdir(ETC)) { # hoàn cảnh vô
* Nói riêng, đây là trật tự mà các tên tệp được cất giữ trong danh
mục - cùng trật tự chưa sắp mà bạn nhận được từ chỉ lệnh find hay
ls -f.
216
hướng, có sắp xếp print “$name\n” ; # in ., .., passwd, group vân vân } closedir(ETC) ;
Lưư ý rằng các tên bao gồm các tệp bắt đầu với một
dấu chấm. Điều này không giống như glob với <*> mà
không cho lại các tên bắt đầu với một chấm (giống như
echo * của lớp vỏ ).
Bài tập
1. Viết một chương trình để thay đổi danh mục cho một
vị trí được xác định như cái vào, rồi liệt kê các tên
của các tệp này theo trình tự abc sau khi thay đổi ở
đó. (Đừng hiện danh sách nếu việc đổi danh mục
không thành công - đơn giản chỉ cảnh báo cho đọc
giả.)
2. Sửa đổi chương trình này để bao quát tất cả các tệp,
không chỉ những tệp không bắt đầu với dấu chấm.
Thử làm điều này với cả một glob và một tước hiệu
danh mục.
217
13
Thao tác tệp
và danh mục
Loại bỏ tệp
Trước đây, bạn đã biết cách tạo ra một tệp từ bên
trong Perl bằng việc mở nó làm cái ra thông qua một
tước hiệu tệp. Bây giờ, chúng ta sẽ gặp nguy hiểm hơn,
và học cách loại bỏ một tệp (rất thích hợp với Chương
13, bạn có nghĩ như vậy không?)
Toán tử unlink() của Perl (được lấy theo tên của lời
gọi hệ thống UNIX) sẽ xoá đi một tên đối với một tệp
(mà có thể mang nhiều tên). Khi tên cuối cùng cho một
tệp bị xoá đi, bản thân tệp này cũng sẽ bị loại bỏ đi. Điều
này đích xác là là điều chỉ lệnh rm đã thực hiện. Bởi vì
một tệp thường chỉ có một tên (trừ phi bạn đã tạo ra móc
nối cứng), bạn có thể nghĩ đến việc loại bỏ một tên như
việc loại bỏ tệp đối với phần lớn các trường hợp. Giả sử
Trong chương này:
. Loại bỏ tệp
. Đổi tên tệp
. Tạo ra tên khác cho
tệp (móc nối)
. Tạo và loại bỏ danh
mục
.Sửa đổi quyền thao tác
. Sửa đổi quyền sở hữu
. Sửa đổi thời gian
218
như thế, sau đây là cách loại bỏ một tệp có tên là fred và
rồi loại bỏ một tệp được xác định trong khi thực hiện
chương trình:
unlink(“fred”) ; # nói lời tạm biệt với fred print “Bạn muốn xoá bỏ tệp nào?” ; chop ($name = <STDIN>) ; unlink ($name) ;
Toán tử unlink có thể nhận một danh sách các tên cần
phải tháo móc nối như:
unlink (“spottedowl”, “meadowlark”) ; # giƠt chết hai con chim
unlink (<*.o>) ; # hệt như “rm *.o” trong lớp vỏ
Lưu ý rằng glob được tính toán theo hoàn cảnh
mảng, tạo ra một danh sách các tên tệp sánh đúng với
khuôn mẫu, và điều này đích thị là điều cần để nạp vào
unlink().
Giá trị cho lại của unlink() là số các tệp đã bị ảnh
hưởng thành công. Nếu có một đối tên tệp và nó bị xoá
đi, kết quả là một, ngoài ra, nó là không. Nếu có ba tên
tệp nhưng chỉ có hai tên có thể bị xoá đi, kết quả là hai.
Lưu ý rằng bạn không thể biết được hai tệp nào, cho nên
nếu bạn cần phải hình dung ra việc xoá nào bị hỏng, bạn
phải thực hiện lần lượt từng việc xoá một. Sau đây là
cách xoá đi tất cả các tệp đích (tận cùng bởi o.) trong khi
báo cáo lại lỗi với bất kì tệp nào mà không thể nào xoá
được.
foreach $file (<*.o>) { # bước qua danh sách các tệp .o unlink ($file) || print “gặp rắc rối với tệp $file\n” ; }
Nếu unlink cho lại 1 (có nghĩa là một tệp xác định
quả thực đã bị xoá), kết quả đúng sẽ nhảy qua câu lệnh
219
print. Nếu tệp này không thể nào bị xoá đi, kết quả 0 là
sai, cho nên câu lệnh print được thực hiện. Lại một lần
nữa, điều này có thể được đọc một cách tuỳ ý như “tháo
móc nối tệp này hay cho tôi biết về nó.”
Nếu toán tử unlink được nêu ra không có đối, biến $_
lần nữa lại được dùng như mặc định. Vậy, chúng ta có
thể viết chu trình trên là:
foreach (<*.o>) { # bước qua danh sách các tệp .o unlink || print “gặp rắc rối khi xoá $_\n” ; }
Đổi tên tệp
Trong lớp vỏ, bạn đổi tên tệp bằng chỉ lệnh mv. Với
Perl, cùng phép toán này được kí hiệu bằng rename($old,
$new). Sau đây là cách thay đổi tệp có tên fred thành
barney:
rename(“fred”, “barney”) || die “Không thể đổi tên fred thành barney”;
Giống như hầu hết các toán tử khác, toán tử rename()
cho lại giá trị đúng nếu công việc diễn ra thành công,
cho nên ở đây tôi đã kiểm tra kết quả này để xem liệu
việc đổi tên có thực sự xảy ra không.
Chỉ lệnh mv thực hiện một chút ít ảo thuật hậu cảnh
để tạo ra một đường dẫn đầy đủ khi bạn nói mv file
some-directory. Tuy nhiên, toán tử rename(), lại không
thể làm điều đó. Phép toán Perl tương đương là:
rename(“file”, “some-directory/file”) ;
Lưư ý rằng trong Perl chúng ta phải nói tên của tệp
220
bên trong danh mục mới một cách tường minh. Cũng
vậy, chỉ lệnh mv sao tệp này khi tệp được đổi tên từ thiết
bị này sang thiết bị khác (nếu bạn có một trong các cài
đặt UNIX tốt hơn). Toán tử rename() không thật khôn
cho lắm, cho nên bạn sẽ nhận được lỗi, chỉ ra bạn phải
chuyển vòng nó theo cách khác (có lẽ bởi việc gọi chỉ
lệnh mv theo cùng tên).
Tạo ra tên thay phiên cho một tệp (móc nối)
Dường như là một tên cho một tệp vẫn không đủ,
đôi khi bạn muốn có hai tên, ba tên hay cả tá tên cho
cùng một tệp. Phép toán tạo ra nhiều tên thay phiên cho
một tệp được gọi là móc nối. Hai dạng chính của móc
nối là móc nối cứng và móc nối tượng trưng (cũng còn
được gọi là móc nối mềm).
Về móc nối cứng và mềm
Móc nối cứng với một tệp là không thể nào phân
biệt được với tên gốc cho tệp này - không có móc nối
đặc biệt nào nhiều hơn là tên thật cho tệp này so với bất
kì tên khác.
Lõi UNIX giữ dấu vết số các móc nối cứng tham
chiếu tới một tệp vào bất kì lúc nào. Khi một tệp lần đầu
tiên được tạo ra, nó bắt đầu với một liên kết. Mỗi liên kết
cứng mới lại làm tăng thêm số đếm này. Mỗi khi liên kết
bị loại bỏ, số đếm lại được giảm đi. Khi liên kết cuối
cùng với một tệp biến mất, tệp đó cũng mất theo.
Mọi móc nối cứng với một tệp phải nằm ở cùng một
221
đơn vị lưu thông tin (thông thường là một đĩa hay một
phần của đĩa). Bởi điều này, bạn không thể nào tạo ra
một móc nối cứng mới với một tệp nằm trên một đơn vị
lưu thông tin khác.
Móc nối cứng cũng bị hạn chế vào các danh mục.
Để giữ cho cây các tệp của UNIX còn là một cây thay vì
là một mớ hỗn độn bất kì, một danh mục mỗi lần chỉ
được phép mang một tên từ gốc, một móc nối từ tệp
chấm bên trong nó, và một chùm các móc nối cứng chấm
chấm từ từng danh mục con của nó. Nếu bạn thử tạo ra
một móc nối cứng khác cho một danh mục, bạn sẽ nhận
được một lỗi (chừng nào mà bạn còn chưa là siêu người
dùng).
Một móc nối tượng trưng là một loại tệp đặc biệt có
chứa đường dẫn như dữ liệu. Khi tệp này được mở, lõi
UNIX sẽ nhìn vào nội dung của nó xem như các kí tự
phụ cho đường dẫn, làm cho lõi phải dò qua cây danh
mục thêm nữa, bắt đầu với tên mới.
Chẳng hạn, nếu một móc nối tượng trưng có tên fred
lại chứa tên barney, việc mở fred thực sự là một chỉ báo
để mở barney. Nếu barney là một danh mục, fred/wilma
sẽ thay vì thế mà tham chiếu tới barney/wilma.
Nội dung của móc nối tượng trưng (nơi móc nối
tượng trưng trỏ tới) không phải tham chiếu tới một tệp
hay danh mục đã có. Khi fred được tạo ra, barney thậm
chí phải không được tồn tại - thực ra, nó có thể chẳng
bao giờ tồn tại cả! Nội dung của một móc nối tượng
trưng có thể tham chiếu tới một đường dẫn mà sẽ đưa
bạn tới nơi cất giữ hiện tại, cho nên bạn có thể tạo ra một
móc nối tượng trưng tới một tệp trên một thiết bị lưu trữ
222
khác.
Trong khi đi theo tên mới, lõi có thể chạy qua một
móc nối tượng trưng khác. Móc nối tượng trưng này
thậm chí còn cho phần mới hơn so với đường dẫn phải
theo. Thực ra, móc nối tượng trưng có thể trỏ tới các
móc nối tượng trưng khác, mà thông thường có ít nhất
tám mức móc nối tượng trưng được phép, mặc dầu điều
này hiếm khi được dùng trong thực hành.
Móc nối cứng bảo vệ cho nội dung của tệp khỏi bị
mất (vì nó vẫn còn đếm khi có một trong các tên của
tệp). Móc nối tượng trưng không thể nào giữ được nội
dung khỏi bị mất. Một móc nối tượng trưng có thể xuyên
qua các thiết bị lưu trữ hiện có trong khi móc nối cứng,
lại không thể thế được. Chỉ móc nối tượng trưng mới có
thể được làm thành một danh mục.
Bạn phải có khả năng ghi lên danh mục nơi bạn
đang tạo ra ra hoặc là một loại móc nối, cho dù bạn có
thể không cần phải có khả năng mở tệp mà đang móc nối
tới. Bạn phải có khả năng thống kê stat cho tệp (như có
thể nói bởi ls -l filename) để tạo ra một móc nối cứng,
nhưng bạn không cần một truy nhập như vậy xem như
một móc nối tượng trưng.
Tạo ra các móc nối cứng và mềm bằng Perl
Chỉ lệnh ln của UNIX tạo ra móc nối cứng. Chỉ lệnh
ln fred bigdumbguy
tạo ra một móc nối cứng từ tệp fred (mà phải tồn tại)
đối với bigdumbguy. Trong Perl điều này được diễn tả là:
223
link (“fred”, “bigdumbguy”) || die “không thể móc nối fred với bigdumbguy” ;
Toán tử link() nhận hai đối, tên tệp cũ và biệt hiệu
mới cho tệp đó. Toán tử này cho lại đúng nếu việc móc
nối thành công. Như với chỉ lệnh mv, chỉ lệnh ln của
UNIX thực hiện vài thủ thuật đằng sau hậu trường, cho
phép bạn xác định danh mục đích đối với biệt hiệu mới
mà không đặt tên tệp bên trong danh mục này. Toán tử
link() (giống như toán tử rename()) cũng không thật thông
minh cho lắm, và bạn phải xác định tên tệp hoàn toàn
tường minh.
Với một móc nối cứng, tên tệp cũ không thể là một
danh mục, còn biệt hiệu mới lại phải là trên cùng hệ
thống tệp. (Hạn chế này là một phần của lí do rằng các
móc nối tượng trưng được tạo ra.)
Trên hệ thống có hỗ trợ cho móc nối tượng trưng,
chỉ lệnh ln của UNIX có thể được cho thêm tuỳ chọn -s
để tạo ra móc nối tượng trưng. Cho nên, để tạo ra một
móc nối tượng trưng từ barney sang neighbor (để cho
tham chiếu tới neighbor thực tại là một tham chiếu tới
barney), bạn phải dùng cái gì đó tựa như thế này:
ln -s barney neighbor
và trong Perl, bạn nên dùng toán tử symlink(), tựa
như
symlink(“barney”, “neighbor”) || die “Không thể tạo móc nối tượng trưng sang
neighbor” ;
Lưu ý rằng barney không cần tồn tại (Betty đáng
thương ơi!), cả bây giờ hay trong tương lai. Trong
trường hợp này, một tham chiếu tới neighbor sẽ cho lại
224
cái gì đó mơ hồ tựa như file not found.
Khi bạn gọi ls -l trên danh mục có chứa một móc nối
tượng trưng, bạn nhận được một chỉ báo về cả hai tên
của móc nối tượng trưng và nơi móc nối trỏ tới. Perl cho
bạn cùng thông tin này qua toán tử readlink(), làm việc
đáng ngạc nhiên giống như lời gọi hệ thống với cùng tên,
cho lại một tên được trỏ tới bởi một móc nối tượng trưng
đặc biệt. Cho nên, phép toán này:
if ($x = readlink(“neighbor”)) { print “neighbor trở tới ‘$x’\n” ; }
nên nói về barney nếu tất cả đã ổn thoả. Nếu móc
nối tượng trưng không tồn tại hay không thể được đọc
hay thậm chí không phải là móc nối tượng trưng,
readlink() cho lại undef (sai tất định), mà đấy là lí do tại
sao tôi lại đang thử nó ở đây.
Trên hệ thống không có móc nối tượng trưng, cả hai
toán tử symlink() và readlink() đã không được dịch, làm
cho chương trình bị bỏ trước khi nó được bắt đầu. Điều
này là vì không có sự tương đương sánh được nào cho
móc nối tượng trưng trên các hệ thống không hỗ trợ cho
nó. Perl có thể che dấu một số tính năng phụ thuộc hệ
thống với bạn, nhưng một số tính năng sẽ dò rỉ ra ngay.
Đây là một trong chúng.
Tạo ra và xoá danh mục
Có lẽ bạn không thể làm được điều này thêm nếu
không biết về chỉ lệnh mkdir của UNIX, chỉ lệnh tạo ra
danh mục chứa các tệp khác, và các danh mục khác.
225
Thành phần tương đương của Perl là toán tử mkdir(),
nhận một tên cho một danh mục mới và một mốt mà sẽ
ảnh hưởng tới quyền về danh mục được tạo ra. Mốt này
được xác định như một số mà được diễn giải theo dạng
thức quyền bên trong. Nếu bạn chưa quen thuộc với các
quyền bên trong này, xem chmod(2) trong tài liệu. Nếu
bạn vội vã, chỉ cần dùng 0777 cho mốt này và mọi thứ sẽ
gần như làm việc cả. Sau đây là một thí dụ về cách tạo ra
một danh mục có tên gravelpit:
mkdir (“gravelpit”, 0777) || die “không thể mkdir gravelpit” ;
Chỉ lệnh rmdir của UNIX loại bỏ đi danh mục rỗng -
bạn sẽ thấy thành phần Perl tương đương với cùng tên.
Sau đây là cách làm cho Fred thành thất nghiệp:
rmdir(“gravelpit”) || die “không thể rmdir gravelpit” ;
Mặc dầu những toán tử Perl này lợi dụng cùng tên
lời gọi hệ thống, chúng ta vẫn làm việc trên các hệ thống
không có những lời gọi này (mặc dầu có chậm hơn đôi
chút). Perl tự động gọi tới các tiện ích /bin/mkdir và
/bin/rmdir cho bạn (hay bất kì cái gì chúng được gọi trên
hệ thống của bạn). Cũng là giúp một phần nhân danh
tính khả chuyển.
Thay đổi phép sử dụng
Phép sử dụng đối với một tệp hay danh mục xác
định ra ai (theo phạm trù rộng) có thể làm gì (nhiều hay
ít) đối với tệp hay danh mục đó. Dưới UNIX, cách điển
hình để thay đổi phép trên tệp là dùng chỉ lệnh chmod
(xem tài liệu nếu bạn chưa quen thuộc với sự vận hành
226
của nó). Tương tự như vậy, Perl thay đổi phép qua toán
tử chmode(). Toán tử này nhận một mốt số và một danh
sách các tên tệp, và cố gắng thay đổi phép của tất cả các
tên tệp sang theo mốt đã chỉ ra. Để làm cho các tệp fred
và barney cả hai đã là đọc/ghi với mọi người, chẳng hạn,
làm điều gì đó tựa như thế này:
chmod(0666, “fred”, “barney”) ;
Tại đây, giá trị của 0666 cho phép đọc/ghi đối với
người dùng, nhóm và người khác, cho chúng phép mong
muốn.
Giá trị cho lại của chmod() là số các tệp được điều
chỉnh thành công (cho dù nếu việc điều chỉnh chẳng làm
gì cả), cho nên nó làm việc giống như unlink(), và bạn nên
xử lí nó như xử lí kiểm tra lỗi. Sau đây là cách thay đổi
phép cho fred và barney trong khi kiểm tra lỗi cho từng
tệp:
foreach $file (“fred”, “barney”) { unless (chmod(0666, $file) { print “Hơ ... không thể đổi được chmod
$file.’n” ; } }
Thay đổi quyền sở hữu
Mọi tệp (hay danh mục, hay lối vào thiết bị, hay bất
kì cái gì) trong hệ thống tệp đã có một người chủ và một
nhóm. Người chủ và nhóm của một tệp xác định ra ai là
người được phép tiến hành (đọc, ghi và/hoặc thực hiện
tệp). Người chủ và nhóm của một tệp được xác định vào
lúc tệp được tạo ra, nhưng trong hoàn cảnh nào đó, bạn
227
có thể thay đổi được chúng. (Hoàn cảnh đúng đắn tuỳ
thuộc vào dạng UNIX đặc biệt mà bạn đang chạy - xin
xem chi tiết tài liệu về chwn.)
Toán tử chown() nhận một số hiệu người dùng ID
(UID), số hiệu nhóm ID (GID), và một danh sách các tên
tệp, dự định thay đổi quyền sở hữu cho từng tệp được
liệt kê như đã xác định. Việc thay đổi thành công được
chỉ ra bằng giá trị cho lại khác không, giá trị bằng số
lượng các tệp đã thay đổi thành công - giống hệt chmod()
hay unlink(). Lưu ý rằng bạn đang thay đổi cả người chủ
và nhóm một lúc; bạn không thể thay đổi chỉ một thành
phần. Cũng cần chú ý rằng bạn phải dùng UIS và GID
số, không phải là tên tượng trưng tương ứng (mặc dầu
chỉ lệnh chmod chấp nhận tên). Chẳng hạn, nếu fred là
UID 1234 còn nhóm mặc định stoners của fred là GID
35, thế, chỉ lệnh sau đây tạo ra tệp slate và granite thuộc
vào fred và nhóm mặc định cho anh ta:
chown (1234, 35, “slate”, “granite”) ; # hệt như: # chown fred slate granite # chgrp stoners slate granite
Trong chương sau, bạn sẽ học cách chuyển fred sang
1234 và stoners sang 35.
Thay đổi nhãn thời gian
Liên kết với từng tệp là một tập ba nhãn thời gian.
Các nhãn thời gian này đã được đề cập ngắn gọn tới khi
chúng nói về việc lấy thông tin về tệp: lần truy nhập cuối
cùng, thời gian sửa đổi cuối cùng, và thời gian thay đổi
inode. Hai nhãn thời gian đầu có thể được đặt cho giá trị
tuỳ ý bằng toán tử utime() (mà tương ứng trực tiếp với lời
228
gọi hệ thống UNIX cùng tên). Việc đặt hai giá trị này sẽ
tự động đặt lại giá trị thứ ba thành thời gian hiện tại, cho
nên không có vấn đề gì liên quan tới việc đặt giá trị thứ
ba.
Các giá trị được đo theo thời gian nội bộ của UNIX,
có nghĩa là một số nguyên chỉ ra số giây đã trôi qua từ
nửa đêm GMT, 1 tháng 1 năm 1970 - một con số đạt tới
quãng bẩy trăm triệu, khi cuốn sách này đang được viết
ra. (Bên trong, nó được biểu diễn như một số 32-bit có
dấu, và đôi khi sẽ bị tràn sớm trong thế kỉ tới.)
Toán tử utime() làm việc giống như chmod() và
unlink(). Nó nhận một danh sách các tên tệp và cho lại số
các tệp bị ảnh hưởng. Sau đây là cách làm cho các tệp
fred và barney trông giống như chúng đã bị thay đổi đâu
đó mới đây:
$atime = $mtime = 700000000 ; # thời gian trước đây utime($atime, $mtime, “fred”, “barney”) ;
Không có giá trị “hợp lí” cho nhãn thời gian: bạn có
thể làm cho một tệp trông thực là cũ kĩ như nó đã được
sửa đổi đâu đó trong tương lai xa xôi (có ích nếu bạn
đang viết chuyện khoa học viễn tưởng). Chẳng hạn, dùng
toán tử time (mà cho lại thời gian hiện tại như một nhãn
thời gian UNIX), sau đây là cách làm cho tệp
max_headroom có vẻ như đã được cập nhật 20 phút trong
tương lai:
$when = time + 20*60 ; # 20 phút nữa từ bây giờ utime($when, $when, “max_headroom”) ;
229
Bài tập
1. Viết một chương trình làm việc giống như rm, xoá đi
các tệp được cho như đối dòng lệnh khi chương trình
này được gọi tới. (Bạn không cần phải giải quyết bất
kì tuỳ chọn nào của rm.)
Kiểm thử cẩn thận chương trình này trong một danh
mục gần rỗng để cho bạn không ngẫu nhiên xoá đi
mất chất liệu có ích! Nhớ rằng đối dòng lệnh thứ
nhất có sẵn trong mảng @ARGV khi chương trình
bắt đầu.
2. Viết một chương trình làm việc giống như mv, đổi
tên đối dòng lệnh thứ nhất thành đối dòng lệnh thứ
hai. (Bạn không cần giải quyết bất kì tuỳ chọn nào
của ln, hay nhiều hơn hai đối dòng lệnh.)
3. Viết một chương trình làm việc giống như ln, tạo ra
một móc nối cứng từ đối dòng lệnh thứ nhất sang đối
dòng lệnh thứ hai. (Bạn không cần giải quyết bất kì
tuỳ chọn nào của ln, hay nhiều hơn hai đối dòng
lệnh.)
4. Nếu bạn có các móc nối tượng trưng, thay đổi
chương trình trong bài tập trước để giải quyết một
khoá tuỳ chọn -s
5. Nếu bạn có móc nối tượng trưng, viết một chương
trình tìm tất cả các tệp có móc nối tượng trưng tương
tự như cách ls -l thực hiện nó (name -> value). Tạo ra
một số móc nối tượng trưng trong danh mục hiện
thời và kiểm thử nó.
230
14
Quản lí
tiến trình
Dùng system() và exec()
Khi bạn trao cho lớp vỏ một dòng chỉ lệnh để thực
hiện, lớp vỏ tạo ra một tiến trình để thực hiện chỉ lệnh
này. Tiến trình mới này trở thành tiến trình con của lớp
vỏ, thực hiện độc lập và không phối hợp gì với lớp vỏ cả.
Tương tự, một chương trình Perl có thể phát động
các tiến trình mới, và giống như hầu hết các phép toán
khác, có nhiều cách để thực hiện nó.
Cách đơn giản nhất để phát động một tiến trình mới
là dùng toán tử system. Dưới dạng đơn giản nhất của nó,
toán tử này trao một xâu cho lệnh lớp vỏ /bin/sh để được
thực hiện như một chỉ lệnh. Khi chỉ lệnh này được hoàn
Trong chương này:
Dùng system() và exec()
Dùng trích dẫn lùi
Dùng tiến trìn hvà tước hiệu tệp
Dùng fork
Tóm tắt về phép toán tiến trình
Gửi và nhận tín hiệu
231
thành, toán tử system cho lại giá trị ra của chỉ lệnh (điển
hình là 0 nếu mọi thứ diễn tiến tốt đẹp). Sau đây là một
thí dụ về một chương trình Perl thực hiện chỉ lệnh date
bằng việc dùng lớp vỏ* :
system (“date”) ;
Lưu ý rằng tôi đang không biết gì về giá trị cho lại ở
đây, nhưng chẳng thể nào mà chỉ lệnh date lại không
thực hiện được.
Cái ra sẽ đi đâu? Thực ra, cái vào tới từ đâu, nếu đấy
là một chỉ lệnh muốn đưa vào? Đây là những câu hỏi
hay, và câu trả lời cho những câu hỏi này là phần lớn
những điều phân biệt các dạng khác nhau của việc tạo ra
tiến trình.
Với toán tử system, ba tệp chuẩn (cái vào chuẩn, cái
ra chuẩn, và lỗi chuẩn) đã được kế thừa từ tiến trình Perl.
Cho nên với chỉ lệnh date trong thí dụ trên, cái ra sẽ
chuyển đến bất kì đâu mà print STDOUT đi ra - có lẽ là
trên màn hình của nơi gọi. Bởi vì bạn đang phát hoả ra
lớp vỏ, nên bạn có thể thay đổi vị trí của cái ra chuẩn
bằng việc dùng cách chuyển hướng vào/ra thông thường
/bin/sh. Chẳng hạn, để đặt cái ra của chỉ lệnh date vào
trong một tệp có tên right_now, điều gì đó tựa thế này sẽ
có tác dụng:
system (“date > right_now”) && die “Không thể tạo ra right_now” ;
Lần này, tôi không chỉ gửi cái ra của chỉ lệnh date
* Điều này thực tế không dùng tới lớp vỏ - Perl thực hiện các phép
toán của lớp vỏ nếu dòng chỉ lệnh đủ đơn giản, và đây là một trường
hợp.
232
vào một tệp với việc đổi hướng sang lớp vỏ, mà tôi còn
kiểm tra trạng thái cho lại. Nếu trạng thái cho lại là đúng
(khác không), cái gì đó bị sai với chỉ lệnh lớp vỏ, và toán
tử die sẽ tiến hành hành động của nó. Đây là nhược điểm
theo qui ước toán tử thông thường của Perl - giá trị cho
lại khác không từ toán tử system nói chung chỉ ra rằng
điều gì đó bị sai.
Đối cho toán tử system có thể là bất kì cái gì mà bạn
sẽ nạp vào /bin/sh, cho nên nhiều chỉ lệnh có thể được
bao hàm vào, tách biệt nhau bằng dấu chấm phẩy hoặc
dòng mới. Và các tiến trình kết thúc trong & sẽ được
khởi động và không đợi, dù cho bạn đã gõ một dòng kết
thúc với một & vào lớp vỏ.
Sau đây là một thí dụ về việc sinh ra chỉ lệnh date và
who cho lớp vỏ, gửi cái ra tới tên tệp được xác định bởi
một biến Perl. Tất cả điều này xảy ra trong hậu cảnh sao
cho chúng không phải đợi nó trước khi tiếp tục với bản
ghi Perl:
$where = “who_out.”.++$i ; # lấy một tên tệp mới system (“(date; who) > $where &”) ;
Giá trị cho lại từ system trong trường hợp này là giá
trị ra của lớp vỏ, và do vậy chỉ ra liệu tiến trình hậu cảnh
đã khởi động thành công hay chưa, nhưng không chỉ ra
liệu các chỉ lệnh date hay who có thực hiện thành công
hay không. Xâu nháy kép là một biến xen lẫn, cho nên
$where được thay thế bằng giá trị của nó trước khi lớp vỏ
thấy nó. Nếu bạn muốn tham chiếu tới biến lớp vỏ có
thên $where, bạn phải đặt dấu sổ chéo ngược trước dấu
đô la, hay dùng một xâu nháy đơn, hay một cái gì đó
giống thế.
233
Một tiến trình con kế thừa nhiều điều từ cha mẹ nó
bên cạnh các tước hiệu tệp chuẩn. Những tiến trình này
bao gồm việc bỏ mặt nạ hiện tại, danh mục hiện tại, và
tất nhiên cả ID người dùng.
Bên cạnh đó, các biến môi trường cũng được tiến
trình con kế thừa. Các biến này được thay đổi điển hình
qua chỉ lệnh csh setenv hay phép gán tương ứng và
export bởi lớp vỏ /bin/sh. Các biến môi trường được
nhiều trình tiện ích dùng, kể cả lớp vỏ, để thay đổi hay
kiểm soát cách thức trình tiện ích này vận hành.
Perl cho bạn một cách kiểm tra và thay đổi biến môi
trường hiện tại qua một mảng kết hợp buồn cười gọi
là %ENV (chữ hoa). Mỗi khoá của mảng này tương ứng
với tên của một biến môi trường, với giá trị tương ứng,
tất nhiên, là giá trị tương ứng. Việc xem xét mảng này
ngay đầu chương trình chỉ ra cho bạn môi trường được
trao cho Perl bởi lớp vỏ cha mẹ - việc thay đổi mảng này
ảnh hưởng tới môi trường do Perl sử dụng và cho các
tiến trình con của nó.
Chẳng hạn, sau đây là một chương trình đơn giản
hành động giống như printenv:
for $key (sort keys %ENV) { print “$key = $ENV{$key}\n” ; }
Lưu ý rằng dấu bằng ở đây không phải là phép gán,
nhưng đơn giản là một kí tự văn bản mà print dùng để in
ra chất liệu như TERM=xterm hay USER=merlyn.
Sau đây là một chương trình nhãi ranh làm thay đổi
giá trị của PATH để bảo đảm rằng chỉ lệnh grep do
system cho chạy được tìm tới chỉ ở những chỗ thông
234
thường:
$oldPATH = $ENV{“PATH”} ; # cất giữ đường dẫn trước $ENV{“PATH”} = “/bin:/usr/bin:/usr/ucb” ; # buộc đường
dẫn đã biết system (“grep fred bedrock > output”) ; cho chạy chỉ lệnh $ENV{“PATH”} = $oldPATH ; # khôi phục lại đường dẫn
trước
Toán tử system cũng có thể nhận một danh sách đối,
thay vì một đối. Trong trường hợp này, thay vì để cho
lớp vỏ diễn giải danh sách đối, Perl xử lí đối thứ nhất
như một chỉ lệnh để chạy (được định vị tương ứng theo
PATH nếu cần thiết) và các đối còn lại như các đối cho
chỉ lệnh không có diễn giải lớp vỏ thông thường. Nói
cách khác, bạn không cần trích dẫn khoảng trắng hay lo
lắng về các đối có chứa dấu ngoặc nhọn bởi vì tất cả
những cái đó đơn thuần chỉ là các kí tự để truyền cho
chương trình. Vậy, hai chỉ lệnh sau đây là tương đương:
system “grep ‘fred flƯnttone’ buffaloes” ; # dùng lớp vỏ system “grep”, “fred flƯnttone”, “buffaloes” ; # dùng danh
sách
Cho một danh sách chứ không cho một xâu đơn sẽ
tiết kiệm cho bạn một tiến trình lớp vỏ, cho nên làm điều
này khi bạn có thể. (Thực ra, khi dạng một đối của
system là đủ đơn giản, Perl tối ưu toàn bộ lời gọi lớp vỏ,
gọi chương trình kết quả một cách trực tiếp dường như
bạn đã dùng lời gọi nhiều đối.)
Sau đây là một thí dụ khác về các dạng tương
đương:
@cfiles = ( “fred.c”, “barney.c”) ; # tệp phải dịch @options = ( “-DHARD”, “-DGRANITE”) ; # tuỳ chọn system “cc -o slate @option @cfiles” ; # dùng lớp vỏ system “cc, “-o”, “slate”, @options, @cfiles ; # dùng danh
235
sách
Dùng dấu nháy đơn ngược
Một cách khác để phát động một tiến trình là đặt
dòng chỉ lệnh vỏ /bin/sh giữa các dấu nháy đơn ngược.
Giống như lớp vỏ, dòng lệnh này phát ra một chỉ lệnh và
đợi việc hoàn tất, đưa ra đầu ra chuẩn. Không giống như
lớp vỏ, văn bản kết quả không được mở rộng tại chỗ (trở
thành nhiều cái vào của kết cấu Perl) mà thay vì thế trở
thành giá trị của xâu nháy đơn ngược. Chẳng hạn:
$snow = “Thời gian bây giờ là ” . `date` ; # lấy văn bản và ngày đưa ra
Giá trị của $now sẽ là văn bản Thời gian bây giờ là
cùng với kết quả của chỉ lệnh date (kể cả dấu xuống
dòng cuối cùng), cho nên nó giống như:
Thời gian bây giờ là Fri Aug 13 23:59:59 PDT 1993
Nếu chỉ lệnh dấu nháy đơn ngược được dùng trong
hoàn cảnh mảng chứ không phải hoàn cảnh vô hướng,
bạn thu được một danh sách các xâu, mỗi một xâu là một
dòng (được kết thúc với dấu xuống dòng) từ cái ra của
chỉ lệnh. Với thí dụ về date, chúng chỉ có một phần tử,
bởi vì nó chỉ sinh ra một dòng văn bản. Cái ra của who,
trong giống thế này:
merlyn tty13 Sep 1 14:55 fred tty1A Aug 31 07:02 barney tty1F Sep 1 09:22
Sau đây là cách lấy cái ra này trong ngữ cảnh mảng:
foreach $_ (`who`) { # một lần cho mỗi dòng văn bản từ who
236
($who, $where, $when) = / (\S+)\s+(\S+)\s+(.*)/ ; print “$who trên $where tại $when\n” ; }
Mỗi bước qua chu trình đã làm việc trên một dòng
tách biệt của cái ra của who, bởi vì chỉ lệnh dấu ngoặc
đến ngược được tính bên trong hoàn cảnh mảng.
Cái vào chuẩn và lỗi chuẩn của chỉ lệnh bên trong
dấu nháy đơn ngược là được kế thừa từ tiến trình Perl.
Điều này có nghĩa là bạn thông thường chỉ lấy được cái
ra chuẩn của các chỉ lệnh bên trong dấu nháy đơn ngược
như giá trị của xâu dấu nháy đơn ngược. Một điều thông
dụng thường làm là gộp lỗi chuẩn vào trong cái ra chuẩn
để cho chỉ lệnh dấu nháy đơn ngược lấy cả hai, dùng kết
cấu 2>&1 của lớp vỏ:
die “rm mawi!” if ‘rm fred 2>&1’ ;
Tại đây, tiến trình Perl được kết thúc nếu rm không
nói gì, hoặc nói với cái ra chuẩn và lỗi chuẩn, bởi vì kết
quả sẽ không là một xâu rỗng (xâu rỗng sẽ sai).
Dùng các tiến trình như tước hiệu tệp
Còn cách khác để phát động một tiến trình là tạo ra
một tiến trình giống như một tước hiệu tệp (tương tự như
trình thư viện popen nếu bạn quen thuộc với nó). Bởi vì
tước hiệu tệp mở để hoặc đọc hoặc ghi, nên chúng ta có
thể tạo ra một tước hiệu tệp-tiến trình mà hoặc lấy cái ra
từ hoặc cung cấp cái vào cho tiến trình. Sau đây là một
thí dụ về việc tạo ra một tước hiệu từ tiến trình who. Bởi
vì tiến trình này đang sinh ra cái ra nên chúng ta tạo ra
một tước hiệu mở để đọc, như:
237
open (WHOPROC, “who |”) ; # mở để đọc
để ý đến thanh sổ đứng ở phía bên phải của who.
Thanh đó báo cho Perl rằng open này không phải là tên
tệp, mà thay vào đó là chỉ lệnh cần bắt đầu. Bởi vì thanh
này là bên phải của chỉ lệnh, nên tước hiệu tệp được mở
để đọc, có nghĩa là cái ra chuẩn của who được dự định bị
bắt giữ. (Cái vào chuẩn và lỗi chuẩn vẫn còn được dùng
chung với tiến trình Perl.) Với phần còn lại của chương
trình này, WHOPROC giải quyết đơn thuần tước hiệu
tệp mà đã mở để đọc, có nghĩa là tất cả các toán tử
vào/ra tệp thông thường đã áp dụng được. Sau đây là
cách đọc dữ liệu từ chỉ lệnh who vào mảng:
@whosaid = <WHOPROC> ;
Tương tự, mở một chỉ lệnh mà trông đợi cái vào,
chúng ta có thể mở một tước hiệu tệp-tiến trình để ghi
bằng việc đặt thanh sổ đứng ở bên trái của chỉ lệnh, như:
open(LPR, “| lpr -Pslatewrite”) ; print LPR @rockreport ; close(LPR) ;
Lưu ý rằng trong trường hợp này, sau việc mở LPR,
chúng ta ghi dữ liệu nào đó lên nó, và rồi đóng nó lại.
Việc mở một tiến trình với tước hiệu tệp-tiến trình cho
phép chỉ lệnh được thực hiện song song với chương trình
Perl. Việc nói close() trên tước hiệu tệp này buộc chương
trình Perl phải đợi đến khi tiến trình này ra. Nếu bạn
không đóng tước hiệu tệp, tiến trình này có thể tiếp tục
chạy thậm chí bên ngoài việc thực hiện chương trình
Perl.
Việc mở một tiến trình để ghi sẽ làm cho đầu vào
chuẩn tới từ tước hiệu tệp. Tiến trình này dùng chung cái
238
ra chuẩn và lỗi chuẩn với Perl. Như trước đây, bạn có thể
dùng việc định hướng vào-ra kiểu /bin/sh, cho nên ở đây
có một cách đơn giản để lược bỏ thông báo lỗi từ chỉ
lệnh lpr trong thí dụ trước:
open(LPR, “| lpr -Pslatewrite > /dev/null 2>&1”) ;
/dev/null gây ra cái ra chuẩn được định hướng lại tới
thiết bị không. 2>&1 gây cho lỗi chuẩn được gửi tới chỗ
cái ra chuẩn được gửi tới, kết quả lỗi sẽ bị cắt bỏ đi.
Bạn thậm chí có thể tổ hợp tất cả những điều này lại,
sinh ra một báo cáo về mọi người chỉ trừ Fred trong danh
sách các ô đã vào, kiểu như:
open(WHO, “who |”) ; open (LPR, “| lpt -Pslatewrite”) ; while (<WHO>) { unless (/fred/) { # không đưa ra fred print LPR $_ ; } } close (WHO) ; close (LPR) ;
Khi đoạn chương trình này đọc từ tước hiệu WHO
một dòng mỗi lúc, nó in ra tất cả các dòng mà không
chứa xâu fred cho tước hiệu LPR. Cho nên chỉ cái ra trên
máy in mới là dòng không chứa fred.
Dùng fork
Vẫn còn một cách khác để tạo ra một tiến trình phụ
là bám theo một tiến trình Perl hiện tại bằng việc dùng
nguyên sơ UNIX có tên là fork. Toán tử fork đơn giản làm
điều mà lời gọi hệ thống fork thực hiện; toán tử này tạo ra
239
một bản bám theo tiến trình hiện tại. Bản bám theo này
(được gọi là con, với bản gốc được gọi là bố mẹ) dùng
chung cùng chương trình thực hiện, biến, và thậm chí cả
các tệp đã mở. Để phân biệt hai tiến trình này, giá trị cho
lại từ toán tử fork là không đối với con, và khác không
đối với bố mẹ. Bạn có thể kiểm tra điều này và hành
động tương ứng:
if (fork) { # tôi là bố mẹ } else { # tôi là con }
Đẻ dùng tốt nhất bản bám theo, chúng ta cần học về
vài điều mới nữa mà song song với các thành phần cùng
tên trong UNIX: các toán tử wait, exit và exec.
Toán tử đơn giản nhất là exec. Nó cũng giống như
toán tử system, ngoại trừ rằng thay vì dùng thực hiện tiến
trình mới để thực hiện chỉ lệnh vỏ, Perl thay thế tiến
trình hiện tại với lớp vỏ. (Trong cách nói UNIX, Perl
thực hiện vỏ). Sau khi toán tử exec thực hiện thành công,
chương trình Perl kết thúc, bị thay thế bởi chương trình
được yêu cầu. Chẳng hạn:
exec “date” ;
thay thế chương trình Perl hiện tại bằng chỉ lệnh
date, làm cho cái ra của date chuyển thẳng ra cái ra
chuẩn của chương trình Perl. Khi chỉ lệnh date kết thúc,
không còn gì phải thực hiện nữa bởi vì chương trình Perl
đã xong.
Một cách khác để nhìn vào điều này là ở chỗ toán tử
system giống như fork có theo sau một exec, như sau:
240
# Phương pháp 1... dùng hệ thống: system (“date”) ; # Phương pháp 2... dùng fork/exec unless (fork) { # fork cho không, cho nên tôi là con và tôi exec: exec (“date”) ; # tiến trình con trở thành chỉ lệnh date }
Việc dùng fork và exec theo cách này không hoàn
toàn phải lắm, bởi vì chỉ lệnh date và tiến trình bố mẹ cả
hai đã chạy đồng thời, có thể đan chéo cái ra và làm mọi
sự lẫn lộn. Điều cần là cách để báo cho tiến trình bố mẹ
đợi cho tới khi tiến trình con hoàn tất. Điều đó được xác
là điều toán tử wait thực hiện - nó đợi cho tới khi tiến
trình con (bất kì con nào, cần phải chính xác) đã hoàn
tất:
unless (fork) { # fork cho không, cho nên tôi là con và tôi exec: exec (“date”) ; # tiến trình con trở thành chỉ lệnh date } wait ; # bố mẹ đợi cho con (date) hoàn tất
Nếu tất cả điều này dường như khá mờ với bạn, bạn
có lẽ nên nghiên cứu các lời gọi hệ thống fork và exec
trong sách UNIX truyền thống, vì Perl thường lấy khá
trực tiếp lời gọi hệ thống UNIX chuyển sang.
Toán tử exit() tạo nên việc đi ra tức khắc khỏi tiến
trình Perl hiện tại. Bạn đã dùng cách này để bỏ chương
trình Perl từ đâu đó ở giữa, hay với toán tử fork để thực
hiện chương trình Perl nào đó đang tiến hành và rồi ra.
Sau đây là một trường hợp của việc loại bỏ một số tệp
trong /tmp trên nền tảng dùng một tiến trình Perl chẽ ra.
unless (fork) { # Tôi là tiến trình con
unkink < /tmp/bedrock.* > ; # phá các tệp này
241
exit ; # tiến trình con dùng thực hiện ở đây } # Tiến trình bố mẹ tiếp tụ ở đây
Không có exit, tiến trình con vẫn tiếp tục thực hiện
chương trình Perl (tại dòng đã đánh dấu Tiến trình bố mẹ
tiếp tục ở đây) và đó dứt khoát không phải là điều chúng
ta muốn.
Toán tử exit nhận một tham biến phụ, phục vụ như
giá trị ra số mà có thể được tiến trình bố mẹ lưu ý tới.
Giá trị mặc định là cho việc ra với giá trị không, chỉ ra
rằng mọi thứ đã ổn thoả.
Tóm tắt về các phép toán tiến trình
Bảng 14-1 tóm tắt các phép toán mà bạn có để khởi
động tiến trình.
Bảng 14-1 Tóm tắt về các phép toán tiến trình
Phép toán Cái vào
chuẩn
Cái ra
chuẩn
Lỗi
chuẩn
Cần đợi
không?
system()
Kế thừa từ chương trình
Kế thừa từ chương trình
Kế thừa từ chương trình
Có
xâu nháy đơn ngược
Kế thừa từ chương trình
được lấy như giá trị xâu
Kế thừa từ chương trình
Có
chỉ lệnh open() xem như tước hiệu tệp cho cái ra
Được nối với tước hiệu tệp
Kế thừa từ chương trình
Kế thừa từ chương trình
Chỉ vào lúc close()
242
Phép toán Cái vào
chuẩn
Cái ra
chuẩn
Lỗi
chuẩn
Cần đợi
không?
chỉ lệnh open() xem như tước hiệu tệp cho cái vào
Kế thừa từ chương trình
Được nối với tước hiệu tệp
Kế thừa từ chương trình
Chỉ vào lúc close()
fork, exec, wait
Người dùng lựa chọn
Người dùng lựa chọn
Người dùng lựa chọn
Người dùng lựa chọn
Việc tạo tiến trình đơn giản nhất là với toán tử
system. Cái vào, cái ra chuẩn và lỗi chuẩn đã không bị
ảnh hưởng (chúng ta được kế thừa từ tiến trình Perl).
Một xâu nháy đơn ngược tạo ra một tiến trình, lấy cái ra
chuẩn của tiến trình này như một giá trị xâu cho chương
trình Perl. Cái vào chuẩn và lỗi chuẩn không bị ảnh
hưởng. Cả hai phương pháp này đã đòi hỏi rằng tiến
trình này kết thúc trước khi bất kì chương trình Perl nào
được thực hiện.
Một cách đơn giản để có tiến trình dị bộ (tiến trình
mà cho phép chương trình Perl tiếp tục trước khi tiến
trình này được hoàn tất) là để mở một chỉ lệnh như một
tước hiệu tệp, tạo ra một đường ống cho cái vào chuẩn
hay cái ra chuẩn của chỉ lệnh. Một chỉ lệnh được mở như
tước hiệu tệp để đọc sẽ kế thừa cái vào chuẩn và lỗi
chuẩn từ chương trình Perl; một chỉ lệnh được mở như
một tước hiệu tệp để ghi sẽ kế thừa cái ra chuẩn và lỗi
chuẩn từ chương trình Perl.
Cách mềm dẻo nhất để bắt đầu một tiến trình là để
cho chương trình của bạn gọi các toán tử fork, exec và
243
wait, mà tương ứng trực tiếp với các tên lời gọi hệ thống
UNIX. Bằng việc dùng các toán tử này, bạn có thể chọn
liệu bạn có chờ đợi hay không, và đặt cấu hình cho cái
vào, cái ra và lỗi chuẩn theo bất kì cách nào bạn chọn* .
Gửi và nhận tín hiệu
Một phương pháp cho việc truyền thông liên tiến
trình là gửi và nhận tín hiệu. Tín hiệu là thông báo một
bit (có nghĩa là “tín hiệu này đã xảy ra”) được gửi cho
một tiến trình từ một tiến trình khác hoặc từ lõi UNIX.
Các tín hiệu được đánh số, thông thường từ một tới một
số nhỏ hơn 15 hay 31. Một số tín hiệu đã có ý nghĩa định
trước và được gửi một cách tự động cho một tiến trình
dưới một điều kiện nào đó (như lỗi bộ nhớ hay ngoại lệ
dấu phẩy động) - một số khác riêng do người dùng sinh
ra từ các tiến trình khác. Các tiến trình này phải có phép
để gửi tín hiệu như vậy. Nói chung, nếu tiến trình có
cùng userID, tín hiệu là được phép.
Việc đáp ứng cho một tín hiệu được gọi là hành
động của tín hiệu đó. Các tín hiệu định sẵn có những
hành động mặc định có ích nào đó, như bỏ tiến trình hay
cho chạy tiếp. Các tín hiệu khác hoàn toàn bị bỏ qua theo
mặc định. Gần như tất cả các tín hiệu đã có thể có hành
động mặc định của nó bị thay đổi, hoặc là bị bỏ qua hoặc
bị bắt giữ (gọi phần chương trình do người dùng xác
định một cách tự động).
Cho tới nay, tất cả điều này đã là chất liệu kiểu
* Mặc dầu cũng có thể có ích là biết về open (STDERR,
“>&STDOUT”) cho lại tước hiệu tệp.
244
UNIX - tại đây nó mang theo nghĩa riêng của Perl. Khi
một tiến trình Perl bắt lấy một tín hiệu, một chương trình
con do bạn chọn sẽ được gọi một cách dị bộ và tự động,
tạm thời ngắt bất kì cái gì đang được thực hiện. Khi
chương trình con này ra, bất kì cái gì đang được thực
hiện cũng đã được chạy lại dường như không có gì xảy
ra cả (ngoại trừ các hành động được thực hiện bởi
chương trình con con này, nếu có).
Một cách điển hình, chương trình con bắt tín hiệu sẽ
thực hiện một trong hai điều: bỏ chương trình sau khi
thực hiện một số việc dọn sạch, hoặc đặt cờ nào đó (như
biến toàn cục) mà chương trình này đã đặn kiểm tra.
Bạn cần biết tín hiệu nào được đặt tên để đóng kí
cho bộ giải quyết tín hiệu với Perl. Bằng việc đóng kí
với bộ giải quyết tín hiệu, Perl sẽ gọi chương trình con
được lựa chọn khi nhận được tín hiệu này.
Các tên tín hiệu được định nghĩa trong tài liệu lời
gọi hệ thống signal, và thông thường cũng có trong tệp
bao hàn của C /usr/include/sys/signal.h. Các tên nói
chung được bắt đầu với SIG, như SIGINT, SIGQUIT và
SIGKILL. Để khai báo chương trình con
&my_sigint_catcher như bộ xử lí tín hiệu giải quyết với
SIGINT, chúng ta đặt một giá trị vào trong mảng kết
hợp %SIG ảo thuật. Trong mảng này, chúng ta đặt một
giá trị của khoá INT (đó là SIGINT không có SIG) cho
tên của trình con mà sẽ bắt tín hiệu SIGINT, kiểu như:
$SIG {‘INT’ } = ‘my_sigint_catcher’ ;
Nhưng chúng ta cũng cần một định nghĩa cho trình
con đó. Sau đây là một thí dụ đơn giản:
sub my_sigint_catcher {
245
$saw_signit = 1 ; # đặt cờ }
Bộ bắt tín hiệu này đặt một biến toàn cục, và rồi ra
ngay lập tức. Việc trở về từ trình con này gây ra việc
thực hiện lại bất kì chỗ nào nó đã bị ngắt. Một cách điển
hình, trước hết bạn đặt không cho cờ $saw_sigint, đặt
trình con này như bộ bắt SIGINT, và rồi thực hiện trình
bạn vẫn chạy, kiểu như:
$saw_sigint = 0 ; # xoá cờ $SIG {‘INT’ } = ‘my_sigint_catcher’ ; # đóng kí bộ bắt foreach (@huge_array) { # làm điều gì đó # làm thêm vài việc # vẫn làm thêm vài việc if ($saw_sigint) { # cần ngắt không? # dọn dẹp gì đó ở đây last ; } } $SIG {‘INT’} = ‘DEFAULT’ ; # khôi phục hành động mặc
định
Mẹo ở đây là chỗ giá trị của cờ này được kiểm tra
tại những điểm có ích trong khi tính toán và được dùng
để ra khỏi chu trình khi chưa xong, tại đây cũng giải
quyết một số hành động dọn dẹp. Lưu ý tới câu lệnh cuối
trong chương trình trên: đặt hành động DEFAULT khôi
phục lại hành động ngầm định trên một tín hiệu đặc biệt
(SIGINT khác sẽ bỏ chương trình này ngay lập tức). Một
giá trị có ích đặc biệt khác giống thế này là IGNORE, có
nghĩa là bỏ qua tín hiệu này (nếu hành động mặc định
không bỏ qua tín hiệu đó, như SIGINT). Bạn có thể làm
một hành động tín hiệu IGNORE nếu không cần hành
động dọn dẹp nào, và bạn không muốn kết thúc các phép
246
toán sớm.
Một trong nhiều cách để tín hiệu SIGINT được sinh
ra là bằng việc để cho người dùng nhấn vào một kí tự
ngắt đã chọn (như Delete hay Control-C) trên bàn phím.
Nhưng một tiến trình cũng có thể sinh ra tín hiệu
SIGINT trực tiếp bằng việc dùng toán tử kill. Toán tử này
nhận một số hiệu hay tên tín hiệu, và gửi tín hiệu đó cho
danh sách các tiến trình được cho theo tín hiệu này. Cho
nên việc gửi một tín hiệu từ một chương trình đòi hỏi
phải xác định số hiệu tiến trình của các tiến trình nhận
(số hiệu tiến trình được cho lại từ một số các toán tử,
như fork hay việc mở một chương trình như một tước
hiệu tệp). Giả sử bạn muốn gửi một tín hiệu 2 (cũng còn
được biết như SIGINT) cho các tiến trình có số hiệu 234
và 237. Đơn giản hơn cả là như thế này:
kill (2, 234, 237) ; # gửi SIGINT cho 234 và 237
Bài tập
Xem trả lời ở phụ lục A
1. Viết một chương trình phân tích cái ra của chỉ lệnh
date để thu được ngày hiện tại của tuần. Nếu ngày
của tuần là ngày làm việc, in chữ làm việc, ngoài ra in
chữ đi chơi.
2. Viết một chương trình nhận tất cả các tên thực của
người dùng từ tệp /etc/passwd, rồi biến đổi thành cái
ra của chỉ lệnh who, bằng việc thay thế tên đăng nhập
(cột thứ nhất) bởi tên thật. (Hướng dẫn: tạo ra một
mảng kết hợp có khoá là tên đăng nhập và giá trị là
247
tên thật.) Thử cả hai với chỉ lệnh who trong trường
hợp dấu nháy đơn ngược và được mở như đường
ống. Cái nào dễ hơn?
3. Sửa đổi chương trình trên để cho cái ra tự động đi ra
máy in.
4. Giả sử hàm mkdir() bị hỏng. Viết một trình con mà
không dùng mkdir(), nhưng thay vào đó gọi /bin/mkdir
bằng system(). (Phải chắc rằng nó làm việc với các
danh mục có một dấu cách trong tên.)
5. Mở rộng trình từ bài tập trước để sử dụng chmod() để
đặt phép sử dụng.
248
249
15
Biến đổi
dữ liệu khác
Tìm một xâu con
Tìm một xâu con phụ thuộc vào nơi bạn mất nó. Nếu
bạn ngẫu nhiên mất nó bên trong một xâu lớn hơn, bạn
còn may mắn, vì index() có thể giúp bạn tìm ra. Sau đây
là dáng vẻ của nó:
$x = index ($string, $substring) ;
Perl định vị lần xuất hiện đầu tiên của substring bên
trong string, cho lại một số nguyên chỉ vị trí của kí tự đầu
tiên. Giá trị chỉ số này được cho lại dựa trên không - tức
là nếu tìm được substring ở chỗ bắt đầu của string, bạn
nhận được 0. Nếu nó là ở một kí tự sau đó, bạn nhận
được 1, và cứ như thế. Nếu không tìm thấy substring
Trong chương này:
Tìm và thay thế
Trích và thay thế xâu con
Định dạng dữ liệu dùng sprìnt()
Sắp xếp nâng cao
Chuyển tự
250
trong string, bạn nhận được -1.
xem những điều sau:
$where = index(“hello”, “e”); # $where nhận 1 $oerson = “barney”; $where = index(“fred barney”, $person) ; # $where nhận
5 @rockers = (“fred”, “barney”) ; $where = index(join(“ “, @rockers), $person); # cũng thế
Chú ý rằng cả hai xâu này đã được tìm kiếm và xâu
được tìm kiếm, có thể là một hằng xâu kí hiệu, một biến
vô hướng có chứa một xâu, hay thậm chí một biểu thức
có giá trị xâu vô hướng. Sau đây là một số thí dụ nữa:
$which = index(“a very long string”, “long”); # $switch nhận 7
$which = index(“a very long string”, “lame”); # $switch nhận -1
Nếu xâu có chứa xâu con tại nhiều vị trí, toán tử
index() sẽ cho lại vị trí bên trái nhất. Để tìm ra các vị trí
sau, bạn có thể cho index() tham biến thứ ba. Tham biến
này là giá trị tối thiểu mà index() sẽ cho lại, cho phép bạn
tìm lần xuất hiện tiếp của xâu con theo sau một vị trí đã
chọn. Nó trông tựa như thế này:
$x = index($bigtring, $littlestring, $skip);
Sau đây là một số thí dụ về cách tham biến thứ ba
làm việc:
$where = index(“hello world”, “l”); # cho lại 2 (l đầu tiên) $where = index(“hello world”, “l”, 0); # cũng thế $where = index(“hello world”, “l”, 1); # vẫn thế $where = index(“hello world”, “l”,3); # bây giờ cho lại 3 # (3 là vị trí đầu tiên lớn hơn hay bằng 3) $where = index(“hello world”, “o”, 5); # cho lại 7 (0 thứ
251
hai) $where = index(“hello world”, “o”, 8); # cho lại -1 (hết sau
8)
Đi theo lối khác, bạn có thể quét từ bên phải để có
được sự xuất hiện bên phải nhất bằng việc dùng rindex().
Giá trị cho lại vẫn là số các kí tự giữa đầu bên trái của
xâu và chỗ bắt đầu của xâu con, như trước, nhưng bạn sẽ
nhận được sự xuất hiện về bên phải nhất thay vì sự xuất
hiện bên trái nhất nếu có nhiều sự xuất hiện. Toán tử
rindex() cũng nhận tham biến thứ ba giống như index() ,
để cho bạn có thể có được sự xuất hiện ít hơn hay bằng
vị trí đã chọn. Sau đây là một số thí dụ về điều bạn nhận
được:
$w = rindex(“hello world”, “he”); # $w nhận 0 $w = rindex(“hello world”, “l”); # $w nhận 9 (l bên phải
nhất) $w = rindex(“hello world”, “o”); # $w nhận 7 $w = rindex(“hello world”, “o ”); # $w nhận 4 $w = rindex(“hello world”, “xx”); # $w nhận -1 (không
thấy) $w = rindex(“hello world”, “o”, 6); # $w nhận 4 (đầu tiên
trước 6) $w = rindex(“hello world”,“o”, 3); # $w nhận -1 (không
thấy trước 3)
Trích và thay thế một xâu con
Việc lấy ra một mẩu của xâu có thể được thực hiện
bằng việc áp dụng cẩn thận các biểu thức chính qui,
nhưng nếu mẩu này bao giờ cũng ở tại một vị trí kí tự đã
biết, việc này là không hiệu quả. Thay vì vậy, bạn nên
dùng substr(). Toán tử này nhận ba đối: một giá trị xâu,
một vị trí bắt đầu (được đo tựa như nó đã được đo cho
252
index()), và một chiều dài, giống như:
$s = substr ($string, $start, $length);
Vị trí bắt đầu làm việc giống như index; kí tự đầu
tiên là không, kí tự thứ hai là một, và cứ thế. Chiều dài là
số các kí tự cần nắm lấy tại điểm đó: chiều dài bằng
không có nghĩa là không có kí tự nào, bằng một có nghĩa
là lấy kí tự đầu tiên, bằng hai có nghĩa là hai kí tự, và
vân vân. (Nó dùng lại tại cuối xâu, cho nên nếu bạn tìm
kiếm quá nhiều, cũng không sao cả.) Nó trông giống thế
này:
$hello = “hello, world!”; $grab = substr($hello, 3, 2); # $grap nhận “lo” $grab = substr($hello, 7, 100); # 7 đến cuối, hay “world!”
Bạn thậm chí có thể tạo ra toán tử “nâng lên luỹ thừa
mười” cho các số mũ nguyên nhỏ, như trong:
$big = substr(“10000000000”, 0, $power+1); # 10**$power
Nếu số đếm các kí tự là không hay bé hơn không,
một xâu rỗng sẽ được cho lại. Mặt khác, nếu vị trí bắt
đầu là bé hơn không, vị trí bắt đầu được tính theo số các
kí tự từ cuối xâu. Cho nên giá trị -1 đối với vị trí bắt đầu
và 1 (hay nhiều) đối với chiều dài sẽ cho bạn kí tự cuối.
Tương tự, -2 cho vị trí bắt đầu với kí tự thứ hai kể từ
cuối. Giống thế:
$stuff = substr(“a very long string”, -3, 3); # ba kí tự cuối $stuff = substr(“a very long string”, -3, 1); # kí tự “i”
Nếu vị trí bắt đầu là trước chỗ mở đầu của xâu
(giống như một số âm khổng lồ lớn hơn chiều dài của
xâu), chỗ mở đầu sẽ là vị trí bắt đầu (dường như bạn đã
dùng 0 làm vị trí bắt đầu). Nếu vị trí bắt đầu là một số
253
dương khổng lồ, xâu rỗng bao giờ cũng được cho lại.
Nói cách khác, nó có thể làm điều bạn trông đợi nó phải
làm, chừng nào bạn còn trông đợi, nó bao giờ cũng cho
lại một cái gì đó khác hơn là một lỗi.
Bỏ đi đối chiều dài, cũng hệt như bạn đã đưa một số
khổng lồ vào cho đối đó - nắm lấy mọi thứ từ vị trí đã
chọn cho tới cuối xâu* .
Nếu đối thứ nhất của substr() là một biến (nói cách
khác, nó có thể xuất hiện bên vế trái của toán tử gán),
bản thân substr() cũng có thể xuất hiện ở vế bên trái của
toán tử gán. Điều này trông có vẻ kì lạ nếu bạn bắt
nguồn từ nền tảng C, nhưng nếu bạn đã chơi với vài dị
bản của BASIC, nó hoàn toàn là thông thường.
Điều nhận được sự thay đổi như kết quả của phép
gán như thế là phần của xâu sẽ được cho lại, mà có
substr() được dùng trong một biểu thức. Nói cách khác,
substr($var, 3, 2) cho lại kí tự thứ tư và thứ năm (bắt đầu
từ 3, vì số đếm 2), cho nên việc gán điều đó làm thay đổi
hai kí tự này cho $var. Giống như:
$hw = “hello world!”; substr($hw, 0, 5) = “howdy”; # $hw bây giờ là “howdy
world!”
Chiều dài của văn bản thay thế (cái nhận được việc
gán vào trong substr) không phải là cùng như văn bản
được thay thế, như trong thí dụ này. Xâu này sẽ tự động
tăng trưởng hay co lại khi cần để điều hoà với văn bản.
* Các bản Perl cổ hơn không cho phép bỏ đi đối thứ ba, dẫn tới việc
những lập trình viên Perl tiền phong đã dùng số khổng lồ cho đối
đó. Bạn có thể vượt qua điều này trong cuộc hành trình khảo cổ Perl
của mình.
254
Sau đây là một thí dụ về việc xâu thành ngắn hơn:
substr($hw, 0, 5) = “hi”; # $hw bây giờ là “hi world!”
và đây là một xâu thành dài hơn:
substr($hw, -6, 5) = “worldwide news”; # thay thế “world”
Việc co lại hay dãn ra, khá hiệu quả, cho nên bạn
đừng lo lắng về việc dùng chúng một cách bất kì, mặc
dầu việc thay thế một xâu bằng một xâu chiều dài tương
đương, vẫn nhanh hơn nếu bạn có cơ hội.
Dạng thức dữ liệu bằng sprintf()
Toán tử printf đôi khi cũng dễ sử dụng khi được dùng
để lấy một danh sách các giá trị và tạo ra một dòng ra
cho hiển thị các giá trị đó theo cách điều khiển được.
Toán tử sprintf() là đồng nhất với printf về các đối, nhưng
cho lại bất kì cái gì đã được printf đưa ra như một xâu
riêng biệt. (Nghĩ về điều này như “xâu printf”.) Chẳng
hạn, để tạo ra một xâu bao gồm chữ X theo sau bởi một
giá trị năm chữ số lấp đầy bởi không của $y, cũng dễ
dàng như:
$result = sprintf(“X%05d”, $y);
Nếu bạn không quen thuộc với xâu dạng thức của
printf và sprintf, tham chiếu tài liệu printf hay sprintf. (Tên
sprintf, thực ra có nguồn gốc từ trình thư viện cùng tên.)
Sắp xếp nâng cao
Trước đây, bạn đã biết rằng bạn có thể lấy một danh
sách rồi sắp xếp nó theo thứ tự ASCII tăng dần (như các
255
xâu), bằng cách dùng toán tử sort có sẵn. Điều gì sẽ xảy
ra nếu bạn không muốn sắp xếp theo thứ tự ASCII tăng
dần, mà thay vì thế là một cái gì đó khác, kiểu như sắp
xếp số? Được, Perl cho bạn công cụ bạn cần để làm việc
này. Thực ra, bạn sẽ thấy rằng sort của Perl là hoàn toàn
tổng quát và có thể thực hiện bất kì thứ tự sắp xếp nào.
Để định nghĩa việc sắp xếp theo mầu sắc khác, bạn
cần định nghĩa một trình so sánh mà mô tả cho cách so
sánh hai phần tử. Tại sao điều này lại cần thiết? Thế này,
nếu bạn nghĩ về nó, sắp xếp là việc đặt một bộ các thứ
theo một trật tự so sánh tất cả chúng với nhau. Vì bạn
không thể nào so sánh chúng ngay một lúc nên bạn cần
so sánh hai phần tử mỗi lúc, để cuối cùng dùng cái bạn
phát hiện ra về thứ tự cho từng cặp mà đặt toàn bộ cả lũ
vào hàng.
Trình so sánh được định nghĩa như một trình thông
thường. Trình này sẽ được gọi đi gọi lại, mỗi lần lại
truyền hai phần tử của danh sách cần được sắp. Trình
này phải xác định liệu giá trị thứ nhất là bé hơn, bằng
hay lớn hơn giá trị thứ hai, và cho lại một giá trị mã hoá
(sẽ được mô tả ngay sau đây). Tiến trình này được lặp lại
cho tới khi danh sách được sắp hoàn toàn.
Để tiết kiệm tốc độ thực hiện một chút, hai giá trị
này không được truyền trong mảng, mà thay vì thế được
trao cho một trình con như giá trị của biến toàn cục $a và
$b. (Bạn đừng lo - giá trị nguyên thuỷ của $a và $b được
bảo vệ an toàn.) Trình này nên cho lại một giá trị âm nào
đó nếu $a “bé hơn’ $b, bằng không nếu $a “bằng” $b, và
bất kì số dương nào nếu $a “lớn hơn” $b. Bây giờ nhớ,
“bé hơn” là tương ứng với ý nghĩa của bạn về “bé hơn” -
nó có thể là một so sánh số, tương ứng với kí tự thứ ba
256
của xâu, hay thậm chí tương ứng với giá trị của bất kì
mảng nào có dùng các giá trị được truyền vào như khoá.
Đấy mới thực sự là mềm dẻo.
Sau đây là một thí dụ về một chương trình con sắp
xếp mà sẽ sắp mọi thứ theo thứ tự số:
sub by_number { if ($a < $b) { -1; } elsif ($a == $b) { 0; } elsif ($a > $b) { 1; } }
chú ý đến tên by_number. Không có gì đặc biệt về
cái tên của trình con này, nhưng bạn sẽ thấy tại sao tôi
lại thích cái tên bắt đầu bởi by_ trong giây lát.
Nhìn qua trình con này. Nếu giá trị của $a là bé hơn
(về mặt số trong trường hợp này) giá trị của $b, thì cho
lại giá trị -1. Nếu các giá trị là bằng nhau về con số, cho
lại không, còn ngoài ra cho lại 1. Vậy, theo mô tả của
cho trình so sánh sắp xếp, điều này sẽ làm việc.
Làm sao dùng được nó? Thử sắp xếp danh sách sau.
$somelist = (1,2,4,8,16,32,64,128,256);
Nếu dùng sort thông thường không sang sửa lại danh
sách, sẽ được các số sắp như chúng là các xâu, và theo
trật tự ASCII, tựa như:
@wronglist = sort @somelist; # @wronglist bây giờ là (1,128,16,2,256,32,64,8)
Chắc chắn đấy không phải là sắp xếp số. Thôi được,
257
sẽ cho sort một trình sắp xếp được định nghĩa mới. Tên
của trình sắp xếp đi ngay sau từ khoá sort, như:
@rightlist = sort by_number @wronglist; # @rightlist bây giờ là (1,2,4,8,32,64,128,256)
Đây quả là mẹo. Chú ý rằng bạn có thể đọc sort với
trình con sắp xếp đi kèm theo kiểu con người: “sắp xếp
theo số”. Đó là lí do tại sao tôi đặt tên cho trình con với
tiền tố by_.
Nếu bạn cũng thiên về như thế và muốn nhấn mạnh
rằng cái đi sau sort là một trình con, bạn có thể đặt trước
nó bằng một dấu và (&), khi đó Perl sẽ không bận tâm
nữa, nhưng nó đã biết rằng cái nằm giữa từ khoá sort và
danh sách phải là một tên trình con.
Cái loại giá trị ba ngả kiểu -1, 0, 1 dựa trên cơ sở so
sánh số thường xuất hiện trong trình con so sánh đến
mức Perl có một toán tử đặc biệt để làm điều này trong
một lần. nó thường được gọi là toán tử tầu vũ trụ, và
trông giống <=>. Dùng toán tử tầu vũ trụ này, trình con
sắp xếp trên đây có thể được thay thế bằng:
sub by_number { $a <=> $b; }
Chú ý đến tầu vũ trụ giữa hai biến này. Quả vậy, nó
thực là toán tử dài ba kí tự. Tầu vũ trụ cho lại cùng giá trị
như dây chuyền if/elsif trong định nghĩa trước của trình
này. Bây giờ điều này là có ngắn gọn, nhưng bạn có thể
viết tắt lời gọi sắp xếp thêm nữa, bằng việc thay thế tên
của trình sắp xếp bằng toàn bộ trình sắp xếp trong dòng,
giống như:
@rightlist = sort { $a <=> $b } @wronglist;
258
Một số người biện minh rằng điều này làm giảm tính
dễ đọc. Một số khác, lại biện minh rằng nó loại bỏ nhu
cầu phải đi đâu đó để tìm ra định nghĩa. Perl chẳng quan
tâm đến điều đó. Qui tắc cá nhân của tôi là ở chỗ nếu nó
không khớp trên một dòng hay tôi phải dùng nó nhiều
lần, nó nên thành một trình con.
Toán tử tầu vũ trụ dành cho so sánh số, có toán tử
xâu so sánh gọi là cmp. Toán tử cmp cho lại một trong ba
giá trị tuỳ theo việc so sánh xâu tương đối của hai đối.
Cho nên, một cách khác để viết trật tự mặc định là:
@result = sort { $a cmp $b } @somelist;
Có lẽ bạn còn chưa hề viết trình con đích xác này
(bắt chước sắp xếp mặc định có sẵn), chừng nào bạn còn
chưa viết một cuốn sách về Perl. Tuy thế, toán tử cmp
vẫn có ích lợi của nó, trong việc xây dựng các lược đồ
sắp thứ tự theo tầng. Chẳng hạn, bạn không cần đặt các
phần tử theo thứ tự số chừng nào chúng không bằng
nhau về mặt số, và trong trường hợp đó chúng phải được
sắp theo trật tự ASCII. (Theo mặc định, trình by_number
trên sẽ chỉ dùng cho các xâu phi số theo một trật tự ngẫu
nhiên nào đó vì không có trật tự số khi so sánh hai giá trị
không.) Sau đây là một cách nói “cần so sánh theo số,
chừng nào chúng chưa bằng nhau về số, còn, so sánh
theo xâu”:
sub by_mostly_number { ($a <=> $b) || ($a cmp $b); }
Điều này làm việc thế nào? Thế này, nếu kết quả của
tầu vũ trụ là -1 hay 1, phần còn lại của biểu thức bị bỏ
qua, và giá trị -1 hay 1 được cho lại. Nếu tầu vũ trụ tính
259
ra giá trị không, toán tử cmp trở thành quan trọng, cho lại
một giá trị thứ tự thích hợp xem như giá trị của xâu.
Giá trị được so sánh không nhất thiết là giá trị được
truyền vào. Chẳng hạn, bạn có một mảng kết hợp trong
đó khoá là tên đăng nhập và các giá trị là tên thật của
từng người dùng. Giả sử bạn muốn in ra một biểu đồ
trong đó tên đăng nhập và tên thật được cất giữ theo thứ
tự tên thật. Bạn sẽ làm điều đó như thế nào?
Thực tại, việc ấy khá dễ dàng. Giả sử các giá trị là
trong mảng %names. Tên đăng nhập vậy là danh sách
của key(%names). Điều muốn đạt tới là một danh sách
các tên đăng nhập được sắp theo giá trị tương ứng, vậy
với bất kì khoá riêng $a nào, cần nhìn vào $names{$a}
và sắp xếp dựa trên điều đó. Nếu bạn nghĩ về điều này
theo cách đó, nó gần như đã tự viết ra rồi, như trong:
@sortedkeys = sort by_names keys(%names);
sub by_names { $names{$a} cmp $names{$b}; }
foreach (@sortedkeys) { print “$_ có tên thật là $names{$_}\n”; }
Với điều này tôi cũng đã thêm vào một so sánh. Giả
sử tên thật của hai người dùng là trùng nhau. Vì bản chất
chợt nẩy ra của trình sort, tôi có thể lấy một giá trị này
trước giá trị kia lần đầu tiên rồi các giá trị theo thứ tự
đảo lại cho lần sau. Điều này không tốt nếu báo cáo có
thể được nạp vào chương trình so sánh cho việc báo cáo,
cho nên tôi phải rất cố gắng tránh những thứ như vậy.
Với toán tử cmp, thật dễ dàng:
260
sub by_names { ($names{$a} cmp $names{$b}) || ($a cmp $b); }
Tại đây, nếu tên thực là giống nhau, tôi sắp xếp dựa
trên tên đăng nhập. Vì tên đăng nhập được đảm bảo là
duy nhất (sau rốt, chúng là các khoá của mảng kết hợp
này, và không có hai khoá nào là như nhau), nên tôi có
thể đảm bảo một trật tự duy nhất và lặp lại được. Việc
lập trình phòng ngự tốt trong những ngày này, tốt hơn là
một cú điện thoại gọi đêm của thao tác viên để hỏi làm
sao tắt đi còi báo động.
Chuyển tự
Khi bạn muốn lấy một xâu và thay thế mọi thể
nghiệm của một kí tự nào đó bằng một kí tự mới, hay
xoá mọi thể nghiệm của một kí tự nào đó, bạn có thể đã
làm điều đó với việc chọn lựa cẩn thận chỉ lệnh s///.
Nhưng giả sử bạn phải thay đổi tất cả các a thành b và tất
cả các b thành a, sao? Bạn không thể làm điều đó với hai
chỉ lệnh s/// vì chỉ lệnh thứ hai sẽ hoàn tác lại tất cả
những thay đổi do chỉ lệnh thứ nhất thực hiện.
Tuy nhiên từ vỏ, một phép chuyển đổi dữ liệu như
vậy là đơn giản - chỉ cần dùng chỉ lệnh tr chuẩn:
tr ab ba < inda >outdata
(Nếu bạn không biết gì về chỉ lệnh tr, xin xem tài
liệu - đó là một công cụ có ích cho túi các mẹo của bạn.)
Tương tự, Perl cung cấp một toán tử tr làm việc rất giống
như thế:
tr/ab/ba/;
261
Toán tử tr nhận hai đối : xâu cũ và xâu mới. Các đối
này làm việc giống như hai đối của s///: nói cách khác, có
một định biên nào đó xuất hiện ngay sau từ khoá tr làm
tách biệt và kết thúc hai đối (trong trường hợp này, một
sổ chéo, nhưng gần như kí tự nào cũng được).
Các đối cho toán tử tr giống hệt như các đối của chỉ
lệnh tr. Toán tử tr sửa đổi nội dung của biến $_ (giống
như s///), tìm các kí tự của xâu cũ bên trong biến $_. Tất
cả các kí tự như thế được tìm thấy đã được thay thế bằng
kí tự tương ứng trong xâu mới. Sau đây là một số thí dụ:
$_ = “fred and barney”; tr/fb/bf/; # $_ bây giờ là “bred and farney” tr/abcde/ABCDE/; # $_ bây giờ là “BrED AnD fArnEy” tr/a-z/A-Z/; # $_ bây giờ là “BRED AND FARNEY”
Lưu ý đến phạm vi các kí tự có thể được chỉ ra bằng
hai kí tự được phân cách bởi sổ chéo. Nếu bạn cần một
sổ chéo hằng kí tự trong xâu, đặt trước nó một sổ chéo
ngược.
Nếu xâu mới ngắn hơn xâu cũ, kí tự cuối cùng của
xâu mới sẽ được lặp lại đủ số lần để làm cho các xâu có
chiều dài như nhau, giống như:
$_ = “fred and barney”; tr/a-z/ABCDE/d; # $_ bây giờ là “ED AD BAE”
Lưu ý đến cách thức mọi chữ sau e đã biến mất vì
không có kí tự tương ứng trong danh sách mới và rằng
các dấu cách không bị tác động vì chúng không xuất
hiện trong danh sách cũ. Điều này là tương tự với thao
tác của tuỳ chọn -d của chỉ lệnh tr.
Nếu danh sách mới là rỗng và nếu không có tuỳ
chọn d, danh sách mới là giống hệt danh sách cũ. Điều
262
này có vẻ hơi ngu, vì tại sao phải thay thế I cho I và 2
cho 2, nhưng thực tế, nó cũng làm đôi điều ích lợi đấy.
Giá trị cho lại của toán tử tr/// là số các kí tự được sánh
theo xâu cũ, và bằng việc thay đổi các kí tự trong chính
chúng, bạn có thể nhận được số các loại kí tự đó bên
trong xâu. Chẳng hạn:
$_ = “fred and barney”; $count = tr/a-z//; # $_ không thay đổi nhưng $count là 13 $count2 = tr/a-z/A-Z/; # $_ là chữ hoa còn $count2 là 13
Nếu bạn thêm c vào cuối (giống như viết thêm d),
điều đó có nghĩa là làm đầy đủ xâu cũ đối với tất cả 256
kí tự. Bất kì kí tự nào bạn liệt kê trong xâu cũ đã bị loại
bỏ khỏi tập tất cả các kí tự có thể; các kí tự còn lại, được
lấy theo dẫy từ thấp đến cao, từ xâu cũ. Vậy, một cách
để đếm hay thay đổi các kí tự phi số trong xâu của chúng
có thể là:
$_ = “fred and barney”; $count = tr/a-z//c; # $_ không đổi, nhưng $count là 2 tr/a-z/_/c; # $_ bây giờ là “fred_and_barney” (phi chữ =>
_) tr/a-z//cd; # $_ bây giờ là “fredandbarney” (xoá kí tự khác
chữ)
Chú ý rằng các tuỳ chọn có thể được tổ hợp, như
được trình bầy trong thí dụ cuối, nơi chúng trước hết bổ
sung cho tập hợp (danh sách các chữ trở thành danh sách
của tất cả các phi chữ) rồi dùng tuỳ chọn d để xoá bất kì
kí tự nào trong tập đó.
Tuỳ chọn cuối cùng cho tr/// là s, mà sẽ làm cứng lại
nhiều bản sao liên tiếp của cùng chữ đã được dịch vào
một bản sao. Xem như một thí dụ nhìn vào điều này:
$_ = “aaabbcccdefghi”;
263
tr/defghi/abcdd/s; # $_ bây giờ là “aaabbcccabcd”
Chú ý rằng def trở thành abc, còn ghi (mà đáng trở
thành ddd nếu không có tuỳ chọn s) lại trở thành một d.
Bạn cũng để ý rằng các chữ liên tiếp tại phần đầu của
xâu, không bị đóng cứng lại vì chúng không là kết quả
của việc dịch. Sau đây là thêm một số thí dụ nữa:
$_ = “fred and barney, wilma and betty”; tr/a-z/X/s; # $_ bây giờ là “X X X, X X X” $_ = “fred and barney, wilma and betty”; tr/a-z/_/cs; # $_ bây giờ là
“fred_and_barney_wilma_and_betty”
Trong thí dụ thứ nhất, mỗi từ (chữ liên tiếp) đã bị
đóng cứng lại chỉ một chữ X. Trong thí dụ thứ hai, tất cả
chùm các phi chữ liên tiếp trở thành dấu gạch thấp duy
nhất.
Giống như s///, toán tử tr có thể hướng đích vào một
xâu khác bên cạnh $_ bằng việc dùng toán tử =~:
$name = “fred and barney”;
$name =~ tr/aeiou/X/; # $name bây giờ là “frXd Xnd bXrnXy”
Bài tập
Xem Phụ lục A về lời giải
1. Viết một chương trình để đọc một danh sách tên tệp,
bẻ mỗi tên thành phần đầu và đuôi. (Mọi thứ cho tới
dấu sổ chéo cuối cùng đã là đầu, còn mọi thứ sau dấu
sổ chéo cuối cùng là đuôi. Nếu không có sổ chéo, nó
tất cả là đuôi.) thử với những thứ như /fred, barney
264
và fred/barney. Kết quả có nghĩa gì không?
2. Viết một chương trình để đọc vào một danh sách các
số rồi sắp xếp chúng theo thứ tự số, in ra danh sách
kết quả theo cột thẳng lề phải.
3. Viết một chương trình in ra các tên thực và tên đăng
nhập của người dùng trong tệp /etc/passwd, được sắp
theo tên cuối của từng người dùng. Giải pháp của
bạn có làm việc nếu hai người có cùng tên cuối
không?
4. Tạo ra một tệp bao gồm các câu, mỗi câu một dòng.
Viết một chương trình làm cho mỗi kí tự đầu câu trở
thành chữ hoa, còn phần còn lại của câu thành chữ
thường. (Liệu nó có làm việc cả khi kí tự đầu tiên
không phải là một chữ không? Bạn làm điều này thế
nào nếu câu không trên một dòng?)
265
16
Truy nhập
cơ sở dữ liệu hệ thống
Lấy mật hiệu và thông tin nhóm
Thông tin mà hệ thống giữ về tên người dùng và
userID là khá công cộng. Thực ra, gần đủ mọi thứ nhưng
mật hiệu đã mật mã rồi, có sẵn cho việc nghiên cứu kĩ
hơn cho bất kì chương trình nào dám quét vào tệp
/etc/password. Tệp này có dạng thức đặc biệt - được xác
định trong passwd(5), có dạng giống như thế này:
name:passwd:uid:gid:gcos:dir:shell
Các trường được định nghĩa như sau:
name Tên đăng nhập của người dùng
passwd Mật hiệu đã mật mã hoá
Trong chương này:
Lấy mật hiệu và thông tin nhóm
Đóng và mở gói dư liệu nhị phân
Lấy thông tin mạng
Lấy thông tin khác
266
uid Số hiệu uerID (0 cho root, khác không
cho người dùng bình thường)
gid Nhóm đăng nhập mặc định (nhóm 0 có
thể có quyền riêng, nhưng không nhất
thiết)
gcos Trường GCOS, điển hình có chứa tên đầy
đủ của người dùng, tiếp theo đó là dấu
phẩy và thông tin nào đó khác
dir Danh mục nhà (nơi bạn tới khi bạn gõ cd
không có đối, và nơi phần lớn các “tệp
chấm” của bạn được lưu giữ
shell Lớp vỏ đăng nhập của bạn, điển hình là
/bin/dh hay /bin/csh (hay có thể
/usr/bin/perl nếu bạn là người quái chiêu)
Phần điển hình của tệp mật hiệu trông giống thế này:
fred:*:123:15:Fred Flintstone, , , :/home/fred:/bin/csh barney:*:125:15:Barney
Rubble, , , :/home/barney:/bin/csh
Bây giờ, Perl đủ công cụ để nhận dạng loại dòng này
một cách dễ dàng (bằng việc dùng split, chẳng hạn), mà
không đưa ra một trình chuyên dụng nào. Nhưng thư
viện lập trình UNIX lại có một tập các trình đặc biệt:
getpwent, getpwuid, getpwnam, vân vân. Những trình
này là có sẵn trong Perl bằng việc dùng cùng tên và đối
tương tự và cho lại các giá trị.
Chẳng hạn, trình getpwnam() trở thành toán tử
getpwnam() của Perl. Đối duy nhất là tên người dùng
(như fred hay barney) và giá trị cho lại là dòng
/etc/passwd chia ra thành một mảng với các giá trị sau:
267
($name, $passwd, $uid, $gid, #quota, $comment, $gcos, $dir, $shell)
Lưu ý rằng có thêm vài giá trị nữa ở đây so với tệp
mật hiệu. Trường $quote bao giờ cũng trống, và trường
$comment và $gcos cả hai đã chứa toàn bộ trường GCOS
(dẫu sao, cũng trên mọi hệ thống UNIX tôi đã chơi). Cho
nên, với anh bạn cũ freud, bạn được:
(“fred”, “*”, 123, 16, “”, “Fred Flintstone,,,”, “Fred Flintstone,,,”, “/home/fred”, “ /bin/csh”)
bằng việc gọi một trong hai lời gọi sau:
getpwuid(123)
getpwnam(“fred”)
Chú ý rằng getpwwui() nhận số hiệu UID, trong khi
getpwman() nhận tên đăng nhập làm đối của nó.
Bạn có lẽ muốn lấy phần này ra, dùng một số phép
toán danh sách mà chúng đã thấy trước đây. Một cách là
lấy một phần của danh sách này bằng việc dùng một lát
cắt mảng, như lấy danh mục nhà cho Fred, dùng:
($fred_home) = (getpwnam(“fred”)) [7] ; # đặt nhà của Fred
Bạn có thể quét qua toàn bộ tệp mật hiệu như thế
nào? Được, bạn có thể làm điều gì đó như:
foreach $i (1..10000) { @stuff = getpwui($i) ; } ### không khuyến cáo!
Nhưng đây có lẽ là cách làm sai. Chỉ bởi vì có nhiều
cách để làm điều đó không có nghĩa là tất cả các cách đã
đầm chắc như nhau.
Bạn có thể nghĩ về các toán tử getpwuid() và
268
getpwnam() như việc truy nhập ngẫu nhiên - chúng lấy
một phần tử đặc biệt theo khoá, cho nên bạn phải có một
khoá để bắt đầu. Một cách khác để truy nhập vào tệp mật
hiệu là truy nhập tuần tự - nắm lấy mỗi phần tử theo một
trật tự ngẫu nhiên nào đó
Các trình truy nhập ngẫu nhiên vào tệp mật hiệu là
các toán tử setpwent(), getpwent(), và endpwent(). Cùng
nhau, ba toán tử này tạo thành việc chuyển tuần tự tất cả
các giá trị trong tệp mật hiệu. Toán tử setpwent() khởi
đầu việc quét từ đầu. Sau khi khởi đầu, mỗi lời gọi tới
getpwent() cho lại phần tử tiếp trong tệp mật hiệu. Khi
không còn dữ liệu để xử lí, trình getpwent() cho lại danh
sách rỗng. Cuối cùng, việc gọi endpwent() giải phóng tài
nguyên do nơi quét sử dụng - điều này được thực hiện
một cách tự động khi ra khỏi chương trình.
Mô tả này cần thí dụ, cho nên đây là một thí dụ:
setpwent ; # khởi đầu việc quét while (@list = getpwent) { # lấy phần tử tiếp ($login, $home) = @list[0, 7] ; # lấy tên đăng nhập và
nhà print “Danh mục nhà cho $login là $home\n” ; } endpwent; # làm xong tất
Mảnh thí dụ này chỉ ra danh mục nhà cho mọi người
trong tệp mật hiệu. Giả sử bạn muốn sắp xếp chúng theo
trật tự chữ cái, sao? Được, chúng ta đã học về sort trong
chương trước, cho nên dùng nó:
setpwent ; # khởi đầu việc quét while (@list = getpwent) { # lấy phần tử tiếp ($login, $home) = @list[0, 7] ; # lấy tên đăng nhập và
nhà $home{$login} = $home; # cất nó đi
269
} endpwent ; # làm xong tất @keys = sort { $home{$a} cmp $home{$b} } key %home; foreach $login (@keys) { # bước qua các tên đã sắp xếp print “nhà của $login là $home{$login}\n” ; }
Đoạn chương trình này, trong khi dài hơn chút ít, lại
minh hoạ cho một vấn đề quan trọng về quét tuần tự qua
tệp mật hiệu - bạn có thể cất giữ các phần thích hợp của
dữ liệu trong cấu trúc dữ liệu bạn chọn. Phần thứ nhất
của thí dụ này quét qua toàn bộ tệp mật hiệu, tạo ra một
mảng kết hợp với khoá là tên đăng nhập và giá trị là
danh mục nhà tương ứng cho tên đăng nhập đó. Dòng
sort lấy khoá của mảng và sắp xếp chúng tương ứng với
xâu giá trị. Chu trình cuối cùng bước qua các khoá đã
sắp xếp, lần lượt in ra từng giá trị.
Nói chung, bạn nên dùng các trình truy nhập ngẫu
nhiên (getpwuid() và getpwnam() ) khi bạn tìm chỉ vài
giá trị. Với việc tìm kiếm vài giá trị hay thậm chí tìm
kiếm vét cạn, nói chung bước truy nhập tuần tự (dùng
setpwent(), getpwent(), và endpwent()) và trích các giá
trị đặc biệt mà bạn tìm để đưa vào mảng kết hợp, dễ
dàng hơn.
Tệp /etc/group được truy nhập theo cách tương tự.
Việc truy nhập tuần tự đươc cung cấp bằng các lời gọi
setgrent(), getgrent() và endgrrent(). Lời gọi getgrent() cho
lại giá trị có dạng:
($name, $passwd, $gid, $member)
Bốn giá trị này tương ứng đúng với bốn trường của
tệp /etc/group cho nên xem các mô tả trong tài liệu tham
chiếu về dạng thức tệp này về chi tiết. Các hàm truy
270
nhập ngẫu nhiên tương ứng là getgrgid() (theo ID nhóm)
và getgrnam() (theo tên nhóm).
Gói và mở dữ liệu nhị phân
Mật hiệu và thông tin nhóm được biểu diễn khá đẹp
theo dạng văn bản. Các cơ sở dữ liệu hệ thống khác được
biểu diễn tự nhiên hơn dưới các dạng khác. Chẳng hạn,
địa chỉ IP của một giao diện được quản lí nội bộ như một
số bốn byte. Trong khi nó thường được giải mã thành
biểu diễn văn bản bao gồm bốn số nguyên nhỏ tách nhau
bởi dấu chấm, việc mã hoá và giải mã này lại là nỗ lực bị
phí phạm nếu con người trong khi ấy không diễn giải dữ
liệu này.
Bởi điều này, các trình mạng trong Perl mà có trông
đợi hay trả lại một địa chỉ IP thường dùng xâu bốn byte
có chứa một kí tự ch từng byte tuần tự trong bộ nhớ.
Trong khi việc xây dựng và diễn giải xâu byte như vậy là
khá trực tiếp bằng việc dùng sprintf() và ord() (không
được trình bầy ở đây), Perl cung cấp một lối tắt có thể áp
dụng được tương đương cho nhiều cấu trúc khó hơn.
Toán tử pack() làm việc có đôi chút như sprintf(),
nhận một xâu điều khiển dạng thức và một danh sách các
giá trị, rồi tạo ra một xâu từ những giá trị này. Tuy nhiên
xâu dạng thức pack được khớp với việc tạo ra một cấu
trúc dữ liệu nhị phân. Chẳng hạn, để lấy bốn số nguyên
nhỏ và gói chúng theo bốn byte liên tiếp trong một xâu
hợp thành:
$buf = pack (“CCCC”, 140, 186, 65, 25) ;
271
Tại đây, xâu dạng thức pack là bốn C. Mỗi C biểu thị
cho một giá trị tách biệt được lấy từ danh sách sau
(tương tự như cái mà một trường % thực hiện trong
sprintf). Dạng thức C (tương ứng với tài liệu Perl, bìa
tham chiếu, Sách con lạc đà, tệp texinfo, hay thậm chí
cuốn Perl: The Motion Picture) nói tới một byte được
tính từ một giá trị kí tự không dấu (một số nguyên nhỏ).
Xâu kết quả trong $buf là xâu bốn kí tự - từng kí tự đã là
một byte có các giá trị 140, 186, 65 và 25.
Tương tự, dạng thức gói l sinh ra một giá trị dài có
dấu. Trên nhiều máy, đây là một số bốn byte, mặc dầu
dạng thức này là phụ thuộc máy. Trên máy “dài” bốn
byte, câu lệnh,
$buf = pack (“1”, 0x41424344) ;
sinh ra một xâu bốn kí tự trông như hoặc ABCD
hoặc DCBA, tuỳ theo liệu máy kết thúc ở đầu cuối bé
hay đầu cuối lớn (hay một cái gì đó hoàn toàn khác nếu
máy không nói theo ASCII). Điều này xảy ra bởi vì
chúng đang gói một giá trị vào thành bốn kí tự (chiều dài
của số nguyên dài), và một giá trị ngẫu nhiên được hợp
thành từ các byte biểu thị cho các giá trị ASCII cho bốn
chữ cái đầu tiên trong bảng chữ. Tương tự:
$buf = pack ( “11”, 0x41424344, 0x45464748) ;
tạp ra một xâu tám byte bao gồm ABCDEFGH hay
DCBAHGFE, một lần nữa tùy thuộc vào liệu máy là kết
thúc ở đầu cuối bé hay đầu cuối lớn.
Danh sách chính xác cho các loại dạng thức gói
khác nhau được cho trong tài liệu tham chiếu (tài liệu
ngôn ngữ Perl, hay sách con lạc đà, hay bất kì sách nào
bạn đang dùng để học về các viên ngọc khác của Perl).
272
Bạn sẽ thấy vài thí dụ ở đây, nhưng tôi không định lập
danh sách cho chúng.
Điều gì xảy ra nếu bạn được cho một xâu tám byte
ABCDEFGH và được bảo rằng nó thực sự là ảnh bộ nhớ
(một kí tự là một byte) của hai giá trị dài có dấu (bốn
byte)? Bạn sẽ diễn giải nó thế nào? Nào, bạn cần làm
ngược lại pack(), gọi là unpack(). Toán tử này lấy một xâu
điều khiển dạng thức (thường đồng nhất với xâu điều
khiển dạng thức mà bạn đã cho trong pack()) và một xâu
dữ liệu, rồi cho lại một danh sách các giá trị tạo nên ảnh
bộ nhớ được xác định trong xâu dữ liệu. Chẳng hạn, lấy
xâu đó từ:
($val1, $val2) = unpack (“11”, “abcdefgh”) ;
Điều này cho lại cái gì đó tựa như 0x61626364 cho
$val1 hay có thể 0x64636261 (tuỳ theo đầu cuối lớn hay
bé). Thực ra, với giá trị cho lại, chúng có thể xác định
liệu chúng ở trên máy đầu cuối bé hay lớn.
Khoảng trắng trong xâu điều khiển dạng thức bị bỏ
qua, và có thể được dùng để làm tăng tính dễ đọc. Một
số trong xâu điều khiển dạng thức nói chung lặp lại đặc
tả trước đó nhiều lần. Chẳng hạn, CCCC cũng có thể
được viết là C4 hay C2C2 mà không làm thay đổi ý
nghĩa. (Một vài đặc tả dùng số đi sau như một phần của
đặc tả, và do vậy không thể được xem lại bội như thế
này.)
Một kí tự dạng thức cũng có thể được phép có theo
sau là dấu *, mà sẽ làm gấp bội kí tự dạng thức theo đủ
số lần để nuốt hết phần còn lại của danh sách hay phần
còn lại của xâu ảnh nhị phân (tuỳ theo liệu bạn đang gói
lại hay mở gói). Do đó, một cách khác để gói bốn kí tự
273
không dấu thành một xâu là:
$buf = pack (“C*”, 140, 186, 65,25) ;
Bốn giá trị ở đây được nuốt hết bởi một đặc tả dạng
thức. Nếu bạn muốn hai số nguyên ngắn theo sau bởi
“nhiều nhất các kí tự không dấu có thể được,” bạn có thể
nói điều gì đó như:
$buf = pack ( “s2 C*”, 3241, 5826, 5, 3 , 5, 8, 9, 7, 9, 3, 2) ;
Tại đây, chúng lấy hai giá trị đầu là số nguyên ngắn
(có thể sinh ra bốn hay tám kí tự) và chín giá trị còn lại
là các kí tự không dấu (sinh ra chín kí tự, gần như chắc
chắn).
Đi theo chiều hướng khác, unpack với đặc tả dấu sao
có thể sinh ra một danh sách các phần tử với chiều dài
không xác định. Chẳng hạn, mở gói với C* tạo ra một
phần tử danh sách (một số) cho từng kí xâu. Do đó, câu
lệnh này:
@values = unpack (“C*”, “hello, world!\n”) ;
cho một danh sách 14 phần tử, mỗi phần tử là một kí
tự trong xâu này.
Lấy thông tin mạng
Perl hỗ trợ cho việc lập trình mạng theo một cách rất
quen thuộc với những người đã viết chương trình mạng
trong chương trình C. Thực ra, phần lớn các trình Perl
cung cấp việc truy nhập mạng đã có cùng tên và các
tham biến tương tự như chương trình C tương ứng.
274
Chúng tôi không thể dạy một giáo trình đầy đủ về lập
trình mạng trong chương này, nhưng chúng ta lấy cái
nhìn vào một trong những đoạn nhiệm vụ đó để xem liệu
nó được thực hiện như thế nào trong Perl.
Một trong những điều bạn cần tìm ra là địa chỉ đi
cùng với tên, hay ngược lại. Trong C, bạn dùng trình
gethostbyname() để chuyển một tên mạng thành địa chỉ
mạng. Thế rồi bạn dùng địa chỉ này để tạo ra một mối
nối từ chương trình của bạn tới chương trình khác ở đâu
đó.
Trình Perl để dịch một tên máy chủ thành địa chỉ có
cùng tên và các tham biến tương tự như trình C, và trông
như thế này:
($name, $aliases, $addrtype, $length, @addrs) =
gethostbyname($name) ; # dạng tổng quát của gethostbyname
Tham biến cho trình này là tên máy chủ - chẳng hạn
slate.bedrock.com. Giá trị cho lại là một danh sách bốn
hay nhiều tham biến, tuỳ thuộc vào bao nhiêu địa chỉ
được liên kết với tên này. Nếu tên máy chủ không hợp
lệ, trình này cho lại danh sách rỗng.
Khi gethostbyname() thực hiện thành công, $name là
tên chính tắc, khác với tên cái vào nếu tên cái vào là một
biệt hiệu. $aliases là danh sách các tên cách nhau bằng
dấu cách mà qua đó biết tới máy chủ. $addrtype cho lại
giá trị mã hoá để chỉ ra dạng của địa chỉ. Trong trường
hợp này, với slate.bedrock.com, chúng ta có thể giả thiết
trước rằng giá trị này chỉ ra một địa chỉ IP, thông thường
được biểu thị như bốn số dưới 256, cách nhau bởi dấu
chấm. $length cho số các địa chỉ, thực tế là thông tin thừa
275
vì dẫu sao bạn có thể nhìn vào chiều dài của @addrs.
Nhưng phần có ích của giá trị cho lại là @addrs. Mỗi
phần tử của mảng là một địa chỉ IP tách biệt, được cất
giữ trong một dạng thức bên trong, được giải quyết trong
Perl như một xâu bốn kí tự. Trong khi xâu bốn kí tự này
đích xác là cái mà các hàm mạng Perl khác tìm kiếm, giả
sử muốn in ra kết quả cho người dùng xem. Trong
trường hợp này, chúng ta cần chuyển đổi giá trị cho lại
thành một dạng thức người đọc được với sự trợ giúp của
hàm unpack() và việc truyền thông báo phụ. Sau đây là
một đoạn chương trình in ra một trong các địa chỉ IP của
slate.bedrock.com:
($addr) = (gethostbyname(‘slate.bedrock.com’) ) [4] ; print “Địa chỉ của slate là ”,
join (“.”, unpack(“C4”, $addr)), “\n” ;
ụnpack() nhận bốn kí tự và cho lại bốn số. Điều này
xảy ra theo thứ tự bên phải để join() gắn các dấu chấm
vào giữa từng cặp số để làm thành dạng người đọc được.
Lấy các thông tin khác
Giống như trình gethostbyname(), bạn cũng có thể
lấy thông tin về máy chủ theo địa chỉ, và thông tin mạng
theo cả tên và địa chỉ. Cũng có các trình để truy nhập
vào các danh sách giao thức mạng và danh sách dịch vụ.
Các trình này tất cả đã làm việc tương tự như cách chúng
làm việc trong chương trình C.
Xem Phụ lục B về các thí dụ việc dùng các trình này
trong ứng dụng thực.
276
Bài tập
Xem câu trả lời ở Phụ lục A.
1. Viết một chương trình tạo ra bảng tương ứng userID
và tên thực tế các ô mật hiệu, rồi dùng ánh xạ đó để
cho hiện ra danh sách các tên thực thuộc về từng
nhóm trong tệp nhóm. (Danh sách của bạn có chứa
những người dùng, người có nhóm mặc định trong ô
mật hiệu nhưng không nói tường minh về cùng nhóm
đó trong ô nhóm, đúng không? Nếu không, làm sao
bạn hoàn thành được điều đó?
277
17
Thao tác
cơ sở dữ liệu
người dùng
Cơ sở dữ liệu DBM và mảng DBM
Phần lớn các hệ thống UNIX đã có một thư viện
chuẩn gọi là DBM. Thư viện này cung cấp một tiện nghi
quản trị cơ sở dữ liệu đơn giản cho phép các chương
trình được cất giữ một tuyển tập các cặp khoá-giá trị
trong một cặp tệp đĩa. Những tệp này duy trì các giá trị
trong cơ sở dữ liệu giữa những lần gọi chương trình có
dùng cơ sở dữ liệu và những chương trình này có thể bổ
sung các giá trị mới, cập nhật các giá trị hiện có, hay xoá
các giá trị cũ.
Thư viện DBM, khá đơn giản, nhưng lại sẵn có, một
số chương trình hệ thống đã dùng nó cho những nhu cầu
Trong chương này:
Cơ sở dữ liệu DBM và mảng DBM
Mở và đóng mảng DBM
Dùng mảng DBM
Cơ sở dữ liệu truy nhập ngẫu nhiên chiều dài cố định
Cơ sở dữ liệu (văn bản) chiều dài biến đổi
278
khá giản dị của chúng. Chẳng hạn, chương trình gửi thư
Berkeley (và các biến thể cùng suy dẫn của nó) cất giữ
cơ sở dữ liệu biệt hiệu (ánh xạ của địa chỉ thư vào nơi
nhận) như một cơ sở dữ liệu DBM. Phần mềm tin Usenet
phổ biến nhất cũng dùng cơ sở dữ liệu DBM để giữ dấu
vết các bài báo hiện tại và mới xem gần đấy.
Perl cung cấp việc truy nhập vào cùng cơ chế DBM
này thông qua một phương tiện còn thông minh hơn:
mảng kết hợp có thể được kết hợp với cơ sở dữ liệu
DBM qua một tiến trình tương tự như mở một tập. Mảng
kết hợp này (còn gọi là mảng DBM) vậy rồi được dùng
để truy nhập và sửa đổi cơ sở dữ liệu DBM. Việc tạo ra
một phần tử mới trong mảng này làm thay đổi ngay lập
tức cơ sở dữ liệu DBM. Việc xoá một phần tử sẽ xoá giá
trị khỏi cơ sở dữ liệu DBM. Và cứ như thế.
Kích cỡ, số lượng và loại khoá cùng giá trị trong cơ
sở dữ liệu DBM, có hạn chế, và một mảng DBM có cùng
những hạn chế đó. xem lidbm về chi tiết. Nói chung, nếu
bạn giữ cả khoá và giá trị xuống khoảng 1000 kí tự bất kì
hay ít hơn, có lẽ là OK.
Mở và đóng mảng DBM
Để kết hợp một cơ sở dữ liệu DBM với một mảng
DBM, dùng toán tử dbmopen() , trông như thế này:
dbmopen(%ARRAYNAME, “dbmfilename”, $mode);
Tham biến %ARRAYNAME là một mảng kết hợp
của Perl. (Nếu mảng này đã có giá trị, các giá trị đó bị bỏ
đi.) Mảng kết hợp này trở thành được nối với cơ sở dữ
liệu DBM có tên dbmfilename, thông thường được cất
279
giữ trên đĩa như một cặp tệp có tên dbmfilenam.dir và
dbmfilename.pag. Bất kì tên mảng kết hợp hợp pháp nào
cũng đã có thể dùng được, mặc dầu các tên mảng chỉ
toàn chữ hoa, được dùng điển hình do sự tương tự với
tước hiệu tệp.
Tham biến $mode là một số kiểm soát các bit cho
phép của cặp tệp này nếu các tệp đó cần được tạo ra. Con
số này điển hình được xác định theo hệ tám: giá trị
thường hay dùng nhất là 0644 cho phép mọi người chỉ
đọc, còn riêng người chủ, có phép đọc-ghi. Nếu các tệp
này đã tồn tại, tham biến này không có tác dụng. Chẳng
hạn:
dbmopen(%FRED, “mydatabase”, 0644); # mở %FRED lên mydatabase
Lời gọi này kết hợp mảng kết hợp %FRED với các
tệp đĩa mydatabase.dir và mydatabase.pag trong danh
mục hiện tại. Nếu các tệp này chưa tồn tại, chúng được
tạo ra với mốt 0644.
Giá trị cho lại từ dbmopen() là đúng nếu cơ sở dữ
liệu có thể mở được hay tạo ra được, và là sai trong
trường hợp ngược lại, hệt như việc gọi open(). Nếu bạn
không muốn các tệp này được tạo ra, dùng giá trị $mode
của undef. Chẳng hạn:
dbmode (%A, “/etc/xx”, undef) || die “không mở được DBM /etc/xx”;
Trong trường hợp này, nếu tệp /etc/xx.dir và
/etc/xx.pag không thể mở được, lời gọi dbmopen() sẽ cho
lại sai, thay vì cố gắng tạo ra các tệp này.
Mảng DBM vẫn còn mở trong suốt lúc thực hiện
chương trình. Khi chương trình kết thúc, sự kết hợp sẽ
280
kết thúc. Bạn cũng có thể phá vỡ sự kết hợp theo cách
thức tương tự như việc đóng tước hiệu tệp, bằng việc
dùng toán tử dbmclose():
dbmclose(%A);
Giống như close(), dbmclose() cho lại sai nếu điều gì
đó đi sai.
Dùng mảng DBM
Một khi cơ sở dữ liệu đã được mở, các tham chiếu
tới mảng DBM sẽ được ánh xạ vào các tham chiếu cơ sở
dữ liệu. Việc thay đổi hay bổ sung giá trị vào mảng này
tạo ra cho các mục tương ứng lập tức được ghi vào tệp
đĩa. Chẳng hạn, một khi %FRED được mở trong thí dụ
trước, có thể thêm vào, xoá bỏ hay truy nhập vào cơ sở
dữ liệu, giống như thế này:
$FRED{“fred”} = “bedrok”; # tạo ra (cập nhật) một phần tử
delete $FRED{“barney”); # bỏ một phần tử của csdl foreach $key (keys %FRED) { # đi qua mọi giá trị print “$key có giá trị của $FRED{$key}\n”; }
Chu trình cuối phải quét qua toàn bộ tệp đĩa hai lần:
một lần để truy nhập vào khoá, và lần thứ hai để tra cứu
các giá trị từ khoá. Nếu chúng ta quét qua một mảng
DBM, nói chung sẽ hiệu quả về đĩa hơn là dùng toán tử
each(), chỉ qua một bước:
while ( ($key, $value) = each (%FRED)) { print “$key có giá trị của $value\n”; }
Nếu bạn đang truy nhập vào cơ sở dữ liệu DBM hệ
281
thống, như các cơ sở dữ liệu đã được tạo ra bởi sendmail
hay tin Usenet, bạn phải biết rằng các chương trình nói
chung đã gắn thêm cái đuôi kí tự NUL (“\0”) vào cuối
của các xâu này. Các trình thư viện DBM không cần cái
NUL này (chúng giải quyết dữ liệu nhị phân bằng cách
đếm byte chứ không phải là xâu kết thúc bằng NUL), và
do vậy NUL được cất giữ như một phần của dữ liệu. Do
đó bạn phải gắn kí tự NUL vào cuối dữ liệu của mình và
bỏ NUL khỏi cuối của giá trị cho lại để cho dữ liệu có
nghĩa. Chẳng hạn, để tra cứu merlyn trong cơ sở dữ liệu
biệt hiệu, thử một điều gì đó kiểu:
dbmopen(%ALI, “/etc/aliases”, undef) || die “Không biệt hiệu?”;
$value = $ALI{“merlyb\0”}; # lưu ý NUL được thêmvào chop($value); #loại bỏ NUL được thêm vào print “Thư của Randal được gắn đầu cho: $value\n”; # in
kết quả
Bản UNIX của bạn có thể đưa cơ sở dữ liệu biệt
hiệu vào /usr/lib thay vì /etc. Bạn phải lục lọi để tìm ra.
Cơ sở dữ liệu truy nhập ngẫu nhiên chiều dài cố định
Một dạng khác của dữ liệu bền vững là các tệp đĩa
hướng bản ghi với chiều dài cố định. Trong lược đồ này,
dữ liệu bao gồm một số các bản ghi với chiều dài như
nhau. Việc đánh số cho các bản ghi, không quan trọng
hay được xác định bởi một lược đồ định chỉ số nào đó.
Chẳng hạn, chúng ta có thể có một loạt các bản ghi
mà trong đó dữ liệu có 40 kí tự của họ, một kí tự tên
đệm, 40 kí tự tên, và rồi một số nguyên hai byte cho tuổi.
Mỗi bản ghi vậy là có chiều dài 83 byte. Nếu đọc tất cả
282
dữ liệu trong cơ sở dữ liệu, phải đọc từng chùm 83 byte
cho tới khi đến cuối. Nếu muốn đi tới bản ghi thứ năm,
phải nhảy qua 4 lần 83 byte (332 byte) và đọc trực tiếp
bản ghi thứ năm.
Perl hỗ trợ cho các chương trình dùng kiểu tệp đĩa
như thế. Có đôi điều cần biết thêm bên cạnh những điều
bạn đã biết:
1. Mở tệp đĩa cho cả đọc và ghi
2. Chuyển quanh tệp này tới một vị trí bất kì
3. Lấy dữ liệu theo độ dài thay vì theo dòng mới tiếp
4. Ghi dữ liệu lên theo các khối chiều dài cố định
Toán tử open() nhận một dấu cộng bổ sung trước đặc
tả hướng vào/ra để chỉ ra rằng tệp thực sự được mở cho
cả đọc và ghi. Chẳng hạn:
open(A, “+<b”); # mở tệp b đọc/ghi (lỗi nếu không có tệp)
open(C, “+>d”); # tạo ra tệp d, với truy nhập đọc/ghi
open(E, “+>>f”); # mở hay tạo tệp f với việc truy nhập đọc/ghi
Lưu ý rằng tất cả những điều đã làm mới chỉ là bổ
sung thêm dấu cộng vào hướng vào/ra.
Một khi đã thu được việc mở tệp, cần di chuyển
quanh nó. Bạn làm điều này với toán tử seek(), cũng
nhận cùng ba tham biến như trình thư viện fseek(). Tham
biến thứ nhất là tước hiệu tệp; tham biến thứ hai là cho
khoảng chênh, được diễn giải đi kèm với tham biến thứ
ba. Thông thường, bạn muốn tham biến thứ ba là không
để cho tham biến thứ hai chọn được vị trí tuyệt đối cho
lần đọc tiếp hay ghi tiếp lên tệp. Chẳng hạn, đi tới bản
283
ghi thứ năm trên tước hiệu tệp NAMES (như được mô tả
ở trên), bạn có thể làm điều này:
seek(NAMES, 4*83, 0);
Một khi con trỏ tệp đã được định vị lại, việc đưa vào
hay đưa ra tiếp sẽ bắt đầu từ đó. Với việc đưa ra, dùng
toán tử print. nhưng phải chắc chắn rằng dữ liệu bạn viết
là đúng chiều dài. Để thu được đúng chiều dài, chúng ta
có thể gọi tới toán tử pack():
print NAMES pack(“A40Â40s”, $first, $middle, $last, $age);
Bộ xác định pack() đó cho 49 kí tự đối với $first, một
kí tự cho $middle, 40 kí tự nữa cho $last và số nguyên
ngắn (2 byte) cho $age. Điều này tính thành 83 byte
chiều dài, và sẽ ghi tại vị trị tệp hiện tại đó.
Cuối cùng, chúng ta cần lấy ra một bản ghi đặc biệt.
Mặc dầu toán tử <NAMES> cho lại tất cả dữ liệu từ vị trí
hiện tại cho tới dòng mới tiếp, điều đó lại không đúng;
dữ liệu được giả thiết là chỉ trải trên 83 kí tự và có thể lại
không có dòng mới ở đúng chỗ. Thay vì thế, dùng toán
tử read(), trông và cách làm việc, hệt như lời gọi hệ thống
UNIX tương ứng:
$count = read(NAMES, $buf, 83);
Tham biến thứ nhất của read() là tước hiệu tệp.
Tham biến thứ hai là biến vô hướng giữ dữ liệu sẽ được
đọc. Tham biến thứ ba cho số byte cần đọc. Giá trị cho
lại từ read() là số byte thực tế đã đọc - điển hình là cùng
số byte được yêu cầu trừ phi tước hiệu tệp không được
mở hay bạn quá gần tới cuối tệp.
Một khi bạn đã có dữ liệu 83 kí tự, chỉ cần chặt nó
284
ra thành các thành phần bằng toán tử unpac():
($first, $middle, $last, $age) = unpack(“A40A40s”, $buf);
Lưư ý rằng các xâu dạng thức cho pack và unpack là
như nhau. Phần lớn các chương trình cất giữ xâu này
trong một biến từ đầu trong chương trình, và thậm chí
còn tính chiều dài của bản ghi bằng việc dùng pack() thay
vì bỏ rải rác các hằng 83 ở mọi nơi:
$names = “A40AA40s”;
$names_length = length(pack($names)) ; # có thể là 83
Cơ sở dữ liệu (văn bản) chiều dài thay đổi
Nhiều cơ sở dữ liệu hệ thống (và có lẽ phần lớn cơ
sở dữ liệu do người dùng tạo ra) đã là các chuỗi dòng
văn bản con người đọc được,với một bản ghi trên mỗi
dòng. Chẳng hạn, tệp mật hiệu chưa một dòng cho mỗi
người trên hệ thống, và tệp máy chủ chứa một dòng cho
mỗi tên máy chủ.
Rất thường là những cơ sở dữ liệu này được cập
nhật bằng các trình soạn thảo văn bản đơn giản. Việc cập
nhật một cơ sở dữ liệu như vậy bao gồm việc đọc nó tất
cả vào một vùng trung gian (hoặc trong bộ nhớ hoặc trên
đĩa khác), thực hiện những thay đổi cần thiết, rồi hoặc
ghi kết quả ngược trở lại tệp nguyên thuỷ hoặc tạo ra
một tệp mới với cùng tên sau khi đã xoá hay đổi tên bản
cũ. Bạn có thể coi việc này như bước sao chép - dữ liệu
được sao từ cơ sở dữ liệu nguyên gốc sang một bản mới
của cơ sở dữ liệu ấy, tiến hành thay đổi trong khi sao
chép.
Perl hỗ trợ cho việc soạn thảo kiểu sao chép này trên
285
các cơ sở dữ liệu hướng dòng bằng cách dùng việc soạn
thảo tại chỗ. Soạn thảo tại chỗ là việc sửa đổi cách thức
toán tử hình thoi (<>) đọc dữ liệu từ một danh sách các
tệp được xác định trong dòng lệnh. Thông thường nhất,
mốt soạn thảo này được truy nhập tới bằng việc đặt đối
dòng lệnh -i, nhưng chúng ta cũng có thể đặt lẫy cho mốt
soạn thảo tại chỗ từ bên trong một chương trình, như
được biểu thị trong thí dụ sau đây.
Để đặt lẫy cho mốt soạn thảo tại chỗ, đặt một giá trị
vào trong biến vô hướng $^I. Giá trị của biến này là quan
trọng, và sẽ được thảo luận ngay đây.
Khi toán tử hình thoi được sử dụng và $^I có một giá
trị (thay vì undef), các bước được đánh dấu ##INPLACE##
trong đoạn mã sau đây sẽ được thêm vào danh sách các
hành động ngầm định mà toán tử hình thoi nhận:
$ARGV = shift @ARGV; open(ARGV, “<$ARGV”); rename($ARGV, “$ARGV$^I”); ## INLACE ## unlink($ARGV); ## INPLACE ## open(ARGVOUT, “>$ARGV”); ## INPLACE ## select(ARGVOUT) ; ## INPLACE ##
Hiệu quả là ở chỗ việc đọc từ toán tử hình thoi lấy từ
tệp cũ, còn việc ghi lên tước hiệu tệp ngầm định, lại
chuyển sang một bản sao mới của tệp này. Tệp cũ vẫn
còn trong tệp dự phòng, mà chính là tước hiệu tệp với
phần hậu tố bằng giá trị của biến $^I. (Cũng có một chút
ảo thuật để sao chép các bit người chủ và phép dùng từ
tệp cũ sang tệp mới.) Những bước này được lặp lại mỗi
lần một tệp mới được rút ra từ mảng @ARGV.
Các giá trị điển hình cho $^I là những cái như .bak
hay ~, để tạo ra các tệp dự phòng rất giống với trình soạn
286
thảo tạo ra. Một giá trị kì lạ nhưng có ích cho $^I là xâu
rỗng, “”, cái gây ra việc tệp cũ bị xoá sạch sau khi việc
soạn thảo hoàn tất. Điều không may là nếu hệ thống hay
chương trình bị hỏng trong khi thực hiện chương trình
bạn, bạn sẽ mất tất cả dữ liệu cũ, cho nên chúng tôi chỉ
khuyến cáo điều này cho những người bạo dạn, dại dột
hay tin cậy.
Sau đây là cách thay đổi việc đăng nhập của ai đó
vào /bin/sh bằng cách sửa đổi tệp mật hiệu:
@ARGV = (“/etc/passwd”); # nhồi vào toán tử hình thoi $^I = “.bak”; # ghi /etc/passwd.bak để an toàn while (<>) { # chu trình chính, mỗi lần cho một dòng của
/etc/passwd s#L[^;]*$#:/bin/sh#; # đổi vỏ thành /bin/sh print; # gửi ra ARGVOUT: bản mới /etc/passwd }
Như bạn có thể thấy, chương trình này khá đơn giản.
Thực ra, cùng chương trình này có thể được sinh ra toàn
bộ với một vài đối dòng lệnh như
perl -p -i.bak -e ‘s#:[^:] ‘*$#:/bin/sh#’ /etc/passwd
Chuyển mạch -p đóng ngoặc nhọn chương trình bạn
với chu trình while có chứa một câu lệnh print. Chuyển
mạch -i đặt một giá trị vào trong biến $^I. Chuyển mạch -
e xác định ra đối sau đây như một phần của chương trình
Perl đối với thân chu trình; và đối cuối cùng cho giá trị
khởi đầu cho @ARGV.
Các đối dòng lệnh được thảo luận rất chi tiết trong
sách con lạc đà về Perl.
287
Bài tập
Xem Phụ lục A về lời giải
1. Tạo ra một chương trình để mở cơ sở dữ liệu
sendmail và in ra tất cả các mục.
2. Tạo ra hai chương trình: một đọc dữ liệu hình thoi,
chặt nó ra thành các từ, rồi cập nhật một tệp DBM có
ghi số lần xuất hiện của từng từ; và chương trình kia,
mở tệp DBN và cho hiển thị kết quả được sắp xếp
theo số đếm giảm dần. Chạy chương trình thứ nhất
trên vài tệp rồi xem liệu chương trình thứ hai có nhặt
ra số đếm đúng không.
288
289
18
Chuyển đổi
các ngôn ngữ khác
sang Perl
Chuyển chương trình awk sang Perl
Một trong nhiều điều dễ chịu về Perl là ở chỗ nó là
(ít nhất) siêu tập ngữ nghĩa của awk. Theo ngôn ngữ thực
hành, điều này có nghĩa là nếu bạn có thể làm điều gì đó
trong awk, bạn cũng có thể làm điều đó bằng cách nào
đó trong Perl. Tuy nhiên, Perl không tương thích về cú
pháp với awk. Chẳng hạn, biến NR (số bản ghi vào) của
awk được biểu diễn là $. trong Perl.
Nếu bạn có một chương trình awk có sẵn và muốn
nó chạy với Perl, bạn có thể thực hiện việc phiên dịch
máy móc bằng việc dùng tiện ích a2p được cung cấp
cùng với Perl. Tiện ích này chuyển cú pháp awk thành cú
pháp Perl, và với đại đa số chương trình awk, nó đã cho
Trong chương này:
Chuyển đổi chương trình awk sang Perl
Chuyển đổi chương trình sed sang Perl
Chuyển đổi chương trình Shell sang Perl
290
ra một bản Perl có thể chạy được trực tiếp.
Để dùng tiện ích a2p, đặt chương trình awk của bạn
vào một tệp tách biệt, và gọi a2p với tên của tệp này như
đối của nó, hay hướng lại cái vào chuẩn của a2p cho tệp
này. Cái ra chuẩn kết quả sẽ là một chương trình Perl
hợp lệ. Chẳng hạn:
$ cat myawkprog BEGIN { sum = 0 } / llama / { sum += $2 } END { print “The llama count is “ sum } $ a2p < myawkprog > myperlprog $ perl myperlprog somefile The llama count is 15 $
Bạn cũng có thể nạp cái ra chuẩn của a2p trực tiếp
vào trong Perl, bởi vì trình thông dịch Perl có thể chấp
nhận một chương trình trên cái vào chuẩn nếu được chỉ
thị:
$ a2p < myawkprog | perl - somefile The llama count is 15 $
Một bản awk được chuyển sang Perl nói chung sẽ
thực hiện chức năng đồng nhất, thường với việc tăng
thêm về tốc độ, và chắc chắn không có giới hạn có sẵn
nào của awk về chiều dài dòng hay số đếm tham biến
hay bất kì cái gì. Một vài chương trình Perl đã chuyển
đổi thực tế có thể chạy chậm hơn - hành động tương
đương trong Perl đối với một thao tác awk đã cho có thể
không nhất thiết là chương trình Perl hiệu quả nhất nếu
so với việc lập trình từ đầu.
Bạn có thể chọn tối ưu thủ công cho chương trình
291
Perl đã chuyển đổi, hay thêm chức năng mới vào bản
Perl của chương trình này. Điều này khá dễ dàng, bởi vì
chương trình Perl, dễ đọc hơn (xem xét rằng việc dịch là
tự động, điều này hoàn toàn thực hiện được.)
Một vài bản dịch, vẫn không cơ giới hoá. Chẳng
hạn, phép so sánh bé hơn cho cả hai số và xâu trong awk
đã được biểu diễn bằng toán tử <. Trong Perl, bạn có lt
cho xâu và < cho số. awk nói chung dự đoán có lí về tính
chất số hay xâu của hai giá trị được so sánh, còn tiện ích
a2p chỉ đoán đơn giản. Tuy nhiên, có thể là không biết
đủ về hai giá trị để xác định liệu phép so sánh số hay xâu
được đảm bảo, cho nên a2p đưa ra toán tử có thể nhất và
đánh dấu khả năng dòng lỗi bằng #?? (chú thích của
Perl) và một giải thích. Phải chắc duyệt qua cái ra cho
những lời chú thích như vậy sau khi chuyển đổi để kiểm
chứng lại việc đoán có đúng không.
Về chi tiết hoạt động của a2p, xin xem tài liệu.
Nếu a2p không có trong cùng danh mục mà bạn lấy
Perl, hỏi người cái đặt Perl của bạn.
Chuyển đổi chương trình sed sang Perl
Được, điều này có thể bắt đầu có vẻ như sự lặp lại,
nhưng thử đoán xem cái gì... Perl là một siêu tệp ngữ
nghĩa của sed cũng như awk.
Và đi kèm với Perl là trình dịch sed sang Perl gọi là
s2p. Như với a2p, s2p nhận một bản viết của sed trên cái
vào chuẩn và ghi ra một chương trình Perl trên cái ra
chuẩn. Không giống a2p, chương trình đã được chuyển
đổi hiếm khi hành xử sai, cho nên bạn có thể tin cậy hơn
292
vào việc nó làm việc, chắn lỗi trong s2p hay Perl.
Chương trình sed đã chuyển đổi có thể làm việc
nhanh hơn hay chậm hơn bản gốc, nhưng nói chung là
nhanh hơn nhiều (nhờ có các trình biểu thức chính qui
tối ưu của Perl).
Bản ghi sed đã chuyển đổi có thể làm việc với hay
không có tuỳ chọn -n, mang cùng ý nghĩa như khoá
tương ứng cho sed. Để làm điều này, bản ghi đã chuyển
đổi phải tự nạp nó vào trong bộ tiền xử lí C, và điều này
làm chậm lại việc bắt đầu một chút ít. Nếu bạn biết rằng
bạn bao giờ cũng sẽ gọi tới bản ghi sed đã chuyển đổi dù
có hay không tuỳ chọn -n (như khi bạn đang chuyển bản
ghi sed được dùng trong chương trình vỏ lớn hơn với các
đối đã biết), bạn có thể thông báo s2p (qua các khoá -s
và -p), và nó sẽ tối ưu bản ghi cho việc thiết đặt khoá đó.
Xem như một thí dụ về việc Perl linh hoạt và mạnh
mẽ đến đâu, trình dịch s2p được viết trong Perl. Nếu bạn
muốn thấy cách Larry lập trình trong Perl (cho dù đó là
việc lập trình rất cổ điển gần như không thay đổi từ bản
Perl 2), nhìn vào trình dịch này. Phải chắc là bạn đang
ngồi xuống xem.
Chuyển đổi chương trình Shell sang Perl
Nào. Bạn có nghĩ là có bộ dịch shell sang Perl
không?
Không. Nhiều người đã hỏi điều như thế, nhưng vấn
đề thực lại là ở chỗ phần lớn những điều mà bản ghi lớp
vỏ shell thực hiện, lại không được shell thực hiện. Phần
lớn các bản ghi shell thực ra đã dành tất cả thời gian
293
trong việc gọi các chương trình tách biệt để trích ra các
mẩu của xâu, so sánh số, nối các tệp, xoá danh mục, cứ
như vậy. Việc chuyển đổi một bản ghi như vậy sang Perl
hoặc sẽ đòi hỏi việc hiểu về sự vận hành của từng tiện
ích được gọi, hoặc bỏ lại cho Perl gọi từng tiện ích này,
mà chẳng thu được gì.
Cho nên, cách tốt nhất mà bạn có thể làm là nhìn
vào bản ghi lớp vỏ, hình dung ra nó làm gì, và bắt đầu từ
đầu với Perl. Tất nhiên, bạn có thể làm việc chuyển tự
nhanh và bẩn, bằng việc đặt những phần chính của bản
ghi gốc vào bên trong các lời gọi system() hoặc dấu nháy
đơn ngược. Bạn cũng có thể thay thế một số trong các
phép toán này bằng Perl tự nhiên: chẳng hạn, thay thế
system(“rm fred”) bằng unlink(“fred”), hay một chu trình for
của lớp vỏ bằng chu trình for của Perl. Nhưng nói chung,
bạn sẽ thấy nó có một chút giống việc chuyển chương
trình COBOL vào C (với cùng việc giảm bớt số kí tự và
tăng tính khó đọc).
Bài tập
Xem trả lời ở Phụ lục A.
1. Chuyển bản ghi lớp vỏ sau đây thành chương trình
Perl:
cat /etc/passwd | awk -F : ‘ {print $1, $6 } ‘ | while read user home do newsrc=”$home/ .newsrc” if [ -r $newsrc ] then
294
if grep -s ‘^comp\.lang\.perl:’ $newsrc then echo “$user is a good person, and read
comp.lang.perl!” fi fi done
295
Phụ lục A
Trả lời các bài tập
Phụ lục này nêu câu trả lời cho các bài tập được cho
ở cuối mỗi chương.
Chương 2, Dữ liệu vô hướng
1. Sau đây là một cách để thực hiện nó:
$pi = 3.141592654; $result = 2 * $pi * 12.5; print “radius 12.5 is circumference $result’n” ;
Trước hết cho một giá trị hằng () cho biến vô
hướng $pi. Tiếp đó tính chu vi bằng việc dùng giá trị này
của $pi trong biểu thức. Cuối cùng in ra kết quả bằng
cách dùng một xâu có chứa một tham chiếu tới kết quả
2. Sau đây là một cách thực hiện nó:
print “What is the radius: “; chop ($radius = <STDIN>) ; $pi = 3.141592654;
296
$result = 2 * $pi * $radius ; print “radius $radius is circumference $result’n” ;
Điều này tương tự với bài tập trước, nhưng ở đây
chúng ta hỏi người cho chạy chương trình này về một giá
trị, dùng câu lệnh print để nhắc, rồi dùng toán tử
<STDIN> để đọc một dòng từ thiết bị cuối.
Nếu chúng ta bỏ chop() đi, được dấu dòng mới ở
giữa xâu hiển thị tại cuối. Điều quan trọng là phải bỏ dấu
dòng mới sớm nhất có thể được.
3. Sau đây là một cách thực hiện nó:
print “First number: “; chop ($a = <STDIN>) ; print “Second number: “; chop ($b = <STDIN>) ; $c = $a * $b ; print “answer is $c.\n” ;
Dòng thứ nhất làm ba việc: nhắc bạn bằng một
thông báo, đọc một dòng từ cái vào chuẩn, rồi bỏ đi dấu
dòng mới không tránh khỏi tại cuối xâu. Lưu ý rằng vì
chúng ta đang dùng biến của $a hoàn toàn là số nên có
thể bỏ chop() ở đây, vì 45\n là 45 khi được dùng theo kiểu
số. Tuy nhiên, việc lập trình bất cẩn như thế có thể quay
lại ám ảnh chúng ta về sau (chẳng hạn, nếu chúng ta đưa
$a vào trong thông báo).
Dòng thứ hai làm cùng điều cho số thứ hai và đặt nó
vào trong biến vô hướng $b.
Dòng thứ ba nhân hai số với nhau và in ra kết quả.
Lưu ý dấu dòng mới tại cuối xâu ở đây, tương phản với
việc thiếu nó ở hai dòng đầu. Hai thông báo đầu là lời
nhắc, mà với nó cái vào của người dùng cần được đặt
trên cùng dòng. Thông báo cuối này là câu lệnh đầy đủ;
297
nếu chúng ta bỏ dấu dòng mới ra khỏi xâu, lời nhắc của
lớp vỏ sẽ xuất hiện ngay sau thông báo này. Không hay
lắm.
3. Sau đây là một cách thực hiện nó:
print “String: “; $a = <STDIN> ; print “Number of times: “; chop ($b = <STDIN>) ; $c = $a * $b ; print “The result is: \n$c” ;
Giống như thí dụ trước, hai dòng đầu hỏi, và nhận,
các giá trị cho hai biến. Không giống bài tập trước,
chúng ta không vứt dấu dòng mới tại cuối xâu, bởi vì cần
nó! Dòng thứ ba lấy hai giá trị đưa vào và thực hiện việc
lặp lại xâu trên chúng, rồi hiển thị câu trả lời. Lưu ý rằng
việc xen lẫn $c là không cho phép dấu dòng mới, bởi vì
chúng ta tin rằng $c bao giờ cũng sẽ kết thúc trong một
dấu dòng mới theo bất kì cách nào.
Chương 3, Mảng và dữ liệu danh sách
1. Một cách thực hiện điều này là:
print “Enter the list of strings:\n “; @list = <STDIN> ; @reverselist = reverse (@list) ; print @reverselist ;
Dòng đầu tiên nhắc các xâu. Dòng thứ hai đọc xâu
vào một biến mảng. Dòng thứ ba tính danh sách theo thứ
tự ngược, cất giữ nó vào trong một biến khác, và dòng
cuối cùng hiển thị kết quả.
Thực tế chúng ta có thể tổ hợp ba dòng cuối, kết quả
là:
298
print “Enter the list of strings:\n “; print reverse (<STDIN>) ;
Điều này làm việc bởi vì phép toán print trông đợi
một danh sách, còn reverse() cho lại một danh sách - cho
nên chúng hoà hợp. Và reverse() cần một danh sách các
giá trị để đảo ngược, còn <STDIN> trong hoàn cảnh
mảng, cho lại một danh sách các dòng, cho nên chúng
cũng hợp nữa!
2. Một cách thực hiện điều này là:
print “Enter the line number: ”; chop ($a = <STDIN>) ; print “Enter the lines, end with ^D:\n”; $b = <STDIN> ; print “Answer: $b[$a-1]” ;
Dòng thứ nhất nhắc một số, đọc nó từ cái vào chuẩn,
và bỏ dấu dòng mới phiền phức. Dòng thứ hai hỏi một
danh dách các xâu, rồi dùng toán tử <STDIN> trong
hoàn cảnh mảng để đọc tất cả các dòng cho tới cuối tệp
vào trong một biến mảng. Câu lệnh cuối cùng in ra câu
trả lời, bằng việc dùng một tham chiếu mảng để chọn ra
đúng dòng. Lưu ý rằng chúng ta không phải thêm dấu
dòng mới vào cuối xâu này, bởi vì dòng này được chọn
từ mảng @b vẫn có kết thúc là dấu dòng mới.
Nếu bạn thử điều này từ một thiết bị cuối được lập
cấu hình theo cách thông thường nhất, bạn sẽ cần gõ
Control-D tại bàn phím để chỉ ra cuối tệp.
3. Một cách thực hiện điều này là:
srand; print “List of strings: ”; $b = <STDIN> ;
299
print “Answer: $b[rand(@b)]” ;
Dòng thứ nhất khởi đầu cho bộ sinh số ngẫu nhiên.
Dòng thứ hai đọc một loạt các xâu. Dòng thứ ba chọn
một phần tử ngẫu nhiên từ loạt các xâu đó và in nó ra.
Chương 4, Cấu trúc điều khiển
1. Sau đây là một cách thực hiện nó:
print “What temperature is it? ”; chop ($temperature = <STDIN>) ; if ($temperature > 72) { print “Too hot!\n” ; } else { print “Too cold!\n” ; }
Dòng đầu tiên nhắc bạn về nhiệt độ. Dòng thứ hai
chấp nhận nhiệt độ làm cái vào. Câu lệnh if trên dòng thứ
5 chọn một trong hai thông báo để in ra, tuỳ theo giá trị
của $temperature.
2. Sau đây là một cách thực hiện nó:
print “What temperature is it? ”; chop ($temperature = <STDIN>) ; if ($temperature > 75) { print “Too hot!\n” ; } elsif ($temperature < 68) { print “Too cold!\n” ; } else { print “Just right!\n” ; }
Tại đây chúng ta đã sửa chương trình này để bao
300
gồm việc chọn lựa ba ngả. Trước hết, nhiệt độ được so
sánh với 75, rồi 68. Lưu ý rằng chỉ một trong ba sự chọn
lựa này sẽ được thực hiện mỗi lần qua chương trình này.
3. Sau đây là một cách thực hiện nó:
print “Enter a number (999 to quit): ”; chop ($n = <STDIN>) ; while ($n != 999) { $sum += $n ;
print “Enter another number (999 to quit): “; chop ($n = <STDIN>) ;
} print “The sum is $sum\n” ;
Dòng thứ nhất nhắc cho số thứ nhất. Dòng thứ hai
đọc số từ thiết bị cuối. Chu trình while tiếp tục thực hiện
chừng nào mà số này chưa lớn hơn 999.
Toán tử += tích luỹ các số vào trong biến $sum. Lưu
ý rằng giá trị khởi đầu của $sum là undef, một giá trị hay
cho bộ tích luỹ bởi vì giá trị thứ nhất được cộng vào sẽ là
cộng với 0 (nhớ rằng undef được dùng như số là không.)
Bên trong chu trình này, chúng ta phải nhắc và nhận
một số khác, để cho phép kiểm thử tại đỉnh của chu trình
lại là một số mới được đưa vào.
Khi chu trình được đi ra, chương trình sẽ in kết quả
đã tích luỹ.
Lưu ý rằng nếu bạn đưa vào 999 ngay, giá trị của
$sum sẽ khác không, nhưng là một xâu rỗng - giá trị của
undef khi được dùng như một xâu. Nếu bạn muốn đảm
bảo rằng chương trình in ra không trong trường hợp này,
bạn nên khởi đầu giá trị của $sum ở đầu chương trình với
301
$sum = 0.
4. Sau đây là một cách thực hiện nó:
print “Enter some strings, end with ^D:\n”; $strings = <STDIN> ; while ($strings) { print pop(@strings) ; }
Trước hết chương trình này yêu cầu các xâu. Các
xâu này được cất giữ trong biến mảng @strings, mỗi xâu
một phần tử.
Biểu thức điều khiển của chu trình while là @strings.
Biểu thức điều khiển đang tìm một giá trị (true hay
false), và do đó tính biểu thức này trong hoàn cảnh vô
hướng. Tên của mảng (như @strings) khi được dùng
trong hoàn cảnh vô hướng là số các phần tử hiện đang
trong mảng. Chừng nào mà mảng còn không rỗng, số
này là khác không, và do đó đúng. Điều này là rất thông
dụng trong khẩu ngữ Perl “làm điều này khi mảng khác
rỗng.”
Thân của chu trình in ra một giá trị, thu được bởi
việc lấy ra phần tử bên phải nhất của mảng. Do vậy, mỗi
lần qua chu trình, mảng lại ngắn bớt đi một phần tử, bởi
vì phần tử đó đã được in ra.
Bạn có thể xét việc dùng chỉ số cho vấn đề này. Như
chúng tôi đã nói, có nhiều cách thực hiện nó. Tuy nhiên,
bạn hiếm khi thấy chỉ số trong các chương trình Perl
thực sự bởi vì gần như bao giờ cũng có cách khác tốt
hơn.
302
5. Sau đây là một cách thực hiện nó không dùng danh
sách:
for ($number = 0 ; $number <= 32 ; $number++) { $square = $number * $number ;
print “%5g %8g\n” , $number, $square; }
Và đây là cách thực hiện nó dùng danh sách
foreach $number (0..32) { $square = $number * $number; printf “%5g %8g\n”, $number, $square ;
Các giải pháp này cả hai đã chứa chu trình, bằng
việc dùng các câu lệnh for và foreach. Thân của các chu
trình này là như nhau, bởi vì với cả hai giải pháp, giá trị
của $number đã từ 0 tới 32 cho mỗi lần lặp.
Giải pháp thứ nhất dùng câu lệnh for kiểu C truyền
thống. Ba biểu thức tương ứng: đặt $number là 0, kiểm
tra để xem liệu $number có bé hơn 32 hay không, và tăng
$number sau mỗi lần lặp.
Giải pháp thứ hai dùng câu lệnh foreach tựa lớp vỏ
C. Danh sách 33 phần tử (0 tới 32) được tạo ra, bằng
việc dùng cấu tử danh sách. Biến $number vậy được đặt
lần lượt cho mỗi phần tử.
Chương 5, Mảng kết hợp
1. Sau đây là một cách thực hiện nó:
%map = (‘red’, ‘apple’, ‘green’, ‘leaves’, ‘blue’, ‘ocean’) ; print “A string please: ”; chop ($some_string =
<STDIN>) ;
303
print “The value for $some_string Ý $map{$some_string}\n”;
Dòng thứ nhất tạo ra mảng kết hợp, cho nó cặp
khoá-giá trị mong muốn. Dòng thứ hai lấy một xâu, loại
bỏ dấu dòng mới phiền phức. Dòng thứ ba in ra giá trị đã
đưa vào và giá trị được ánh xạ của nó.
Bạn cũng có thể tạo ra mảng kết hợp qua một loạt
các phép gán tách biệt, kiểu như:
$map {‘red’} = ‘apple’ ; $map {‘green’} = ‘leaves’ ; $map {‘blue’} = ‘ocean’ ;
2. Sau đây là một cách thực hiện nó:
@words = <STDIN> ; # read the words foreach $word (@words) { chop ($words) ; # remove that pesky newline $count {$word} = $count {$word} + 1 ; # or $count
{$word}++ } foreach $word (keys %count) { print “$word was seen $count {$word} times\n” ; }
Dòng đầu tiên đọc các dòng vào mảng @words. nhớ
rằng điều này sẽ làm cho từng dòng kết thúc như một
phần tử tách biệt của mảng, với kí tự dòng mới vẫn
không bị động đến.
Bốn dòng tiếp bước qua mảng, đặt $word lần lượt
bằng mỗi dòng. Dấu dòng mới được bỏ đi bằng chop(),
và rồi điều ảo thuật xảy ra. Mỗi từ được dùng như một
khoá trong mảng kết hợp. Giá trị của phần tử được chọn
bởi khoá này (word) là số đếm số lần chúng ta đã thấy từ
304
đó cho tới lúc đó. Khởi đầu, không có phần tử nào trong
mảng cả, cho nên nếu từ wild được thấy lần đầu tiên,
chúng ta có $count {“wild”}, vốn là undef. Giá trị undef
cộng với một trở thành không cộng một, hay một. (Nhớ
rằng undef trông giống như không nếu được dùng như
số.) Lần tiếp chạy qua, chúng ta cộng thêm một, hay hai
vân vân.
Một cách thông thường khác để viết việc tăng lên
được cho trong phần chú thích. Người lập trình Perl
thành thạo có khuynh hướng lười nhác (chúng ta gọi
điều đó là “cô đọng”), và sẽ không bao giờ đi viết tham
chiếu cùng mảng kết hợp ở cả hai vế của phép gán khi
một phép tự tăng đơn giản cũng làm được điều đó.
Sau khi các từ đã được đếm xong, vài dòng cuối
bước qua mảng kết hợp bằng cách nhìn vào từng khoá
của nó mỗi lúc. Khoá và giá trị tương ứng được in ra sau
khi đã được xen lẫn vào trong xâu.
Câu trả lời thách thức phụ thêm trông như câu trả lời
này, với toán tử sort được chèn ngay trước từ keys trên
dòng thứ ba kể từ cuối. Không sắp xếp, cái ra kết quả
dường như là ngẫu nhiên và không thể đoán nổi. Tuy
nhiên, một khi đã được sắp xếp, cái ra là dự đoán được
và nhất quán. (Riêng cá nhân tôi, tôi thà dùng toán tử
keys mà không thêm sắp xếp vào ngay lập tức trước nó -
điều này đảm bảo rằng việc cho chạy lại trên cùng dữ
liệu hay dữ liệu tương tự sẽ sinh ra kết quả so sánh
được.)
305
Chương 6, Vào/ra cơ sở
1. Sau đây là một cách thực hiện nó:
print reverse <> ;
Bạn có thể ngạc nhiên ở sự vắn tắt của câu trả lời
này, nhưng điều này sẽ làm cho mọi việc được thực hiện.
Đây là điều vẫn xảy ra, từ bên trong ra:
Trước hết, toán tử reverse tìm danh sách các đối của
nó. Điều này có nghĩa là toán tử hình thoi (<>) được tính
trong hoàn cảnh mảng. Vậy, tất cả các dòng của các tệp
có tên trong đối dòng lệnh (hoặc cái vào chuẩn, nếu
không tệp nào được nêu tên) được đọc vào, và được
nhào nặn thành danh sách với mỗi dòng một phần tử.
Tiếp đó toán tử reverse đảo ngược danh sách từ đầu
nọ sang đầu kia.
Cuối cùng toán tử print tạo ra danh sách kết quả, và
hiển thị nó.
2. Sau đây là một cách thực hiện nó:
print “List of strings:\n” ; chop (@strings = <STDIN>) ; foreach (@strings) { printf “%20s\n”, $_ ; }
Dòng đầu tiên nhắc danh sách các xâu.
Dòng tiếp đọc tất cả các xâu vào một mảng, và bỏ
dấu dòng mới tại cuối mỗi dòng.
Chu trình foreach bước qua mảng này, cho $_ giá trị
của mỗi dòng.
306
Toán tử printf nhận hai đối: đối thứ nhất xác định
dạng thức - “%20s\n” có nghĩa là cột dồn phải 20 kí tự,
tiếp sau đó là dòng mới.
3. Sau đây là một cách thực hiện nó:
print “Chiều rộng trường: ” ; chop ($width = <STDIN>) ; print “List of strings:\n” ; chop(@strings = <STDIN>) ; foreach (@strings) { prìnt “%$ {width}s\n”, $_ ; }
Từ lời giải của bài tập trước, chúng ta đã thêm lời
nhắc và đáp ứng cho chiều rộng trường.
Việc thay đổi khác là ở chỗ xâu dạng thức cho printf
bây giờ có chứa một tham chiếu biến. Giá trị của $width
được đưa vào bên trong xâu trước khi printf xem xét dạng
thức này. Lưu ý rằng chúng ta không thể viết xâu này
như:
printf “%$width\n”, $_ ; # sai
vì thế, Perl sẽ tìm một biến có tên là $widths, không
phải là biến có tên $width mà gắn cho s. Cách khác để
viết điều này là:
printf “%$width”.”s\n”, $_ ; # đúng
bởi vì việc kết thúc của xâu này cũng sẽ kết thúc tên
biến, bảo vệ kí tự sau khỏi bị hút hết vào tên.
307
Chương 7, Biểu thức chính qui
1. Sau đây là một số câu trả lời có thể
(a) /a+b*/
(b) /\\*\**/ (Nhớ rằng dấu sổ chéo ngược phủ định ý
nghĩa của kí tự đặc biệt đi sau.)
(c) / ($whatver) {3} / (Bạn phải có dấu ngoặc tròn,
hoặc nếu không, phép nhân sẽ áp dụng vào kí tự
cuối của $whatever; điều này cũng sai nếu
$whatever có kí tự đặc biệt.)
(d) / [\000-\377] {5} / hay / (.| \n) {5} / (Bạn không thể
dùng chấm một mình ở đây, bởi vì chấm không
sánh với dấu dòng mới.)
(e) / (^| \s) (\S+) (\s+\2)+/ (\S là không khoảng trắng,
còn \2 là tham chiếu tới bất kì cái gì mà “từ” có
thể là; dấu mũ hay thay thế khoảng trắng đảm
bảo rằng \S+ bắt đầu tại biên khoảng trắng.)
2. (a) Một cách thực hiện điều này là:
while (<STDIN>) { if (/a/i && /e/i && /i/i && /o/i && /u/i) { print ; } }
Tại đây, chúng ta có một biểu thức bao gồm năm
toán tử đối sánh. Các toán tử này tất cả đã nhòm vào nội
dung của biến $_, mà là nơi biểu thức điều khiển của chu
trình while đặt vào từng dòng. Toán tử đối sánh sẽ đúng
chỉ khi tìm thấy tất cả năm nguyên âm.
308
Lưu ý rằng ngay khi không tìm thấy bất kì một trong
năm nguyên âm này, phần còn lại của biểu thức này bị
bỏ qua, bởi vì toán tử && không tính đối dúng của nó
nếu đối bên trái sai.
(b) Một cách để làm điều này là:
while (<STDIN>) { if (/a,*e.*i.*o.*u/i) { print ; } }
Câu trả lời này trở nên dễ dàng hơn phần khác của
bài tập này. Tại đây chúng ta có một biểu thức chính qui
đơn giản, tìm năm nguyên âm tuần tự, tác nhau bởi một
số dấu cách.
3. Một cách thực hiện điều này là:
while (<STDIN>) { ($user, $pass, $uid, $gid, $gcos) = split(/:/) ; ($real) = split (/,/,$gcos); print “$user is $real\n” ; }
Chu trình while ngoài đọc một dòng mỗi lúc từ tệp
dạng thức mật hiệu, vào trong biến $_, kết thúc khi
không còn dòng nào được đọc vào.
Dòng thứ nhất của thân chu trình while chia phần
dòng này ra theo dấu hai chấm, cất giữ năm giá trị đầu
vào các biến vô hướng riêng với tên có ý nghĩa.
Trường GCOS (trường thứ năm) được chặt ra theo
dấu phẩy, với danh sách kết quả được gán cho một biến
vô hướng đến được bao trong ngoặc tròn. Các ngoặc tròn
309
này là quan trọng: chúng ta làm cho phép gán này thành
phép gán mảng, thay vì là phép gán logic. Biến vô hướng
$real nhận phần tử đầu của danh sách và các phần tử còn
lại là được bỏ qua.
4. Một cách thực hiện điều này là:
while (<STDIN>) { ($user, $pass, $uid, $gid, $gcos) = split(/:/) ; ($real) = split (/,/,$gcos); ($first) = split(/\s+/, $real) ; $seen {$first}++; } foreach (key %seen) { if ($seen {$_} > 1) {
print “$_ was seen $seen {$_} times\n” ; }
}
Chu trình while làm việc khá giống với cho trình
while trong bài tập trước. Bên cạnh việc chặt dòng này ra
thành các trường và trường GCOS thành tên thực (và các
phần khác), chu trình này cũng chặt tên thực thành tên
đầu (và phần còn lại). Một khi tên đầu được biết tới,
phần tử mảng kết hợp trong %seen được tăng lên, lưu ý
rằng chúng đã thấy một tên đầu tiên đặc biệt. Lưu ý rằng
chu trình này không làm việc in nào.
Chu trình foreach bước qua tất cả các khoá
của %seen (các tên đầu lấy từ tệp mật hiệu), gán cho mỗi
khoá lần lượt vào trong $_. Nếu giá trị này được cất giữ
trong %seen tại một khoá đã cho mà lớn hơn một, chúng
ta thấy cái tên đầu này nhiều lần. Câu lệnh if kiểm thử
điều này, và in ra thông báo nếu cần.
310
5. Một cách thực hiện điều này là:
while (<STDIN>) { ($user, $pass, $uid, $gid, $gcos) = split(/:/) ; ($real) = split (/,/,$gcos); ($first) = split(/\s+/, $real) ; $name {$first} .= “ $user” ; } foreach (key %seen) { if ($seen {$_} =~ /. /) {
print “$_ is used by: $seen {$_} times\n” ; }
}
Chương trình này giống như câu trả lời ở bài tập
trước, nhưng thay vì đơn thuần cất giữ số đếm, chúng ta
gắn thêm tên đăng nhập của người dùng vào phần
tử %names mà có khoá cho tên đầu. Vậy với Fred Rogers
(đăng nhập mrrogers), $names {“Fred”} trở thành
“ mrrogers”, và khi Fred Flintstone (đăng nhập fred) tới,
được $names {“Fred”} là “ mrrogers fred”. Sau khi chu
trình này hoàn tất, chúng ta có một bảng tương ứng tất cả
các tên đầu cho tất cả những người dùng có chúng ta.
Chu trình foreach, giống như câu trả lời của bài tập
trước, sẽ bước qua mảng kết hợp kết quả. Tuy nhiên,
thay vì kiểm thử một giá trị phần tử mảng kết hợp với
một số lớn hơn một, bây giờ chúng ta phải xem liệu có
nhiều tên đăng nhập theo giá trị này không. Chúng ta
làm điều này bởi việc cất giữ giá trị này vào trong biến
vô hướng $names (lấy tên giống %names để cho tiện) và
xem liệu giá trị này có dấu cách theo sau bất kì kí tự nào
không. Nếu có, tên đầu sẽ được dùng chung, và thông
báo kết quả cho biết đăng nhập nào sẽ dùng chung tên
311
đó.
Chương 8, Hàm
1. Sau đây là một cách để thực hiện nó
sub card { @card_map = (0, “one”, “two”, “three”, “four”, “five”, “six”, “seven”, “eight”, “nine”) ; local ($num) = @_ ; if ($card_map[$num]) { $card_map[$num] ; # return value } else { $num ; # return value } } # driver routine: while (<>) { chop ; print “card of $_ is “, &card($_), “\n” ; }
Trình &card (tên như vậy vì nó cho lại một tên số
lượng với một giá trị đã cho) bắt đầu bằng việc khởi đầu
một mảng hằng gọi là @card_map. Mảng này có các giá
trị sao cho $card_map[6] là six, làm cho nó khá dễ thực
hiện ánh xạ này.
Câu lệnh if xác định liệu giá trị này có trong phạm vi
hay không bằng việc nhìn vào số trong mảng - nếu có
một phần tử mảng tương ứng, phép thử là đúng, sao cho
phần tử mảng được cho lại. Nếu không có phần tử nào
tương ứng (như khi $num là 11 hay -4, giá trị cho lại từ
việc tra bảng là undef , cho nên nhánh else của câu lệnh if
được thực hiện, cho lại số gốc. Bạn cũng có thể thay thế
312
toàn bộ câu lệnh if với một biểu thức:
$card_map {$num} || $num ;
Nếu giá trị vế bên trái của || là đúng, nó là giá trị của
toàn bộ biểu thức, mà sẽ được cho lại. Nếu nó là sai (như
khi $num ở ngoài phạm vi), vế phải của toán tử || được
tính, cho lại $num xem như giá trị cho lại.
Trình điều khiển nhận các dòng liên tiếp, cắt bỏ dấu
dòng mới của chúng ta và mỗi lúc trao chúng cho trình
&card, in ra kết quả.
2. Sau đây là một cách thực hiện nó:
sub card { ... ; } # from previous problem print “Enter first number: “ ; chop ($first = <STDIN>) ; print “Enter second number: “ ; chop ($second = <STDIN>) ; $message = &card($first) . “ plus “ . &card($second) . “ equals “ . &card($first+$second) . “.\n” ; $message =~ s/^./\u$&/ ; print $message ;
Hai câu lệnh print thứ nhất nhắc đưa vào hai số, tiếp
ngay sau đó là hai câu lệnh đọc các giá trị vào $first và
$second.
Một xâu có tên $message được xây dựng nên bằng
việc gọi &card ba lần, mỗi lần cho một giá trị, và một lần
cho tổng.
Khi thông báo này được xây dựng, kí tự thứ nhất
của nó được viết hoa lên bằng toán tử sổ chéo ngược \u.
Thông báo này tiếp đó được in ra. Bạn có thể tổ hợp hai
313
câu lệnh cuối là:
print “\u$message” ;
Nhưng không may là bạn không thể tổ hợp cách xây
dựng thông báo này với việc làm thành chữ hoa cho kí tự
đầu tiên một cách dễ dàng.
3. Sau đây là một cách thực hiện nó:
sub card { @card_map = (0, “one”, “two”, “three”, “four”, “five”, “six”, “seven”, “eight”, “nine”) ; local ($num) = @_ ; if ($num < 0) { $negative = “negative” ; $num = - $num ; } if ($card_map[$num]) { $negative . $card_map[$num] ; #
return value } else { $negative . $num ; # return value } }
Tại đây chúng ta cho mảng @card_map một tên cho
số không.
Hai câu lệnh if đầu tiên đổi dấu của $num, và đặt
$negative thành từ phủ định, nếu số tìm được là bé hơn
không. Sau câu lệnh if này, giá trị của $num, bao giờ
cũng khác không, nhưng chúng ta sẽ có một xâu tiền tố
thích hợp trong $negative.
Câu lệnh if thứ hai xác định liệu $num (bây giờ
314
dương) có bên trong mảng hay không. Nếu có, mảng kết
quả được gắn thêm phần tiền tố với $negative, và được
cho lại. Nếu không, giá trị bên trong $negative được gắn
với số gốc.
Câu lệnh if thứ ba có thể được thay thế bằng biểu
thức:
$negative . ($card_map[$num] || $num) ;
Chương 9, Các cấu trúc điều khiển khác
1. Sau đây là một cách để thực hiện nó
sub card { } # from previous exercise
while ( ) { ## NEW ## print “Enter first number: “ ; chop ($first = <STDIN>) ; last if $first eq “end” ; ## NEW ## print “Enter second number: “ ; chop ($second = <STDIN>) ; last if $second eq “end” ; ## NEW ## $message = &card($first) . “ plus “ . &card($second) . “ equals “ . &card($first+$second) . “.\n” ; $message =~ s/^./\u$&/ ; print $message ; } ## NEW $$
Chú ý tới việc bổ sung thêm chu trình while và hai
toán tử last. Vậy đấy!
315
Chương 10, Tước hiệu tệp và kiểm thử tệp
1. Sau đây là một cách để thực hiện nó
print “What file? “ ; chop ($filename = <STDIN>) ; open (THAFILE, “$filename”) || die “cannot open
$filename” ; while (<THAFILE>) { print “$filename: $_” ; # presume $_ ends in \n }
Hai dòng đầu tiên nhắc tên tệp, rồi tên đó được mở
với tước hiệu THAFILE. Nội dung của tệp này được đọc
bằng việc dùng tước hiệu này, và được in ra STDOUT.
2. Sau đây là một cách để thực hiện nó
print “Input file name: “ ; chop ($inputfilename = <STDIN>) ;
print “Output file name: “ ; chop ($outputfilename = <STDIN>) ; print “Search string: “ ; chop ($search = <STDIN>) ; print “Replacement string: “; chop ($replace = <STDIN>) ; open (IN, $infilename) || die “cannot open $infilename
for reading”; ## kiểm thử tuỳ chọn cho khỏi ghi đè... die “will not overwrite $outputfilename” if -e
$outpufilename ; open (OUT, $outfilename) || die “cannot create
$outfilename”; while (<IN>) { # read a line from file $a into $_ s/$search/$replace/g ; # change the lines print OUT $_; # print that line to file $b }
316
close (IN); close (OUT);
Chương trình này dựa trên chương trình sao tệp đã
trình bầy trước đây trong chương này. Các tính năng mới
đưa vào ở đây để nhắc cho các xâu, và chỉ lệnh thay thế
ở giữa chu trình while, cũng như phép kiểm tra cho việc
ghi đè tệp.
3. Sau đây là một cách để thực hiện nó
while (<>) { chop ; # eliminate the newline print “$_ is readable\n” if -r ; print “$_ is writable\n” if -w _ ; print “$_ is executable\n” if -x _; print “$_ does not exist\n” unless -e ;
}
Chu trình while này đọc một tước hiệu tệp mỗi lần.
Sau khi bỏ đi dấu dòng mới, loạt các câu lệnh kiểm tra
tệp theo các phép khác nhau. Lưu ý rằng phép kiểm tra
thứ hai và tiếp sau đó dùng tước hiệu _ để khỏi phải hỏi
lặp đi lặp lại hệ điều hành về cùng một tệp.
4. Sau đây là một cách để thực hiện nó
while (<>) { chop ; $age = -M ; if ($oldest_age < $age) { $oldest_name = $_ ; $oldest_age = $age ; }
}
317
print “The oldest file Ý $oldest_name “, “and is $oldest_age days old.\n” ;
Trước hết, chúng ta lặp trên từng tên tệp được đọc
vào. Dấu dòng mới bị bỏ đi, và thế rồi tuổi tính theo
ngày được tính bằng toán tử -M. Nếu tuổi cho tệp này
vượt quá tệp cũ nhất mà đã thấy cho tới giờ, chúng ta
nhớ tên tệp này và tuổi tương ứng của nó. Khởi đầu,
$oldest_age sẽ là 0, cho nên chúng ta đếm ở đó ít nhất
một tệp khác 0 ngày cũ.
Câu lệnh print cuối cùng sinh ra báo cáo khi chúng ta
hoàn thành.
Chương 11, Dạng thức
1. Sau đây là một cách để thực hiện nó
open (PW, “/etc/passwd”) || die “How did you get logged in?”
while (<PW>) { ($user, $passwd, $uid, $gid, $gcos) = split(/:/) ;
($real) = split(/,/, $gcos) ; write;
} format STDOUT = @<<<<<<< @>>>>>>> @<<<<<<<<<<<<<<<<<<<<<<< $user, $uid, $real .
Dòng đầu tiên mở tệp mật hiệu. Chu trình while xử
lí tệp mật hiệu theo từng dòng. Mỗi dòng được xé ra (với
định biên hai chấm). Nạp vào biến vô hướng. Tên thực
của người dùng được lấy ra trường GCOS. Câu lệnh cuối
cùng của chu trình while gọi tới việc ghi ra thiết bị hiển
thị tất cả các dữ liệu này.
318
Dạng thức cho tước hiệu tệp STDOUT xác định ra
một dòng đơn giản với ba trường. Các giá trị bắt nguồn
từ ba biến vô hướng được cho giá trị trong chu trình
while.
2. Sau đây là một cách để thực hiện nó
# append to program from the first problem... format STDOUT_TOP = Username User ID Real Name ======== ====== ========= @<<<<<< @>>>>> @<<<<<<<< .
Tất cả mọi việc để làm tiêu đề cho chương trình
trước là thêm vào dạng thức đầu trang. Tại đây chúng ta
đặt tiêu đề cột vào các cột.
Để cho các cột thẳng hàng, tôi đã sao văn bản của
dạng thức STDOUT và dùng mốt ghi đè trong trình soạn
thảo văn bản của tôi để thay thế các trường @<<< bằng
===. đó là điều hay về tương ứng một-một giữa các kí tự
trong dạng thức và kết quả hiển thị.
3. Sau đây là một cách để thực hiện nó
# append to program from the first problem... format STDOUT_TOP = Page @<<< $% Username User ID Real Name ======== ====== ========= .
319
Được rồi, lần nữa ở đây lại được chất liệu ở đầu
trang. Tôi đã thêm vào dạng thức đầu trang. Dạng thức
này cũng chứa một tham chiếu tới $%, sẽ cho việc đánh
số trang.
Chương 12, Truy nhập danh mục
1. Sau đây là một cách để thực hiện nó
print “Where to? “ ; chop ($newdir = <STDIN>) ; chdir ($newdir) || die “Cannot chdir to $newdir” ; foreach (<*>) { print “$_\n”; }
Hai dòng đầu nhắc và đọc tên của danh mục.
Dòng thứ ba cố gắng thay đổi danh mục sang tên đã
cho, bỏ ra nếu điều này là không thể được.
Chu trình foreach đi qua danh sách. Nhưng danh
sách là gì? Đó là việc dò qua trong hoàn cảnh mảng, mở
rộng tới một danh sách tất cả các tên tệp mà sánh đúng
với mẫu này (ở đây là *).
2. Sau đây là một cách để thực hiện nó
print “Where to? “ ; chop ($newdir = <STDIN>) ; chdir ($newdir) || die “Cannot chdir to $newdir” ; opendir (DOT, “.”) || die “Cannot opendir . (serious
dainbramage)” ; foreach (sort readdir(DOT)) { print “$_\n”; }
320
close (DOT) ;
Giống như chương trình trước, chúng ta nhắc và đọc
danh mục mới. Một khi chúng ta đã đổi danh mục chdir
ở đó, chúng ta mở danh mục bằng việc tạo ra một tước
hiệu danh mục có tên DOT. Trong chu trình foreach,
danh sách được cho lại bởi readdir (trong hoàn cảnh
mảng) được sắp xƠpm và rồi được duyệt qua, bằng việc
gán từng phần tử lần lượt cho $_.
Và đây là cách thực hiện nó với glob:
print “Where to? “ ; chop ($newdir = <STDIN>) ; chdir ($newdir) || die “Cannot chdir to $newdir” ; foreach (sort <* .*>) { print “$_\n”; }
Vâng, về cơ bản nó là chương trình khác với bài tập
trước đây, nhưng tôi đã thêm toán tử sort vào phía trước
của glob, và cũng thêm .* vào glob để lấy ra các tệp bắt
đầu với dấu chấm. Vhúng cần sort bởi vì một tệp có
tên !fred đi trước tệp chấm, nhưng barney đi sau chúng,
và không có glob dễ dàng có thể làm cho tất cả chúng
theo trình tự đúng.
Chương 13, Thao tác tệp và danh mục
1. Sau đây là một cách để thực hiện nó
unlink(@ARGV) ;
Đấy, có vậy thôi. Mảng @ARGV là một danh sách
các tên cần phải loại bỏ. Toán tử unlink nhận một danh
sách các tên, cho nên chúng chỉ lấy hai tên là đã xong.
321
Tất nhiên, điều này không giải quyết việc báo cáo
lỗi, hay các tuỳ chọn -f hay -i, hay bất kì cái gì giống thế,
nhưng đó chỉ là cái lợi bất ngờ. Nếu bạn làm thế, tốt!
2. Sau đây là một cách để thực hiện nó
($old, $new) = @ARGV; # name them if (-d $new) { # new name is a directory, need to patch it
up ($basename = $old) =~ s#.*/## ; # get basename of
$old $new .= “/$basename” ; and append it to new name } rename ($old, $new) ;
Toàn bộ công việc trong chương trình này là ở dòng
cuối, nhưng phần còn lại của chương trình này là cần
thiết cho trường hợp khi tên chúng ta đang nhằm đổi là
một danh mục.
Trước hết, chúng ta đặt tên dễ hiểu cho hai phần tử
của @ARGV. Thế rồi, nếu tên $new là một danh mục,
chúng ta cần vá thêm cho nó bằng việc thêm tên mới vào
tên cơ sở của tên $old ở cuối. Điều này có nghĩa là đổi
tên /usr/src/fred thành /etc sẽ gây ra việc đổi tên
/usr/src/fred thành /etc/fred
Cuối cùng, một khi tên cơ sở được vá thêm, chúng
về gốc, với lời gọi rename.
3. Sau đây là một cách để thực hiện nó
($old, $new) = @ARGV; # name them if (-d $new) { # new name is a directory, need to patch it
up
322
($basename = $old) =~ s#.*/## ; # get basename of $old
$new .= “/$basename” ; and append it to new name } link ($old, $new) ;
Chương trình này giống như chương trình trước
ngoại trừ dòng cuối cùng, bởi vì chúng móc nối chứ
không đổi tên.
4. Sau đây là một cách để thực hiện nó
if ($ARGV[0] eq “-s”) { # want a symlink $symlink++ ; # remember that shift (@ARGV) ; # and toss the -s flag }
($old, $new) = @ARGV; # name them if (-d $new) { # new name is a directory, need to patch it
up ($basename = $old) =~ s#.*/## ; # get basename of
$old $new .= “/$basename” ; and append it to new name } if ($symlink) { # want a symlink $symlink($old, $new) ; } else { # want a hard link link ($old, $new) ; }
Phần giữa của chương trình này cũng giống như hai
bài tập trước. Cái mới là ở vài dòng đầu và vài dòng
cuối.
Vài dòng đầu nhìn vào đối thứ nhất của chương
trình. Nếu đối này là -s, biến vô hướng $symlink được
tăng lên, làm sinh ra giá trị 1 cho biến này. Mảng
323
@ARGV được dịch đi, bỏ cờ -s. Nếu như cờ -s không có
đó, chẳng phải làm gì, và $symlink sẽ vẫn còn là undef.
Việc dịch chuyển mảng @ARGV xuất hiện thường
xuyên đến mức mảng @ARGV là đối ngầm định cho
shift - tức là chúng ta có thể nói:
shift;
thay cho
shift (@ARGV);
Vài dòng cuối nhìn vào giá trị của $symlink. Nó hoặc
là 1 hoặc là undef, và dựa trên đó, sẽ symlink các tệp hay
link chúng.
5. Sau đây là một cách để thực hiện nó
foreach $f (<*>) { print “$f -> $where\n” if $where = readlink($f) ; }
Biến vô hướng $f được bật cho từng tên tệp trong
danh mục hiện tại. Với mỗi tên, $where lấy tập các tên đó
từ readlink(). Nếu tên này không phải là một symlink,
toán tử readlink cho lại undef, cho lại một giá trị sai cho
phép kiểm tra if , và print bị nhảy qua. Nhưng khi toán tử
readlink cho lại một giá trị, print hiển thị các giá trị nguồn
và nhận symlink.
Chương 14, Quản lí tiến trình
1. Sau đây là một cách để thực hiện nó
if (`date` =~ /^S/) { print “Go play~\n” ;
324
} else { print “Get to work!\n” ; }
Điều hay xảy ra là kí tự ra đầu tiên của chỉ lệnh date
chỉ là một S vào cuối tuần (Sat hay Sun), làm cho
chương trình thành tầm thường. Chúng ta gọi date, rồi
dùng biểu thức chính qui để xem liệu kí tự đầu tiên có là
S hay không. Dựa trên điều này, chúng ta in ra thông báo
này hay khác.
2. Sau đây là một cách để thực hiện nó
open (PW, “/etc/passwd”) ; while (<PW>) { chop; ($user, $pw, $uid, $gid, $gcos) = split(/:/) ; ($real) = split(/,/.$gcos) ; $real {$user} = $real ; } close (PW) ; open(WHO, “who|”) || die “cannot open who pipe”; while (<WHO>) { ($login, $rest) = /^(\S+)\s+(.*)/; $login = $real{$login} if $real{$login} ;
printf “%-30s %s\n”, $login, $rest ; }
Chu trình thứ nhất tạo ra một mảng kết hợp %real
lấy các tên đăng nhập làm khoá và các tên thực tương
ứng làm giá trị. Mảng này được dùng trong chu trình tiếp
sau để thay đổi tên đăng nhập thành tên thực.
Chu trình thứ hai duyệt qua cái ra kết quả từ việc mở
chỉ lệnh who xem như tước hiệu tệp. Mỗi dòng cái ra của
325
who lại được bẻ ra bằng việc dùng việc sánh biểu thức
chính qui trong hoàn cảnh mảng. Từ đầu tiên của dòng
này (tên đăng nhập) được thay thế bằng tên thật trong
mảng này, nhưng chỉ nếu nó tồn tại. Khi tất cả những
điều đó đã được thực hiện, printf đưa kết quả ra
STDOUT.
Bạn có thể thay thế việc mở tước hiệu tệp và việc
bắt đầu chu trình chỉ bằng:
foreach $_ (`who`) {
để hoàn thành cùng kết quả. Sự khác biệt duy nhất là
ở chỗ bản với tước hiệu tệp có thể bắt đầu làm việc ngay
khi who bắt đầu chẻ các kí tự, trong khi bản với who
trong dấu nháy đơn ngược phải đợi để who kết thúc.
3. Sau đây là một cách để thực hiện nó
open (PW, “/etc/passwd”) ; while (<PW>) { chop; ($user, $pw, $uid, $gid, $gcos) = split(/:/) ; ($real) = split(/,/.$gcos) ; $real {$user} = $real ; } close (PW) ; open(LPR, “|lpt”) || die “cannot open LPR pipe”; open(WHO, “who|”) || die “cannot open who pipe”; while (<WHO>) { # or replace previous two lines with: foreach $_
(`who`) { ($login, $rest) = /^(\S+)\s+(.*)/; $login = $real{$login} if $real{$login} ;
printf LPR “%-30s %s\n”, $login, $rest ; }
326
Sự khác biệt giữa chương trình này và chương trình
trong bài tập trước là ở chỗ chúng ta đã bổ sung thêm
một tước hiệu tệp LPR được mở đối với tiến trình lpr, và
sửa đổi câu lệnh printf để gửi dữ liệu ra đó thay vì ra
STDOUT.
4. Sau đây là một cách để thực hiện nó
sub mkdir { !system “/bin/mkdir”, @_ ;
}
Tại đây, chỉ lệnh mkdir được cho các đối trực tiếp từ
các đối của chương trình con. Tuy nhiên, giá trị cho lại
phải được phủ định về mặt logic bởi vì một trạng thái ra
khác không từ system phải dịch thành giá trị sai cho nơi
gọi Perl.
5. Sau đây là một cách để thực hiện nó
sub mkdir { local($dir, $mode) = @_; (!system “/bin/mkdir”, $dir) && chmod ($mode, $dir) ;
}
Đầu tiên, các đối cho trình này được đặt tên là $dir
và $mode. Tiếp đó, chúng ta gọi mkdir trên danh mục
được $dir chỉ tên. Nếu điều đó thành công, toán tử chmod
cho lại mốt đúng cho danh mục này.
Chương 15, Biến đổi dữ liệu khác
1. Sau đây là một cách để thực hiện nó
327
while (<>) { chop; $slash = rindex($_, “/”) ; $head = substr($_,0,$slash) ; $tail = substr($_, $slash+1) ; print “head = ‘$head’, tail = ‘$tail’\n” ; }
Mỗi dòng được đọc bởi toán tử hình thoi đã được
chặt bỏ (dấu dòng mới phiền phức) trước hết. Tiếp đó
chúng ta tìm dấu sổ chéo bên phải nhất trong dòng này,
bằng việc dùng rindex(). Hai dòng tiếp bẻ xâu này ra bằng
việc dùng substr(). Lưu ý rằng các biểu thức này “làm
đúng việc” cho dù khi kết quả của rindex là -1 (không có
dấu sổ chéo trong xâu). Dòng cuối cùng bên trong chu
trình in ra kết quả.
2. Sau đây là một cách để thực hiện nó
chop(@nums = <STDIN>) ; # note special use of chop @nums = sort { $a <=> $b } @nums; foreach (@nums) { printf “%30g\n”, $_ ; }
Dòng thứ nhất nắm lấy tất cả các số trong mảng
@nums. Dòng thứ hai sắp xếp mảng theo số, bằng cách
dùng một định nghĩa trong dòng cho thứ tự sắp xếp. Chu
trình foreach in ra kết quả.
3. Sau đây là một cách để thực hiện nó
open (PW, “/etc/passwd”) || die “How did you get logged in?”
while (<PM>) {
328
chop ; ($user, $pm, $uid, $gid, $gcos) = split(/:/) ; ($real) = split(/,/,$gcos) ; $real {$user} = $real ; ($last = $real) =~ s/^(.*[^a-z]) ? ([^a-z]+.*).*/$2/i ; $last =~ tr/A-Z/a-z/; $last {$user} = $last;
} close (PW) ; for (sort by_last keys %last) { printf “%30s %8s\n”, $real{$_}, $_ ; } sub by_last { $last{$a} cmp $;ast{$b}) || ($a cmp $b)}
Chu trình đầu tiên tạo ra mảng %last, bao gồm tên
đăng nhập làm khoá, và tên cuối của người dùng là giá
trị tương ứng, và mảng %real , chứa các tên thực đầy đủ.
Chu trình thứ hai in ra %real được sắp thứ tự theo
các giá trị của %last , dùng định nghĩa sắp xếp được trình
bầy trong chương trình con by_last.
4. Sau đây là một cách để thực hiện nó
while (<>) { substr($_,0,1) =~ tr/a-z/A-Z/; substr($_,1) =~ tr/A-Z/a-z/; print ; }
Với mỗi dòng được toán tử hình thoi đọc vào, dùng
hai phép toán tr, mỗi phép toán trên một phần khác nhau
của xâu này. Toán tử tr thứ nhất biến kí tự đầu tiên của
dòng thành chữ hoa, còn toán tử tr thứ hai, biến phần còn
lại thành chữ thường. Kết quả được in ra.
329
Một cách khác để thực hiện việc này, bằng cách
dùng toán tử xâu nháy kép, là:
while (<>) { print “\u\L$_”; }
cho mình năm điểm phụ nếu bạn đã nghĩ tới điều
đó.
Chương 16, Truy nhập cơ sở dữ liệu hệ thống
1. Sau đây là một cách để thực hiện nó
$: = “ “ ; while (@pw = getpwent) { ($user, $gid, $gcos) = @pw[0,3,6];
($real) = split(/,/,$gcos) ; $real { $user} = $real ; $member {$gid} .= “ $user” ; ($last = $real) =~ s/^(.*[^a-z]) ? ([a-z]+.*).*/$2/i; $last =~ tr/A-Z/a-z/; $last {$user} = $last;
} while (@gr = getgrent) { ($gname, $Gid, $members) = @gr[0,2,3];
$members {$gid} .= “ $members”; $gname {$grid} = $gname ;
} for $gid (sort by_gname keys %gname) { %all = () ; for (split (/\s+/, $members {$gid} )) {
$all {$_} ++ if length $_ ; } @members = () ; foreach (sort by_last key %all) {
330
push(@members, “$real {$_} ($_)”) ; } $members = join(“, “, @members); write;
} sub by_gname { $gname {$a} cmp $gname {$b};} sub by_last { ($last {a} cmp $last {$b}) || ($a cmp $b) ; } format STDOUT = @<<<<<<<< @<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<< $gname {$gid}, “ ($gid)”, $memberlist ~~ ^<<<<<<<<<<<<<<<<<<<<< $memberlist .
Chương 17, Thao tác cơ sở dữ liệu người dùng
1. Sau đây là một cách để thực hiện nó
dbmopen(ALIAS, “/etc/aliases”, undef) || die “No aliases!”;
while ($key, $value) = each (%ALIAS)) { chop ($key, $value) ; print “$key $value\n” ; }
Dòng thứ nhất mở các biệt hiệu DBM. (Hệ thống
của bạn có thể giữ các biệt hiệu DBM trong
/usr/lib/aliases - thử điều đó nếu điều này không làm
việc.) Chu trình while duyệt qua mảng DBM. Dòng thứ
nhất bên trong cho trình sẽ cắt bỏ đi kí tự NUL tại cuối
khoá và giá trị. Dòng cuối cùng của chu trình này in ra
kết quả.
331
2. Sau đây là một cách để thực hiện nó
# program 1: dbmopen(%WORDS, “words”, 0644) ; while (<>) { foreach $word (split(/\W+/)) {
$WORDS {$word}++; }
} dbmclose (%WORDS);
Chương trình thứ nhất (người viết) mở một DBM
trong danh mục hiện tại được gọi là words, tạo ra các tệp
có tên words.dir và words.pag. Chu trình while lấy từng
dòng bằng việc dùng toán tử hình thoi. Dòng này được
chẻ ra từng phần bằng việc dùng toán tử split, với định
biên /\W+/, có nghĩa là kí tự không là từ. Mỗi từ tiếp đó
được đếm trong mảng DBM, bằng việc dùng câu lệnh
foreach để đi qua các từ.
# program 2: dbmopen(%WORDS, “words”, undef) ; foreach $word (sort { $WORDS {$b} <=> $WORDS {$a} }
keys %WORDS) {
print “$word $WORDS {$word}\n” ; } dbmclose (%WORDS);
Chương trình thứ hai mở một DBM trong danh mục
hiện tại có tên là words. Dòng foreach trông phức tạp, đã
làm hầu hết các công việc bẩn thỉu. Giá trị của $word
mỗi lần qua chu trình này sẽ là phần tử tiếp của danh
sách. Danh sách này được sắp thứ tự theo khoá trong
mảng %WORDS, sắp theo giá trị của chúng (số đếm)
theo thứ tự giảm dần. Với mỗi từ trong danh sách, chúng
ta in ra từ đó và số lần từ đã xuất hiện.
332
Chương 18, Chuyển các ngôn ngữ khác sang Perl
1. Sau đây là một cách để thực hiện nó
for ( ; ; ) { ($user, $home) = (getpwent) [0,7]; last unless $user; next unless open (N, “$home/.newsrc”) ; while (<N>) { if (/^comp\.lang\.perl:/) { print “$user Ý a good person, “, “and reads comp.lang.perl!\n”) ; last; } }
}
Chu trình bên ngoài nhất là chu trình for mà cứ chạy
mãi - tuy nhiên chu trình này ra bằng toán tử last bên
trong. Mỗi lần qua chu trình, một giá trị mới cho $user
(tên người dùng) và $home (danh mục nhà của họ) được
lấy ra bằng việc dùng toán tử getpwent.
Nếu giá trị của $user là rỗng, chu trình for thoát ra.
Hai dòng tiếp tìm tệp .newsrc mới đây trong danh mục
này của người dùng. Nếu không thể mở được tệp này,
hay thời gian sửa đổi cho tệp này là quá xa, việc lặp tiếp
cho chu trình for sẽ được bật lẫy.
Chu trình while đọc một dòng mỗi lúc từ
tệp .newsrc. Nếu dòng này bắt đầu với comp.lang.perl:,
câu lệnh print nói thế, và cho trình while ra sớm.
333
Phụ lục B
Cơ sở về nối mạng
Mọi người bao giờ cũng tới gặp tôi trên phố, hàng
chục lần một ngày, và hỏi, “Này, Randal, làm sao tôi làm
cho Perl giải quyết khe cắm vào/ra?”
Được, tôi muốn dạy về TCP/IP, nhưng không may
là lề của trang này quá hẹp. Cho nên, bạn phải giải quyết
một thí dụ làm việc thật, và có lẽ phải đợi
O’Reilly&Associates đưa ra một cuốn sách Perl nâng
cao.
Mô hình khe cắm
Vấn đề đại loại là thế này:
1. Tiến trình phục vụ tạo ra một khe cắm tổng quát
bằng socket().
2. Bộ phục vụ kết ghép khe cắm này với một địa chỉ đã
thoả thuận qua bind().
334
3. Bộ phục vụ lưu ý hệ thống rằng nó đã nối qua listen().
4. Bộ phục vụ ngồi sau và đợi lần nối ghép đầu tiên qua
accept().
5. Tiến trình khách tạo ra một khe cắm tổng quát bằng
socket().
6. Khách kết ghép khe cắm này với bất kì địa chỉ nào
do hệ thống chọn qua bind(). (Việc kết ghép có thể
xảy ra trong bước tiếp nếu khách không cầu kì về địa
chỉ.)
7. Một khi đã được kết ghép, khách nối khe cắm của
mình với khe cắm của bộ phục vụ qua connect(), bằng
việc dùng địa chỉ đã thoả thuận. Điều này thiết lập
lên một ghép nối.
8. Bộ phục vụ để ý đến ghép nối mới. Các bộ phục vụ
điển hình phân nhánh cho một bộ phục vụ con để
giải quyết việc ghép nối đặc biệt, với bộ phục vụ bố
mẹ giải quyết cho ghép nối tiếp. (Bộ phục vụ mẫu
của chúng không phân nhánh về sau, bởi vì nó chỉ
làm một việc ngắn và tháo bỏ ghép nối này. Điều này
sẽ là vấn đề nếu 20 yêu cầu tới một lúc.)
9. Bộ phục vụ con đọc dữ liệu từ khe cắm đã được
khách gửi. Bộ phục vụ con cũng ghi dữ liệu lên khe
cắm, mà do khách có sẵn. Trong Perl, vào/ra được
thực hiện dường như nó là một tước hiệu tệp bình
thường.
10. Khi bộ phục vụ con và khách đã kết thúc nói chuyện,
chúng đóng tước hiệu tệp, do vậy kết thúc việc ghép
nối.
335
Một khách mẫu
Và là một khách đơn giản. Khách làm việc thực tế
này nói tới một địa chỉ đã xác định (trong trường hợp
này, “daytime” chuẩn) trên một máy chủ đặc biệt (máy
chủ cục bộ), và in ra bất kì cái ra nào mà cổng sinh ra.
require ‘sys/socket.ph’; $sockaddr = ‘ S n a4 x8’; chop ($hostname = `hostname`) ; ($name, $aliases, $proto) = getprotobyname(‘tcp’); ($name, $aliases, $port) = getservbyname(‘daytime’,
‘tcp’); ($name, $aliases, $type, $len, $thisaddr) =
gethostbyname (‘$hostname’); $thisport = pack($socaddr, &AF_INET, 0, $thisaddr); $thatport = pack($socaddr, &AF_INET, $port, $thisaddr); socket(S, &PF_INET, &SOCK_STREAM, $proto) || die “cannot create socket\n” ; bind (S, $thisport) || die “cannot bind socket\n”; # optional connect (S, $thatport) || die “cannot connect socket\n” ; while (<S>) { print; } exit 0;
Bộ phục vụ mẫu
Và là một bộ phục vụ đơn giản. Bộ phục vụ này đặt
khe cắm tại địa chỉ 4242 trên máy hiện tại. Bất kì ai nối
với cổng này đã lấy được bánh may mắn đa tuyến. Mỗi
bánh may mắn đã được dùng một lần (ngẫu nhiên) cho
tới khi tất cả các bánh đã được ăn hết. Vậy cơ sở bánh
được khởi động lại từ đầu.
336
Bạn có thể dùng chương trình này với khách mẫu
bằng việc thay thế đường tính $port bởi câu lệnh đơn
$port = 43242;. Hay bạn có thể nói telnet localhost 4242 để
quan sát cái ra - sự chọn lựa của bạn.
require ‘sys/socket.ph’; $sockaddr = ‘ S n a4 x8’; chop ($hostname = `hostname`) ; ($name, $aliases, $proto) = getprotobyname(‘tcp’); $port = 4242; # some big number $thisport = pack($socaddr, &AF_INET, $port, “\0\0\0\0”); #wildcard addr socket(S, &PF_INET, &SOCK_STREAM, $proto) || die “cannot create socket\n” ; bind (S, $thisport) || die “cannot bind socket\n”; listen(S,5) || die “cannot listen socket\n”; for (; ;) { accept(NS,S) || die “cannot accept socket\n”; print NS &fortune; close NC; } sub fortune { @fortunes = split(/\n%%\n/, <<’END’) unless
@fortunes; A fool and hs money are soon parted. %% A penny saved is a penny earned. %% Ask not what your country can do for you; ask what you can do for your country. %% END splice(@fortunes, int(rand(@fortunes)), 1). ”\n”’ }
337
Phụ lục C
Các chủ đề còn chưa
nói tới
Vâng, điều này đáng ngạc nhiên. Cuốn sách này quả
dài, thế mà vẫn còn cái gì đó nó vẫn chưa bao quát hết.
Các chú thích cuối trang chứa thêm thông tin phụ.
Mục đích của mục này không phải là để dạy cho bạn
về những điều đã được liệt kê ra ở đây, mà đơn giản đưa
ra một danh sách. Bạn sẽ cần tới Sách con lạc đà hay tài
liệu dùng Perl (trên nhóm hỗ trợ Usenet) để có thêm
thông tin.
Trình gỡ lỗi
Perl có trình gỡ lỗi mức nguồn tuyệt vời.
338
Dòng lệnh
Bộ thông dịch Perl có quá thừa thãi các khoá dòng
lệnh.
Các toán tử khác
Toán tử phẩy là một. Và có cách diễn đạt do { block; }
khác có trong tay khi bạn cần một khối câu lệnh ở nơi
đòi hỏi biểu thức.
Và có một số biến thể về các phép toán, giống như
việc dùng bộ sửa đổi g cho việc đối sánh.
Nhiều, nhiều hàm nữa
Vâng, Perl có thật nhiều hàm, tôi không định liệt kê
chúng ra đây, bởi vì cách nhanh nhất để tìm ra chúng là
đọc qua mục các hàm trong cuốn Lập trình Perl hay tài
liệu về perl và nhìn vào bất kì cái gì bạn nhận ra không
đáng quan tâm.
Nhiều, nhiều biến định nghĩa sẵn
Bạn đã thấy vài biến đã xác định trước, như $_.
Được, còn nhiều nữa cơ.
Xâu ở đây
Bên cạnh các xâu nháy đơn và nháy kép, bạn cũng
có thể có các xâu ở đây, giống như tài liệu ở đây trong
lớp vỏ:
339
$a = <<”HEAD” . “\n” . $body; To: [email protected] From: $username Subject: What, do you think? Date: $now HEAD
Chúng thực sự là không cần thiết, bởi vì bạn có thể
chỉ viết ra một xâu nháy kép dài thay thế, nhưng vài
người thấy chúng rõ ràng. Và cách đó bạn có thể nói “Có
nhiều cách trích dẫn nó!”
Trở về (từ trình con)
Có câu lệnh return để ra ngay lập tức khỏi chương
trình con, kéo theo một giá trị cùng nó. Nhưng trong Perl
4.0 nó có hơi không hiệu quả, cho nên tôi bỏ lại nó ra
khỏi mô tả này. Tuy thế bạn cứ thoảo mái mà dùng nó.
Toán tử eval (và s///e)
Vâng, bạn có thể xây dựng một mẩu chương trình
vào lúc chạy rồi tính eval nó, như bạn có thể làm với lớp
vỏ. Nó thực tế có ích, bởi vì bạn có thể thu được những
tối ưu về thời gian dịch (như biểu thức chính qui đã được
dịch) vào lúc chạy. Bạn cũng có thể dùng nó để bẫy các
lỗi định mệnh khác trong một đoạn chương trình: lỗi
định mệnh bên trong eval đơn giản ra khỏi eval và cho
bạn trạng thái lỗi.
Chẳng hạn, sau đây là một chương trình đọc một
dòng chương trình Perl từ người dùng và rồi thực hiện
nó dường như nó là một phần của chương trình Perl:
340
print “code line: “ ; chop ($code = <STDIN>) ; eval $code; die “eval: $@” if $@ ;
Bạn có thể đặt chương trình Perl bên trong xâu thay
thế của toán tử thế với cờ e. Điều này làm thủ công nếu
bạn muốn xây dựng cái gì đó phức tạp cho xâu thay thế,
như gọi trình con cho lại kết quả của việc tra cơ sở dữ
liệu. Sau đây là một chu trình làm tăng giá trị của cột thứ
nhất của một loạt dòng:
while (<>) { s/^(S+)/$1+1/e; # $1+1 is Perl code, not a string print ; }
Thao tác bảng kí hiệu bằng *FRED
Bạn có thể làm b thành một biệt hiệu cho a qua *b =
*a. Điều này có nghĩa là $a và $b tham chiếu tới cùng
một biến, như @a và @b, và thậm chí các tước hiệu tệp
và dạng thức a và b. Bạn cũng có thể định vị *b bên trong
một khối với local(*b), và điều đó để cho bạn có tước hiệu
tệp cục bộ và dạng thức và các thứ khác. Một chất liệu
khá đồng bóng nhưng có ích khi bạn cần nó. Perl 5.0
thậm chí còn có cách đặt biệt hiệu phức tạp hơn (giống
nhiều hơn với con trỏ của C) nhưng khi viết điều này, tôi
chưa đủ tư liệu về nó.
Toán tử goto
Vâng, có, Perl có toán tử goto. Nhưng nó không
hiệu quả khủng khiếp, và nó tồn tại chỉ để hỗ trợ cho bộ
dịch sed sang Perl. Cho nên đừng dùng nó. Bạn thực sự
341
không cần nó. Và chương trình Perl có thể trở thành đủ
khó hiểu cho dù không có một đống các goto.
Toán tử require
Toán tử require là dạng của include của Perl. Các
phần của chương trình Perl có thể bị mắc kẹt vào trong
một tệp tách biệt (hay tập các tệp) và rồi được đưa vào
trong bất kì chương trình Perl nào cần chúng. Chẳng
hạn:
require ‘fred.pl’ ;
đưa vào trong văn bản của tệp fred.pl dường như nó
là một phần của tệp này, cho phép nhiều chương trình
dùng chung mã Perl.
Thư viện
Nói về chùm các chương trình Perl được bao hàm,
nhiều người thạo (và tôi) đã đóng góp nhiều chương
trình để làm những việc có ích. Để tìm ra thư viện Perl ở
đâu, yêu cầu Perl in ra phần tử đầu tiên của mảng @INC.
(Nếu không có gì trong danh mục đó, Perl của bạn đã
được cài đặt không đúng.) Gần như tất cả các trình này
đã có lời chú thích trong chúng để mô tả cách sử dụng.
Tin vui về Perl 5.0
Và thậm chí còn có cả chùm những chất liệu mà
không ai (ngoại trừ Larry Wall) biết tới, vì bản mới nhất
của Perl không được đưa ra vào lúc tôi gõ những điều
342
này. Bởi thế, bạn phải kiểm tra trong tài liệu xem Larry
đã tiến hành những nỗ lực lớn để đảm bảo rằng sự khác
biệt giữa bản bạn đang chạy và sách in đã được làm tài
liệu tốt. Cám ơn, Larry.