Ngôn ngữ Perl là gì ? ( Chương 1: Giới thiệu về Perl)

1.1 Lịch sử Perl
PERL là cách viết tắt cho “Practical Extraction and Report Language”, mặc dù còn được gọi là “Pathologically Eclectic Rubbish Lister". Larry Wall đã 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 về hệ thống báo lỗi, và lệnh awk..và đưa ra bản đầu tiên của Perl. Sau đó Larry đưa nó cho các độc giả Usenet, thường vẫn được gọi là “the Net”. Kết quả là Perl phát triển dần và cũng cùng tỉ lệ như kernel của UNIX. Nó đã phát triển các tính năng và tính khả chuyển.
Larry sẽ đưa ra bản Perl mới nhất, bản 5.0, chắc chắn sẽ 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. Tuy nhiên, cuốn sách này đã được thử với Perl bản 4.0. Mọi thứ ở đây đều sẽ làm việc với bản 5.0 và các bản sau của Perl. Trong thực tế, 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 khác cần cho sự nâng cấp.
1.2 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 nhậy cảm với tính khả chuyển đối với trình shell, vì nó ngắn hơn và không phức tạp như các ngôn ngữ lập trinh C hay một ngôn ngữ công cụ UNIX nào khác.
Khi đã quen thuộc với Perl, bạn sẽ mất ít thời gian để lấy được các trích dẫn dòng lệnh shell (hay khai báo C), và mất ít thời gian để lập trình vì Perl là một công cụ trợ giúp tuyệt vời. Các cấu trúc chặt chẽ của Perl cho phép tạo ra một số giải pháp với những công cụ mang tính tổng quát. Cũng vậy, bạn có thể lấy những công cụ này sang công việc tiếp, vì Perl có tính khả chuyển cao và lại có sẵn trên hầu hết các hệ thống.
Giống như mọi ngôn ngữ khác, Perl có thể “chỉ viết” - tức là có thể viết ra chương trình cho máy hiểu mà con người không thể "đọc hiểu" được. Nhưng nếu chú ý cẩn thận, bạn có thể tránh được điều này. Thật vậy, đôi khi Perl trông như khó với những ai không quen, nhưng với người lập trình đã thạo Perl, nó cũng... khá dễ (???). Nếu bạn làm theo những hướng dẫn trong cuốn sách này thì chương trình của bạn sẽ dễ đọc, dễ bảo trì và nâng cấp.
1.3 Tính sẵn có
Nếu bạn nhận được một lỗi nhỏ: Perl: not found khi bạn thử gọi Perl từ dòng lệnh shell thì điều đó có nghĩa là Perl chưa được cài đặt trên hệ thống của bạn. Nhưng bạn hoàn toàn có thể lấy được Perl miễn phí vài cài vào hệ thốgn của mình. Perl được phân phối theo Giấy phép Công cộng GNU (GNU Public License), nghĩa là bạn có thể phân phát bản binary của Perl với điều kiện là phải kèm theo mã ngồn miễn phí; và nếu bạn sửa đổi Perl thep ý bạn bạn cũng phải phân phối mã nguồn của phần mà bạn sửa đổi. Bạn có thể lấy mã nguồn của Perl hoàn toàn miễn phí qua đường download mà chỉ tốn vài MB để lưu trữ và một khoảng thời gian để tải xuống.
Trong thực tế, nó không chỉ là miễn phí mà 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. Đó là vì là Perl có 1 phần được gọi là "Cấu hình" sẽ có nhiệm vụ gọi 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, và cuối cùng nó sẽ 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, các "tín đồ" của Perl đã đem nó sang Amiga, Atari ST, họ Macintosh, VMS, OS/2, thậm chí MS/DOS, Windows - và có lẽ còn nhiều hơn nữa khi bạn đang đọc những dòng 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 động, cho nên phải hỏi trên nhóm tin Usenet chẳng hạn để có được thông tin mới nhất. Nếu bạn là một newbie, 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), hoặc bạn có thể tìm và download ở http://www.perl.org/.
1.4 Hỗ trợ (nếu như có khúc mắc)
Bạn có thể liên hệ trực tiếp với Larry hoặc 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. hoặc gửi yêu cầu tới perl-users-request@virgina.edu.
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 & Associaté, 1990).
1.5 Các khái niệm cơ bản về Perl
Một kịch bản shell là một dãy các lệnh shell đưa vào trong một tệp văn bản. Tệp này chạy bằng cách bật một bit thực hiện (qua chmod +x filename) rồi gõ tên của tệp đó tại dấu nhắc của shell. Chẳng hạn, một kịch bản shell để chạy lệnh date và sau đó là lệnh who sẽ được viết như sau:
$ echo date > somecript
$ echo who >> somecript
$ cat somescript
date
who
$ chmod +x somescript
$ somescript
[output of date followed by who]
$
Tương tự, một chương trình Perl là một bó các câu lệnh và định nghĩa Perl được đưa 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 shell, nên chúng ta cần một bước phụ: đặt #!/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 được cài vào nơi không chuẩn (không phải là /usr/bin), hay hệ thống tựa UNIX của bạn không hiểu dòng #!, thì...bạn hỏi người quản trị hệ thống của bạn để được giúp đỡ. Các thí dụ trong sách này giả sử rằng hệ thống của bạn đã được cài đặt Perl vào /usr/bin.
Perl là một ngôn ngữ phi định dạng kiểu như C - khoảng trắng giữa các phần tử của chương trình là tuỳ chọn, trừ phi hai phần tử của chương trình dính liền với nhau có thể bị lầm lẫn thành một cá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, dấu tab, xuống dòng, về đầu dòng hay sang trang mới). 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 đừng lo, tài liệu sẽ chỉ rõ cho bạn biết là các khoảng trắng phải được đặt như thế nào với số lượng bao nhiêu. Có thể giả thiết rằng loại và số lượng khoảng trắng giữa các phần tử trong chương trình là tuỳ ý trong các trường hợp khác.
Mặc dù gần như tất cả các chương trình Perl đều có thể được viết tất cả trên một dòng, nhưng một chương trình Perl điển hình được viết xuống dòng và canh lề như chương trình C, với những phần câu lệnh lồng nhau được viết vào sâu hơn một chút chẳng hạn, sẽ làm cho chương trình của bạn dễ nhìn và dễ hiểu hơn.
Cũng giống như một kịch bản shell, 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ề hàm chính main như trong C.
Chú thích của Perl giống như chú thích của kịch bản shell: bất kì cái gì nằm giữa một dấu # tới cuối dòng đều là một chú thích. Perl không có chú thích trên nhiều dòng như C.
Không giống hầu hết các shell (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 sẽ được lược bỏ mất và sẽ không làm chậm chương trình. Trong thực tế, 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ệ thống nhất đơn thuần dựa trên nền tảng là C được coi là trình biên dịch.
Việc biên dịch này khá tiêu tốn thời gian. Và sẽ là phi hiệu quả nếu một chương trình Perl lớn lại chỉ thực hiện một nhiệm vụ nhỏ bé (trong số nhiều nhiệm vụ tiềm năng) và rồi thoát, vì thời gian chạy cho chương trình sẽ "nhỏ xíu" nếu so với thời gian biên 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 chiếm không gian đĩa. Hiểu theo một cách nào đó, nó là tốt nhất cho cả hai loại này.
1.6 Sơ lược về Perl
Sau đâ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 ngắn gọn - mỗi vùng chủ đề đều được thảo luận chi tiết hơn nhiều về sau trong cuốn sách này. Nhưng việc "xem qua" này sẽ cho bạn kinh nghiệm nhanh chóng về ngôn ngữ.
1.6.1 Chương trình “Hello”
Ta hãy nhìn vào một chương trình nhỏ sau:
#!/usr/bin/perl
print "Hello!\n";
Dòng đầu tiên báo đây là chương trình Perl. Nó cũng là lời chú thích cho Perl cho tới cuối dòng, giống như hầu hết các kịch bản shell 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. Hàm print bắt đầu chương trình, và nó có một đối số là 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. Giống như trong C, tất cả các câu lệnh đơn giản đều kết thúc bằng dấu chấm phảy (;).
Khi bạn gọi chương trình này, phần kernel sẽ gọi bộ thông dịch Perl, phân tích toàn bộ chương trình (hai dòng, kể cả dòng chú thích đầu tiên) và thực hiện dạng đã dịch. Thao tác đầu tiên và duy nhất là thực hiện hàm print. Sau khi chương trình đã hoàn tất, thì tiến trình Perl sẽ trả về một mã cho shell để báo rằng chương trình đã kết thúc.
1.6.2 Hỏi câu hỏi và nhớ kết quả
Ta hãy viết lại chương trình ở trên một chút, thay vì Hello trống không, chương trình sẽ chào tên người nhập vào từ bàn phím. Để làm việc này, ta cần một chỗ lưu giữ tên, một cách hỏi tên, và một cách nhận câu trả lời.
Để đặt chỗ giữ giá trị (VD: tên) ta dùng 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 (xem chi tiết trong chương sau: Dữ liệu vô hướng, về những gì mà biến này có thể giữ, và những gì có thể làm với chúng). Bây giờ, giả sử rằng bạn có thể giữ một số hay một 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 đó, ta cần một cách nhắc và một cách nhận vào. Chương trình trước đã chỉ ra cho ta cách nhắc - dùng hàm print. Và để nhận một dòng từ thiết bị cuối ta dùng toán tử <STDIN>. Ta dữ liệu nhập vào cho biến $name:
print "Ten ban la gi? ";
$name = <STDIN>;
Giá trị của $name tại điểm này có một ký tự xuống dòng ở cuối (vì bạn phải nhấn Enter để kết thúc việc nhập tên). Để bỏ qua kí tự đó, chúng ta dùng hàm chop(). Hàm này lấy một biến vô hướng làm đối số duy nhất và bỏ đi ký tự cuối từ giá trị xâu của biến:
chop($name);
Sau đó in ra câu chào:
print "Xin chao ban $name";
Và ta có chương trình hoàn chỉnh:
#!/usr/bin/perl
print "Ten ban la gi? ";
$name = <STDIN>;
chop($name);
print "Xin chao ban $name!\n";
1.6.3 Bổ sung chọn lựa
Bây giờ ta muốn có một lời chào đặc biệt cho Jenny, 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, ta cần so sánh tên đã được đưa vào với xâu Jenny, và nếu hai xâu trùng khớp, thì làm điều gì đó đặc biệt. Ta hãy bổ sung thêm lệnh if-then-else và phép so sánh vào chương trình:
#!/usr/bin/perl
print "Ten ban la gi? ";
$name = <STDIN>;
chop($name);
if ($name eq "Jenny") {
    #chao Jenny
    print "Chao Jenny! Lam bai tap di chu!\n";
} else {
    #chao binh thuong
    print "Xin chao ban $name!\n";
}
Toán tử eq so sánh hai xâu. Nếu bằng nhau (từng kí tự một, và có cùng chiều dài) kết quả sẽ là True, ngược lại sẽ là False. Câu lệnh if chọn xem "khối" câu lệnh nào so sánh đúng được thực hiện - nếu biểu thức ($name eq "Jenny") là True, khối chưa câu lệnh print "Chao Jenny! Lam bai tap di chu!\n"; sẽ được thực hiện, còn trong trường hợp ngược lại sẽ là khối chưa câu lệnh print "Xin chao ban $name!\n";.
1.6.4 Đoán từ bí mật
Để một người chạy chương trình đoán một từ bí mật. Với mọi người trừ Jenny, ta hãy để cho cho chương trình cứ hỏi lặp lại để đoán đến khi nào người này đoán được đúng. Hãy xem chương trình sau:
#!/usr/bin/perl
$secretword = "Jenny";    #tu bi mat
print "Ten ban la gi? ";
$name = <STDIN>;
chop($name);
if ($name eq "Jenny") {
    print "Chao Jenny! Lam bai tap di chu!\n";
} else {
    print "Xin chao ban $name!\n";
    print "Ban thu doan xem toi ten la gi? ";
    $guess = <STDIN>;
    chop($guess);
    while ($guess ne $secretword) {
        print "Sai roi, doan lai di";
        $guess = <STDIN>;
        chop($guess);
    }
}
Trước hết, ta định nghĩa tên cần đoán 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 Jenny) sẽ được yêu cầu (với một câu lệnh print khác) đoán chữ. Lời đoán được đem ra so sánh với từ cần đoán bằng việc dùng toán tử ne, mà sẽ cho True nếu các xâu này không bằng nhau (ne là toán tử ngược với toán tử eq). Kết quả của việc so sánh sẽ kiểm soát while, chu trình này thực hiện khi việc so sánh vẫn còn True.
1.6.5 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 ta đã 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 thiết bị khác.
Một giải pháp tốt hơn là cất giữ tất cả các câu trả lời 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 đều là một biến vô hướng tách biệt mà có thể được đặt giá trị hay truy cập độc lập. Toàn bộ mảng cũng có thể được trao cho một giá trị để ta có thể "truy nhập" vào. 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:
@words =("littlecat", "smalldog", "bigmouse");
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 cập đến từng phần tử bằng cách dùng một tham khảo chỉ số. Cho nên $words[0] là littlecat, $words[1] là smalldog, $words[2] là bigmouse. 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à bigmouse. (Tham khảo chỉ số bắt đầu với $ thay vì @ là do chúng tham khảo 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
#cac tu bi mat
@words = ("littlecat", "smalldog", "bigmouse");
print "Ten ban la gi? ";
$name = <STDIN>;
chop($name);
if ($name eq "Jenny") {
    print "Chao Jenny! Lam bai tap di chu!\n";
} else {
    print "Xin chao ban $name!\n";
    print "Tu bi mat la gi";
    $guess = <STDIN>;
    chop($guess);
    $i = 0;    #thu truoc 1 tu
    $correct = "co the";            #tu nay co doan dung hay khong?
    while ($correct eq "co the") {  #cu kiem tra den khi het
        if ($words[$i] eq $guess) {
            $correct = "co";        #dung roi
        } elsif ($i < 2) {
            $i = $i + 1;            #xet tu tiep theo
        } else {
            print "Sai roi, thu lai di. Tu bi mat ma gi? ";
            $guess = <STDIN>;
            chop($guess);
            $i = 0;    #kiem tra lai tu dau 1 lan nua
        }
    } #key thuc cua while ($correct eq "co the")
} #Ket thuc cua "not Jenny"
Chú ý 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, hoặc chúng ta không tìm thấy. Chương trình này cũng giới thiệu 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 else và theo sau là một if mới nhưng không lồng trong cặp dấu () 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 việc phân tầng if-elsif-elsif-elsif-else.
1.6.6 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 đều 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 so sánh giữa người và từ:
Người Từ bí mật
Chip littlecat
Dale smalldog
Tom bigmouse
Jerry bigmouse
Chú ý rằng Jerry và Tom có chung từ bí mật là bigmouse; điều này hoàn toàn hợp lệ.
Cách dễ nhất để cất giữ một bảng như thế trong Perl là bằng một "mảng băm" (hash array). Mỗi phần tử của mảng này giữ một giá trị vô hướng tách biệt (giống như kiểu mảng khác), nhưng các mảng lại được tham khảo theo khoá, 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 băm %words (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 = (
    "Chip" , "littlecat",
    "Dale" , "smalldog",
    "Tom"  , "bigmouse",
    "Jerry", "bigmouse"
);
Mỗi cặp giá trị trong danh sách đều biểu thị cho một khoá và giá trị tương ứng của nó trong mảng băm. Chú ý rằng ta đã chia phép gán này ra 4 dòng mà không có bất kì loại kí tự nối dòng nào, vì khoảng trắng giữa các phần tử chương trình nói chung là vô nghĩa trong Perl.
Để tìm ra từ bí mật cho Tom, ta cần dùng Tom như khoá trong một tham khảo vào mảng kết hợp %words, qua một biểu thức nào đó như $words{"Tom"}. Giá trị của tham khảo này là bigmouse, 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 gán $person bằng Tom và $words{$person} cũng sẽ trả về giá trị bigmouse. Chương trình hoàn chỉnh như sau:
#!/usr/bin/perl
%words = (
    "Chip" , "littlecat",
    "Dale" , "smalldog",
    "Tom"  , "bigmouse",
    "Jerry", "bigmouse"
);
print "Ten ban la gi? ";
$name = <STDIN>;
chop($name);
if ($name eq "Jenny") {
    print "Chao Jenny! Lam bai tap di chu!\n";
} else {
    print "Xin chao ban $name!\n";    #chao thuong thuong
    $secretword = $words{$name};      #lay tu bi mat
    print "Tu bi mat la gi? ";
    $guess = <STDIN>;
    chop($guess);
    while ($correct ne $secretword) {
        print "Sai roi thu lai di. Tu bi mat la gi? ";
        $guess = <STDIN>;
        chop($guess);
    } #ket thuc cua while
}
Bạn hãy 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 mặc định cho ai đó khác không. Đây là cách xem nó:
#............................................
$secretword = $words{$name};  #lay tu bi mat
if ($secretword eq "") {      #khong thay
    $secretword = "none";
}
print "Tu bi mat la gi?";
#............................................
1.6.7 Giải quyết định dạng đầu vào thay đổi
Nếu đưa vào Jenny L. Schwartz hay jenny thay vì Jenny thì chương trình sẽ không đưa ra kết quả như mong đợi. Vì toán tử so sánh eq thì lại so sánh đúng sự bằng nhau của từng ký tự một. 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 Jenny, thay vì chỉ là một xâu bằng Jenny. 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 tiêu bản 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 Jenny là ^Jenny. Để sánh xâu này với xâu trong $name, chúng ta dùng toán tử so sánh như sau:
if ($name =~ /^Jenny/) {
    ## yes - True
} else {
    ## no - False
}
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 thì các dấu cách và khoảng trắng là có nghĩa, hệt như chúng ở bên trong xâu.
Nhưng chúng ta vẫn chưa giải quyết việc lựa chọn jenny hay loại bỏ Jennyy. Để chấp nhận jenny, chúng ta thêm tuỳ chọn bỏ qua không xét chữ hoa và chữ thường, một chữ i nhỏ được thêm vào sau dấu sổ chéo thứ hai. Để loại bỏ Jennyy, 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 \b trong biểu thức chính qui. Điều này đảm bảo rằng kí tự đi sau chữ y đầu tiên trong biểu thức chính qui không phải là một kí tự a,b,c..khác. Điều này làm thay đổi biểu thức chính qui thành /^jenny\b/i, có nghĩa là "jenny 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". Ta có chương trình như sau:
#!/usr/bin/perl
%words = (
    "Chip" , "littlecat",
    "Dale" , "smalldog",
    "Tom"  , "bigmouse",
    "Jerry", "bigmouse"
);
print "Ten ban la gi? ";
$name = <STDIN>;
chop($name);
if ($name =~ /^jenny\b/i) {
    print "Chao Jenny! Lam bai tap di chu!\n";
} else {
    print "Xin chao ban $name!\n";    #chao thuong thuong
    $secretword = $words{$name};      #lay tu bi mat
    if ($secretword eq "") {          #khong thay
        $secretword = "none";
    }
    print "Tu bi mat la gi? ";
    $guess = <STDIN>;
    chop($guess);
    while ($correct ne $secretword) {
        print "Sai roi thu lai di. TU bi mat la gi? ";
        $guess = <STDIN>;
        chop($guess);
    } #ket thuc cua while
}
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 lúc đầu nhưng nó vẫn còn rất nhỏ bé và làm việc được.
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 mấy vẫn được). Không chỉ có thế, cách thức Perl giải quyết cho việc so sánh xâu là cách nhanh nhất trên hành tinh nay (Một chương trình giống như grep được viết trong Perl thường tốt hơn nhiều so vớ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 dữ liệu vào).
1.6.8 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 Jenny hay jenny hoặc Jenny L. Schwartz, nhưng với những người khác thì sao? Tom vẫn phải nói đúng là Tom (thậm chí không được có Tom với một dấu cách theo sau).
Để công bằng cho Tom (và cho những người khác), chúng ta cần nắm được từ đầu của bất kì cái gì được đưa vào, và 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ử thay thế sẽ tìm ra một biểu thức chính qui và thay thế nó bằng một xâu; và toán tử hoán chuyển để chuyển toàn bộ 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 dưới) 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 cá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 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, B trở thành b và cứ như thế.
Ta có chương trình hoàn chỉnh:
#!/usr/bin/perl
%words = (
    "Chip" , "littlecat",
    "Dale" , "smalldog",
    "Tom"  , "bigmouse",
    "Jerry", "bigmouse"
);
print "Ten ban la gi? ";
$name = <STDIN>;
chop($name);
$original_name = $name;    #cat giu de chao mung
$name =~ s/\W.*//;         #bo moi thu sau tu dau tien
$name =~ tr/A-Z/a-z/;      #chuyen thanh chu thuong
if ($name eq "jenny") {
    print "Chao Jenny! Lam bai tap di chu!\n";
} else {
    print "Xin chao ban $original_name!\n";    #chao thuong thuong
    $secretword = $words{$name};      #lay tu bi mat
    if ($secretword eq "") {          #khong thay
        $secretword = "none";
    }
    print "Tu bi mat la gi? ";
    $guess = <STDIN>;
    chop($guess);
    while ($correct ne $secretwords) {
        print "Sai roi thu lai di. Tu bi mat la gi? ";
        $guess = <STDIN>;
        chop($guess);
    } #ket thuc cua while
}
Bạn hãy để ý đến cách thức biểu thức chính qui sánh đúng với tên Jenny đã lại trở thành việc so sánh đơn giản (dùng eq). Sau rốt, cả Jenny L. Schwartz và Jenny đều trở thành jenny 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ì Jenny và Jenny Flinstone cả hai đều trở thành jenny; Tom Rubble và Tom sẽ trở thành tom...
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 cú gõ phím là một trong nhiều điểm mạnh của Perl.
Tuy nhiên, 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 ta vừa đưa vào. Cho nên, trước khi 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 dưới và có thể có chiều dài gần như không giới hạn). Sau khi lưu giữ, ta có thể truy cập tới $original_name về sau.
Perl có nhiều cách để giám sát và 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.
1.6.9 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 ta 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 ta cần là tá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ừ đã 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 phần chi tiết.
Perl cung cấp các chương trình con có tham biến và giá trị trả lại. Một chương trình con được định nghĩa một lần ở đâu đó trong chương trình, và có thể được dùng lại nhiều lần ở những nơi khác trong chương trình.
Với chương trình nhỏ nhưng phát triển nhanh của chúng ta (hihihi, đến mức...chóng hết cả mặt rồi), ta hãy tạo ra một chương trình con tên là good_word sẽ nhận một tên đã "sạch" và một từ đoán, rồi trả lại True nếu từ đó là đúng, và trả lại False nếu không đúng. Chương trình con đó được viết kiểu như thế này (đại loại thế):
sub good_word {
    my ($somename, $someguess) = @_;  #lay cac tham so
    $somename =~ s/\W.*//;              #bo moi thu sau tu dau tien
    $somename =~ tr/A-Z/a-z/;           #chuyen thanh chu thuong
    if ($somename eq "jenny") {         #huh, Jenny, khong doan nua
        return 1;                       #tra ve True
    } elsif (($words{$somename} || "none") eq $someguess) {
        return 1;                       #tra ve True
    } else {
        return 0;                       #tra ve False
    }
} #ket thuc good_word
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 tiếp nữa là một khối mã lệnh. Đị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ấy các giá trị của hai tham số của chương trình con này vào hai biến cục bộ có tên $somename và $someguess. (my() 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. Nếu là Jenny thì thôi không kiểm tra nữa.
Biểu thức cuối cùng được tính trong chương trình con là để trả về giá trị. Chúng ta sẽ thấy cách trả về lại giá trị được dùng sau khi 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 - ta hãy chia nó ra:
($words{$somename} || "none") eq $someguess
Phần thứ nhất bên trong dấu ngoặc là mảng băm quen thuộc, trả về 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 "none" là toán tử || (phép logic hoặc) như được dùng trong C, awk và các kịch bản shell khác. Nếu việc tra cứu từ mảng băm có một giá trị (nghĩa là khoá $somename tồn tại trong mảng), thì giá trị của biểu thức chính là là giá trị đó. Nếu khoá không tìm được, thì xâu "none" sẽ được dùng thay. Đây chính là một cách 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 "none", chúng ta đều 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ì return 1, nếu không return 0.
Và đây là chương trình hoàn chỉnh:
#!/usr/bin/perl
%words = (
    "Chip" , "littlecat",
    "Dale" , "smalldog",
    "Tom"  , "bigmouse",
    "Jerry", "bigmouse"
);
print "Ten ban la gi? ";
$name = <STDIN>;
chop($name);
if ($name =~ /^jenny\b/i) {
    print "Chao Jenny! Lam bai tap di chu!\n";
} else {
    print "Xin chao ban $name!\n";    #chao thuong thuong
    print "Tu bi mat la gi? ";
    $guess = <STDIN>;
    chop($guess);
    while (! good_word($name, $guess)) {
        print "Sai roi thu lai di. Tu bi mat la gi? ";
        $guess = <STDIN>;
        chop($guess);
    } #ket thuc cua while
}
sub good_word {
    my ($somename, $someguess) = @_;  #lay cac tham so
    $somename =~ s/\W.*//;              #bo moi thu sau tu dau tien
    $somename =~ tr/A-Z/a-z/;           #chuyen thanh chu thuong
    if ($somename eq "jenny") {         #huh, Jenny, khong doan nua
        return 1;                       #tra ve True
    } elsif (($words{$somename} || "none") eq $someguess) {
        return 1;                       #tra ve True
    } else {
        return 0;                       #tra ve False
    }
} #ket thuc good_word

Chú ý rằng chúng ta đã quay trở lại với biểu thức chính qui để kiểm tra Jenny, 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, chừng nào còn liên quan tới chương trình chính.
Sự 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 tham số $name và $guess. Bên trong chương trình con này, giá trị của $somename được đặt từ tham số 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 trả vềi (hoặc 1 hoặc 0) được đảo ngược với toán tử tiền tố ! (phép phủ định logic not). Như trong C, toán tử này trả lại True nếu biểu thức đi sau là False, 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. 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.
1.6.10 Chuyển danh sách từ bí mật vào tệp
Giả sử ta muốn dùng chung danh sách từ bí mật cho ba chương trình. Nếu ta cất giữ danh sách từ như ta đã làm thì ta sẽ cần phải thay đổi tất cả ba chương trình này khi Tom quyết định rằng từ bí mật của mình sẽ là smallmouse thay vì bigmouse. Điều này có thể thành phiền phức, đặc biệt khi xem xét tới việc Tom lại thường xuyên thích thay đổi ý định (!!!!)
Cho nên, ta hãy đặt danh sách các 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, ta 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 được gọn gàng. Điều này cũng có nghĩa là vào thời điểm sau tôi có thể thay đổi nơi cất giữ danh sách từ, hay thậm chí định dạng của danh sách mà hoàn toàn không gây xáo trộn trong chương trình chính.
Đị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:
Chip
littlecat
Dale
smalldog
Tom
bigmouse
Jerry
bigmouse
Hàm 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 thư 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 đều đượ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ẽ là Falsr cho chu trình while, và kết thúc nó. Đó là cách chúng ta đi thoát init_words.
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 $name. Trước hết, ta bỏ dấu xuống dòng bằng việc dùng hàm chop(). Sau đó đọc dòng tiếp để lấy từ bí mật, giữ nó trong biến $word, nó một lần nữa cũng phải bỏ xuống 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 để 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().
Đị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 tất cả những điều này có thể giống thế này:
#!/usr/bin/perl
init_words();
print "Ten ban la gi? ";
$name = <STDIN>;
chop($name);
if ($name =~ /^jenny\b/i) {
    print "Chao Jenny! Lam bai tap di chu!\n";
} else {
    print "Xin chao ban $name!\n";    #chao thuong thuong
    print "Tu bi mat la gi? ";
    $guess = <STDIN>;
    chop($guess);
    while (! good_word($name, $guess)) {
        print "Sai roi thu lai di. Tu bi mat la gi? ";
        $guess = <STDIN>;
        chop($guess);
    } #ket thuc cua while
}
sub good_word {
    my ($somename, $someguess) = @_;  #lay cac tham so
    $somename =~ s/\W.*//;              #bo moi thu sau tu dau tien
    $somename =~ tr/A-Z/a-z/;           #chuyen thanh chu thuong
    if ($somename eq "jenny") {         #huh, Jenny, khong doan nua
        return 1;                       #tra ve True
    } elsif (($words{$somename} || "none") eq $someguess) {
        return 1;                       #tra ve True
    } else {
        return 0;                       #tra ve False
    }
} #ket thuc good_word
sub init_words {
    open(WORDSLIST, "wordslist");
    while ($name = <WORDSLIST>) {
        chop($name);
        $word = <WORDSLIST>;
        chop($word);
        $words{$name} = $word;
    }
    close(WORDSLIST);
} #ket thuc init_words

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 số nào được truyền cả, cho nên chúng ta được tự do bỏ đi dấu ( ) (tuy nhiên tôi không khuyên bạn làm điều này). Cũng vậy, giá trị trả về tử init_words không được dùng biểu thức nào hết, thì cũng là tốt vì ta đã không trả về điều gì đáng kể.
1.6.11 Đảm bảo một lượng an toàn (nhỏ thôi- nhưng an toàn vẫn hơn)
Xếp bạn quyết định: "Danh sách các từ bí mật phải thay đổi ít nhất một lần mỗi tuần". 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.
Nơi tốt nhất để thực hiện việc kiểm tra là bên trong chương trình con init_words. Toán tử -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 7 hay không đối với tước hiệu tệp WORDSLIST:
sub init_words {
    open(WORDSLIST, "wordslist");
    if (-M WORDSLIST > 7) {
        die "Rat tiec, danh sach tu cu hon 7 ngay roi!";
    }
    while ($name = <WORDSLIST>) {
        chop($name);
        $word = <WORDSLIST>;
        chop($word);
        $words{$name} = $word;
    }
    close(WORDSLIST);
}
Giá trị của -M WORDSLIST được so sánh với 7, và nếu lớn hơn, thế thì ta vi phạm điều lệ an toàn rồi! Tại đây, ta thấy một toán tử mới: toán tử die. die chỉ làm một công việc đơn giản là một thông báo lên thiết bị xuất và kết thúc chương trình.
Phần còn lại của chương trình vẫn không đổi, cho nên tôi sẽ không lặp lại nó ở đây. Bên cạnh việc lấy số ngày của tệp, ta cũng có thể tìm ra người chủ của nó, kích cỡ, thời gian thâm 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.
1.6.12 Cảnh báo ai đó khi mọi việc đi sai
Ta hãy xem ta có thể làm cho hệ thống bị sa lầy thế nào khi ta 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. Ta cần sửa đổi mỗi chương trình con good_word vì ta có tất cả thông tin ngay đây. Thư sẽ được gửi cho bạn nếu bạn cũng cấp địa chỉ email của mình vào chỗ "dia_chi_cua_ban_o_day". Đây là điều ta phải làm - ngay trước khi trả 0 về từ chương trình con, ta 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 {
    my ($somename, $someguess) = @_;  #lay cac tham so
    $somename =~ s/\W.*//;              #bo moi thu sau tu dau tien
    $somename =~ tr/A-Z/a-z/;           #chuyen thanh chu thuong
    if ($somename eq "jenny") {         #huh, Jenny, khong doan nua
        return 1;                       #tra ve True
    } elsif (($words{$somename} || "none") eq $someguess) {
        return 1;                       #tra ve True
    } else {
        open(MAIL, "|mail dia_chi_cua_ban_o_day");
        print MAIL "tin xau: $somename da doan $someguess\n”;
        return 0;                       #tra ve False
    }
} #ket thuc good_word
Câu lệnh mới thứ nhất ở đây là open(), mà có một kí hiệu | trong tên tệp. Đây là một chỉ dẫn đặc biệt rằng ta đang mở một lệnh ngoài thay vì một tệp. Vì nó nằm tại chỗ bắt đầu của lệnh ngoài nên có nghĩa là ta đang mở một lệnh ngoài để ta có thể ghi-xuất thông tin-lên nó (nếu bạn đặt | tại cuối thay vì đầu thì có nghĩa là bạn ở lệnh ngoài và đọc-thu tóm kết quả output của lệnh).
Câu lệnh tiếp, print, có một tước hiệu tệp (MAIL) giữa từ khoá print và giá trị được in cho ta biết là kết quả sẽ được ghi lên MAIL thay vì STDOUT.
Cuối cùng, ta đóng tước hiệu tệp MAIL, cũng có nghĩa là kết thúc việc gởi dữ liệu ra lệnh ngoài.
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 tham số, 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. Tất cả những điều này sẽ được mô tả trong Chương 14:Quản lí tiến trình.
1.6.13 Nhiều tệp từ bí mật trong thư mục hiện tại
Ta hãy 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ì ta hãy tìm bất kì tệp nào trong thư mục hiện tại mà có tận cùng là .secret. Nếu dùng các kịch bản shell, bạn có thể sẽ dùng lệnh sau: 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ự. Ta 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>;
                chop($word);
                $words{$name} = $word;
            } #ket thuc while
            close(WORDSLIST);
        } #ket thuc if
    } #ket thuc while
} #ket thuc init_words
Trước hết, ta đã đưa ra một chu trình while mới trên phần lớn chương trình con của bản cũ. Điều mới ở đây là toán tử <*.secret>. Toán tử này sẽ trả về danh sách các tệp trong thư mục hiện thành có tên kết thúc bởi .secret, và trong chu trình while đầu tiên, biến $filename sẽ lần lượt nhận các giá trị của danh sách các tệp này. Khi không có thêm tên tệp nào thoã mãn điều kiện nữa được cho lại thì nó trả về xâu rỗng.
Cho nên nếu thư mục hiện tại có chứa tom.secret và jerry.secret, thì $filename sẽ là jerry.secret ở bước đầu của chu trình while (tên tệp được xếp trật tự a,b,c...). Ở bước thứ hai, $filename là tom.secret. Và vì không có tình huống thứ ba nên nó trả lại xâu rỗng khi lần thứ ba được gọi tới, làm cho điều kiện của chu trình while thành False, và ta kết thúc chu trình while (đồng thời kết thúc luôn chương trình con).
Bên trong chu trình while phía trong, chúng ta mở tệp và kiểm chứng rằng nó còn đủ "trẻ" hay không (ít hơn 7 ngày từ lần sửa đổi trước). Với những tệp thoã mãn điều kiện, chúng ta duyệt qua như trước.
Chú ý rằng nếu không có tệp nào so sánh với *.secret và lại ít hơn 7 ngày thì chương trình con sẽ thoát 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ừ "none". Thế cũng được! Nhưng trong thực tế thì bạn nên thêm một lệnh kiểm tra nữa xem mảng %words có chứa phần tử nào hay không. Bạn hãy xem hàm keys() ở Chương 5: Mảng băm.
1.6.14 Mặc dù vậy, chúng ta biết họ là ai!
Phần này đã được lược bỏ trong lần tải bản mới của quyển Learning Perl!
  
