Thursday, December 19, 2013

Stdin,stdout,stderr

Hôm nay nhân chuyện có người hỏi về làm thế nào để chạy một lệnh một cách "âm thầm" tức là không in cái gì ra màn hình cả, mình viết một cái tut nhỏ về chuyện này. Ví dụ đưa ra ở đây là lệnh curl, chi tiết về cú pháp lệnh thì sử dụng "man curl" trên Linux hoặc trên Google.

Trước hết, ta sẽ đi thẳng vào vấn đề rồi sau đó mới giải thích. Để một lệnh chạy trong chế độ "âm thầm" như vậy thì ta thêm đoạn sau vào đuôi lệnh:

> /dev/null 2>&1
Mà cụ thể nếu ta muốn làm công việc đó với lệnh curl thì như sau:

$ curl http://osg.vnu.edu.vn/ > /dev/null 2>&1
Vì sao lại làm thế? Dấu ">" có ý nghĩa gì? "/dev/null" là cái gì mà ghê gớm thế? "2>&1" là cái gì mà trông kì lạ thế? Đó cũng là những thắc mắc của mình khi bước vào thế giới Linux/Unix.

1. Giới thiệu
Trên hầu hết các hệ điều hành nói chung và Linux/Unix nói riêng thì có 3 dòng xuất nhập chuẩn (I/O) là STDIN, STDOUT STDERR mà chức năng tương ứng là dòng nhập chuẩn, dòng xuất chuẩn và dòng xuất lỗi chuẩn. Chúng được gọi là các open file và hệ thống gán cho mỗi file này một con số gọi là file descriptor. Ba con số tương ứng với 3 dòng xuất nhập chuẩn ở trên là 0, 1 và 2. Cụ thể:

Quote
standard input -> stdin -> 0<
standard output -> stdout -> 1>
standard error -> stderr -> 2>
Trong C++ thì 3 dòng xuất nhập chuẩn này tương ứng với 3 đối tượng cin, cout và cerr.

Chú ý: Trong bài tut này thì mình sử dụng Bourne shell trong đó dấu $ thể hiện user bình thường và # thể hiện user root. Tuy nhiên hầu hết nội dung trong bài này có thể áp dụng với một số loại shell khác như sh, csh, tcsh... Với C chell (csh, tcsh) thì không sử dụng được các con số (file descriptor).

2. Xuất/Nhập
Trong chế độ command line của hầu hết các hệ điều hành thì "<" dùng cho chuyển hướng nhập và ">" dùng cho chuyển hướng xuất. Vì sao phải chuyển hướng? Vì có nhiều lúc ta muốn kết quả xuất ra màn hình được lưu lại vào một file và dữ liệu nhập vào thay vì từ bàn phím thì lại từ một file.

2.1. STDIN
STDIN chỉ các dòng nhập chuẩn nói chung và nó thường là từ bàn phím. Khi chúng ta gõ bàn phím tức là chúng ta đang nhập vào STDIN. Để dữ liệu đầu vào là một file thì ta dùng dấu "<". Ví dụ, nếu ta dùng lệnh cat mà không có tham số thì khi ta gõ gì nó sẽ hiển thị ra cái đó, hay nói đúng hơn sẽ hiển thị lại những gì ta nhập vào từ input chuẩn. Vậy thì giả dụ ta cần hiển thị file /etc/passwd thì ngoài cách truyền thống là

$ cat /etc/passwd
thì ta có thể sử dụng:

$ cat < /etc/passwd
hoặc

$ cat 0< /etc/passwd
Tại sao lại có thể bỏ số 0 mà chức năng vẫn tương tự? Đó là vì mỗi khi khởi tạo một process thì hệ thống đã gắn một dòng nhập chuẩn cho process đó mà ở đây là STDIN hay 0.

2.2. STDOUT
STDOUT là các dòng xuất chuẩn nói chung và nó thường là xuất ra màn hình, ra cửa sổ console hoặc terminal. Để dữ liệu đầu ra được ghi vào một file thì ta sử dụng dấu ">". Ví dụ ta muốn danh sách các file trong một thư mục được ghi vào file dir.txt thì ta sử dụng lệnh sau:

$ ls -al > dir.txt
hoặc

$ ls -al 1> dir.txt
Lí do vì sao có thể bỏ số 1 đi tương tự như với STDIN, tức là khi khởi tạo một process thì hệ thống đã gắn một dòng xuất chuẩn cho process đó mà ở đây là STDOUT hay 1.

Đến đây ta có thể kết hợp sử dụng song song STDIN và STDOUT để làm thao tác copy file. Ví dụ ta muốn backup file /etc/passwd thì ta có thể làm như sau:

$ cat < /etc/passwd > ~/passwd.bak
Lệnh này tương đương với lệnh:

$ cp /etc/passwd ~/passwd.bak
Có một ứng dụng cực kì có ích của việc kết hợp này là chuyển đổi file text giữa Windows và Unix. Như các bạn đều biết thì trong file text của Windows, việc xuống dòng được thể hiện bằng cặp kí tự \r\n còn trong Linux/Unix thì chỉ là \n. Ai phải code trên cả hai môi trường đều thấy sự bất tiện của việc chuyển đổi đó. Giải pháp đưa ra ở đây là sử dụng lệnh tr, cụ thể như sau:

tr -d '\r' < win.cpp > unix.cpp
Lệnh này sẽ nhận dòng nhập chuẩn sau đó xoá các kí tự \r rồi ghi ra dòng xuất chuẩn. Dòng nhập và dòng xuất ở đây được định hướng lại để đến từ một file và ghi ra một file.

Tuy nhiên nếu dùng ">" thì nội dung của file sẽ bị xoá trước khi ghi nội dung mới. Nếu ta muốn nội dung mới sẽ được ghi nối tiếp vào file thì ta sử dụng 2 dấu lớn hơn, tức là ">>". Ví dụ nếu bạn muốn nối nội dung của thư mục /home vào cuối file passwd.bak ở trên thì bạn làm như sau:

$ ls /home >> ~/passwd.bak
Bây giờ nếu ta muốn lấy mã HTML của trang chủ của OSG và ghi vào file osg.html thì ta sử dụng lệnh sau:

$ curl http://osg.vnu.edu.vn/ > osg.html
Thực hiện lệnh trên các bạn có thấy gì lạ không? Mặc dù mã HTML thay vì xuất ra màn hình mà được đưa vào file osg.html nhưng vẫn có các thông tin thể hiện trạng thái download hiển thị trên màn hình. Làm thế nào mà lại được như thế? Làm thế nào để lệnh curl câm lặng hoàn toàn? Hồi sau sẽ rõ :D .

2.3. STDERR
STDERR là dòng xuất lỗi chuẩn nói chung và nó cũng thường xuất trực tiếp ra màn hình, console hay terminal. Cú pháp tương tự như STDOUT, tức là sử dụng ">" để xuất ra file và ">>" để nối vào một file đã có (chưa có thì hệ thống sẽ tự tạo ra). Tuy nhiên điểm khác biệt là bạn phải chỉ rõ số 2, tức là "2>" hoặc "2>>". Lí do là vì chỉ có 1 dòng xuất chuẩn và 1 dòng nhập chuẩn cho mỗi process mà thông thường hệ thống chỉ định là STDOUT và STDIN.

Vậy trong trường hợp của lệnh curl trong phần 2.2 ở trên, nếu ta muốn ghi cả 2 loại output đó ra file thì ta làm như sau:

$ curl http://osg.vnu.edu.vn/ > osg.html 2> osg.log
Thế nào? Không có cái gì xuất ra màn hình hết đúng không? Vì nội dung trang web đã được lưu vào file osg.html còn các dòng lưu trạng thái download đã được ghi vào file osg.log.

Nhưng thế thì tốn dung lượng đĩa và có nguy cơ gây hỏng đĩa vì phải ghi file mà. Con người quả thật quá tham lam :lol: . Vậy thì phải sáng tạo ra cái gì đó như kiểu cái thùng không đáy hay gọi mĩ miều hơn thì nó là "lỗ đen" hay "black hole", tức là một nơi mà cho cái gì vào cũng mất hút luôn. Linux/Unix có cái đó cho bạn, đó là /dev/null.

