$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 ) ; 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 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 = ( %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ừ 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 " ; . 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. 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. 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)