1.6.15 Liệt kê các từ bí mật
Bây giờ ta 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. Điều này cũng có thể làm được. Trước hết, ta hãy lấy ra tất cả các từ bí mật, bằng việc lấy một đoạn mã trong chương trình con init_words:
while ($filename = <*.secret>) {
    open (WORDSLIST, $filename);
    if (-M WORDSLIST <= 7) {
        while ($name = <WORDSLIST>) {
            chop($name);
            $word = <WORDSLIST>;
            chop($word);
            #[Phần mới sẽ được đưa vào đây] (*)
            $words{$name} = $word;
        } #ket thuc while
        close(WORDSLIST);
    } #ket thuc if
} #ket thuc while
Ở (*) ta biết 3 điều: tên của tệp (trong $filename), tên một ai đó (trong $name), và từ bí mật của người đó (trong $word). Sau đây là chỗ để chúng ta dùng công cụ sinh báo cáo của Perl. Ta định nghĩa một định dạng ở đâ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 định dạng 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à phần định dạng. Dòng đầu của định dạng này là dòng định nghĩa các trường sẽ được in ra, chiều dài và kiểu của trường. Với định dạng này, chúng ta 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 định dạng 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 đó. Ta gọi định dạng này với toán tử write, như thế này:
while ($filename = <*.secret>) {
    open (WORDSLIST, $filename);
    if (-M WORDSLIST <= 7) {
        while ($name = <WORDSLIST>) {
            chop($name);
            $word = <WORDSLIST>;
            chop($word);
            write;    #<== phan moi
            $words{$name} = $word;
        } #ket thuc while
        close(WORDSLIST);
    } #ket thuc if
} #ket thuc while
format STDOUT =
@<<<<<<<<<<<<<<< @<<<<<<<<<< @<<<<<<<<<<<<<<
$filename, $name, $word
.

