Vietnamese Developers’ Blog

Unix programming with standard I/O (2)

Posted in C/C++, Unix/Linux/BSD by kiennguyen on January 22, 2008

Phần 2: Chương trình hiển thị nội dung file theo từng trang màn hình

1. Đặt vấn đề

Khi xem nội dung một file dài, chúng ta thường muốn nội dung file đó được hiển thị lần lượt theo từng trang màn hình. Hai lệnh phổ biến để xem nội dung file là cat và more không đáp ứng được nhu cầu này (chúng không có khả năng này, hoặc có nhưng không tiện dụng, “tác giả” cũng không biết rõ về tất cả các khả năng của hai lệnh này). Bởi vậy, chúng ta sẽ phát triển một chương trình tên là p làm nhiệm vụ in ra nội dung một file theo từng trang màn hình (screenful-at-a-time). Chương trình sẽ đợi người dùng ấn một phím để chuyển sang hiển thị trang tiếp theo. Giống như vis, p nhận dữ liệu vào từ cả file lẫn standard input. Ví dụ:

p nhận dữ liệu vào từ file

$ p vis.c

p nhận dữ liệu vào từ standard input

$ grep  ‘#define’  *.[ch]  |  p

Ở phiên bản đầu tiên, p sẽ hiển thị nội dung file theo từng khối, mỗi khối 22 dòng (phần lớn các terminal gồm 24 dòng văn bản). Một cách đơn giản để nhắc người dùng ấn một phím để tiếp tục là không in ra kí tự new line nằm cuối dòng thứ 22. Khi đó, con trỏ sẽ nằm ở cuối dòng thứ 22 thay vì đầu dòng thứ 23. Khi người dùng ấn phím enter, kí tự new line còn thiếu của dòng 22 sẽ được thêm vào, nhờ đó dòng tiếp theo (dòng 23) sẽ được in ra ở đúng vị trí của nó. Nếu người dùng ấn ctrl-d hay q thay vì enter, p sẽ kết thúc. Chúng ta sẽ không quan tâm đến các dòng quá dài. Ngoài ra, khi hiển thị nhiều file cùng lúc thì nội dung các file sẽ được in ra liên tục mà không có sự phân cách gì cả. Tức là, đầu ra của hai lệnh sau đây là như nhau

$ p file1 file2 …

$ cat file1 file2 …  |  p

Chú ý rằng lệnh cat in ra nội dung các file một cách liên tục mà không có sự phân cách gì cả. Nếu muốn nội dung các file được phân cách bởi tên file, chúng ta có thể dùng vòng lặp sau đây (ôn lại lập trình shell luôn :-D )

$ for i in filenames
> do
>      echo $i:
>      cat $i
> done  |  p

Có rất nhiều tính năng có thể được đưa vào chương trình p. Quan điểm của chúng ta là: Trước hết tạo ra một phiên bản đơn giản, sau đó dần dần thêm vào các tính năng phức tạp hơn khi cần thiết. Những tính năng được thêm vào phải là những cái mà người dùng thực sự muốn, chứ không phải những cái mà chúng ta nghĩ rằng họ muốn.

2. Phiên bản đầu tiên của p

Cấu trúc của p cũng tương tự như vis: Hàm main duyệt qua các file đầu vào và gọi một hàm tên là print để xử lí từng file. Dưới đây là hàm main

/* p: print input in chunks (version 1) */

#include 

#define PAGESIZE 22

char *progname; /* program name for error message */

main( int argc, char *argv[ ] )

{

  int i;

  FILE *fp, *efopen( char*, char* );

  void print( FILE*, int );

  progname = argv[ 0 ];

  if( argc == 1 )

    print( stdin, PAGESIZE );

  else

    for( i = 1; i < argc; i++ ) {

      fp = efopen( argv[ i ], “r” );

      print( fp, PAGESIZE );

      fclose( fp );

    }

  exit( 0 );

}

