Làm cách nào để tách một chuỗi thành nhiều chuỗi cách nhau bởi ít nhất một khoảng trắng trong bash shell?


224

Tôi có một chuỗi chứa nhiều từ với ít nhất một khoảng trắng giữa mỗi hai. Làm cách nào tôi có thể chia chuỗi thành các từ riêng lẻ để tôi có thể lặp qua chúng?

Chuỗi được thông qua như là một đối số. Ví dụ ${2} == "cat cat file". Làm thế nào tôi có thể lặp qua nó?

Ngoài ra, làm thế nào tôi có thể kiểm tra nếu một chuỗi chứa khoảng trắng?


1
Loại vỏ nào? Bash, cmd.exe, powershell ...?
Alexey Sviridov

Bạn chỉ cần lặp (ví dụ thực hiện một lệnh cho mỗi từ)? Hay bạn cần lưu trữ một danh sách các từ để sử dụng sau?
DVK

Câu trả lời:


281

Bạn đã thử chỉ cần chuyển biến chuỗi vào một forvòng lặp? Bash, cho một, sẽ tự động phân chia trên khoảng trắng.

sentence="This is   a sentence."
for word in $sentence
do
    echo $word
done

 

This
is
a
sentence.

1
@MobRule - nhược điểm duy nhất của việc này là bạn không thể dễ dàng nắm bắt (ít nhất là tôi không nhớ lại cách nào) đầu ra để xử lý thêm. Xem giải pháp "tr" của tôi bên dưới để biết thứ gì đó gửi công cụ tới STDOUT
DVK

4
Bạn chỉ có thể nối nó vào một biến : A=${A}${word}).
Lucas Jones

1
đặt $ text [điều này sẽ đặt các từ thành $ 1, $ 2, $ 3 ... vv]
Rajesh

32
Trên thực tế thủ thuật này không chỉ là một giải pháp sai lầm, nó còn cực kỳ nguy hiểm do vỏ quả cầu. touch NOPE; var='* a *'; for a in $var; do echo "[$a]"; doneđầu ra [NOPE] [a] [NOPE]thay vì dự kiến [*] [a] [*](LFs được thay thế bằng SPC để dễ đọc).
Tino

@mob tôi nên làm gì nếu tôi muốn tách chuỗi dựa trên một số chuỗi cụ thể? ví dụ dấu phân cách ".xlsx" .

296

Tôi thích chuyển đổi thành một mảng, để có thể truy cập các phần tử riêng lẻ:

sentence="this is a story"
stringarray=($sentence)

bây giờ bạn có thể truy cập các phần tử riêng lẻ trực tiếp (bắt đầu bằng 0):

echo ${stringarray[0]}

hoặc chuyển đổi trở lại chuỗi để lặp:

for i in "${stringarray[@]}"
do
  :
  # do whatever on $i
done

Tất nhiên việc lặp qua chuỗi trực tiếp đã được trả lời trước đó, nhưng câu trả lời đó có nhược điểm là không theo dõi các yếu tố riêng lẻ để sử dụng sau:

for i in $sentence
do
  :
  # do whatever on $i
done

Xem thêm Tham khảo Bash Bash .


26
Đáng buồn là không hoàn hảo, vì toàn cầu: touch NOPE; var='* a *'; arr=($var); set | grep ^arr=kết quả đầu ra arr=([0]="NOPE" [1]="a" [2]="NOPE")thay vì dự kiếnarr=([0]="*" [1]="a" [2]="*")
Tino

@Tino: nếu bạn không muốn toàn cầu can thiệp thì chỉ cần tắt nó đi. Giải pháp sau đó cũng sẽ hoạt động tốt với các ký tự đại diện. Đó là cách tiếp cận tốt nhất theo ý kiến ​​của tôi.
Alexandros

