Shell Script && Command Line

The Shell

Shell là gì và tại sao lại sử dụng Shell?

Ngày nay, máy tính có vô vàn các giao diện khác nhau để người dùng tương tác với chúng: từ những giao diện đồ họa, đến giao diện âm thanh hay thậm chí là giao diện thực tế ảo AR/VR ở khắp nơi. Những giao diện này đáp ứng đủ đến 80% các trường hợp sử dụng mà chúng được thiết kế để sử dụng, tuy nhiên chúng lại vô cùng hạn chế về khả năng thật sự mà chúng cho phép ta thao tác với. Một ví dụ điển hình là bạn không thể bấm nút để thực hiện một thao tác nào đó nếu thao tác ấy không được lập trình thành nút bấm cho gia diện. Hay là việc ra lệnh bằng giọng nói cho một câu lệnh lạ hoặc mà máy tính chưa được lập trình để hiểu được.

Vì vậy để tận dụng được hoàn toàn sức mạnh mà máy tính cho phép trong các tác vụ của chúng ta, chúng ta cần đi theo hướng truyền thống và vô cùng cơ bản: giao diện câu lệnh bằng chữ - Shell.

Trong CS101, chúng ta sẽ tập trung vào Shell có tên là Bourne Again SHell, hay “bash”. Đây là một loại shell vô cùng thông dụng và cú pháp câu lệnh của nó rất cơ bản, được sử dụng trong nhiều loại shell khác. Để mở một dòng nhắc shell (prompt) - nơi mà bạn có thể ra lệnh, bạn cần trước nhất một phần mềm được gọi là terminal. Phần mềm này thường được mặc định cài đặt trong hệ điều hành, hoặc bạn có thể tự cài đặt một cách dễ dàng. Lưu ý: nếu bạn đang sử dụng hệ điều hành Windows, bạn nên cân nhắc cài đặt máy ảo Linux.

Sử dụng Shell

Khi bạn mở một terminal, bạn sẽ thấy được một dòng nhắc prompt như sau:

dui@cs ➜ ~$

Đây là giao diện câu chữ chính của trình shell. Nó cho bạn biết ta đang ở trên thiết bị có tên là cs và thư mục mà ta đang ở hiện tại là ~ (ngắn gọn cho “home” hay thư mục của tài khoản người dùng hiện tại dui). Dấu hiệu $ lại cho ta biết người dùng hiện tại dui, không phải là người dùng gốc (root). Trên dòng nhắc prompt này, bạn có thể nhập một câu lệnh (command), thứ mà sau đó sẽ được thông dịch bởi shell. Một câu lệnh vô cùng đơn giản là:

dui@cs ➜ ~$ date
Thu Oct 28 12:46:07 +07 2021
dui@cs ➜ ~$

Ta đã chạy lệnh date và in ra được ngày giờ hiện tại. Sau đó Shell sẽ chờ ta nhập câu lệnh tiếp theo. Hơn thế nữa chúng ta có thể chạy lệnh với các đối số (arguments):

dui@cs ➜ ~$ echo hello
hello
dui@cs ➜ ~$

Trong trường hợp trên chúng ta chạy lệnh echo với đối số là hello. Lệnh echo in ra cửa sổ terminal đối số của nó. Shell phân tích từ loại của câu lệnh bằng cách phân câu lệnh ra theo khoảng trắng, và sau đó chạy câu lệnh được nhắc đến trong từ đầu tiên, nhập các từ tiếp theo thành một đối số của trình/câu lệnh này. Nếu bạn muốn nhập một đối số có khoảng trống (ví dụ như thư mục có tên là “Hello World”), ta có hai cách. Một là bao đối số đó với dấu ' hoặc " ("Hello World"), hoặc hai là nhập ký tự đặc biệt với dấu \ (Hello\ World).

Khi chúng ta muốn tạo một thư mục, chúng ta có thể sử dụng lệnh mkdir và truyền đối số thứ nhất là tên của thư mục chúng ta muốn khởi tạo.

dui@cs ➜ ~$ mkdir cs101
dui@cs ➜ ~$ ls
cs101
dui@cs ➜ ~$ mkdir hello world
dui@cs ➜ ~$ ls
cs101 hello world
dui@cs ➜ ~$ mkdir /bin/hello
dui@cs ➜ ~$ ls /bin/hello
        ... hello ...

Ở ví dụ trên chúng ta có thể tạo nhiều thư mục cùng một lúc bằng cách truyền thêm nhiều đối số vào lệnh mkdir . mkdir cũng có thể nhận đường dẫn tương đối và đường dẫn tuyệt đối để tạo thư mục một cách dễ dàng nếu chúng ta không muốn di chuyển khỏi thư mục hiện tại. Chúng ta có thể tạo thư mục có tên chưa khoảng trống bằng cách sử dụng ' hoặc " hoặc \ như chúng ta đã đề cặp khi nãy:

dui@cs ➜ ~$ mkdir 'cs101 cs102'
dui@cs ➜ ~$ ls
'cs101 cs102'
dui@cs ➜ ~$ mkdir "hello world"
dui@cs ➜ ~$ ls
'cs101 cs102' 'hello world'
dui@cs ➜ ~$ mkdir cs103\ cs104
dui@cs ➜ ~$ ls
'cs101 cs102' 'cs103 cs104' 'hello world' 

Nếu ở đây các bạn thắc mắc tại sao lệnh date và echo có thể thực thi. Vì Shell thực chất là một môi trường lập trình giống như C++, Python hay Javascript vì thế nên Shell có những thứ rất cơ bản của một ngôn ngữ lập trình như biến số, câu điều kiện, vòng lặp và hàm. Khi bạn chạy câu lệnh trong Shell, bạn thật ra đang viết một dòng mã mà Shell thông dịch. Nếu shell được ra lệnh để chạy một câu lệnh không có trong từ khóa lập trình, nó sẽ tham vấn một biến số môi trường (environment variable) tên là $PATH, nơi mà các thư mục mà trình shell có thể tìm kiếm các trình được ra lệnh được tạo mặc định cho Shell để chạy câu lệnh được đính kèm.

dui@cs ➜ ~$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
dui@cs ➜ ~$ which echo
/usr/bin/echo
dui@cs ➜ ~$ /bin/echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Khi chúng ta chạy lệnh echo $PATH, Shell sẽ in ra những những tệp có cùng tên trong dãy các thư mục của $PATH và được phân cách bởi ký tự :, chúng ta cũng có thể chạy lệnh which để xuất ra những tệp của đối số. Chúng ta cũng có thể bỏ qua việc tìm kiếm trong $PATH bằng cách nhập câu lệnh bằng đường dẫn đến trình mà ta cần chạy.

Di chuyển trong Shell

Một đường dẫn trong Shell là một dãy các thư mục được giới hạn bởi dấu / trên hệ điều hành Linux và macOS và dấu \ trên Windows. Trên Linux và macOS, đường dẫn / là “gốc”(root) của hệ thống tệp (file system), một loại cây thư mục mà mọi tệp và thư mục khác trực thuộc. Trên Windows thì mỗi ổ đĩa hay phần đĩa (disk partition) như ổ C:\ sẽ có một gốc cây thư mục riêng. Ở đây khuyên các bạn sử dụng Linux. Một đường dẫn bắt đầu với dấu / được gọi là đường dẫn tuyệt đối(absolute). Các đường dẫn khác được gọi là tương đối(relative). đường dẫn tương đối sẽ dựa trên thư mục hiện tại của bạn làm gốc, nơi mà bạn có thể dùng pwd(print working directory) để kiểm tra và thay đổi, di chuyển với cd(change directory). Trong một đường dẫn, dấu . có nghĩa là thư mục hiện tại còn .. là thư mục bố mẹ. Lưu ý răng mỗi khi bạn mở terminal thì thư mục hiện tại luôn là thư mục người dùng và ký hiệu là ~:

dui@cs ➜ ~$ pwd
/home/dui
dui@cs ➜ ~$ cd /home
/home
dui@cs ➜ /home$ pwd
/home
dui@cs ➜ /home$ cd ..
dui@cs ➜ /$ pwd
/
dui@cs ➜ /$ cd ./home
dui@cs ➜ /home$ pwd
/home
dui@cs ➜ /home$ cd dui
dui@cs ➜ ~$ pwd
/home/dui
dui@cs ➜ ~$ cd ../../bin
dui@cs ➜ /bin$

Để ý rằng câu nhắc của Shell sẽ luôn cho ta biết về thư mục hiện tại mà chúng ta đang ở.

Để xem trong thư mục hiện tại có gì, ta dùng ls:

dui@cs ➜ ~$ ls
/projects
dui@cs ➜ ~$ cd ..
dui@cs ➜ /home$ ls
dui
dui@cs ➜ /home$ cd ..
dui@cs ➜ /$ ls
bin   dev  home  lib   lib64   lost+found  mnt  proc  run   snap  sys
usr boot  etc  init lib32  libx32  media opt  root  sbin  srv   tmp  var

Trừ khi một đường dẫn thư mục cụ thể được gán vào đối số thứ nhất của câu lệnh ls, thì ls luôn in ra nội dung (tệp và thư mục con) của thư mục hiện tại. Đa số các câu lệnh cũng cho phép dùng cờ (flag) và tùy chỉnh (option - cờ với giá trị ) bắt đầu bằng dấu - để thay đổi chức năng. Thông thường, dùng cờ tùy chỉnh -h hay --help (/? trên Windows) sẽ chạy chương trình bằng cách in ra thông tin hướng dẫn sử dụng chương trình ấy, cũng như những loại cờ tùy chỉnh mà nó hỗ trợ. Ví dụ câu lệnh ls --help cho ta biết:

dui@cs ➜ ~$ ls --help
...
-a         do not ignore entries starting with
-l         use a long listing format
-t         sort by modification time, newest first
...
dui@cs ➜ ~$ ls -l
drwxr-xr-x 10 dui dui 4096 Oct 11 15:05 projects

Tùy chỉnh này cho ta biết rất nhiều thông tin về tệp và thư mục con. Đầu tiên, chữ d ở đầu dòng cho ta biết rằng projects là một thư mục. Sau đó là các nhóm 3 chữ (rwx). Các nhóm này cho ta biết, theo thứ tự của nhóm, phân quyền (permissions) của chủ (owner) tập tin (projects) , nhóm chủ (owning group) (users), và tất cả người dùng còn lại trên tập tin này. Dấu - thể hiện rằng người dùng hoặc nhóm người dùng đó không có phân quyền nhất định đó. Trong ví dụ trên, chỉ có người chủ tập tin có quyền thay đổi (w) tập tin project(tức là tạo và xóa tệp trong nó). Để di chuyển vào trong thư mục, người dùng cần có quyền “tìm kiếm” (thể hiện bằng quyền “thực hiện”:x) của thư mục đó (và kéo theo là cả thư mục bố mẹ hiện tại). Để liệt kê nội dung của thư mục ấy, người dùng cần có quyền xem, đọc (r) trên thư mục đó. Chú ý rằng các tệp trong tập tin \bin đều có phân quyền x trong nhóm phân quyền cuối cùng, tức “bất cứ người dùng nào”, vì nó cho phép ai cũng có thể chạy được các trình nằm trong tập tin đó.

Một vài trình hữu dụng khác mà ta cần biết lúc này là mv (di chuyển hoặc đổi tên một tệp), cp (sao chép một tệp), và mkdir (tạo thư mục).

Để biết thêm thông tin về các đối số, dữ liệu nhập, xuất hay cách dùng câu lệnh nói chúng, ta dùng lệnh man. Câu lệnh này sẽ dùng tên một câu lệnh hay trình khác làm đối số và in ra trang hướng dẫn sử dụng cần có. Lưu ý để thoát ra khỏi trang này, ta bấm q.

dui@cs ➜ ~$ man ls

Trong shell, các chương trình thường có hai “dòng” (streams): dòng nhập(input stream) và dòng xuất(output stream). Khi chương trình muốn nhập dữ liệu, nó sẽ đọc hoặc nhập từ dòng nhập, còn khi nó in hay xuất dữ liệu, nó sẽ in hay xuất ra dòng xuất. Thông thường chương trình cửa sổ đầu cuối (terminal) sẽ là nơi chương trình nhập và xuất dữ liệu. Điều đó có nghĩa, mặc định dữ liệu được nhận vào từ bàn phím và xuất ra trên màn hình của máy tính Tuy nhiên, ta có thể thay đổi dòng nhập, xuất của các chương trình và tiếp nối chúng với nhau!

Đơn giản nhất để tiếp nối, thay đổi các dòng này đó là < file> file. Chúng cho phép ta có thể thay đổi dòng nhập và xuất từ một tệp nào đó:

dui@cs ➜ ~$ echo hello > message.txt
dui@cs ➜ ~$ cat message.txt
hello
dui@cs ➜ ~$ cat < message.txt
hello
dui@cs ➜ ~$ cat < message.txt > message2.txt
dui@cs ➜ ~$ cat message2.txt
hello

Ở trên chúng ta sử dụng lên cat(concatenates) để in ra nội dung của các tập tin. Khi tên tập tin là một đối số, cat sẽ in nội dung của tập tin đó lên outputstream của mình. Nhưng khi trình cat được sử dụng trong trường hợp không có đối số, nó sẽ in tất cả nội dung từ inputstream của mình ra outputstream (ví dụ 3).

Ký tự | được dùng để nối các chương trình với nhau sao cho dữ liệu xuất ra từ chương trình này lại là dữ liệu nhập của chương trình khác:

dui@cs ➜ ~$ ls | cat > message.txt
dui@cs ➜ ~$ cat message.txt
message.txt
projects

Kết nối các chương trình

Trong shell, các chương trình thường có hai “dòng” (streams): dòng nhập(input stream) và dòng xuất(output stream). Khi chương trình muốn nhập dữ liệu, nó sẽ đọc hoặc nhập từ dòng nhập, còn khi nó in hay xuất dữ liệu, nó sẽ in hay xuất ra dòng xuất. Thông thường chương trình cửa sổ đầu cuối (terminal) sẽ là nơi chương trình nhập và xuất dữ liệu. Điều đó có nghĩa, mặc định dữ liệu được nhận vào từ bàn phím và xuất ra trên màn hình của máy tính Tuy nhiên, ta có thể thay đổi dòng nhập, xuất của các chương trình và tiếp nối chúng với nhau!

Đơn giản nhất để tiếp nối, thay đổi các dòng này đó là < file> file. Chúng cho phép ta có thể thay đổi dòng nhập và xuất từ một tệp nào đó:

dui@cs ➜ ~$ echo hello > message.txt
dui@cs ➜ ~$ cat message.txt
hello
dui@cs ➜ ~$ cat < message.txt
hello
dui@cs ➜ ~$ cat < message.txt > message2.txt
dui@cs ➜ ~$ cat message2.txt
hello

Ở trên chúng ta sử dụng lên cat(concatenates) để in ra nội dung của các tập tin. Khi tên tập tin là một đối số, cat sẽ in nội dung của tập tin đó lên outputstream của mình. Nhưng khi trình cat được sử dụng trong trường hợp không có đối số, nó sẽ in tất cả nội dung từ inputstream của mình ra outputstream (ví dụ 3).

Ký tự | được dùng để nối các chương trình với nhau sao cho dữ liệu xuất ra từ chương trình này lại là dữ liệu nhập của chương trình khác:

dui@cs ➜ ~$ ls | cat > message.txt
dui@cs ➜ ~$ cat message.txt
message.txt
projects

Shell Scripting

Bash hay shell script là một cách tuyệt vời để tự động hóa các tác vụ lặp đi lặp lại và có thể giúp bạn tiết kiệm rất nhiều thời gian với tư cách là một lập trình viên. Bash script chạy bên trong terminal qua compiler Bash shell. Bất kỳ lệnh nào bạn có thể chạy trong terminal của mình đều có thể chạy trong tập lệnh Bash. Khi bạn có một command hoặc tập hợp các command mà bạn sẽ sử dụng thường xuyên, hãy cân nhắc việc viết một file Bash script để thực hiện nó.

Ở đầu mỗi file Bash thường có 1 dòng lệnh: #!bin bash script để thông báo cho máy tính xài trình biên soạn nào cho script của chúng ta (bash, sh) /code

Một tệp script cũng cần phải sử dụng lệnh chmod +x (tên file) để có thể chạy được.

Để có thể sử dụng bash script nhiều nơi khác trên máy tính thì ta sẽ phải thêm dòng PATH=~/bin:$PATH trong file ~/.bash_profile (trên OSX) hoặc ~/.bashrc (trên Linux). Sau bước này các script trong đường dẫn ~/bin có thể được truy cập ở bất cứ đâu

Biến

Trong một file bash script, ta có thể khai báo biến bằng cách cho tên biến bằng giá trị

hcmus="hcmue"

Ta cần chú ý là không có khoảng cách giữa tên biến, dấu bằng và giá trị. Bởi khi đó máy tính sẽ tìm 1 chương trình với cái tên biến của chúng ta, ở đây là hcmus.

Ta có thể lấy giá trị của một biến bằng cách cho thêm dấu $ phía trước biến đó

echo $hcmus
>>> hcmue

echo $(pwd)
>>> /User

Một phương thức phổ biến khác là lấy output của một CMD, ta đặt $( CMD ), máy tính sẽ chạy CMD và lấy output thay vào trong đây. Ví dụ như

for file in $(ls)          # Vòng lặp này sẽ duyệt qua tất cả các file,
#folder có trong đường dẫn nó đang thực thi

Điều kiện

Bash script sử dụng danh sách các toán tử khác với các ngôn ngữ thông dụng hiện hành.

if [ $index -lt 5 ]
then
  echo $index
else
  echo 5
fi

Ở đây -lt có nghĩa là less than.

  • Equal: -eq
  • Not equal: -ne
  • Less than or equal: -le
  • Less than: -lt
  • Greater than or equal: -ge
  • Greater than: -gt
  • Is null: -z

So sánh giữa 2 string ta sử dụng ==


Vòng lặp

Có 3 cách để chúng ta có thể biểu diễn vòng lặp trong bash script. Đó là for, while và until.

for word in sentence
do
    echo "Something Loop"
done

index=1
while [ $index -lt 5 ]
do
    echo $index index=$((index + 1))
done

While loop khi điều kiện còn đúng và until loop cho tới khi đạt được điều kiện


Input

Ta có thể đọc input bằng lệnh

read input

Thông thường, user sẽ đưa các input khi đưa ra 1 command, Bash hỗ trợ rất nhiều biến đặc biệt để tham chiếu đến các tham số.

$0                 #tên lệnh (pwd, cd)
$1 - $9            #argument truyền vào script
$@                 #tất cả argument
$?                 #trả về code của cmd ngay trước
!!                 #nguyên dòng lệnh vừa được thực thi
$_                 #argument cuối cùng của cmd cuối cùng

Output

Command thường trả về output thông qua STDOUT, error qua STDERRReturn Code để thông báo lỗi. Return code hoặc exit code là cách các script thông báo cmd đã thực thi như thế nào. Giá trị 0 thường có nghĩa mọi thứ chạy tốt, các giá trị còn lại nghĩa là đã có error.

Exit code có thể sử dụng để thực thi các command có điều kiện bằng && hoặc ||, command còn có thể tách rời trên 1 dòng bằng dấu ; . Chương trình true trả về 0 và false trả về 1. Một vài ví dụ:

false || echo "Failed, hix"
>> Failed, hix

true || echo "Yay"
#

false ; echo "This will always run"
>> This will always run

Shell globbing

Shell globbing là cách gọi chung của các kỹ thuật để expand các argument, mỗi khi các bạn cần các argument có các pattern giống nhau, Bash sẽ khiến việc này nhanh gọn lẹ hơn. Ví dụ như.

  • Wildcards: * cho 0 hoặc nhiều ký tự, ? cho duy nhất 1 ký tự.
ls
>> foo1 foo2 foo10
rm foo*             #Xoá hết tất cả các file trong đường dẫn
rm foo?            #Chỉ xoá foo1, foo2
  • Curly brace: Bất cứ khi nào bạn có một chuỗi con chung trong một chuỗi lệnh, bạn có thể sử dụng dấu ngoặc nhọn cho bash để tự động mở rộng chuỗi này.
touch file{1..10}.{jpg,png}
#Tạo ra file1.jpg, file2.jpg ... file10.png

Công cụ của Shell

Cách sử dụng lệnh

Có phải các bạn thường thắc mắc các cờ trong các lệnh như ls -l, ls -t, mkdir -p là gì đúng không. Các bạn luôn có thể google nó, bên cạnh đó Unix đã có sẵn nhiều built-in để các bạn có thể tìm kiếm các thông tin này đầy đủ.

Một trong số cách đó là gắn cờ -h hoặc --help vào command cần tìm kiếm thông tin. Cách khác để có được thông tin một cách đầy đủ hơn là sử dụng command man kèm với tên của command cần tìm kiếm thông tin.

Đôi khi, man cung cấp quá nhiều thông tin khiến ta khó hiểu các cờ/ syntax đó được sử dụng trong trường hợp nào. Ta có thể sử dụng TLDR pages là một cách tìm kiếm thông tin khác tập trung vào việc đưa ra các ví dụ để ta hiểu rõ hơn.

man vim

Tìm files

Một trong những tác vụ mà lập trình viên hay lặp đi lặp lại nhất là tìm kiếm file hoặc folder. Mọi hệ điều hành UNIX đều có sẵn package find, một shell tool tìm kiếm file rất tốt. find sẽ tìm kiếm trong tất cả cả folder con phù hợp với các pattern. Một số ví dụ:

find . -name src -type d
# Find all python files that have a folder named test in their path
find . -path '*/test/*.py' -type f
# Find all files modified in the last day
find . -mtime -1
# Find all zip files with size in range 500k to 10M
find . -size +500k -size -10M -name '*.tar.gz'

Bên cạnh việc list file, ta còn có thể thực hiện các lệnh trên kết quả tìm được như:

# Delete all files with .tmp extension
find . -name '*.tmp' -exec rm {} \;
# Find all PNG files and convert them to JPG
find . -name '*.png' -exec convert {} {}.jpg \;

Mặc dù find khá phổ biến nhưng các cấu trúc command của nó khá khó nhớ. Ta có thể viết các alias cho những câu lệnh ta hay sử dụng nhưng thông thường, ta có thể tìm tới các package thay thế. Ở đây, fd là một trong những package gọn nhẹ và thân thiện với user thay thế cho find.


Tìm codes

Bên cạnh tìm file, bash cũng hỗ trợ cho chúng ta 1 tool rất tiện dụng là grep trong các trường hợp chúng ta muốn tìm các file chứa các input mà chúng ta cần. grep là 1 tool rất tiện lợi mà chúng ta sẽ sử dụng rất nhiều trong các bài giảng tiếp theo.

#Default, grep sẽ tìm theo case sensitive
grep find_word src.txt

#Thêm cờ -i để bật case insensitive
grep -i find_word src.txt

#Thêm cờ -R để tìm kiếm trong tất cả file con
trong folder và output tên file kèm dòng trùng
#khớp với input
grep -Rl find_word /src

Đã có một số package được phát triển để thay thế cho grep như ack, ag và rg.


Tìm lệnh Shell

Cho tới hiện tại, ta đã biết cách tìm kiếm file và tìm kiếm code, nhưng làm thế nào ta có thể truy cập lịch sử command ta đã thực thi. Thường thì ta có thể nhấn nút upward lên dần dần để tìm ra lệnh ta cần, nhưng bash cũng có hỗ trợ 1 tool khác cho việc này là history. Kết hợp cùng grep find, ta có thể tìm ra các command ta cần.

history | grep find

Có một tính năng rất hay mà đã được hầu như mọi lập trình viên sử dụng để tự động hoàn tất command mình đang gõ đó là autosuggestions, lần đầu tiên được giới thiệu bởi fish shell và bạn có thể bật nó lên nếu đang sử dụng zsh.

Command Line

Trong bài học này, chúng ta sẽ tìm hiểu về một số cách giúp bạn có thể cải thiện tiến trình làm việc khi sử dụng shell. Chúng ta sẽ xem một số cách tiến trình thực hiện cùng lúc, cách dừng hoặc tạm dừng một tiến trình cụ thể và cách thực hiện một quá trình chạy trong nền.

Chúng ta cũng sẽ tìm hiểu về các cách khác nhau để cải thiện shell, bằng cách xác định aliases (bí danh) và cấu hình chúng bằng dotfiles. Cả 2 điều này sẽ giúp bạn tiết kiệm thời gian thao tác với shell.

Job control (điều kiển tiến trình làm việc)

Trong một số trường hợp cần thiết, bạn có thể sẽ cần phải ngắt một tiến trình khi nó đang được thực thi, chẳng hạn như một tiến trình mất quá nhiều thời gian để hoàn thành (chẳng hạn như tìm kiếm một thư mục nằm trong một cấu trúc thư mục rất lớn). Hầu hết trường hợp, bạn có thể dùng tổ hợp phím nóng Ctrl+C để dừng tiến trình. Nhưng tiến trình có thực sự dừng lại trong một số trường hợp đặc biệt hay không?

Killing a process (“giết” một tiến trình)

Trình shell sử dụng cơ chế giao tiếp UNIX được gọi là tín hiệu để truyền thông tin cho tiến trình. Khi 1 tiến trình nhận được tín hiệu, nó sẽ dừng việc thực thi tiến trình, xử lý tín hiệu đó và có khả năng thay đổi luồng thực thi dựa trên thông tin mà tín hiệu đã truyền đạt. Vì lý do này, các tín hiệu sẽ làm software interrupts (tạm dịch là “gián đoạn phần mềm).

Hãy thử 1 ví dụ sau đây:

dangcpr@DANG:~$ sleep 1000
^C
dangcpr@DANG:~$

Trong ví dụ trên, sau khi gõ câu lệnh “sleep 1000” và nhấn Enter thì tiến trình “ngủ” sẽ thực hiện. Để dừng, ta nhấn tổ hợp phím Ctri+C, khi đó sẽ có ký tự “^C” xuất hiện và bạn đã làm gián đoạn được tiến trình. Khi bạn nhấn tổ hợp phím Ctrl+C, điều này sẽ nhắc trình shell gửi tín hiệu SIGINT (signal interrupt???)

Ngoài ra, tổ hợp phím Ctrl+Z sẽ nhắc trình shell gửi tín hiệu SIGQUIT tới tiến trình.

Ngoài còn có 1 số loại POSIX signals khác bạn có thể tham khảo: POSIX signals (tsinghua.edu.cn)

Pausing and backgrounding processes (dừng tiến trình và chạy tiến trình dưới nền)

Tín hiệu còn có thể làm những việc khác ngoài killing a process. Ví dụ: Trong terminal trên máy tính, gõ Ctrl + Z để nhắc shell gửi tín hiệu SIGTSTP, viết tắt của Signal Terminal Stop.

Bạn có thể tiếp việc thực hiện tiến trình bằng cách sử dụng fg (foreground - chạy tường minh) hoặc bg (chạy dưới nền).

Lệnh jobs liệt kê các tiến trình chưa hoàn được liên kết với phiên hoạt động hiện tại trên terminal. Sau đó có thể sẽ hiện ra 1 số tiến trình được đánh số [1], [2], … Những con số đó có tác dụng giúp ta điều khiển những tiến trình đó sau này.

Để chạy dưới nền 1 chương trình. Bạn có thể nhấn tổ hợp phím Ctrl+Z trước rồi sau đó chạy lệnh “bg %<i>” với i là số thứ tự của tiến trình.

dangcpr@DANG:~$ sleep 1000
^C
[1]+  Stopped                 sleep 1000
dangcpr@DANG:~$ bg %1
[1]+ sleep 1000 &

Lưu ý các tiến trình chạy dưới nền vẫn là các tiến trình con của terminal và nó sẽ chết” nếu bạn đóng terminal. Để ngăn điều đó xảy ra, bạn có thể chạy chương trình với nohup (để có thể bỏ qua SIGHUP).

Dưới đây là một số ví dụ cho một số khái niệm này.

dangcpr@DANG:~$ sleep 1000
^C
[1]+  Stopped                 sleep 1000
dangcpr@DANG:~$ nohup sleep 2000
[2] 75
dangcpr@DANG:~$ nohup: ignoring input and appending output to 'nohup.out'
dangcpr@DANG:~$ jobs
[1]+  Stopped                 sleep 1000
[2]-  Running                 nohup sleep 2000 &
dangcpr@DANG:~$ bg %1
[1]+ sleep 1000 &
dangcpr@DANG:~$ jobs
[1]-  Running                 sleep 1000 &
[2]+  Running                 nohup sleep 2000 &
dangcpr@DANG:~$ kill -STOP %1
[1]+  Stopped                 sleep 1000
dangcpr@DANG:~$  jobs
[1]+  Stopped                 sleep 1000
[2]-  Running                 nohup sleep 2000 &
dangcpr@DANG:~$ kill -HUP %2
dangcpr@DANG:~$ kill -HUP %1
[1]+  Hangup                  sleep 1000
dangcpr@DANG:~$ jobs #Tín hiệu SIGHUP bị bỏ qua
[2]+  Running                 nohup sleep 2000 &
dangcpr@DANG:~$ kill %2
dangcpr@DANG:~$ jobs
[2]+  Terminated              nohup sleep 2000
dangcpr@DANG:~$ jobs

Terminal Multiplexers (bộ ghép kênh đầu cuối)

Khi sử dụng Command line, bạn chắc hẳn sẽ có nhu cầu thực hiện nhiều công việc cùng lúc.Terminal Multiplexers sẽ giúp bạn điều này. Nó có tác dụng chia terminal thành nhiều cửa sổ làm việc khác nhau, hay sử dụng nhiều phiên làm việc đồng thời.

Cài đặt

Trong terminal, các bạn chạy lệnh

sudo apt install tmux

Để kiểm tra chắc chắn tmux đã được cài chưa, ta thực hiện lệnh

tmux -v

Sử dụng

Sessions

  • Để chạy Tmux, bạn mở session mới bằng lệnh sau: tmux new
  • Hoặc có thể chạy lệnh này để mở session và đặt tên NAME: tmux new -s NAME
  • Để liệt kê các session hiện tại: tmux ls
  • Trong tmux, nhấn tổ hợp Ctrl+B (<c-b>) sau đó nhấn phím d để tách session hiện tại.

Windows

Copy of Windows

Thao tác Tổ hợp phím
Cửa sổ mới <c-b>+c
Cửa sổ tiếp theo <c-b>+n
Liệt kê toàn bộ cửa sổ <c-b>+w
Đặt tên cửa sổ <c-b>+,

Panel

Copy of Panel

Thao tác Tổ hợp phím
Chia panel theo chiều ngang <c-b>+“
Tắt pane <c-b>+x
Hiển thị số panel <c-b>+q
Chuyển qua lại giữa các panes <c-b>+arrow key
Chia panel theo chiều dọc <c-b>+%

Bạn có thể học thêm cách sử dụng tmux qua: https://www.hamvocke.com/blog/a-quick-and-easy-guide-to-tmux/

Aliases (bí danh)

Bạn thấy mệt mỏi và phiền phức phải gõ những đoạn lệnh dài dòng, aliases sẽ giúp bạn rút ngắn thời gian viết đoạn lệnh bằng cách quy ước 1 đoạn lệnh trong shell bằng 1 aliases name.

Syntax

alias alias_name="command_to_alias arg1 arg2"

Chú ý: không có dấu khoảng trắng xong quanh “=”, vì alias là lệnh shell nhận 1 đối số duy nhất.

Ví dụ

# Save a lot of typing for common commands
alias gs="git status"
alias gc="git commit"
alias v="vim"
# Save you from mistyping
alias sl=ls
# Overwrite existing commands for better defaults
alias mv="mv -i"           # -i prompts before overwrite
alias mkdir="mkdir -p"     # -p make parent dirs as needed
alias df="df -h"           # -h prints human readable format
# Alias can be composed
alias la="ls -A"
alias lla="la -l"
# To ignore an alias run it prepended with \
\ls
# Or disable an alias altogether with unalias
unalias la
# To get an alias definition just call it with alias
alias ll
# Will print ll='ls -lh'

Dotfiles

Nhiều chương trình được cấu hình bằng việc sử dụng tệp văn bản thuần tuý, được biết đến là dotfiles (bởi vì tên file được bắt đầu bằng dấu “.”, vd: ~/.vimrc, do đó chúng bị ẩn trong danh sách thư mục ls theo mặc định). Dotfiles là các tệp cấu hình cho các chương trình khác nhau và chúng giúp các chương trình đó quản lý chức năng của chúng.

Ví dụ

  • Nếu bạn sử dụng vỏ Bash, bạn có thể có tệp .bash_profile và .bashrc, cả hai đều chứa các tập lệnh tải mỗi khi bạn bắt đầu một terminal và cấu hình shell
  • Nếu bạn sử dụng vỏ Zsh, bạn sẽ có (hoặc sẽ tạo) tệp .zshrc cấu hình và tùy chỉnh shell.
  • Nếu bạn sử dụng trình soạn thảo mã dòng lệnh Vim, bạn sẽ lưu trữ cấu hình của nó trong tệp .vimrc.
  • Sau khi thiết lập và cấu hình Git trên máy cục bộ của bạn, bạn sẽ có một tệp .gitconfig, trong đó sẽ chứa tất cả thông tin và cài đặt của bạn.
  • Nhiều chương trình, thay vì lưu trữ cấu hình của chúng trong thư mục chính của bạn thì chúng lưu trữ chúng trong thư mục .config (thư mục) ẩn trên hệ thống của bạn.

Benefits

  • Cài đặt dễ dàng: nếu bạn đăng nhập vào một máy mới, việc áp dụng các tùy chỉnh của bạn sẽ chỉ mất một phút.
  • Tính di động: các công cụ của bạn sẽ hoạt động theo cùng một cách ở khắp mọi nơi.
  • Đồng bộ hóa: bạn có thể cập nhật dotfiles của mình ở bất cứ đâu và giữ cho tất cả chúng đồng bộ.
  • Thay đổi theo dõi: bạn có thể sẽ duy trì dotfiles của mình cho toàn bộ sự nghiệp lập trình của mình và lịch sử phiên bản là tốt đẹp để có cho các dự án lâu dài.

Portability (tính di động)

Một khó khăn chung khi sử dụng dotfiles là các cấu hình có thể không hoạt động khi làm việc với một số máy, ví dụ: nếu chúng có hệ điều hành hoặc shell khác nhau. Đôi khi bạn cũng muốn một số cấu hình chỉ được áp dụng trong một máy nhất định.

Có một số trick để làm cho điều này dễ dàng hơn. Nếu tệp cấu hình hỗ trợ máy đó, hãy sử dụng câu lệnh if-statement để áp dụng các tùy chỉnh cụ thể của máy. Ví dụ, shell của bạn có thể có một cái gì đó giống như:

if [[ "$(uname)" == "Linux" ]]; then {do_something}; fi

# Check before using shell-specific features
if [[ "$SHELL" == "zsh" ]]; then {do_something}; fi

# You can also make it machine-specific
if [[ "$(hostname)" == "myServer" ]]; then {do_something}; fi

Nếu tệp cấu hình hỗ trợ, hãy sử dụng một include Ví dụ: một ~/.gitconfig có thể có một thiết lập:

[include]
    path = ~/.gitconfig_local

Và sau đó trên mỗi máy, ~/.gitconfig_local có thể chứa các cài đặt dành riêng cho máy. Bạn thậm chí có thể theo dõi chúng trong một kho lưu trữ riêng cho các cài đặt dành riêng cho máy.

Ý tưởng này cũng hữu ích nếu bạn muốn các chương trình khác nhau chia sẻ một số cấu hình. Ví dụ: nếu bạn muốn cả bash và zsh chia sẻ cùng aliases, bạn có thể viết chúng dưới .aliases và có khối sau trong cả hai:

# Test if ~/.aliases exists and source it
if [ -f ~/.aliases ]; then
        source ~/.aliases
fi

Shells and Frameworks

Trong quá trình công cụ shell và script, chúng tôi đã sử dụng bash vì nó là shhell phổ biến nhất và hầu hết các hệ thống đều có nó làm tùy chọn mặc định. Tuy nhiên, đó không phải là lựa chọn duy nhất.

Ví dụ, zsh là một shell cung cấp nhiều tính năng tiện lợi như:

  • Thông minh hơn **
  • Sửa lỗi chính tả
  • Hoàn thành/lựa chọn tab tốt hơn
  • Mở rộng đường dẫn (cd /u/lo/b sẽ mở rộng dưới dạng /usr/local/bin)

Frameworks cũng có thể cải thiện shell của bạn. Một số frameworks phổ biến là prezto hoặc oh-my-zsh và các frameworks nhỏ hơn tập trung vào các tính năng cụ thể như zsh-syntax-highlighting hoặc zsh-history-substring-search. Một số tính năng bao gồm:

  • Lời nhắc.
  • Tô sáng cú pháp lệnh.
  • Tìm kiếm lịch sử.
  • Tự động hoàn thành thông minh hơn.
  • Themes.

Terminal Emulators (Trình giả lập Terminal)

Cùng với việc tùy chỉnh shell của bạn, bạn nên dành thời gian tìm ra lựa chọn trình giả lập Terminal và cài đặt của nó. Có rất nhiều trình giả lập Terminal (vd: Window Terminal, …)

Một số thông tin bạn có thể tuỳ chỉnh:

  • Lựa chọn phông
  • Phối màu
  • Lối tắt bàn phím
  • Hỗ trợ Tab/Ngăn
  • Cấu hình Scrollback
  • Hiệu suất (một số thiết bị đầu cuối mới hơn như Alacritty hoặc kitty cung cấp tăng tốc GPU).

Quiz và Exercise

Quiz

Shell

  1. Đường dẫn ../../bin/look là đường dẫn tương đối hay tuyệt đối?
    • Tương đối
    • Tuyệt đối

    Giải thích: bởi vì không bắt đầu bằng / .

  1. Thư mục root trong hệ thống file của Linux và MacOS được biểu diễn bởi:
    • /root
    • /home
    • C:
    • /

  1. Ký tự ~ được dùng để biểu diễn:
    • Không có ý nghĩa gì
    • Thư mục cha mẹ
    • Thư mục hiện tại
    • Thư mục của người dùng

  1. Đường dẫn /bin/look là đường dẫn tương đối hay tuyệt đối?
    • Tương đối
    • Tuyệt đối

    Giải thích: bởi vì bắt đầu bằng / .

  1. Ví dụ chúng ta có đường dẫn thư mục trong thử mục CS101 như thế này:

Chúng ta đang ở thư mục basic của thử mục command line làm thể nào di chuyển để thư mục đến thư mục quiz của thư mục shell trong một câu lệnh duy nhất:

  • cd ../shell/quiz
  • cd ../quiz
  • cd ../../shell/quiz
  • cd /shell/quiz

Giải thích: chúng ta đi ra ngoài hai thư mục là sẽ ở vị trí CS101 .

  1. Các thư mục ẩn thường được bắt đầu với ký tự:
    • Sao (*)
    • Cộng (+)
    • Chấm (.)
    • Trừ (-)
  1. Ký tự đầu tiên của mỗi dòng khi bạn thực hiện câu lệnh ls -l thể hiện điều gì?
    • Người dùng
    • Quyền truy cập
    • Có phải là một file, thư mục, hay một liên kết tượng trưng.
    • Kích cỡ