Hàm efopen đóng gói các xử lí quen thuộc: Mở một file, nếu không thành công thì in ra thông báo lỗi và dừng chương trình. Thông báo lỗi bao gồm tên chương trình chứa trong một biến external tên là progname được khởi tạo giá trị trong hàm main.

/* open file, die if cannot */

FILE *efopen( char *file, char *mode )

{

  FILE *fp;

  extern char *progname;

  if( ( fp = fopen( file, “r” ) ) != NULL )

    return fp;

  fprintf( stderr, “%s: cannot open file %s mode %s\n”, progname, file, mode );

  exit( 1 );

}

Có nhiều phương án khác để thiết kế hàm efopen. Cách thứ nhất là khi gặp lỗi mở file, chúng ta in ra thông báo lỗi và trả về con trỏ NULL. Phương án này cho phép người dùng tự quyết định có tiếp tục xử lí hay dừng chương trình. Cách thứ hai là đưa vào một tham số thứ ba cho efopen, chỉ định hàm sẽ dừng chương trình hay trả về NULL trong trường hợp không thể mở file. Trong ngữ cảnh cụ thể của chương trình p, không có lí do gì để chương trình tiếp tục chạy khi không thể mở một file đầu vào. Bởi vậy, thiết kế hiện thời của hàm print là chấp nhận được. Phần xử lí quan trọng nhất của p được thực hiện bởi hàm print.

/* print fp in pagesize chunks */

void print( FILE *fp, int pagesize )

{

  char ttyin();

  static int lines = 0; /* Số dòng đã được in ra trên một trang màn hình */

  char buf[ BUFSIZ ];

  while( fgets( buf, sizeof buf, fp ) != NULL ) {

  if( ++lines < pagesize ) {

      fputs( buf, stdout );

    } else {

      buf[ strlen( buf ) – 1 ] = ‘’;

      fputs( buf, stdout );

      fflush( stdout );

      ttyin();

      lines = 0;

    }

  }

}

BUFSIZ là một hằng số được định nghĩa trong stdio.h. Hàm fgets( buf, size, fp ) đọc dòng văn bản tiếp theo từ fp vào buf và thêm kí tự ‘’ vào cuối dòng đó. Dòng được đọc bao gồm cả kí tự new line ở cuối. Ngoài ra, độ dài của nó không được vượt quá size – 1 kí tự. Thiết kế của hàm fgets có hai điểm chưa tốt. Thứ nhất, nó nên trả về số kí tự vừa được đọc thay vì giá trị buf. Thứ hai, nó không hề đưa ra cảnh báo khi phải xử lí một dòng văn bản dài quá size – 1 kí tự. Mặc dù không có kí tự nào bị mất, chúng ta sẽ phải kiểm tra nội dung của buf để biết thực sự fgets xử lí các dòng quá dài như thế nào!!! Hàm strlen trả về độ dài của xâu, không tính kí tự ‘’ ở cuối. Hãy chú ý cách sử dụng strlen để loại bỏ kí tự cuối cùng khỏi một xâu, trong trường hợp này là kí tự new line. Việc đọc vào phím được ấn được thực hiện bởi hàm ttyin. Hàm ttyin bắt buộc phải đọc dữ liệu từ bàn phím (/dev/tty) chứ không phải từ standard input để đề phòng trường hợp standard input bị định hướng lại thành một file hay một pipeline. Chúng ta thiết kế hàm ttyin sao cho nó trả về kí tự đầu tiên trong chuỗi các kí tự được ấn bởi người dùng, tuy nhiên giá trị này chưa được sử dụng trong phiên bản đầu tiên của p.

/* process response from /dev/tty (version 1) */

char ttyin()