3
@Alexandros Cách tiếp cận của tôi là chỉ sử dụng các mẫu được bảo mật theo mặc định và hoạt động hoàn hảo trong mọi ngữ cảnh. Yêu cầu thay đổi toàn cầu để có được giải pháp an toàn không chỉ là một con đường rất nguy hiểm, đó là điều tối kỵ. Vì vậy, lời khuyên của tôi là đừng bao giờ quen với việc sử dụng mô hình như thế này ở đây, vì sớm hay muộn bạn sẽ quên một số chi tiết, và sau đó ai đó khai thác lỗi của bạn. Bạn có thể tìm thấy bằng chứng cho việc khai thác như vậy trên báo chí. Mỗi. Độc thân. Ngày.
Tino

86

Chỉ cần sử dụng shell "set" tích hợp. Ví dụ,

đặt $ văn bản

Sau đó, các từ riêng lẻ trong văn bản $ sẽ ở mức $ 1, $ 2, $ 3, v.v ... Để mạnh mẽ, người ta thường làm

đặt - rác $ văn bản
ca

để xử lý trường hợp $ text trống hoặc bắt đầu bằng dấu gạch ngang. Ví dụ:

text = "Đây là một bài kiểm tra"
đặt - rác $ văn bản
ca
cho từ; làm
  tiếng vang "[$ word]"
làm xong

Bản in này

[Điều này]
[Là]
[a]
[kiểm tra]

5
Đây là một cách tuyệt vời để phân chia var để các phần riêng lẻ có thể được truy cập trực tiếp. +1; đã giải quyết vấn đề của tôi
Cheekysoft

Tôi sẽ đề nghị sử dụng awknhưng setdễ dàng hơn nhiều. Tôi bây giờ là một setfanboy. Cảm ơn @Idelic!
Yzmir Ramirez

22
Xin lưu ý về shell globalbing nếu bạn làm những việc như vậy: touch NOPE; var='* a *'; set -- $var; for a; do echo "[$a]"; doneđầu ra [NOPE] [a] [NOPE]thay vì dự kiến [*] [a] [*]. Chỉ sử dụng nó nếu bạn chắc chắn 101% rằng không có siêu ký tự SHELL trong chuỗi được tách!
Tino

4
@Tino: Vấn đề đó được áp dụng ở mọi nơi, không chỉ ở đây, mà trong trường hợp này bạn có thể ngay set -ftrước set -- $varset +fsau đó để vô hiệu hóa toàn cầu.
Idelic

3
@Idelic: Bắt tốt. Với set -fgiải pháp của bạn là an toàn, quá. Nhưng set +flà mặc định của mỗi shell, vì vậy nó là một chi tiết thiết yếu, cần phải lưu ý, bởi vì những người khác có thể không nhận thức được nó (như tôi cũng vậy).
Tino

81

Cách dễ nhất và an toàn nhất trong BASH 3 trở lên là:

var="string    to  split"
read -ra arr <<<"$var"

(trong đó arrmảng lấy các phần bị tách của chuỗi) hoặc, nếu có thể có dòng mới trong đầu vào và bạn muốn nhiều hơn chỉ dòng đầu tiên:

var="string    to  split"
read -ra arr -d '' <<<"$var"

(xin lưu ý không gian trong -d '', không thể bỏ đi), nhưng điều này có thể cung cấp cho bạn một dòng mới bất ngờ từ <<<"$var"(vì điều này hoàn toàn bổ sung một LF ở cuối).

Thí dụ:

touch NOPE
var="* a  *"
read -ra arr <<<"$var"
for a in "${arr[@]}"; do echo "[$a]"; done

Đầu ra dự kiến

[*]
[a]
[*]

vì giải pháp này (trái ngược với tất cả các giải pháp trước đây ở đây) không dễ bị bất ngờ và thường không kiểm soát được vỏ.

Ngoài ra, điều này cung cấp cho bạn toàn bộ sức mạnh của IFS như bạn có thể muốn:

Thí dụ:

