Các công cụ của shell và viết ngôn ngữ kịch bản

Ở trong buổi học này, chúng tôi sẽ trình bày vài điều cơ bản trong việc sử dụng bash như một ngôn ngữ kịch bản và giới thiệu một số công cụ của shell được sử dụng thường xuyên trên môi trường dòng lệnh trong các công việc hằng ngày của bạn.

Shell Scripting - Ngôn ngữ kịch bản Shell

Chúng ta đã làm quen với việc thực hiện các lệnh bằng shell (vỏ) và pipe (liên kết) chúng lại với nhau thành một quy trình. Tuy nhiên, trong một vài trường hợp, bạn sẽ cần phải thực thi hàng loạt câu lệnh và sử dụng các cấu trúc điều khiển như câu điền kiện hoặc vòng lặp.

Ngôn ngữ shell là bước tiếp theo để có thể sử dụng những thứ phức tạp hơn. Hầu hết các shell đều có một ngôn ngữ kịch bản riêng với những cú pháp riêng biệt để tương tác với biến, cấu trúc điều khiển.Điều đặc biệt khiến ngôn ngữ shell khác biệt khi so sánh chúng với các ngôn ngữ kịch bản khác chính là ngôn ngữ shell đã được tối ưu cho việc thực thi các tác vụ liên quan tới shell (ở môi trường dòng lệnh). Do đó, việc tạo quy trình cho lệnh (pipe), lưu kết quả vào file, đọc dữ liệu từ thiết bị nhập chuẩn là những thứ nguyên thuỷ trong khi viết shell, điều này khiến shell script dễ dàng để sử dụng hơn là những ngôn ngũ kịch bản tổng quát. Ở trong phần này chúng ta sẽ sử dụng bash để lập trình shell vì bash rất phổ biến.

Ghi chú (người dịch):

Để gán giá trị cho biến bằng bash, sử dụng cú pháp foo=bar và truy cập giá trị của biến bằng cú pháp $foo. Lưu ý foo = bar sẽ không chạy bởi vì câu lệnh sẽ được biên dịch thành việc gọi chương trình foo với đối số là =bar. Tóm lại, dấu khoảng cách đóng vai trò dấu phân cách các đối số trong các ngôn ngữ shell. Việc này có thể sẽ hơi khó hiểu và gây nhầm lẫn ở giai đoạn đầu, nên hãy luôn kiểm tra việc này.

Chuỗi có thể được khai báo bằng dấu '" trong bash, nhưng chúng không bằng nhau. Chuỗi được khai báo bằng ' là chuỗi theo nghĩa đen (giá trị cụ thể) và sẽ không được thay thế bằng các giá trị của biến, trong khi chuỗi được khai báo bằng " thì có.

foo=bar
echo "$foo"
# prints bar
echo '$foo'
# prints $foo

Như hầu hết các ngôn ngữ lập trình khác, bash cũng hỗ trợ các cấu trúc điều khiển như if, case, whilefor. Và tất nhiên, bash hỗ trợ các hàm nhận vào các đối số và thực hiện các tác vụ. Đây là một ví dụ về hàm có chức năng là tạo một thư mục và cd vào nó.

mcd () {
    mkdir -p "$1"
    cd "$1"
}

Ở đây $1 mang ý nghĩa là đối số được truyền vào đầu tiên cho hàm. Không như các ngôn ngữ khác, bash sử dụng một tập hợp các biến đặc biệt để chỉ đến các đối số, mã lỗi và các biến liên quan. Ở dưới là một danh sách của chúng. Các bạn có thể tìm một danh sách đầy đủ và chi tiết hơn ở đây.

Các lệnh thường sẽ trả về kết quả ở STDOUT, lỗi ở STDERR, và một mã kết quả phục vụ cho mục đích lập trình. Mã kết quả hoặc mã kết thúc (return code or exit status) là cách để các đoạn mã/lệnh tương tác với nhau nhằm xác định việc thực thi như thế nào. Giá trị của mã kết thúc là 0 biểu hiện mọi thứ vẫn bình thường, khác 0 nghĩa là có lỗi xảy ra. Mã kết thúc có thể được sử dụng để xử lý điều kiện thực hiện các lệnh tiếp theo bằng việc sử dụng toán tử &&|| (toán tử andor ), cả 2 điều là toán tử short-circuiting. Các lệnh có thể được ngăn cách với nhau trên cùng một dòng sử dụng dấu ; (semicolon-chấm phẩy). Lệnh true sẽ luôn trả về mã kết thúc là 0 và lệnh false sẽ luôn trả về mã kết thúc là 1.

Ghi chú (người dịch):

Xem một vài ví dụ sau:

false || echo "Oops, fail"
# Oops, fail

true || echo "Will not be printed"
#

true && echo "Things went well"
# Things went well

false && echo "Will not be printed"
#

true ; echo "This will always run"
# This will always run

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

Một ứng dụng khác là lưu trữ giá trị đầu ra của một lệnh vào một biến. Việc này có thể thực hiện bằng việc sử dụng command substitution. Bất cứ khi nào bạn sử dụng cú pháp $( CMD ), shell sẽ thực thi lệnh CMD và sau đó lấy kết quả được trả về và thay thế tại chỗ. Ví dụ, đối với lệnh sau for file in $(ls), shell thưc thi lện $(ls) trước và sau đó mới thực hiện lặp qua từng giá trị. Một ứng dụng tương đương nhưng ít phổ biến hơn là process substitution, <( CMD ) sẽ thực thi lệnh CMD và lưu trữ giá trị của kết quả vào một file tạm thời và thay thế bằng tên file tạm thời đó. Điều này rất hữu dụng khi những lệnh đọc dữ liệu từ file thay vì thiết bị nhập chuẩn STDIN. Ví dụ diff <(ls foo) <(ls bar) sẽ chỉ ra những files khác nhau trong 2 thư mục foobar.

Bỏi vì có quá nhiều thông tin, hãy xem các ví dụ cụ thể kỹ hơn. Đoạn mã sẽ lặp qua các đối số được cung cấp, thực thi lệnh grep để so sánh đối số với chuỗi foobar, và thêm chuỗi foobar này vào file như là 1 dòng comment nếu trong file không có từ bất kỳ chuỗi foobar.

#!/bin/bash

echo "Starting program at $(date)" # Date will be substituted

echo "Running program $0 with $# arguments with pid $$"

for file in "$@"; do
    grep foobar "$file" > /dev/null 2> /dev/null
    # When pattern is not found, grep has exit status 1
    # We redirect STDOUT and STDERR to a null register since we do not care about them
    if [[ $? -ne 0 ]]; then
        echo "File $file does not have any foobar, adding one"
        echo "# foobar" >> "$file"
    fi
done

Câu lệnh điều kiện kiểm tra giá trị của biến $? khác 0. Bash đã cài đặt nhiều lệnh so sánh - tham khảo ở danh sách đầy đủ ở trang web manpage dành cho test. Khi thực hiện lệnh so sánh, cố gắng sử dụng 2 cặp dấu ngoặc vuông [[ ]] thay vì chỉ 1 [ ].Điều này sẽ giảm tỉ lệ lỗi xuống mặc dù điều này không tương thích với sh. Giải thích chi tiết có thể tìm ở đây. Khi mã nguồn được thực thi, việc truyền nhiều đối số tương tự nhau khá phổ biến. Bash cung cấp cách để làm cho mọi chuyện đơn giản hơn, khả năng expanding expressions (mở rộng biểu thức) bằng việc gom nhóm các filename expansion (định dạng mở rộng của file). Kỹ thuật này được gọi là shell globbing.

Chú thích (người dịch):

convert image.{png,jpg}
# Will expand to
convert image.png image.jpg

cp /path/to/project/{foo,bar,baz}.sh /newpath
# Will expand to
cp /path/to/project/foo.sh /path/to/project/bar.sh /path/to/project/baz.sh /newpath

# Globbing techniques can also be combined
mv *{.py,.sh} folder
# Will move all *.py and *.sh files


mkdir foo bar
# This creates files foo/a, foo/b, ... foo/h, bar/a, bar/b, ... bar/h
touch {foo,bar}/{a..h}
touch foo/x bar/y
# Show differences between files in foo and bar
diff <(ls foo) <(ls bar)
# Outputs
# < x
# ---
# > y

Viết mã bash đôi khi khó khăn và không trực quan. Có một vài công cụ như shellcheck giúp chúng ta tìm lỗi ở trong mã sh/bash của bạn.

Lưu ý rằng mã không nhất thiết phải được viết ở bằng bash thì mới có thể chạy được ở giao diện dòng lệnh. Đây là một đoạn mã Python sẽ trả về kết quả là các tham số được truyền vào với thứ tự nghịch đảo.

#!/usr/local/bin/python
import sys
for arg in reversed(sys.argv[1:]):
    print(arg)

Kernel biết thực thi một đoạn mã sử dụng trình biên dịch của python thay vì shell, bởi vì chúng ta đã đính kèm dấu shebang ở đầu file script. Đây là một ví dụ tốt để viết shebang bằng lệnh env để có thể giải quyết trường hợp nơi lưu trữ cách lệnh trong hệ thống, tằng cường khả năng di động của mã. Để xử lý vị trí, env sẽ sử dụng biến môi trường PATH đã được giới thiệu ở bài đầu tiên. Ví dụ, dòng shebang sẽ trông như thế này #!/usr/bin/env python.

Một vài điều khác biệt giữa function (hàm) và script (mã) của shell cần ghi nhớ:

Công cụ của shell

Cách sử dụng lệnh

Tại thời điểm này, bạn có thể đang tự hỏi rằng làm thế nào để tìm các tuỳ chọn (flags) dành cho các lệnh giống như ls -l, mv -i and mkdir -p. Tổng quát hơn, đối với một lệnh, làm sao ta có thể tìm ra các chức năng của chúng và cái tuỳ chọn tương ứng? Chúng ta có thể bắt đầu tìm google cho câu hỏi này, tuy nhiên UNIX thì có trước cả StackOverflow, và có 1 cách truyền thống để lấy các thông tin này.

Như chúng ta đã biết ở trong bài đầu tiên, cách tiếp cận đầu tiên là gọi lệnh với với tuỳ chọn -h hoặc --help. Cách tiếp cận chi tiết hơn là sử dụng lệnh man. Viết ngắn gọn của từ manual, man cung cấp 1 trang hướng dẫn sử dụng (gọi là manpage) cho một lệnh bất kỳ. Ví dụ man rm sẽ trả về tất cả các tác vụ của lệnh rm cùng với các tuỳ chọn mà lệnh cung cấp, cùng với tuỳ chọn -i đã được giới thiệu trước đó. Thực tế, những đường link tôi đã đính kèm cho các lệnh thực chất một phiên bản trực tuyến của Linux manpage. Thậm chí những lệnh không có sẵn mà bạn cần phải cài đặt cũng có một trang manpage nếu lập trình viên có viết chúng và đóng gói chúng cùng quá trình cài đặt. Đối với những công cụ tương tác trực tiếp ví dụ như những thứ dựa trên ncurses, hướng dẫn sử dụng của các lệnh thường được truy cập ngay trong chương trình bằng việc viết :help hoặc ?.

Đôi khi các trang manpages có thể cung cấp rất nhiều thông tin chi tiết mô tả về lệnh, khiến nó trở lên khó để xác định xem những tuỳ chọn/ cú pháp nào để sử dụng trong các trường hợp cơ bản.TLDR pages là một giải pháp đơn giản, tập trung vào việc đưa ra các ví dụ cụ thể và trường hợp sử dụng của từng lệnh giúp bạn nhanh chóng nhận ra các tuỳ chọn nào cần thiết. Cụ thể hơn, tôi thường hay sử dụng trang tldr cho các lệnh tarffmpeg hơn là manpages.

Finding files

Một trong các công việc thường xuyên lặp đi lặp lại đối với lập trình viên là tìm files hoặc thư mục. Các hệ thống dựa trên UNIX thường có sẵn lệnh find, một công cụ tuyệt vời của shell để tìm kiếm file. find sẽ tìm kiếm đệ quy các file có tên khớp với một vài tiêu chuẩn nào đó.

# Find all directories named src
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'

Ngoại trừ các thứ được kể trên, find có thể thực hiện nhiều tác vụ trên nhiều file là kết quả của câu truy vấn. Đặc điểm này thực sử hữu dụng để đơn giản hoá những công việc nhàm chán

# 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ù sự phổ biến của find, cú pháp của nó thực sự khó ghi nhớ. Ví dụ, để thực thiện công việc đợn giản như là tìm kiếm các file khớp với mẫu PATTERN, bạn phải viết câu lệnh như thế này find -name '*PATTERN*' (hoặc -iname nếu muốn không phân biệt chữ hoa, chữ thường). Bạn có thể bắt đầu viết một vài bí danh (alias) cho những trường hợp trên, nhưng tư tưởng của shell khuyến khích tìm kiếm những thứ thay thế tốt hơn. Luôn nhớ rằng, một trong những thuộc tính thú vị nhất của shell là bạn chỉ đang gọi những chương trình mà thôi, cho nên bạn có thể tìm (hoặc thậm chí là viết mới) những thứ thay thế cho cùng 1 công việc. Điển hình, fd là một chương trình đơn giản, nhanh và dễ sử dụng thay thế cho find. Chương trình này hỗ trợ một vài thứ mặc định xịn xò như tô màu kết quả, sử dụng biểu thức chính quy (regex - regular expression), và hỗ trợ unicode. Và tất nhiên, theo quan điểm cá nhân của tôi (tác giả), một cú pháp dễ hơn. Một trường hợp cụ thể, cú pháp để tìm một mẫu như PATTERNfd PATTERN

Hầu hết các ý kiến đều đồng ý rằng findfd rất tốt, nhưng chắc một vài người sẽ tự hỏi rằng về độ hiệu quả của việc tìm kiếm so với việc đánh chỉ mục hoặc sử dụng cơ sở dữ liệu cho việc tìm kiếm nhanh. Và đây là điều mà locate phát triển. locate sử dụng cơ sở dữ liệu được cập nhật sử dụng updatedb. Trong hầu hết hệ thông, updatedb được cập nhật hàng ngày thông qua cron. Bởi vì lý do này nên có một sử đánh đổi giữa tốc độ và độ trực tuyến (fresness). Một điều nữa là find và các công cụ tương tự thì có thể tìm kiếm file dựa trên nhiều thuộc tính khác như kích cỡ, ngày chỉnh sửa, quyền hạn, trong khi locate chỉ sử dụng tên file. So sánh chi tiết có thể tìm ở đây.

Finding code

Tìm kiếm file bằng tên khá hữu ích, tuy nhiên thao tác tìm kiếm dựa trên nội dung file cũng cần thiết không kém. Một trường hợp phổ biến là tìm tất cả các file chứa nhiều hơn một mẫu, cùng với nơi mà kết quả xuất hiện (số dòng). Để làm việc này, hầu hết hệ thống lõi UNIX đều cung cấp grep, một công cụ giúp tìm kiếm các mẫu dựa trên văn bản đầu vào. grep là một công cụ cực kỳ mạnh mà chúng ta sẽ tìm hiểu thêm, chi tiết về nó ở bài sau (data wrangling).

Ở hiện tại, grep cung cấp rất nhiều tuỳ chọn (flags) khiến nó trở nên linh hoạt. Một vài tuỳ chọn tôi hay dùng là -C để lấy ngữ cảnh kết quả và -v để nghịch đảo kết quá, ví dụ, tìm những đoạn không khớp với mẫu. Ví dụ grep -C 5 sẽ in ra 5 dòng trước và sau kết quả. Khi cần thực hiện việc tìm kiếm nhiều files, bạn có thể sử dụng -R bởi vì tuỳ chọn này sẽ đi vào từng file ở trong thư mục và tìm kiếm kết quả.

Nhưng grep -R có thể được cải tiến bằng nhiều cách, ví dụ như bỏ qua .git folded, sử dụng CPU đa nhân để hỗ trợ, et cetera Có nhiều công cụ thay thế grep dã được phát triển, bao gồm ack, agrg. Tất cả những công cụ trên rất tuyệt vời và cung cấp cùng chức năng. Hiện tại tôi đang sử dụng ripgrep rg, bởi vì nó rất nhanh và dễ. Ví dụ:

# Find all python files where I used the requests library
rg -t py 'import requests'
# Find all files (including hidden files) without a shebang line
rg -u --files-without-match "^#!"
# Find all matches of foo and print the following 5 lines
rg foo -A 5
# Print statistics of matches (# of matched lines and files )
rg --stats PATTERN

Lưu ý rằng với find/fd, điều quan trọng là các vấn đề này có thể được giải quyết rất là dễ dàng, còn việc sử dụng công cụ nào thì không quan trọng.

Finding shell commands

Chúng ta tìm hiểu việc tìm kiếm các files và code, khi bắt đầu sử dụng shell nhiều hơn, bạn có thể muốn tìm các lệnh mà đã gõ ở vài thời điểm. Điều đầu tiên cần phải biết đó là mũi tên lên sẽ trả về bạn lệnh cuối cùng, và nếu bạn giữ nó thì nó sẽ từ từ duyệt qua lịch sử shell của bạn.

Lệnh history sẽ giúp bạn truy cập tới lịch sử của shell . Nó sẽ in ra thiết bị xuất chuẩn lịch sử của shell. Nếu bạn muốn tìm kiếm lịch sử, ta có thể nối đầu ra với grep để thực hiện việc tìm kiếm. history | grep find sẽ in ra các lệnh có chứa chuỗi “find”.

Trong hầu hết các shell, bạn có thể sử dụng Ctrl+R để thực hiện việc tìm kiếm lịch sử. Sau khi nhấn phím Ctrl+R, bạn có thể nhập một chuổi mà bạn muốn tìm kiếm ở trong lịch sử. Và nếu bạn tiếp tục giữ phím, bạn sẽ đi vào vòng lặp các kết quả. Điều này cũng có làm được bằng việc nhấn phím UP/DOWN (mũi tên lên/xuống) đối với zsh

Một điều thú vị là Ctrl+R là sử dụng fzf. fzf là một công cụ tìm kiếm tổng quát có thể được sử dụng với rất nhiều lệnh khác. Do đó, nó được sử dụng để tìm kiếm nhanh chóng dựa trên lịch sử của bạn và hiển thị kết quả một cách tiện lợi và dễ nhìn.

Một thứ hay ho về lịch sử shell nữa mà tôi rất thích đó là gợi ý tự động dựa trên lịch sử (history-based autosuggestion). Lần đầu được giới thiệu bởi fish, tính năng này sẽ giúp bạn hoàn thành lệnh shell hiện tại dựa trên lịch sử các lệnh gần nhất có các điểm chung. Tính năng này có thể được kích hoạt ở zsh và nó sẽ giúp cuộc đời bạn dễ dàng hơn khi làm việc với shell.

Bạn cũng có thể tuỳ chỉnh các hành vi của lịch sử, ví dụ chặn các lệnh có các dấu khoảng trắng dư thừa. Điều này sẽ có ích khi nhập mật khẩu hoặc các thông tin nhạy cảm khác. Để kích hoạt chức năng này, thêm HISTCONTROL=ignorespace vào file .bashrc hoặc setopt HIST_IGNORE_SPACE vào file .zshrc. Nếu có sai sót không thêm khoảng cách thừa, bạn có thể xoá thủ công các chúng ở trong .bash_history hoặc .zhistory

Directory Navigation

Ở đoạn trước, chúng ta đã mặc định rằng chúng ta đang ở đúng nơi cần thực hiện các tác vụ đó. Tuy nhiên làm thể để nào để di chuyển nhanh chóng giữa các thư mục. Có rất nhiều cách đơn giản cho bạn thử, ví dụ như viết một alias hoặc tạo symlink sử dụng ln -s, nhưng thực tế rằng là các lập trình viên tìm ra cách thông minh và đầy chất xám hơn tại thời điểm hiện tại.

Với phương châm của khoá học này, bạn sẽ đối mặt với những tình huống phổ biến nhất. Tìm những file và/hoặc thư mục thường xuyên được sử dụng có thể được được xử lý bằng fasdautojump. Fasd xếp hạng các files và thư mục dựa trên frecency, nghĩa là , cả 2 tần sốgần đây. Mặc định, fasd thêm một lệnh z mà bạn có thể dủng để có thể nhanh chóng cd chỉ với một chuỗi của thự mục frecent. Ví dụ Nếu bạn thường xuyên di chuyển tới /home/user/files/cool_project thì bạn chỉ đơn giản là sử dụng z cool để di chuyển tới đó. Sử dụng autojump thì cú pháp tương tự sẽ là j cool.

Một vài công cụ giúp bạn nhanh chóng nắm bắt được cấu trúc thư mục : tree, broot thậm chí là một trình quản lý file toàn diện như nnn hoặc ranger.

Exercises

  1. Đọc man ls và viết các lệnh ls giúp liệt kê các thư mục theo yêu cầu sau
    • Liệt kê tất cả các file, kể cả file bị ẩn
    • Kích thước được liệt kê phải ở dạng dễ dọc (vd: 454M thay vì 454279954)
    • File được sắp xếp dựa trên thời gian truy cập cuối cùng
    • Tô màu các kết quả

    Ouput ví dụ

     -rw-r--r--   1 user group 1.1M Jan 14 09:53 baz
     drwxr-xr-x   5 user group  160 Jan 14 09:53 .
     -rw-r--r--   1 user group  514 Jan 14 06:42 bar
     -rw-r--r--   1 user group 106M Jan 13 12:12 foo
     drwx------+ 47 user group 1.5K Jan 12 18:08 ..
    
  2. Viết các hàm bash marcopolo có chức năng sau Bất cứ khi nào marco được gọi, thư mục hiện hành được lưu trữ (bất kể lưu trữ kiểu gì), và khi polo được thực thi, bất kể thư mục hiện hành là gì, phải luôn cd đến thư mục hiện hành gần nhất được lưu trữ bởi macro Để tiện cho việc tìm lỗi, bạn có thể viết code vào 1 file marco.sh và tải (lại) định nghĩa hàm vào shell bằng việc thực thi source marco.sh

  3. Giả sử bạn có một lệnh hiếm như xảy ra lỗi, Và để tìm lỗi cho nó, bạn cần ghi nhớ lại kết quả của nó nhưng sẽ tốn thời gian để nó có thể bị lỗi. Hãy viết một đoạn mã giúp bạn chạy hàng loạt các đoạn mã đó tới khi nó xảy ra lỗi và lưu trữ lại kết quả đó (cả STDOUT và STDERR) vào file và in ra mọi thứ vào cuối cùng. Điểm cộng nếu như bạn có thể tìm ra được việc chạy bao nhiêu lần thì gặp lỗi

    Chương trình cần tìm lỗi

     #!/usr/bin/env bash
    
     n=$(( RANDOM % 100 ))
    
     if [[ n -eq 42 ]]; then
        echo "Something went wrong"
        >&2 echo "The error was using magic numbers"
        exit 1
     fi
    
     echo "Everything went according to plan"
    
  4. Như chúng ta đã đề cập ở trên thì tuỳ chọn -exec của find thực sự rất mạnh mẽ trong việc thực thi các tác vụ trên file chúng ta cần tìm kiếm. Tuy nhiên, nếu chúng ta muốn làm điều gì đó với toàn bộ file, ví dụ như là nén lại thành một file zip? Dựa vào các kiến thức trên, Lệnh sẽ nhận đầu vào từ các đối số và cả STDIN. Khi piping các lệnh, chúng ta thực chất đang kết nối STDOUT với STDIN, nhưng một vài lệnh như tar lại nhận đầu vào từ những đối số (nhận tên file); Để tạo cầu nối cho sự bất tiện này, xargs sẽ giúp chúng ta thực thi lệnh sử dụng STDIN như đối số. Ví dụ, ls | xargs rm sẽ xoá tất cả các file ở thư mục hiện hành.

    Công việc của bạn là viết một lệnh tìm kiếm đệ quy tất cả các HTML files trong các folder và tạo một file zip. Lưu ý rằng lệnh bạn viết phải hoạt động cả với những file có dấu khoảng trắng trong tên (gợi ý: tìm hiểu vể -d của xargs)

    Nếu bạn sử dụng macOS, lưu ý rằng lệnh find mặc định của BSD thì khác với cái của GNU coreutils. Bạn có thể sử dụng -print0 đối vối find-0 đối với xargs. Là một người sử dụng macOS thì bạn phải lưu ý rằng những tiện ích của môi trường giao diện dòng lệnh khác với của GNU, bạn cũng có thể cài đặt phiên bản của GNU trên macOS nếu muốn bằng việc sử dụng brew

  5. (Nâng cao) Việt một lệnh hoặc một đoạn mã tìm kiếm các file được chỉnh sửa gần đây nhất. Hoặc tổng quát hơn, bạn có thể liệt kê các file dựa trên lịch sử chỉnh sửa của chúng?

Edit this page.

Licensed under CC BY-NC-SA.