Các hàm xử lý chuỗi nâng cao trong Bash (Phần 1)

Trong bài viết này, mình sẽ giới thiệu một số cách và viết một số hàm xử lý chuổi nâng cao thay thế cho các công cụ sed, awk, perl bằng ngôn ngữ Bash. Các hàm sau mình xin được trích dẫn từ cuốn sách Pure Bash Bible và giải thích cách hoạt động của các hàm.

Trim leading and trailing white-space from string

Function này hoạt động bằng cách tìm tất các các khoản trắng và xoá nó khỏi đầu và cuối chuỗi.

Dấu “:” được sử dụng như 1 biến tạm thời.

trim_string()
{
    # Usage: trim_string "   example   string    "
    : "${1#"${1%%[![:space:]]*}"}"
    : "${_%"${_##*[![:space:]]}"}"
    printf '%s\n' "$_"
}

Giải thích:

  • Đầu tiên, ta sẽ phân tích dòng đầu tiên của function trên: : “${1#”${1%%[![:space:]]*}”}”
# đoạn này có tác dụng loại bỏ chuỗi dài nhất không phải khoản trống từ bên phải chuỗi
# kết quả ta sẽ được khoản trắng ở đầu chuỗi cần cắt.

"${1%%[![:space:]]*}"

vd:  string =     "    Hello,  World    "
                       |________________|
                             %%

# tiếp theo ta thực hiện loại bỏ chuỗi vừa thu được ở trên
# ra khỏi chuỗi cần cắt từ bên trái chuỗi.

"${1#"${1%%[![:space:]]*}"}"

vd:  string =     "    Hello,  World    “
                  |___|
                    #

Kết quả ta được chuỗi “Hello, World ”

  • Tiếp theo ta sẽ phân tích dòng tiếp theo:   “${_%”${_##*[![:space:]]}”}”
# đoạn này có tác dụng lấy phần khoản trắng phía sau của chuỗi

"${_##*[![:space:]]}"

vd:  string =     "Hello,  World    "
                  |____________|
                        ##

# tiếp đến sẽ cắt đoạn string mới lấy được phía trên ra khỏi chuỗi được xét.

"${_%"${_##*[![:space:]]}"}"

vd:  string =     "Hello,  World    “
                                |__|
                                 %

Kết quả ta sẽ được chuỗi “Hello, World”

  • Tổng hợp, ta có sử dụng dòng đầu tiên để xoá đi khoản trắng phía đầu chuỗi rồi gán vào biến “:” tiếp đó ta cắt tiếp khoản trắng phía sau chuỗi vừa nhận được. Cuối cùng ta nhận được chuỗi đã loại bỏ hết khoản trắng trước và sau chuỗi.

Trim all white-space from string and truncate spaces

# shellcheck disable=SC2086,SC2048
trim_all() {
    # Usage: trim_all "   example   string    "
    set -f
    set -- $*
    printf '%s\n' "$*"
    set +f
}

Đầu tiên ta sẽ tìm hiểu về “set” , “set” được sử dụng để thiết lập và sửa đổi các biến nội bộ của shell. Trong trường hợp của hàm ở trên, “set” được sử dụng để thiết lâp gía trị cho các biến vị trí.

Ví dụ: ta có file set.sh có nội dung:

var="Welcome to thegeekstuff"
set -- $var
echo "\$1=" $1
echo "\$2=" $2
echo "\$3=" $3

khi thực hiện ./set.sh ta sẽ được kết quả in ra màn hình như sau:

$1=Welcome
$2=to
$3=thegeekstuff

Tương tự với hàm trim_all() trên, khi ta chạy:

trim_all "    Hello,    World    "

sau khi thực hiện set — $* ta sẽ có được giá trị $1 = “Hello,”, $2 = ” World”, như ta biết biến $* sẽ chứa tất cả các tham số đầu vào của script, có nghĩa là biến $* sẽ bao gồm các biến $1, $2. Vì vậy khi ta thực hiện printf ‘%s\n’ “$*” bash sẽ thực hiện in ra màn hình giống như printf ‘%s\n’ “$1 $2”. Vì vậy ta được kết quả là “Hello, World”.

Tương tự với:

name="   John   Black  is     my    name.    "
trim_all "$name"

ta sẽ có được $1=”John”, $2=”Black”, $3=”is”, $4=”my”, $5=”name.”

cuối cùng ta sẽ có được kết quả là:   “John Black is my name.”

Ngoài ra trong hàm trên ta cũng thấy được set -f là set và option -f, option này có tác dụng là vô hiệu hóa các phần mở rộng filename.

 

Use regex on a string

Kết quả của việc so trùng bash’s regex có thể được sử dụng để thay thế “sed” trong phần lớn các trường hợp ứng dụng.

Tuy nhiên, chức năng này của Bash là một trong số ít chức năng phụ thuộc vào nền tảng hệ điều hành. Bash sẽ sử dụng bất cứ trình xử lý regex (regex engine) nào được cài đặt trên hệ thống của người dùng.

Ví dụ1: ta có 1 đoạn hội thoại

string="hue, Hex, Rgb, Hsl. 0, #ff0000, rgb(255, 0, 0), hsl(0, 100%, 50%). 15, #ff4000, rgb(255, 64, 0), hsl(15, 100%, 50%). 30, #ff8000, rgb(255, 128, 0), hsl(30, 100%, ..."

# ta muốn lấy ra tất cả các mã màu theo dạng "#......" ta dùng hàm sau

regex() {
    # Usage: regex "string" "regex"
    for word in $1
    do
        [[ $word =~ $2 ]]
        if [[ ${BASH_REMATCH[1]} ]]
        then
            printf '%s\n' "${BASH_REMATCH[1]}"
        fi
    done
}

regex "$string" '(#([a-fA-F0-9]{6}|[a-fA-F0-9]{3}))'

ta sẽ được kết quả:

#ff0000
#ff4000
#ff8000

Ví dụ 2: ta có 1 chuỗi string, ta muốn kiểm tra nó có phải là 1 email hay không

string="[email protected]"
string2="[email protected]"

regex() {
    # Usage: regex "string" "regex"
    [[ $1 =~ $2 ]] && printf '%s\n' "${BASH_REMATCH[1]}"
}

khi ta chạy lệnh:

regex "$string" '^([A-Za-z0-9._%+-][email protected][A-Za-z0-9.-]+\.[A-Za-z]{2,4})$'

ta sẽ được kết quả: [email protected]

regex "$string2" '^([A-Za-z0-9._%+-][email protected][A-Za-z0-9.-]+\.[A-Za-z]{2,4})$'

ta sẽ được kết quả rỗng.

Tích hợp vào script ta có hàm kiểm tra màu như sau:

is_hex_color() {
    if [[ $1 =~ ^(#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3}))$ ]]; then
        printf '%s\n' "${BASH_REMATCH[1]}"
    else
        printf '%s\n' "error: $1 is an invalid color."
        return 1
    fi
}

color="#444444"
is_hex_color "$color" || color="#FFFFFF"

# kết quả: #444444

color="#444444134235"
is_hex_color "$color" || color="#FFFFFF"

# kết quả: error: #444444134235 is an invalid color. //echo $color    =>     #FFFFFF

 

Split a string on a delimiter

Lưu ý: hàm dưới đây yêu cầu phiên bản bash từ 4. trở lên

split() {
   # Usage: split "string" "delimiter"
   IFS=$'\n' read -d "" -ra arr <<< "${1//$2/$'\n'}"
   printf '%s\n' "${arr[@]}"
}

Trước khi thực thi hàm trên, ta hãy tìm hiểu 1 số khái niệm sau đây nhé:

  • Word splitting
    • Shell sẽ quét các đối số đầu vào (arguments như parameter expansion, command substitution, arithmetic expansion) không nằm trong dấu ngoặc kép để phân tách từ.
    • Shell xem các ký tự trong IFS, có giá trị mặc định là , và như là 1 ký tự phân tách và tách đối số đầu vào trên thành các từ (words) bằng cách sử dụng các ký tự trên làm ký tự kết thúc.  Bất cứ ký tự nào trong IFS ở đầu và cuối chuỗi sẽ bị bỏ qua, và bất cứ ký tự nào trong IFS không nằm ở đầu và cuối đều sẽ được sử dụng để để phân định các từ.
    • Nếu giá trị của IFS là null thì sẽ không có sự phân cách từ nào cả. Ta có thể kiểm tra giá trị của biến IFS bằng lệnh sau: cat -etv <<<“$IFS”.
  • lệnh read
    • có chức năng đọc vào 1 dòng từ input hoặc từ file nếu option -u được sử dụng. Mặc định, read nhận định ký tự newline là kết thúc của 1 dòng, nhưng ta có thể thay đổi điều này bằng option -d. (các bạn có thể tham khảo thêm tại đây).
    • Trong bài này mình sẽ nói về 2 option chính của read được sử dụng trong hàm trên là -r và -a:
      • “-r” : khi sử dụng option này ta vẫn giữ nguyên được ký tự “\“ theo đúng nghĩa đen của nó.
      • “-a” : gán các từ tìm thấy được vào 1 mảng chỉ mục, vị trí của mảng bắt đầu từ 0.

Sau khi làm rõ được các khái niệm trên, bây giờ ta hãy cùng phân tích hàm ở trên:

Đầu tiên, ta gán IFS bằng ký tự newline $’\n’ sau đó ta dùng hàm read với option -d để thay đổi ký tự kết thúc dòng là “” cùng với option -ra để ghi các giá trị tìm thấy vào một mảng. Ta dùng phép replace trên chuỗi để đổi chổ ký tự mà ta muốn dùng để phân tách bằng ký tự xuống dòng ${1//$2/$’\n’}.Đầu tiên, ta gán IFS bằng ký tự newline $’\n’ sau đó ta dùng hàm read với option -d để thay đổi ký tự kết thúc dòng là “” cùng với option -ra để ghi các giá trị tìm thấy vào một mảng. Ta dùng phép replace trên chuỗi để đổi chổ ký tự mà ta muốn dùng để phân tách bằng ký tự xuống dòng ${1//$2/$’\n’}.

Ví dụ: khi ta thực hiện lệnh

split "apples,oranges,pears,grapes" ","

hàm split sẽ replace chuỗi “apples,oranges,pears,grapes” với ký tự “,” bằng ký tự $’\n’, ta sẽ có được chuỗi mới là:

[[email protected] ~]# string="apples,oranges,pears,grapes"
[[email protected] ~]# a=","
[[email protected] ~]# echo "${string//$a/$'\n'}"
apples
oranges
pears
grapes
[[email protected] ~]#

Sau đó, hàm read với các option -d, -r, -a sẽ ghi mỗi từ trong chuỗi mới kết thúc bằng ký tự ‘\n’ vào mảng thứ tự, bắt đầu từ vị trí 0. Cuối cùng ta có được 1 mảng chứa các giá trị đã cắt bằng ký tự phân cách là “,”

Ví dụ 2: Như trên, ta thực hiện hàm split với các chuỗi sau:

split "1, 2, 3, 4, 5" ", "
split "hello---world---my---name---is---john" "—"

ta sẽ được các kết quả cho lệnh đầu tiên:

1

2

3

4

5

và chuỗi tiếp sau là:

hello

world

my

name

is

john

 

Trong bài này. mình đã giới thiệu cho các bạn 4 hàm xử lý chuỗi nâng cao, thật đơn giản phải không, các hàm này là tổ hợp của các cách xử lý chuỗi cơ bản mình đã giới thiệu ở các bài trước chứ không có nhiều kiến thức mới. Nếu nắm vững được cơ bản, các bạn sẽ dễ dàng đọc hiểu được cách hoạt động của các hàm trên. Ở bài sau, mình sẽ tiếp tục với các hàm xử lý chuỗi tiếp theo.

Các hàm xử lý chuỗi nâng cao trong Bash (Phần 2)

Các toán tử cơ bản, cấu trúc so sánh và mảng trong Bash

Shell là gì ? Sự khác nhau giữa SH và Bash

Khởi đầu với Python

Kiểu dữ liệu cơ bản trong Python

Hướng dẫn cài đặt Website WordPress với công cụ WordPress Toolkit trên cPanel

Hướng dẫn thay đổi PHP version trên hosting

Hướng dẫn khôi phục mật khẩu admin trên website WordPress