IFS=: read -ra arr < <(grep "^$USER:" /etc/passwd)
for a in "${arr[@]}"; do echo "[$a]"; done

Xuất ra một cái gì đó như:

[tino]
[x]
[1000]
[1000]
[Valentin Hilbig]
[/home/tino]
[/bin/bash]

Như bạn có thể thấy, không gian cũng có thể được bảo tồn theo cách này:

IFS=: read -ra arr <<<' split  :   this    '
for a in "${arr[@]}"; do echo "[$a]"; done

đầu ra

[ split  ]
[   this    ]

Xin lưu ý rằng việc xử lý IFStrong BASH là một chủ đề của riêng nó, do đó, hãy thực hiện các bài kiểm tra của bạn, một số chủ đề thú vị về điều này:

  • unset IFS: Bỏ qua các lần chạy SPC, TAB, NL và trên dòng bắt đầu và kết thúc
  • IFS='': Không tách trường, chỉ đọc mọi thứ
  • IFS=' ': Chạy SPC (và chỉ SPC)

Một số ví dụ cuối cùng

var=$'\n\nthis is\n\n\na test\n\n'
IFS=$'\n' read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

đầu ra

1 [this is]
2 [a test]

trong khi

unset IFS
var=$'\n\nthis is\n\n\na test\n\n'
read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

đầu ra

1 [this]
2 [is]
3 [a]
4 [test]

BTW:

  • Nếu bạn không quen $'ANSI-ESCAPED-STRING'làm quen với nó, đó là một trình tiết kiệm thời gian.

  • Nếu bạn không bao gồm -r(như trong read -a arr <<<"$var") thì đọc sẽ thoát dấu gạch chéo ngược. Điều này được để lại như là bài tập cho người đọc.


Đối với câu hỏi thứ hai:

Để kiểm tra một cái gì đó trong chuỗi tôi thường dính vào case, vì điều này có thể kiểm tra nhiều trường hợp cùng một lúc (lưu ý: trường hợp chỉ thực hiện trận đấu đầu tiên, nếu bạn cần sử dụng các casecâu lệnh nhân), và nhu cầu này khá thường xuyên (chơi chữ dự định):

case "$var" in
'')                empty_var;;                # variable is empty
*' '*)             have_space "$var";;        # have SPC
*[[:space:]]*)     have_whitespace "$var";;   # have whitespaces like TAB
*[^-+.,A-Za-z0-9]*) have_nonalnum "$var";;    # non-alphanum-chars found
*[-+.,]*)          have_punctuation "$var";;  # some punctuation chars found
*)                 default_case "$var";;      # if all above does not match
esac

Vì vậy, bạn có thể đặt giá trị trả về để kiểm tra SPC như thế này:

case "$var" in (*' '*) true;; (*) false;; esac

Tại sao case? Bởi vì nó thường dễ đọc hơn một chút so với chuỗi regex, và nhờ các siêu ký tự Shell, nó xử lý 99% tất cả các nhu cầu rất tốt.


2
Câu trả lời này xứng đáng nhận được nhiều sự ủng hộ hơn, do các vấn đề toàn cầu được nêu bật và tính toàn diện của nó
Brian Agnew

@brian Cảm ơn. Xin lưu ý rằng bạn có thể sử dụng set -fhoặc set -o noglobđể chuyển đổi toàn cầu, sao cho các ký tự đại diện hệ vỏ không còn gây hại trong bối cảnh này. Nhưng tôi không thực sự là một người bạn của điều đó, vì điều này để lại nhiều sức mạnh của vỏ / rất dễ bị lỗi khi chuyển đổi qua lại cài đặt này.
Tino

2
Câu trả lời tuyệt vời, thực sự xứng đáng nâng cao hơn. Lưu ý bên lề về trường hợp rơi vào - bạn có thể sử dụng để ;&đạt được điều đó. Không hoàn toàn chắc chắn trong phiên bản bash xuất hiện. Tôi là người dùng 4.3
Sergiy Kolodyazhnyy