{

  char buf[ BUFSIZ ];

  FILE *efopen( char*, char* );

  static FILE *tty = NULL;

  if( tty == NULL )

    tty = efopen( “/dev/tty”, “r” );

  if( ( fgets( buf, BUFSIZ, tty ) == NULL ) || ( buf[ 0 ] == ‘q’ ) )

    exit( 0 );

  else

    return buf[ 0 ];

}

3. Phát triển chương trình

Phiên bản thứ nhất của p làm duy nhất một việc: In ra nội dung file theo từng trang 22 dòng và đợi phản ứng từ người dùng. Có rất nhiều tính năng mới có thể được thêm vào p mà không đòi hỏi nhiều công sức. Tuy nhiên, thực tế cho thấy những tính năng đó hiếm khi được sử dụng!!! Sự mở rộng đơn giản đầu tiên của p là cho phép người dùng nhập vào số dòng của file sẽ được in ra trên một trang màn hình. Lệnh sau đây

$ p –n file

sẽ in ra nội dung của file theo từng trang, mỗi trang n dòng. Tương tự như chương trình vis, chúng ta thêm đoạn mã sau đây vào hàm main.

/* p: print input in chunks (version 2) */

…

progname = argv[ 0 ];

if( ( argc > 1 ) && ( argv[ 1 ][ 0 ] == ‘-‘ ) ) {

  pagesize = atoi( &argv[ 1 ][ 1 ] );

  argc--;

  argv++;

}

if( argc == 1 )

…

Một tính năng mở rộng nữa của p là tạm thời dừng hiển thị nội dung file để thực thi các lệnh khác. Giống như trong ed và nhiều chương trình khác của Unix, khi người dùng gõ vào một dòng văn bản bắt đầu bởi dấu chấm than (exclaimation mark) thì phần còn lại của dòng đó sẽ được coi như một câu lệnh và sẽ được chuyển tới shell để thực hiện. Tính năng này được cài đặt khá đơn giản nhờ hàm system. Chúng ta có phiên bản mới của hàm ttyin như sau.

/* process response from /dev/tty (version 2) */

char ttyin()

{

  char buf[ BUFSIZ ];

  FILE *efopen( char*, char* );

  static FILE *tty = NULL;

  if( tty == NULL )

    tty = efopen( “/dev/tty”, “r” );

  for( ; ; ) {

    if( ( fgets( buf, BUFSIZ, fp ) == NULL ) || ( buf[ 0 ] == ‘q’ ) ) {

      exit( 0 );

    } else if( buf[ 0 ] == ‘!’ ) {

      system( buf + 1 ); /* BUG here!!! */

      printf( “!\n” );

    } else {

      return buf[ 0 ];

    }

  }

}

Tuy nhiên, phiên bản này của ttyin có mỗi lỗi rất tinh vi. Các lệnh chạy bởi hàm system kế thừa standard input từ p. Khi p đọc dữ liệu vào từ một pineline hay một file, do standard input bị định hướng lại, thì lệnh chạy bởi system cũng sẽ nhận dữ liệu từ chính standard input đó chứ không nhận từ người dùng thông qua terminal nữa. Trong trường hợp dưới đây, khi người dùng nhập vào dòng văn bản !ed, nhẽ ra chương trình ed sẽ được gọi và sẽ chờ người dùng nhập dữ liệu vào từ bàn phím. Tuy nhiên do standard input đã bị định hướng lại sang pinepline nên ed lại đọc dữ liệu từ chính file /etc/passwd và sau đó kết thúc ngay lập tức.

$ cat /etc/passwd | p -1

root:3D.fHR5KoB.3s:0:1:S.User:/:!ed

?

!

Lỗi này sẽ được giải quyết trong phần sau. Ở thời điểm này chúng ta chỉ cần nhớ rằng sử dụng hàm system có thể gây ra lỗi.