2.4. /dev/null
Theo định nghĩa trên Wikipedia của /dev/null:

Quote
In Unix-like operating systems, /dev/null or the null device is a special file that discards all data written to it (but reports that the write operation succeeded), and provides no data to any process that reads from it (it returns EOF). In Unix programmer jargon, it may also be called the bit bucket or black hole.
Tạm dịch là:

Quote
Trong các hệ điều hành kiểu Unix, /dev/null hay thiết bị null là một tệp tin đặc biệt, nó bỏ qua mọi dữ liệu ghi lên nó (nhưng có báo cáo về việc ghi dữ liệu thành công) và không cung cấp bất kì dữ liệu gì khi đọc từ nó (trả về EOF). Trong biệt ngữ của các lập trình viên Unix, nó đuợc gọi là "bit bucket" hoặc "black hole".
Vậy thì đó chính là cái ta cần rồi. Như vậy câu lệnh curl ở trên có thể cho nó thực hiện câm lặng bằng cách:

$ curl http://osg.vnu.edu.vn/ > /dev/null  2> /dev/null
Không có cái gì xuất ra màn hình cả, cũng không có cái gì được ghi lại cả. Nhưng... lại nhưng, con người vẫn tham lắm, làm thế nào để cái lệnh trên ngắn gọn hơn, trông technical hơn, nói chung là để ai không biết thì sẽ không hiểu gì (đôi khi đó là cái thú của dân kĩ thuật). Ta sẽ dùng "2>&1" ở đây, tức là:

$ curl http://osg.vnu.edu.vn/ > /dev/null  2>&1
Câu lệnh trên tức là dòng xuất chuẩn (1) sẽ bị đưa vào /dev/null và dòng lỗi chuẩn (2) sẽ được đưa vào dòng xuất chuẩn (1) mà ở đây là /dev/null.

Đặc biệt lưu ý là với cú pháp sử dụng dấy & thì dấu & và dấu > phải đi liền nhau, không có khoảng cách.

Ngoài các file descriptor 0, 1, 2 ở trên thì còn có từ 3 -> 9 nữa. Tuy nhiên bài viết này chỉ dành cho mức độ newbie nên không để cập sâu, chi tiết các bạn có thể tự tìm hiểu thêm trên Internet hoặc trong các sách về lập trình shell.

3. Pipe
Như vậy chúng ta đã biết cách để chuyển hướng dòng xuất/nhập của một lệnh hay một process. Bằng cách này ta có thể chuyển dữ liệu xuất của một lệnh thành dữ liệu nhập của một lệnh khác thông qua một file trung gian. Tuy nhiên ta không muốn có file trung gian đó, một phần vì việc ghi lên đĩa cứng, phần khác là do... tham. Đó chính là vấn đề mà pipe giải quyết. Trong Linux, ta sử dụng dấu "|" để làm việc này.

Ví dụ khi ta muốn xem lại nội dung thư mục /etc nhưng kết quả của nó lại dài quá mà ta muốn xem lại thì ta làm như sau

$ ls -al /etc | more
hoặc

$ ls -al /etc | less
(thoát bằng phím q).

hoặc ta muốn đếm số user trong hệ thống có sử dụng mặc định bash shell thì ta làm như sau:

$ cat /etc/passwd | grep "/bin/bash" | wc -l
Lệnh này có nghĩa là đưa nội dung file /etc/passwd ra dòng xuất chuẩn; dòng xuất chuẩn này thành dòng nhập chuẩn của lệnh grep và lệnh này chỉ lọc ra các dòng có chưa xâu "/bin/bash" để đưa ra dòng xuất chuẩn; dòng xuất chuẩn này lại thành dòng nhập chuẩn của lệnh wc -l là lệnh đếm số dòng của dòng nhập chuẩn và đưa ra số dòng ra dòng xuất chuẩn; cuối cùng dòng xuất chuẩn này sẽ được đưa ra trực tiếp màn hình vì nó không thành dòng nhập chuẩn của lệnh nào nữa.