2
@Serg cảm ơn vì đã chú ý, vì tôi chưa biết điều này! Vì vậy, tôi nhìn nó, nó xuất hiện trong Bash4 . ;&là sự sụp đổ bắt buộc mà không có kiểm tra mẫu như trong C. Và đó cũng là ;;&tiếp tục thực hiện kiểm tra mẫu tiếp theo. Thế ;;là thích if ..; then ..; else if ..;;&giống như if ..; then ..; fi; if .., nơi ;&giống như m=false; if ..; then ..; m=:; fi; if $m || ..; then ..- người ta không bao giờ ngừng học hỏi (từ người khác);)
Tino

@Tino Điều đó hoàn toàn đúng - học tập là một quá trình liên tục. Trên thực tế, tôi đã không biết ;;&trước khi bạn nhận xét: D Cảm ơn, và có thể cái vỏ sẽ ở bên bạn;)
Sergiy Kolodyazhnyy

43
$ echo "This is   a sentence." | tr -s " " "\012"
This
is
a
sentence.

Để kiểm tra khoảng trắng, sử dụng grep:

$ echo "This is   a sentence." | grep " " > /dev/null
$ echo $?
0
$ echo "Thisisasentence." | grep " " > /dev/null     
$ echo $?
1

1
Trong BASH echo "X" |thường có thể được thay thế bằng <<<"X", như thế này : grep -s " " <<<"This contains SPC". Bạn có thể nhận ra sự khác biệt nếu bạn làm điều gì đó echo X | read vartương phản với read var <<< X. Chỉ nhập biến sau varvào vỏ hiện tại, trong khi để truy cập vào biến thể đầu tiên, bạn phải nhóm như sau:echo X | { read var; handle "$var"; }
Tino

17

(A) Để tách một câu thành các từ của nó (khoảng cách được phân tách), bạn chỉ cần sử dụng IFS mặc định bằng cách sử dụng

array=( $string )


Ví dụ chạy đoạn mã sau

#!/bin/bash

sentence="this is the \"sentence\"   'you' want to split"
words=( $sentence )

len="${#words[@]}"
echo "words counted: $len"

printf "%s\n" "${words[@]}" ## print array

sẽ xuất

words counted: 8
this
is
the
"sentence"
'you'
want
to
split

Như bạn có thể thấy, bạn cũng có thể sử dụng dấu ngoặc đơn hoặc dấu ngoặc kép mà không gặp vấn đề gì

Ghi chú:
- về cơ bản đây là câu trả lời của mob , nhưng theo cách này, bạn lưu trữ mảng cho bất kỳ nhu cầu nào khác. Nếu bạn chỉ cần một vòng lặp duy nhất, bạn có thể sử dụng câu trả lời của anh ấy, đó là một dòng ngắn hơn :)
- vui lòng tham khảo câu hỏi này để biết các phương pháp thay thế để phân tách một chuỗi dựa trên dấu phân cách.


(B) Để kiểm tra một ký tự trong chuỗi, bạn cũng có thể sử dụng kết hợp biểu thức chính quy.
Ví dụ để kiểm tra sự hiện diện của một ký tự khoảng trắng bạn có thể sử dụng:

regex='\s{1,}'
if [[ "$sentence" =~ $regex ]]
    then
        echo "Space here!";
fi

Đối với gợi ý regex (B) +1, nhưng -1 cho giải pháp sai (A) vì đây là lỗi dễ bị đổ vỏ. ;)
Tino

6

Để kiểm tra không gian chỉ với bash:

[[ "$str" = "${str% *}" ]] && echo "no spaces" || echo "has spaces"

1
echo $WORDS | xargs -n1 echo

Điều này xuất ra mỗi từ, bạn có thể xử lý danh sách đó khi bạn thấy phù hợp sau đó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.