Khi định dạng này được gọi tới, thì 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 được gọi một lần mỗi khi đi qua chu trình nên ta 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.
Uhh, mà chúng ta còn chưa có nhãn cho các cột, thế mới chán chứ-Ohh, nhưng mà điều đó thì cũng không khó lắm. Ta chỉ cần thêm vào định dạng trên đầu trang, như sau:
format STDOUT_TOP =
Page @<<
$%
Ten tep    Ten      Tu bi mat
========== ======== ==========
.
Định dạng này mang tên STDOUT_TOP, và sẽ được dùng đầu tiên ngay khi gọi tới định dạng 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 định dạng STDOUT, cho nên mọi thứ "khớp" vào với nhau thật đẹp!
Dòng đầu tiên trong định dạng 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 định dạng đầu trang.
Dòng thứ ba của định dạng 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 output, 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 định dạng này cũng không chứa trường nào, cho nên chúng được sao trực tiếp ra output. Do vậy định dạng 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 định dạng đầu trang tự động. Perl cũng có các trường được canh giữa hay căn lề phải, và hỗ trợ cho cả canh lề 2 bên. Chúng ta sẽ nói kỹ về vấn đề này trong Chương 11: Định dạng.
1.6.16 Làm "ấn tượng" cho danh sách từ cũ
Khi ta đọc qua các tệp *.secret trong thư mục hiện tại, ta có thể tìm thấy các tệp quá cũ (ta thường bỏ qua những tệp này từ trước). Ta hãy đi thêm một bước nữa - ta sẽ đổi tên chúng thành *.secret.old để cho khi nhìn vào ta sẽ thấy ngay tệp những tệp nào quá cũ. 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 {
            #doi ten tep de no "an tuong" hon
            rename($filename, "$filename.old");
        }
        close (WORDSLIST);
    }
}
Bạn hãy chú đến phần else mới của việc kiểm tra tuổi. Nếu tệp cũ hơn 7 ngày, thì nó sẽ được đổi tên bằng hàm rename(). Toán tử này lấy hai tham số: đổi tệp có tên trong tham số thứ nhất thành tên trong tham số 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 trong chương trình C, bạn cũng có thể làm từ Perl. Vấn đề này sẽ được bàn ký hơn trong chương 17.
1.6.17 Duy trì một cơ sở dữ liệu đoán đúng cuối cùng
Ta hãy 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 mà dường như mới thoáng nhìn thì có vẻ được là mảng băm. 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 hơn 700 triệu-số giây trôi qua kể từ 1/1/1970) cho một phần tử của %last_good mà có tên với khoá đó. Qua thời gian, điều này sẽ dường như cho ta 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ì ta 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 kết thúc.
Ta dùng hàm dbmopen() để ánh xạ một mảng băm vào một tệp trên đĩa (thực tế là một cặp tệp trên đĩa), còn được gọi là DBM-Database Management. Nó được dùng như thế này:
dbmopen(%last_good, "lastdb", 0666);
$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 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 thuộc tính về tệp UNIX được dùng cho hai tệp này nếu các tệp đó phải được tạo ra (khi chúng chưa có sẵn) là 0666. 0666 này có nghĩa là bất kì ai cũng có thể đọc hay ghi lên tệp. (bạn xem thêm phần manual của lệnh chomod).
Câu lệnh thứ hai chỉ ra rằng chúng ta dùng mảng băm đã được ánh xạ này giống hệt như mảng băm 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. Điều này cho mảng băm có một 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 băm 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 ddầu), chúng ta vẫn không có cách nào để xem xét thông tin trong đó. Để làm điều đó, ta có thể tạo ra một chương trình chỏ 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;    #tinh gio da qua
    write;
}
dbmclose(%last_good);
format STDOUT =
User @<<<<<<<<<<: lan doan dung cuoi cung la @<<< gio da qua.
$name, $hour
.
Chúng ta 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, hàm keys() lấy một tên mảng băm như một tham số và trả về 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ì đó giông giống như Dale, Chip, Tom, Jerry 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.
Hàm 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 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ữ cái.
Thân của chu trình foreach nạp vào hai biến được dùng trong định dạng STDOUT, và rồi gọi tới định dạng đó. Chú ý rằng chúng ta đ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.
1.7 Bài tập
Chương này, và hầu hết các chương sau đó, sẽ có phần bài tập để bạn ôn luyện lại những gì đã học được. Bài tập của chương này thật đơn giản: bạn hãy gõ lại các chương trình ở trên vào đĩa và thử chạy chúng (đừng quên tạo thêm các tệp từ bí mật nữa nhé)
(Sưu tầm từ diễn đàn tin học)

No comments:

Post a Comment

Bài đăng mới: