8 hàm shell thiết yếu để cải thiện dòng lệnh Linux của bạn

Tác giả Starlink, T.M.Một 07, 2025, 09:00:06 CHIỀU

« Chủ đề trước - Chủ đề tiếp »

0 Thành viên và 1 Khách đang xem chủ đề.

Có chức năng dành cho mọi người.

Khi bạn đã hiểu rõ về Linux và thành thạo các lệnh khác nhau, chiến thắng lớn tiếp theo của bạn sẽ đến dưới dạng hàm shell. Mã được đóng gói trong một hàm có thể được sử dụng lại bởi các tập lệnh shell của bạn, nhưng bạn cũng có thể sử dụng nó trên dòng lệnh, giống như bạn sử dụng bất kỳ chương trình, lệnh tích hợp hoặc bí danh nào.


Các hàm ngắn thực hiện các tác vụ phổ biến có thể giúp bạn tiết kiệm rất nhiều thời gian và việc ghép chúng lại với nhau cũng rất thú vị. Dưới đây là một số hàm shell mà tôi thấy hữu ích nhất.

1. mkd: tạo một thư mục và nhập vào đó

Tôi sẽ bắt đầu bằng một ví dụ về một hàm shell lý tưởng: nó hữu ích và dễ hiểu, nhưng cũng giúp minh họa một số khái niệm chính. Về cơ bản, một hàm shell sẽ đóng gói một hoặc hai lệnh mà bạn thường muốn chạy cùng nhau, mang lại sự tiện lợi.

Lần cuối cùng bạn chạy lệnh mkdir để tạo một thư mục mà không chạy lệnh cd ngay để vào thư mục đó là khi nào? Tôi đoán là chưa bao giờ, vậy tại sao không bổ sung mkdir bằng một phiên bản có chức năng tương tự? Hàm này như sau:

Mã nguồn [Chọn]
mkd() {
    mkdir -p -- "$1" && cd -P -- "$1"
}
   
Bạn có thể lưu tất cả các hàm shell tùy chỉnh của mình trong một tệp và mã nguồn khi khởi động. Tôi lưu trữ các hàm này trong một thư mục ẩn tại nhà, vì vậy tôi chạy lệnh này từ tệp ~/.zshrc để tải chúng vào shell:. ~/.config/shell_funcs.sh.

&& ở đây là một mẹo terminal thông minh, cho phép chạy lệnh thứ hai sau lệnh đầu tiên, nhưng chỉ khi lệnh đầu tiên thành công. Trong trường hợp này, việc chuyển sang thư mục khác nếu nó chưa được tạo, chẳng hạn như do vấn đề về quyền, sẽ không có ích gì.

Tùy chọn mkdir -p cho phép bạn tạo nhiều hơn một cấp thư mục: ví dụ: mkd docs/letters/urgent. Nó có một tác dụng phụ khá hay: nếu bạn chạy hàm này và truyền vào một thư mục đã tồn tại, bạn sẽ không thấy lỗi nào, nhưng cuối cùng bạn vẫn sẽ thay đổi thư mục.

Điều cuối cùng cần lưu ý là các đối số "--". Đây là một đối số đặc biệt, biểu thị sự kết thúc của các tùy chọn và sự bắt đầu của các đối số khác: trong trường hợp này là tên thư mục. Đây là một ví dụ về lập trình phòng thủ để tránh sự cố nếu bạn cố gắng tạo một thư mục bắt đầu bằng ký tự "-". Điều này có vẻ khó xảy ra, nhưng phòng bệnh vẫn hơn chữa bệnh.


2. prompt: đặt lời nhắc của bạn thành nhiều thứ khác nhau

Một trong những điều đầu tiên mà mọi người đều học về shell Linux là cách tùy chỉnh dấu nhắc. Khi bắt đầu mỗi dòng lệnh, shell của bạn sẽ in ra văn bản để báo cho bạn biết rằng nó đã sẵn sàng để nhập liệu:


Dấu nhắc trên hiển thị thư mục hiện tại trong ngoặc đơn, theo sau là dấu đô la, nhưng dấu nhắc có thể tối giản hoặc cung cấp nhiều thông tin tùy ý. Giá trị của dấu nhắc được điều khiển bởi một biến môi trường tên là PS1. Bạn có thể đặt dấu nhắc này thành một chuỗi cố định, chẳng hạn như "$", chỉ cần nhớ thoát dấu $, dấu này có ý nghĩa đặc biệt đối với shell:

Mã nguồn [Chọn]
PS1="\$ "   
Hàm sau đây đặt lời nhắc thành một thứ gì đó động hơn một chút. Phiên bản này kiểm tra xem bạn đang chạy bash hay zsh, do đó nó có thể được sử dụng bởi cả hai shell. Chúng có đôi chút khác biệt về khả năng: mỗi shell chỉ cố gắng hiển thị ba phần cuối cùng của đường dẫn hiện tại, nhưng bash sẽ thêm logic riêng của nó vào.

Mã nguồn [Chọn]
        # Default prompt
prompt() {
NEWLINE=$'\n'

    if [ ! "${BASH_VERSINFO}" == "" ]; then
        PROMPT_DIRTRIM=3
        PS1="${NEWLINE}[\w] \$ "
    fi

    if [ ! "${ZSH_VERSION}" == "" ]; then
        PS1="${NEWLINE}[%3~] \$ "
    fi
}
   
Dấu nhắc này cũng bao gồm một ký tự xuống dòng, dễ dàng thêm hơn bằng cách định nghĩa một biến trước. Dấu xuống dòng này tạo ra một dòng trống giữa đầu ra của lệnh trước và dấu nhắc tiếp theo, giúp dễ dàng nhóm các lệnh và đầu ra của chúng trong nháy mắt.

Mặc dù bash và zsh xử lý dấu nhắc lệnh theo cách gần như giống nhau, các shell khác lại không nhất quán như vậy. Một số shell hiện đại hơn, chẳng hạn như fish, áp dụng cách tiếp cận chức năng cho dấu nhắc lệnh, mang lại sự linh hoạt hơn nhiều. Nếu bạn không sử dụng bash hoặc zsh, hãy kiểm tra hướng dẫn sử dụng shell để tìm hiểu cách cấu hình dấu nhắc lệnh.

Nhưng tại sao lại phải có một hàm để thực hiện việc này, thay vì chỉ cần thiết lập một lần trong tệp ~/.bashrc hoặc ~/.zshrc? Tôi thấy việc có nhiều lời nhắc khác nhau để tôi có thể dễ dàng chuyển đổi rất hữu ích:


Tính năng này thực sự hữu ích khi chụp ảnh màn hình terminal của tôi khi tôi muốn ẩn đường dẫn hiện tại. Tuy nhiên, bạn có thể sử dụng nó trong các trường hợp khác: ví dụ như để phân biệt sự khác biệt giữa hai phiên terminal, hoặc để tiết kiệm không gian màn hình trong các cửa sổ nhỏ hơn.

3. trim: xóa khoảng trắng đầu và cuối

Hàm này xóa khoảng trắng ở đầu và cuối mỗi dòng:

Mã nguồn [Chọn]
        trim() {
    sed 's/^[ \t]*//;s/[ \t]*$//'
}
   
Dòng lệnh Linux chủ yếu là truyền dữ liệu từ lệnh này sang lệnh khác, thông qua đường ống, tệp, v.v. Nhưng điều đó đòi hỏi dữ liệu sạch, có thể dự đoán được, và các lệnh không phải lúc nào cũng tạo ra dữ liệu đó tốt. Lấy ví dụ wc:


Đầu ra này sử dụng căn chỉnh để tạo ra một danh sách đẹp mắt, dễ đọc, nhưng lại bất tiện khi sử dụng trong pipeline vì các lệnh khác sẽ phải tính đến khoảng trắng đầu dòng đó. Thay vào đó, việc chuyển đầu ra sang trim sẽ loại bỏ khoảng trắng thừa, giúp quá trình xử lý tiếp theo trở nên đơn giản hơn:


Hàm trim này sử dụng sed, tiện ích biên tập luồng. Nó chỉ định hai lệnh thay thế, được phân tách bằng dấu chấm phẩy. Lệnh đầu tiên tìm kiếm một chuỗi ký tự khoảng trắng/tab ở đầu mỗi dòng và thay thế bằng không, tức là xóa nó. Lệnh thay thế thứ hai cũng làm tương tự, nhưng ở cuối dòng.

Lưu ý rằng sed hoạt động trên đầu vào chuẩn nếu không có tên tệp nào được truyền vào nó và việc gọi nó thông qua một hàm như thế này cũng thực hiện điều tương tự, truyền đầu vào chuẩn cho lệnh.

4. rgrep: viết tắt của grep đệ quy

Đôi khi, một hàm không hề đơn giản hơn một lệnh duy nhất, được thiết lập sẵn các tùy chọn cụ thể. Mặc dù lệnh rgrep có thể tồn tại trên hệ thống của bạn và hoạt động gần giống như hàm này, nhưng nó lại không có trên macOS, vì vậy tôi sử dụng giải pháp thay thế này:

Mã nguồn [Chọn]
        rgrep() {
grep -Id recurse "$@"
}
   

Tùy chọn -I ngăn grep tìm kiếm trong các tệp nhị phân, thường chỉ tạo ra lỗi. Cờ -d chỉ định cách grep xử lý các thư mục; trong trường hợp này, "recurse" khiến chúng được đọc đệ quy.

Lưu ý việc sử dụng "$@" để truyền bất kỳ đối số bổ sung nào cho grep. Điều này rất quan trọng vì nó có nghĩa là bạn có thể chạy "rgrep" chính xác như khi chạy "grep", chỉ khác là nó sẽ có hành vi tùy chỉnh của bạn. Ví dụ: rgrep -i todo sẽ được mở rộng thành grep -Id recurse -i todo.

Bạn có thể xác nhận cách thức hoạt động của lệnh này bằng cách thêm "set -o xtrace" vào đầu hàm để in ra các lệnh khi chúng chạy. Đây là một trong những cách tốt nhất để tinh chỉnh các tập lệnh shell của bạn vì nó giúp bạn gỡ lỗi chính xác những gì đang diễn ra bên trong.

5. ucase/lcase: chuyển đổi giữa các trường hợp

Hầu hết các ngôn ngữ lập trình đều có chức năng viết hoa/viết thường; ngay cả các ứng dụng như LibreOffice cũng tích hợp sẵn tính năng này vì nó thường rất hữu ích. Bạn có thể dễ dàng có chức năng tương tự trên dòng lệnh Linux, thậm chí còn mạnh mẽ hơn, vì nó có thể hoạt động trên mọi dòng của tệp bạn gửi:

Mã nguồn [Chọn]
        ucase() {
    tr '[:lower:]' '[:upper:]'
}

lcase() {
    tr '[:upper:]' '[:lower:]'
}
   
Cặp hàm bổ sung này sử dụng lệnh tr, thực hiện các phép biến đổi đơn giản trên luồng văn bản.

tr sử dụng hai đối số: danh sách các ký tự cần tìm kiếm và danh sách các ký tự cần thay thế. Mỗi ký tự thay thế sẽ tác động lên ký tự tương ứng trong chuỗi tìm kiếm, vì vậy "tr 'AB' 'ab'" sẽ chuyển đổi "A" thành "a" và "B" thành "b". Các lớp ký tự đặc biệt "[:upper:]" và "[:lower:]" sẽ mở rộng thành tập hợp đầy đủ các ký tự viết hoa và viết thường trong ngôn ngữ hiện tại.

6. today: lấy ngày hiện tại theo định dạng ISO ngắn

Mã nguồn [Chọn]
        today() {
    date '+%Y-%m-%d'
}

Tạm thời hãy quên sự khác biệt giữa ngày tháng ở Mỹ và phần còn lại của thế giới đi: định dạng ngày tháng này không thực sự được ai sử dụng, nhưng nó chắc chắn là định dạng tôi yêu thích nhất. Lý do rất đơn giản: ngày tháng ở dạng này sẽ được sắp xếp tự nhiên, và đó là một điểm cộng lớn đối với tôi. Tôi sử dụng định dạng này ở rất nhiều nơi: tên tệp, tiêu đề trong tài liệu, cơ sở dữ liệu, v.v.

Đây có thể là một phím tắt đơn giản khác, nhưng lại rất tiện lợi. Định dạng ngày tháng chính xác thật khó nhớ: tiền tố "+", ký hiệu "%", và việc viết đúng chữ hoa chữ thường của từng phần. Tôi thích quên hết những thứ đó đi và chỉ cần gõ " ngày hôm nay" thôi: thỏa mãn vô cùng!

7. filesize: hiển thị các tập tin trong thư mục hiện tại, được sắp xếp theo kích thước

Một tác vụ bảo trì thường gặp mà tôi thấy mình đang thực hiện là dọn dẹp các tệp lớn. Mặc dù ncdu là một cách tuyệt vời để định vị các tệp lớn, nhưng đôi khi nó lại quá mức cần thiết. Điều tôi thường muốn là xem chính xác những tệp nào trong thư mục hiện tại của mình là lớn nhất.

Mã nguồn [Chọn]
        filesize() {
du -sk * | sort -n
}
   
du là công cụ sử dụng đĩa ; nó báo cáo dung lượng mà mỗi tệp chiếm dụng, bao gồm cả thư mục, theo cách đệ quy. Vì vậy, hàm này sẽ cho bạn biết tổng dung lượng của bất kỳ thư mục nào hiện có, giúp bạn dễ dàng phân tích sâu hơn hệ thống tệp và loại bỏ bất kỳ tệp không mong muốn nào.

8. paths: in biến PATH một cách đẹp mắt

Mã nguồn [Chọn]
        paths() {
echo $PATH | tr ':' '\n'
}
   
Một nhiệm vụ bảo trì khác mà tôi thích thực hiện thỉnh thoảng là dọn dẹp PATH. macOS có một thói quen khó chịu là thêm các thư mục bổ sung vào biến môi trường, bao gồm cả những thư mục không thực sự tồn tại! Nhưng vì biến này sử dụng dấu hai chấm để phân tách từng đường dẫn, nên việc đọc nó có thể khá khó khăn:


Chức năng này làm cho đầu ra dễ hiểu hơn nhiều:


Đây là một cách sử dụng tuyệt vời khác cho chương trình tr tuyệt vời đó và bạn có thể mở rộng hơn nữa: bằng cách kiểm tra xem mỗi thư mục trong PATH của bạn có tồn tại hay không, ví dụ:

Mã nguồn [Chọn]
        check_paths() {
    paths | while read path
    do
        if [ ! -d "$path" ]; then
            echo "bad PATH: dir does not exist: $path" >&2
        fi
    done
}