Chúng ta vừa phát triển hai chương trình vis và p, có thể coi là các biến thể của lệnh cat. Liệu hai chương trình này có nên được tích hợp vào cat dưới dạng các tham số tùy chọn –v và –p hay không? Chúng ta thường xuyên phải đứng trước hai sự lựa chọn: Viết một chương trình hoàn toàn mới hay thêm tính năng vào một chương trình cũ? Nguyên tắc cơ bản ở đây là: Mỗi chương trình chỉ nên làm một công việc cụ thể mà thôi. Một chương trình làm quá nhiều việc sẽ rất cồng kềnh, chạy chậm, khó bảo trì và khó sử dụng. Rất nhiều tính năng không được sử dụng đến bởi người dùng không thể nhớ được chúng!!! Chúng ta không nên nhập vis và cat vào làm một. cat thuần túy sao chép dữ liệu từ “input” sang “output” mà không xử lí gì cả. Trong khi đó, vis thực hiện chuyển đổi (transform) từ “input” sang “output”. Nhập vis và cat làm một sẽ tạo ra một chương trình làm hai việc khác nhau. Chúng ta cũng không nên gộp p và cat làm một. cat được dùng để in dữ liệu văn bản nhanh và hiệu quả. Trong khi đó, p được dùng để “duyệt” (browse) dữ liệu văn bản theo cách tiện lợi nhất đối với người dùng. Để ba chương trình cat, vis và p tồn tại riêng rẽ là một quyết định thiết kế hợp lí.

4. Một số hướng phát triển chương trình

- p sẽ chạy như thế nào nếu giá trị pagesize nhỏ hơn 0?
- Những tính năng nào có thể được thêm vào p? Bổ sung khả năng tìm kiếm một dòng theo vị trí hay nội dung theo cả hai chiều xuôi (fordward) và ngược (backward). Bổ sung khả năng in ra nội dung file theo từng khối nhỏ hơn pagesize.
- Sử dụng hàm exec để sửa lỗi gây ra bởi hàm system.
- Phiên bản hiện thời của p sẽ “im lặng” đợi người dùng nhập dữ liệu vào từ bàn phím. Sử dụng hàm isatty để khắc phục nhược điểm này.

Phần tiếp theo: Chương trình lựa chọn tham số PICK và chương trình chấm dứt các tiến trình ZAP


7 Responses

Subscribe to comments with RSS.

  1. Hoang Tran said, on January 25, 2008 at 11:29 am

    Việc hiển thị một file văn bản theo từng trang màn hình thì chính là lệnh more rồi còn gì? Có vẻ quyển sách này viết trước khi more ra đời rồi :-P

  2. kiennguyen said, on January 28, 2008 at 11:33 pm

    Làm sao để more in ra nội dung file theo từng trang 22 dòng :-D

  3. kiennguyen said, on January 31, 2008 at 12:50 pm

    Trong vòng 1 tháng tới em sẽ rất bận, không viết lách gì được. Anh bảo ku Tân chịu khó viết đi!

  4. Hoang Tran said, on February 8, 2008 at 10:47 am

    Anh dạo này cũng bận rộn lắm :-(

  5. Hoang Tran said, on March 7, 2008 at 2:10 pm

    Theo triết lý của UNIX programming thì có khi đừng viết bằng C++ mà viết tất cả bằng C cho nó đỡ … phức tạp.

  6. Kien Nguyen said, on April 11, 2008 at 6:36 pm

    Dạo này đang fix bugs cho 1 chương trình tên là tlpm, tức là The London Partnership MORE. Thú vị ra phết!

  7. kiennguyen said, on May 14, 2008 at 11:03 am

    Triết lý của Unix có bắt phải viết bằng C đâu nhỉ? Thực ra ngôn ngữ lập trình chỉ là công cụ để mô tả vấn đề dưới dạng mà máy tính hiểu được. Các ngôn ngữ lập trình hướng đối tượng phù hợp để mô tả các bài toán trong thế giới thực. Còn các bài toán khoa học thì đâu có cần đến object làm gì.


Leave a Reply