Shell Script

  1. Syntax nào được sử dụng cho phép "lớn hơn" trong shell scripting?
    1. >
    1. -lt
    1. -gte
    1. -gt
  1. Đâu là ba loại vòng lặp trong shell script?
    1. for, while và until
    1. for, while và do-while
    1. for, do và until
    1. for, continue và done
  1. Những thành phần nào tạo nên block điều kiện trong shell script?
    1. if, then, otherwise
    1. if, then, else và fi
    1. if, else, fi
    1. if then, else, endif
  1. Toán tử so sánh "-eq" được sử dụng cho
    1. Int
    1. Char
    1. String
    1. Bất kì loại nào
  1. Mọi dòng trong shell script kết thúc với
    1. :
    1. ;
    1. >
    1. Không có quy định nào

Command Line

  1. Tổ hợp phím nào sau đây cho phép kết thúc tiến trình hiện tại trong Shell?
    1. Ctrl + C
    1. Ctrl + A
    1. Ctrl + E
    1. Ctrl + L
  1. Để truyền tín hiệu SIGTSTP, ta sử dụng tổ hợp phím nào?
    1. Ctrl + C
    1. Ctrl + Z
    1. Ctrl + L
    1. Ctrl + A
  1. Biến shell nào chứa đối số dòng lệnh đầu tiên cho một tập lệnh shell?
    1. $0
    1. $1
    1. #0
    1. None of the above
  1. Lệnh tạm dừng thực hiện lệnh trong 3 giây là?
    1. sleep 3
    1. sleep 30
    1. sleep 300
    1. sleep 3000
  1. Câu lệnh để thực hiện tạm dừng thực hiện lệnh trong 2 giây dưới nền là?
    1. sleep 2
    1. sleep 2 &
    1. sleep 2000
    1. sleep 2000 &

Exercise

Shell Script

  1. Tham khảo man ls và viết ra một lệnh ls in ra các file ở thư mục hiện tại theo các điều kiện sau đây:
    • Bao gồm tất cả các file và cả file ẩn
    • Theo trình tự gần đây
    • Kích cỡ của file ở dạng con người có thể đọc được (ví dụ 15K thay vì 15651)
                                                                    
    drwxr-xr-x 10  dui  dui 4.0K Oct 11 15:05 projects
    drwxr-xr-x 19  dui  dui 4.0K Oct 30 12:13 .
    drwxr-xr-x  3 root root 4.0K Apr 26  2021 ..
                                                                    

    Đáp án: ls -lath , l: long, a: all, t: time, h: human.

  1. Viết hàm bash marco và polo thực hiện các thao tác sau. Bất cứ khi nào bạn thực thi marco, thư mục làm việc hiện tại sẽ được lưu theo một cách nào đó, sau đó khi bạn thực thi polo, bất kể bạn đang ở thư mục nào, polo sẽ cd bạn trở lại thư mục mà bạn đã thực thi marco.

    Solution

    `marco.sh`:
    
    ```bash
    #!/usr/bin/env bash
    marco() {
        export MARCO=$(pwd)
    }
    polo() {
        cd "$MARCO"
    }
    ```
        Begin `marco.sh` with the shebang line for bash because `marco` and `polo` are defined to be bash functions. `export` command ensures the value `MARCO` is set to be an environment variable and not a local variable confined within the scopes of the `marco()` function only. This way `polo()` can read the value of `MARCO` defined outside its scope. `source` command is used to execute the file since `./marco.sh` will not be executed. (Permission Denied)
                                    
  1. Viết một lệnh hoặc tập lệnh để tìm một cách đệ quy tệp được sửa đổi gần đây nhất trong một thư mục. Nói một cách khác, bạn có thể liệt kê tất cả các tệp theo thứ tự truy cập gần đây không?

    Solution

    find . -type f | ls -t | head -n 1

Command Line

  1. Job Control

    Hãy viết lệnh để đánh dấu thời gian 2 lần, lần 1 là thời gian hiện tại, sau 60 giây sẽ tự động đánh dấu thời gian hiện tại 1 lần nữa.

    In ra thời gian tại thời điểm 2 lần đánh dấu.

    Gợi ý: sử dụng cặp dấu ngoặc đơn để nhóm câu lệnh và phân tách câu lệnh bằng dấu ;

    Solution:

    $ (date; sleep 60; date) > date.out & #Main solution
    $ jobs
    [1]  + Running     ( date; sleep 60; date ) > date.out
    $ cat date.out
    Wed Mar 12 16:38:42 PDT 2014 #Example
    $ cat date.out
    Wed Mar 12 16:38:42 PDT 2014 #Example
    Wed Mar 12 16:39:42 PDT 2014 #Example
  1. Aliases

    Sử dụng aliases để làm cho câu lệnh sau trở nên ngắn hơn:

    
    history | awk '{$1="";print substr($0,2)}' | sort | uniq -c | sort -n | tail -n 10
    

    Solution:

    
    alias topcmd="history 1 | awk '{$1="";print substr($0,2)}' | sort | uniq -c | sort -n | tail -n 10"
    
  1. Dotfiles
    1. Tạo ra một thư mục cho những dotfiles của bạn và thiết lập kiểm soát phiên bản.

      Solution:

      mkdir ~/dotfiles
      git init ~/dotfiles
    1. Cài đặt cấu hình cho ít nhất một chương trình.

      Solution: Giả sử như thay đổi lời nhắc Shell, thay vì dangcpr@DANG:~$ thì có thể là touch me:

      dangcpr@DANG:~$ echo $PS1
      [\u@\h \W]\$ #Output Example
      dangcpr@DANG:~$ PS1="touch me : "
      touch me : #Output Example
      touch me : PS1="[\d \t \u@\h:\w ] $ "
      Sat Jun 02 server $ #Example Output