Chia chuỗi thành một mảng trong Bash


641

Trong một kịch bản Bash, tôi muốn chia một dòng thành nhiều phần và lưu trữ chúng trong một mảng.

Dòng:

Paris, France, Europe

Tôi muốn có chúng trong một mảng như thế này:

array[0] = Paris
array[1] = France
array[2] = Europe

Tôi muốn sử dụng mã đơn giản, tốc độ của lệnh không thành vấn đề. Tôi làm nó như thế nào?


22
Đây là số 1 Google đạt được nhưng có nhiều tranh cãi trong câu trả lời vì câu hỏi không may hỏi về việc phân định , (dấu phẩy-không gian) và không phải là một ký tự đơn lẻ như dấu phẩy. Nếu bạn chỉ quan tâm đến câu hỏi sau, câu trả lời ở đây sẽ dễ thực hiện hơn: stackoverflow.com/questions/918886/
mẹo

Nếu bạn muốn munge một chuỗi và không quan tâm đến việc có nó như là một mảng, thì đó cutcũng là một lệnh bash hữu ích. Dấu phân cách có thể xác định en.wikibooks.org/wiki/Cut Bạn cũng có thể trích xuất dữ liệu từ cấu trúc bản ghi chiều rộng cố định. vi.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

Câu trả lời:


1089
IFS=', ' read -r -a array <<< "$string"

Lưu ý rằng các nhân vật trong $IFSđược điều trị riêng là dải phân cách để trong trường hợp này trường có thể được tách ra bởi một trong hai dấu phẩy hoặc một không gian chứ không phải là chuỗi của hai nhân vật. Điều thú vị là, các trường trống không được tạo khi dấu phẩy xuất hiện trong đầu vào vì không gian được xử lý đặc biệt.

Để truy cập một yếu tố riêng lẻ:

echo "${array[0]}"

Để lặp lại các yếu tố:

for element in "${array[@]}"
do
    echo "$element"
done

Để có được cả chỉ số và giá trị:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

Ví dụ cuối cùng hữu ích vì mảng Bash rất thưa thớt. Nói cách khác, bạn có thể xóa một phần tử hoặc thêm một phần tử và sau đó các chỉ mục không liền kề nhau.

unset "array[1]"
array[42]=Earth

Để có được số lượng phần tử trong một mảng:

echo "${#array[@]}"

Như đã đề cập ở trên, các mảng có thể thưa thớt, do đó bạn không nên sử dụng độ dài để lấy phần tử cuối cùng. Đây là cách bạn có thể trong Bash 4.2 trở lên:

echo "${array[-1]}"

trong bất kỳ phiên bản Bash nào (từ đâu đó sau 2.05b):

echo "${array[@]: -1:1}"

Độ lệch âm lớn hơn chọn xa hơn từ cuối mảng. Lưu ý khoảng trắng trước dấu trừ ở dạng cũ. Nó là bắt buộc.


15
Chỉ cần sử dụng IFS=', ', sau đó bạn không phải loại bỏ các khoảng trắng riêng biệt. Kiểm tra:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
l0b0

4
@ l0b0: Cảm ơn. Tôi không biết mình đang nghĩ gì. Nhân tiện, tôi thích sử dụng declare -p arraycho đầu ra thử nghiệm.
Tạm dừng cho đến khi có thông báo mới.

1
Điều này dường như không tôn trọng báo giá. Ví dụ, France, Europe, "Congo, The Democratic Republic of the"điều này sẽ phân chia sau congo.
Yisrael Dov

2
@YisraelDov: Bash không có cách nào tự xử lý CSV. Nó không thể nói sự khác biệt giữa dấu phẩy bên trong dấu ngoặc kép và những dấu chấm bên ngoài chúng. Bạn sẽ cần sử dụng một công cụ hiểu CSV như lib trong ngôn ngữ cấp cao hơn, ví dụ mô-đun csv trong Python.
Tạm dừng cho đến khi có thông báo mới.

5
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"sẽ chia thành array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")một ghi chú. Vì vậy, điều này chỉ hoạt động với các trường không có khoảng trắng vì IFS=', 'là một tập hợp các ký tự riêng lẻ - không phải là một dấu phân cách chuỗi.
dawg

332

Tất cả các câu trả lời cho câu hỏi này là sai theo cách này hay cách khác.


Câu trả lời sai # 1

IFS=', ' read -r -a array <<< "$string"

1: Đây là một lạm dụng $IFS. Giá trị của $IFSbiến được không lấy làm một chiều dài thay đổi đơn chuỗi phân cách, thay vì nó được thực hiện như một bộ của đơn ký tự tách chuỗi, trong đó mỗi lĩnh vực mà readchia ra từ dòng đầu vào có thể được chấm dứt bởi bất kỳ nhân vật trong bộ (dấu phẩy hoặc dấu cách, trong ví dụ này).

Trên thực tế, đối với các sticklers thực sự ngoài kia, ý nghĩa đầy đủ của $IFScó liên quan nhiều hơn một chút. Từ hướng dẫn bash :

Shell xử lý từng ký tự của IFS như một dấu phân cách và chia kết quả của các phần mở rộng khác thành các từ bằng cách sử dụng các ký tự này làm dấu kết thúc trường. Nếu IFS không được đặt hoặc giá trị của nó chính xác là <dấu cách> <tab> <dòng mới> , mặc định, sau đó là chuỗi <dấu cách> , <tab><dòng mới> ở đầu và cuối kết quả của các lần mở rộng trước đó được bỏ qua và bất kỳ chuỗi ký tự IFS nào không ở đầu hoặc cuối phục vụ để phân định các từ. Nếu IFS có một giá trị khác với mặc định, thì các chuỗi ký tự khoảng trắng <dấu cách> , <tab><được bỏ qua ở đầu và cuối của từ, miễn là ký tự khoảng trắng nằm trong giá trị của IFS ( ký tự khoảng trắng IFS ). Bất kỳ ký tự nào trong IFS không phải là khoảng trắng IFS , cùng với bất kỳ ký tự khoảng trắng IFS liền kề nào , sẽ phân định một trường. Một chuỗi các ký tự khoảng trắng IFS cũng được coi là một dấu phân cách. Nếu giá trị của IFS là null, không có sự phân tách từ nào xảy ra.

Về cơ bản, đối với các giá trị không null không mặc định của $IFS, các trường có thể được phân tách bằng (1) một chuỗi gồm một hoặc nhiều ký tự nằm trong tập hợp các "ký tự khoảng trắng IFS" (nghĩa là, bất kỳ ký tự nào của <space> , <tab><newline> ("dòng mới" có nghĩa là nguồn cấp dữ liệu dòng (LF) ) có mặt ở bất cứ đâu trong $IFS) hoặc (2) bất kỳ ký tự khoảng trắng IFS nào không xuất hiện $IFScùng với bất kỳ "ký tự khoảng trắng IFS" nào bao quanh nó trong dòng đầu vào.

Đối với OP, có thể chế độ phân tách thứ hai mà tôi đã mô tả trong đoạn trước chính xác là những gì anh ta muốn cho chuỗi đầu vào của mình, nhưng chúng ta có thể khá tự tin rằng chế độ phân tách đầu tiên tôi mô tả là hoàn toàn không đúng. Ví dụ, nếu chuỗi đầu vào của anh ta là 'Los Angeles, United States, North America'gì?

IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")

2: Ngay cả khi bạn đã sử dụng giải pháp này với dấu phân cách một ký tự (chẳng hạn như dấu phẩy, nghĩa là không có không gian sau hoặc hành lý khác), nếu giá trị của $stringbiến xảy ra có chứa bất kỳ LF nào, thì readsẽ dừng xử lý một khi nó gặp LF đầu tiên. Nội dung readchỉ xử lý một dòng trên mỗi lần gọi. Điều này đúng ngay cả khi bạn đang đường ống hoặc chuyển hướng đầu vào chỉ để readtuyên bố, như chúng ta đang làm trong ví dụ này với đây-chuỗi cơ chế, và đầu vào như vậy, chưa qua chế biến đảm bảo sẽ bị mất. Mã cung cấp năng lượng cho readnội dung không có kiến ​​thức về luồng dữ liệu trong cấu trúc lệnh chứa của nó.

Bạn có thể lập luận rằng điều này khó có thể gây ra vấn đề, nhưng vẫn là một mối nguy hiểm tinh vi nên tránh nếu có thể. Điều này được gây ra bởi thực tế là readnội dung thực sự thực hiện hai cấp độ phân tách đầu vào: đầu tiên thành các dòng, sau đó thành các trường. Vì OP chỉ muốn một cấp độ phân tách, nên việc sử dụng readnội dung này là không phù hợp và chúng ta nên tránh nó.

3: Một vấn đề tiềm năng không rõ ràng với giải pháp này là readluôn bỏ trường theo dõi nếu nó trống, mặc dù nó bảo tồn các trường trống. Đây là bản demo:

string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")

Có lẽ OP sẽ không quan tâm đến điều này, nhưng nó vẫn là một hạn chế đáng để biết. Nó làm giảm tính mạnh mẽ và tổng quát của giải pháp.

Vấn đề này có thể được giải quyết bằng cách nối thêm một dấu phân cách giả vào chuỗi đầu vào ngay trước khi cho nó vào read, như tôi sẽ trình bày sau.


Câu trả lời sai # 2

string="1:2:3:4:5"
set -f                     # avoid globbing (expansion of *).
array=(${string//:/ })

Ý tưởng tương tự:

t="one,two,three"
a=($(echo $t | tr ',' "\n"))

(Lưu ý: Tôi đã thêm các dấu ngoặc đơn bị thiếu xung quanh thay thế lệnh mà người trả lời dường như đã bỏ qua.)

Ý tưởng tương tự:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)

Các giải pháp này tận dụng việc tách từ trong một phép gán mảng để tách chuỗi thành các trường. Thật thú vị, giống như read, chia tách từ chung cũng sử dụng $IFSbiến đặc biệt, mặc dù trong trường hợp này, nó được ngụ ý rằng nó được đặt thành giá trị mặc định của <space> <tab> <newline> và do đó, bất kỳ chuỗi nào của một hoặc nhiều IFS các ký tự (hiện là tất cả các ký tự khoảng trắng) được coi là một dấu phân cách trường.

Điều này giải quyết vấn đề của hai cấp độ phân tách được cam kết bởi readvì từ tách chính nó chỉ cấu thành một cấp độ phân tách. Nhưng cũng giống như trước đây, vấn đề ở đây là các trường riêng lẻ trong chuỗi đầu vào có thể chứa các $IFSký tự và do đó chúng sẽ bị phân tách không chính xác trong quá trình tách từ. Điều này xảy ra không phải là trường hợp của bất kỳ chuỗi đầu vào mẫu nào được cung cấp bởi những người trả lời này (tiện lợi như thế nào ...), nhưng tất nhiên điều đó không làm thay đổi thực tế rằng bất kỳ cơ sở mã nào sử dụng thành ngữ này sau đó sẽ gặp rủi ro nổ tung nếu giả định này đã từng bị vi phạm tại một số điểm xuống dòng. Một lần nữa, hãy xem xét ví dụ của tôi về 'Los Angeles, United States, North America'(hoặc 'Los Angeles:United States:North America').

Ngoài ra, tách từ thường theo sau là mở rộng tên tập tin ( hay còn gọi là mở rộng tên đường dẫn aka globbing), trong đó, nếu được thực hiện, sẽ từ có khả năng tham nhũng có chứa các ký tự *, ?hoặc [tiếp theo ](và, nếu extglobđược thiết lập, mảnh ngoặc trước bởi ?, *, +, @, hoặc !) bằng cách kết hợp chúng với các đối tượng hệ thống tệp và mở rộng các từ ("globs") tương ứng. Người trả lời đầu tiên trong số ba người trả lời đã khéo léo khắc phục vấn đề này bằng cách chạy set -ftrước để vô hiệu hóa toàn cầu. Về mặt kỹ thuật điều này hoạt động (mặc dù bạn có thể nên thêmset +f sau đó có khả năng toàn cầu hóa cho mã tiếp theo có thể phụ thuộc vào nó), nhưng không mong muốn phải làm rối với cài đặt shell toàn cầu để hack một hoạt động phân tích cú pháp chuỗi cơ bản trong mã cục bộ.

Một vấn đề khác với câu trả lời này là tất cả các trường trống sẽ bị mất. Điều này có thể hoặc không thể là một vấn đề, tùy thuộc vào ứng dụng.

Lưu ý: Nếu bạn sẽ sử dụng giải pháp này, tốt hơn là sử dụng ${string//:/ }hình thức mở rộng tham số "thay thế mẫu" , thay vì gặp rắc rối khi gọi thay thế lệnh (tạo vỏ), khởi động đường ống và chạy một thực thi bên ngoài ( trhoặc sed), vì mở rộng tham số hoàn toàn là một hoạt động bên trong vỏ. (Ngoài ra, đối với các giải pháp trsed, biến đầu vào phải được trích dẫn kép bên trong thay thế lệnh; nếu không, việc tách từ sẽ có hiệu lực trong echolệnh và có khả năng gây rối với các giá trị trường. Ngoài ra, $(...)hình thức thay thế lệnh được ưu tiên hơn so với cũ`...` hình thức vì nó đơn giản hóa việc lồng các thay thế lệnh và cho phép tô sáng cú pháp tốt hơn bởi các trình soạn thảo văn bản.)


Câu trả lời sai # 3

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

Câu trả lời này gần giống như # 2 . Sự khác biệt là người trả lời đã đưa ra giả định rằng các trường được phân tách bằng hai ký tự, một trong số đó được thể hiện trong mặc định $IFS, còn các trường khác thì không. Ông đã giải quyết trường hợp khá cụ thể này bằng cách loại bỏ ký tự không được đại diện bởi IFS bằng cách sử dụng mở rộng thay thế mẫu và sau đó sử dụng phân tách từ để phân tách các trường trên ký tự phân cách được đại diện IFS còn tồn tại.

Đây không phải là một giải pháp rất chung chung. Hơn nữa, có thể lập luận rằng dấu phẩy thực sự là ký tự phân cách "chính" ở đây, và việc tước nó và sau đó tùy thuộc vào ký tự khoảng trắng để phân tách trường đơn giản là sai. Một lần nữa, hãy xem xét ví dụ của tôi : 'Los Angeles, United States, North America'.

Ngoài ra, một lần nữa, việc mở rộng tên tệp có thể làm hỏng các từ được mở rộng, nhưng điều này có thể được ngăn chặn bằng cách tạm thời vô hiệu hóa tính toàn cầu cho việc gán với set -fvà sau đó set +f.

Ngoài ra, một lần nữa, tất cả các trường trống sẽ bị mất, điều này có thể hoặc không thể là một vấn đề tùy thuộc vào ứng dụng.


Câu trả lời sai # 4

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

Điều này tương tự như # 2# 3 ở chỗ nó sử dụng phân tách từ để hoàn thành công việc, chỉ bây giờ mã được đặt rõ ràng $IFSđể chỉ chứa dấu phân cách trường ký tự đơn có trong chuỗi đầu vào. Cần nhắc lại rằng điều này không thể hoạt động đối với các dấu phân cách trường đa vi khuẩn như dấu phân cách dấu phẩy không gian của OP. Nhưng đối với một dấu phân cách một ký tự như LF được sử dụng trong ví dụ này, nó thực sự gần như hoàn hảo. Các trường không thể vô tình bị tách ra ở giữa như chúng ta đã thấy với các câu trả lời sai trước đó và chỉ có một cấp độ phân tách, theo yêu cầu.

Một vấn đề là việc mở rộng tên tệp sẽ làm hỏng các từ bị ảnh hưởng như được mô tả trước đó, mặc dù một lần nữa điều này có thể được giải quyết bằng cách gói câu lệnh quan trọng vào set -fset +f.

Một vấn đề tiềm năng khác là, do LF đủ điều kiện là "ký tự khoảng trắng IFS" như được xác định trước đó, tất cả các trường trống sẽ bị mất, giống như trong # 2# 3 . Tất nhiên điều này sẽ không thành vấn đề nếu dấu phân cách tình cờ không phải là "ký tự khoảng trắng IFS", và tùy thuộc vào ứng dụng, dù sao thì nó cũng không quan trọng, nhưng nó làm thay đổi tính tổng quát của giải pháp.

Vì vậy, để tóm tắt, giả sử bạn có một dấu phân cách một ký tự và đó không phải là "ký tự khoảng trắng IFS" hoặc bạn không quan tâm đến các trường trống và bạn bao bọc câu lệnh quan trọng set -fset +fsau đó, giải pháp này hoạt động , nhưng nếu không thì không.

(Ngoài ra, vì lợi ích của thông tin, việc gán một biến cho một biến trong bash có thể được thực hiện dễ dàng hơn với $'...'cú pháp, ví dụ IFS=$'\n';.)


Câu trả lời sai # 5

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

Ý tưởng tương tự:

IFS=', ' eval 'array=($string)'

Giải pháp này thực sự là một giao thoa giữa # 1 (trong đó nó được đặt $IFSthành dấu phẩy) và # 2-4 (trong đó nó sử dụng phân tách từ để tách chuỗi thành các trường). Bởi vì điều này, nó phải chịu đựng hầu hết các vấn đề gây ra tất cả các câu trả lời sai ở trên, giống như điều tồi tệ nhất trong tất cả các thế giới.

Ngoài ra, liên quan đến biến thể thứ hai, có vẻ như evalcuộc gọi là hoàn toàn không cần thiết, vì đối số của nó là một chuỗi ký tự đơn được trích dẫn, và do đó được biết đến tĩnh. Nhưng thực sự có một lợi ích rất không rõ ràng khi sử dụng evaltheo cách này. Thông thường, khi bạn chạy một lệnh đơn giản trong đó bao gồm một giao biến chỉ , có nghĩa là không có một từ lệnh thực tế sau đó, việc chuyển nhượng có hiệu lực trong môi trường shell:

IFS=', '; ## changes $IFS in the shell environment

Điều này đúng ngay cả khi lệnh đơn giản liên quan đến nhiều phép gán biến; một lần nữa, miễn là không có từ lệnh, tất cả các phép gán biến đổi đều ảnh hưởng đến môi trường shell:

IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment

Nhưng, nếu gán biến được gắn vào tên lệnh (tôi muốn gọi đây là "gán tiền tố") thì nó không ảnh hưởng đến môi trường shell và thay vào đó chỉ ảnh hưởng đến môi trường của lệnh đã thực hiện, bất kể đó có phải là lệnh dựng sẵn không hoặc bên ngoài:

IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it

Trích dẫn có liên quan từ hướng dẫn bash :

Nếu không có kết quả tên lệnh, các phép gán biến ảnh hưởng đến môi trường shell hiện tại. Mặt khác, các biến được thêm vào môi trường của lệnh đã thực hiện và không ảnh hưởng đến môi trường shell hiện tại.

Có thể khai thác tính năng gán biến này $IFSchỉ để thay đổi tạm thời, điều này cho phép chúng ta tránh toàn bộ gambit lưu và khôi phục giống như đang thực hiện với $OIFSbiến trong biến thể đầu tiên. Nhưng thách thức chúng ta gặp phải ở đây là lệnh chúng ta cần chạy chính nó là một phép gán biến đơn thuần, và do đó nó sẽ không liên quan đến một từ lệnh để làm cho $IFSphép gán tạm thời. Bạn có thể tự suy nghĩ, tại sao không thêm một từ lệnh no-op vào câu lệnh như : builtinđể làm cho $IFSbài tập tạm thời? Điều này không hoạt động vì sau đó nó cũng sẽ thực hiện $arraychuyển nhượng tạm thời:

IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command

Vì vậy, chúng tôi thực sự đang ở tình trạng bế tắc, một chút khó khăn. Nhưng, khi evalchạy mã của nó, nó chạy nó trong môi trường shell, như thể nó là bình thường, mã nguồn tĩnh và do đó chúng ta có thể chạy $arrayphép gán bên trong evalđối số để nó có hiệu lực trong môi trường shell, trong khi $IFSgán tiền tố được thêm tiền tố vào evallệnh sẽ không tồn tại lâu hơn evallệnh. Đây chính xác là thủ thuật đang được sử dụng trong biến thể thứ hai của giải pháp này:

IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does

Vì vậy, như bạn có thể thấy, đây thực sự là một mẹo khá thông minh và hoàn thành chính xác những gì được yêu cầu (ít nhất là liên quan đến hiệu ứng chuyển nhượng) theo một cách khá không rõ ràng. Tôi thực sự không chống lại thủ thuật này nói chung, mặc dù có sự tham gia của eval; chỉ cần cẩn thận để trích dẫn chuỗi đối số để bảo vệ chống lại các mối đe dọa bảo mật.

Nhưng một lần nữa, vì sự kết tụ các vấn đề "tồi tệ nhất trong tất cả các thế giới", đây vẫn là một câu trả lời sai cho yêu cầu của OP.


Câu trả lời sai # 6

IFS=', '; array=(Paris, France, Europe)

IFS=' ';declare -a array=(Paris France Europe)

Ừm ... cái gì? OP có một biến chuỗi cần được phân tích thành một mảng. "Câu trả lời" này bắt đầu với nội dung nguyên văn của chuỗi đầu vào được dán vào một mảng bằng chữ. Tôi đoán đó là một cách để làm điều đó.

Có vẻ như người trả lời có thể đã giả định rằng $IFSbiến đó ảnh hưởng đến tất cả phân tích cú pháp bash trong tất cả các bối cảnh, điều này không đúng. Từ hướng dẫn bash:

IFS     Bộ tách trường nội bộ được sử dụng để phân tách từ sau khi mở rộng và để phân tách các dòng thành các từ bằng lệnh dựng sẵn đã đọc . Giá trị mặc định là <dấu cách> <tab> <dòng mới> .

Vì vậy, $IFSbiến đặc biệt thực sự chỉ được sử dụng trong hai bối cảnh: (1) phân tách từ được thực hiện sau khi mở rộng (nghĩa là không phân tích mã nguồn bash) và (2) để phân tách các dòng đầu vào thành các từ của readnội dung.

Hãy để tôi cố gắng làm cho điều này rõ ràng hơn. Tôi nghĩ rằng có thể tốt để rút ra sự khác biệt giữa phân tích cú phápthực thi . Trước tiên Bash phải phân tích mã nguồn, rõ ràng là một sự kiện phân tích cú pháp , và sau đó nó thực thi mã, đó là khi mở rộng đi vào hình ảnh. Mở rộng thực sự là một sự kiện thực hiện . Hơn nữa, tôi có vấn đề với mô tả về $IFSbiến mà tôi vừa trích dẫn ở trên; thay vì nói rằng việc tách từ được thực hiện sau khi mở rộng , tôi sẽ nói rằng việc tách từ được thực hiện trong quá trình mở rộng, hoặc, có lẽ chính xác hơn nữa, việc tách từ là một phần củaquá trình mở rộng. Cụm từ "tách từ" chỉ đề cập đến bước mở rộng này; nó không bao giờ nên được sử dụng để chỉ phân tích cú pháp mã nguồn bash, mặc dù không may là các tài liệu dường như ném xung quanh các từ "split" và "words" rất nhiều. Đây là một đoạn trích có liên quan từ phiên bản linux.die.net của hướng dẫn bash:

Mở rộng được thực hiện trên dòng lệnh sau khi nó đã được chia thành các từ. Có nhiều loại bảy trong việc mở rộng thực hiện: mở rộng cú đúp , dấu ngã mở rộng , tham số và mở rộng biến , thay thế lệnh , mở rộng số học , tách từ , và mở rộng tên đường dẫn .

Thứ tự mở rộng là: mở rộng cú đúp; mở rộng dấu ngã, mở rộng tham số và biến, mở rộng số học và thay thế lệnh (được thực hiện theo kiểu từ trái sang phải); tách từ; và mở rộng tên đường dẫn.

Bạn có thể lập luận phiên bản GNU của hướng dẫn sử dụng tốt hơn một chút, vì nó thay cho từ "mã thông báo" thay vì "từ" trong câu đầu tiên của phần Mở rộng:

Việc mở rộng được thực hiện trên dòng lệnh sau khi nó được chia thành các mã thông báo.

Điểm quan trọng là, $IFSkhông thay đổi cách bash phân tích mã nguồn. Phân tích mã nguồn bash thực sự là một quá trình rất phức tạp, bao gồm sự công nhận các yếu tố khác nhau của ngữ pháp shell, chẳng hạn như chuỗi lệnh, danh sách lệnh, đường ống, mở rộng tham số, thay thế số học và thay thế lệnh. Đối với hầu hết các phần, quá trình phân tích cú pháp bash không thể được thay đổi bằng các hành động ở cấp độ người dùng như gán biến (thực ra, có một số ngoại lệ nhỏ cho quy tắc này; ví dụ: xem các cài đặt shell khác nhaucompatxx, có thể thay đổi các khía cạnh nhất định của hành vi phân tích cú pháp nhanh chóng). Các "từ" / "mã thông báo" ngược dòng phát sinh từ quá trình phân tích cú pháp phức tạp này sau đó được mở rộng theo quy trình "mở rộng" chung như được chia nhỏ trong đoạn trích tài liệu ở trên, trong đó phân tách từ của văn bản mở rộng (mở rộng?) từ ngữ chỉ đơn giản là một bước của quá trình đó. Chia tách từ chỉ chạm vào văn bản đã được nhổ ra khỏi bước mở rộng trước đó; nó không ảnh hưởng đến văn bản theo nghĩa đen đã được phân tích cú pháp ngay khi tắt nguồn.


Câu trả lời sai # 7

string='first line
        second line
        third line'

while read -r line; do lines+=("$line"); done <<<"$string"

Đây là một trong những giải pháp tốt nhất. Lưu ý rằng chúng tôi đang quay lại sử dụng read. Không phải tôi đã nói trước đó readlà không phù hợp bởi vì nó thực hiện hai cấp độ phân tách, khi chúng ta chỉ cần một cấp độ? Mẹo ở đây là bạn có thể gọi readtheo cách mà nó thực sự chỉ thực hiện một cấp độ phân tách, cụ thể bằng cách tách ra chỉ một trường cho mỗi lần gọi, đòi hỏi chi phí phải gọi liên tục trong một vòng lặp. Đó là một chút ánh sáng của bàn tay, nhưng nó hoạt động.

Nhưng có vấn đề. Đầu tiên: Khi bạn cung cấp ít nhất một đối số NAME cho readnó, nó sẽ tự động bỏ qua khoảng trắng hàng đầu và dấu trong mỗi trường được tách ra khỏi chuỗi đầu vào. Điều này xảy ra cho dù $IFSđược đặt thành giá trị mặc định của nó hay không, như được mô tả trước đó trong bài viết này. Bây giờ, OP có thể không quan tâm đến điều này cho trường hợp sử dụng cụ thể của mình và trên thực tế, nó có thể là một tính năng mong muốn của hành vi phân tích cú pháp. Nhưng không phải ai muốn phân tích một chuỗi thành các trường cũng sẽ muốn điều này. Tuy nhiên, có một giải pháp: Cách sử dụng hơi không rõ ràng readlà truyền các đối số NAME không . Trong trường hợp này, readsẽ lưu trữ toàn bộ dòng đầu vào mà nó nhận được từ luồng đầu vào trong một biến có tên $REPLYvà, như một phần thưởng, nó khôngdải khoảng trắng hàng đầu và dấu vết từ giá trị. Đây là một cách sử dụng rất mạnh mẽ readmà tôi đã khai thác thường xuyên trong sự nghiệp lập trình shell của mình. Đây là một minh chứng cho sự khác biệt trong hành vi:

string=$'  a  b  \n  c  d  \n  e  f  '; ## input string

a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a  b" [1]="c  d" [2]="e  f") ## read trimmed surrounding whitespace

a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="  a  b  " [1]="  c  d  " [2]="  e  f  ") ## no trimming

Vấn đề thứ hai với giải pháp này là nó không thực sự giải quyết trường hợp của dấu tách trường tùy chỉnh, chẳng hạn như dấu phẩy của OP. Như trước đây, phân tách đa vi khuẩn không được hỗ trợ, đó là một hạn chế đáng tiếc của giải pháp này. Chúng ta có thể cố gắng ít nhất phân tách bằng dấu phẩy bằng cách chỉ định dấu phân cách cho -dtùy chọn, nhưng hãy xem điều gì xảy ra:

string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")

Có thể dự đoán, khoảng trắng xung quanh không được đếm đã được kéo vào các giá trị trường và do đó, điều này sẽ phải được sửa sau đó thông qua các thao tác cắt xén (điều này cũng có thể được thực hiện trực tiếp trong vòng lặp while). Nhưng có một lỗi rõ ràng khác: Châu Âu đang mất tích! Chuyện gì đã xảy ra với nó? Câu trả lời là readtrả về mã trả về không thành công nếu nó chạm vào phần cuối của tệp (trong trường hợp này chúng ta có thể gọi nó là phần cuối của chuỗi) mà không gặp phải dấu kết thúc trường cuối cùng trên trường cuối cùng. Điều này làm cho vòng lặp while bị hỏng sớm và chúng ta mất trường cuối cùng.

Về mặt kỹ thuật, lỗi này cũng ảnh hưởng đến các ví dụ trước đó; sự khác biệt ở đây là trình phân tách trường được lấy là LF, là mặc định khi bạn không chỉ định -dtùy chọn và cơ chế <<<("đây-chuỗi") sẽ tự động nối thêm một chuỗi vào chuỗi ngay trước khi nó cung cấp cho nó như là đầu vào cho lệnh. Do đó, trong những trường hợp đó, chúng tôi đã vô tình giải quyết vấn đề của trường cuối cùng bị bỏ bằng cách vô tình nối thêm một đầu cuối giả vào đầu vào. Chúng ta hãy gọi giải pháp này là giải pháp "dummy-terminator". Chúng ta có thể áp dụng giải pháp kết thúc giả theo cách thủ công cho bất kỳ dấu phân cách tùy chỉnh nào bằng cách tự nối nó với chuỗi đầu vào khi khởi tạo nó trong chuỗi ở đây:

a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

Có, vấn đề được giải quyết. Một giải pháp khác là chỉ phá vỡ vòng lặp while nếu cả hai (1) readlỗi trả về và (2) $REPLYtrống, nghĩa readlà không thể đọc bất kỳ ký tự nào trước khi nhấn vào cuối tệp. Bản giới thiệu:

a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

Cách tiếp cận này cũng cho thấy hoạt động bí mật của LF tự động được thêm vào chuỗi ở đây bởi <<<toán tử chuyển hướng. Tất nhiên nó có thể bị loại bỏ một cách riêng biệt thông qua một hoạt động cắt tỉa rõ ràng như được mô tả trước đây, nhưng rõ ràng phương pháp tiếp cận giả bằng tay giải quyết nó trực tiếp, vì vậy chúng ta có thể thực hiện điều đó. Giải pháp kết thúc giả thủ công thực sự khá thuận tiện ở chỗ nó giải quyết được cả hai vấn đề này (vấn đề trường rơi cuối cùng và vấn đề được nối thêm) trong một lần.

Vì vậy, về tổng thể, đây là một giải pháp khá mạnh mẽ. Điểm yếu duy nhất còn lại là thiếu sự hỗ trợ cho các dấu phân cách đa vi khuẩn, mà tôi sẽ giải quyết sau.


Câu trả lời sai # 8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(Đây thực sự là từ cùng một bài với số 7 ; người trả lời đã cung cấp hai giải pháp trong cùng một bài.)

Nội dung readarray, đó là một từ đồng nghĩa với mapfile, là lý tưởng. Đó là một lệnh dựng sẵn phân tích cú pháp bytestream thành một biến mảng trong một lần bắn; không lộn xộn với các vòng lặp, điều kiện, thay thế, hoặc bất cứ điều gì khác. Và nó không lén lút tước bất kỳ khoảng trắng nào khỏi chuỗi đầu vào. Và (nếu -Okhông được cung cấp), nó sẽ xóa mảng mục tiêu một cách thuận tiện trước khi gán cho nó. Nhưng nó vẫn chưa hoàn hảo, do đó tôi chỉ trích nó là "câu trả lời sai".

Đầu tiên, chỉ để giải quyết vấn đề này, lưu ý rằng, giống như hành vi readkhi thực hiện phân tích trường, readarraybỏ trường theo dõi nếu nó trống. Một lần nữa, đây có lẽ không phải là mối quan tâm của OP, nhưng nó có thể dành cho một số trường hợp sử dụng. Tôi sẽ trở lại vấn đề này trong giây lát.

Thứ hai, như trước đây, nó không hỗ trợ các dấu phân cách đa vi khuẩn. Tôi sẽ đưa ra một sửa chữa cho điều này trong một thời điểm là tốt.

Thứ ba, giải pháp như được viết không phân tích chuỗi đầu vào của OP và trên thực tế, nó không thể được sử dụng như là để phân tích nó. Tôi cũng sẽ mở rộng về điều này trong giây lát.

Vì những lý do trên, tôi vẫn coi đây là một "câu trả lời sai" cho câu hỏi của OP. Dưới đây tôi sẽ đưa ra những gì tôi coi là câu trả lời đúng.


Câu trả lời đúng

Đây là một nỗ lực ngây thơ để làm cho số 8 hoạt động bằng cách chỉ định -dtùy chọn:

string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

Chúng tôi thấy kết quả này giống hệt với kết quả mà chúng tôi nhận được từ cách tiếp cận có điều kiện kép của readgiải pháp lặp được thảo luận trong # 7 . Chúng ta gần như có thể giải quyết điều này bằng thủ thuật kết thúc giả thủ công:

readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')

Vấn đề ở đây là việc readarraybảo tồn trường theo dõi, vì <<<toán tử chuyển hướng đã nối thêm LF vào chuỗi đầu vào, và do đó trường theo dõi không trống (nếu không nó sẽ bị loại bỏ). Chúng ta có thể xử lý vấn đề này bằng cách bỏ qua phần tử mảng cuối cùng sau thực tế:

readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

Hai vấn đề duy nhất còn tồn tại, thực sự có liên quan, là (1) khoảng trắng bên ngoài cần được cắt bớt và (2) thiếu sự hỗ trợ cho các dấu phân cách đa vi khuẩn.

Khoảng trắng tất nhiên có thể được cắt bớt sau đó (ví dụ, xem Làm thế nào để cắt khoảng trắng từ một biến Bash? ). Nhưng nếu chúng ta có thể hack một dấu phân cách đa vi khuẩn, thì điều đó sẽ giải quyết cả hai vấn đề trong một lần bắn.

Thật không may, không có cách nào trực tiếp để làm cho một dấu phân cách đa vi khuẩn hoạt động. Giải pháp tốt nhất mà tôi nghĩ đến là xử lý trước chuỗi đầu vào để thay thế dấu phân cách đa vi khuẩn bằng dấu phân cách một ký tự sẽ được đảm bảo không va chạm với nội dung của chuỗi đầu vào. Ký tự duy nhất có bảo đảm này là byte NUL . Điều này là do, trong bash (mặc dù không phải trong zsh, tình cờ), các biến không thể chứa byte NUL. Bước tiền xử lý này có thể được thực hiện nội tuyến trong một sự thay thế quá trình. Đây là cách thực hiện bằng awk :

readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

Ở đó, cuối cùng! Giải pháp này sẽ không phân tách nhầm các trường ở giữa, sẽ không bị cắt sớm, sẽ không bỏ các trường trống, sẽ không bị hỏng khi mở rộng tên tệp, sẽ không tự động thoát khỏi khoảng trắng dẫn đầu và dấu vết, sẽ không để lại một khoảng trống cuối cùng, không yêu cầu các vòng lặp và không giải quyết cho một dấu phân cách một ký tự.


Giải pháp cắt tỉa

Cuối cùng, tôi muốn chứng minh giải pháp cắt tỉa khá phức tạp của riêng mình bằng cách sử dụng -C callbacktùy chọn tối nghĩa của readarray. Thật không may, tôi đã hết phòng chống lại giới hạn 30.000 ký tự hà khắc của Stack Overflow, vì vậy tôi sẽ không thể giải thích điều đó. Tôi sẽ để nó như một bài tập cho người đọc.

function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

8
Cũng có thể hữu ích để lưu ý (mặc dù có thể hiểu rằng bạn không có chỗ để làm như vậy) rằng -dtùy chọn readarrayxuất hiện đầu tiên trong Bash 4.4.
fbicknel

2
Câu trả lời tuyệt vời (+1). Nếu bạn thay đổi awk của mình thành awk '{ gsub(/,[ ]+|$/,"\0"); print }'và loại bỏ sự kết hợp của trận chung kết ", " thì bạn không cần phải trải qua môn thể dục dụng cụ để loại bỏ kỷ lục cuối cùng. Vì vậy: readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")trên Bash mà hỗ trợ readarray. Lưu ý rằng phương pháp của bạn là Bash 4.4+ Tôi nghĩ vì -dtrongreadarray
dawg

3
@datUser Thật không may. Phiên bản bash của bạn phải quá cũ readarray. Trong trường hợp này, bạn có thể sử dụng giải pháp tốt nhất thứ hai được xây dựng trên read. Tôi đang đề cập đến điều này: a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";(với sự awkthay thế nếu bạn cần hỗ trợ phân tách đa vi khuẩn). Hãy cho tôi biết nếu bạn gặp phải bất kỳ vấn đề nào; Tôi khá chắc chắn rằng giải pháp này sẽ hoạt động trên các phiên bản bash khá cũ, trở lại phiên bản 2-một cái gì đó, được phát hành như hai thập kỷ trước.
bgoldst

1
Wow, thật là một câu trả lời tuyệt vời! Hee hee, phản ứng của tôi: bỏ kịch bản bash và bắn con trăn!
artfulrobot

1
@datUser bash trên OSX vẫn bị kẹt ở mức 3.2 (phát hành năm 2007); Tôi đã sử dụng bash được tìm thấy trong Homebrew để nhận các phiên bản bash 4.X trên OS X
JDS

222

Đây là một cách mà không cần thiết lập IFS:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

Ý tưởng là sử dụng thay thế chuỗi:

${string//substring/replacement}

để thay thế tất cả các kết quả của chuỗi con $ bằng khoảng trắng và sau đó sử dụng chuỗi được thay thế để khởi tạo một mảng:

(element1 element2 ... elementN)

Lưu ý: câu trả lời này sử dụng toán tử split + global . Vì vậy, để ngăn chặn việc mở rộng một số ký tự (chẳng hạn như *), nên tạm dừng toàn cầu hóa cho tập lệnh này.


1
Sử dụng phương pháp này ... cho đến khi tôi bắt gặp một chuỗi dài để phân chia. 100% CPU trong hơn một phút (sau đó tôi đã giết nó). Thật đáng tiếc vì phương pháp này cho phép phân tách bằng một chuỗi, không phải một số ký tự trong IFS.
Werner Lehmann

Đối với tôi, 100% thời gian CPU trong hơn một phút nghe có vẻ như có gì đó không đúng ở đâu đó. Chuỗi đó dài bao nhiêu, nó có kích thước MB hay GB? Tôi nghĩ, thông thường, nếu bạn chỉ cần tách một chuỗi nhỏ, bạn muốn ở lại Bash, nhưng nếu đó là một tệp lớn, tôi sẽ thực hiện một cái gì đó như Perl để làm điều đó.

12
CẢNH BÁO: Chỉ gặp vấn đề với phương pháp này. Nếu bạn có một phần tử có tên *, bạn cũng sẽ nhận được tất cả các phần tử của cwd của mình. do đó, chuỗi = "1: 2: 3: 4: *" sẽ cho một số kết quả bất ngờ và có thể nguy hiểm tùy thuộc vào việc triển khai của bạn. Không gặp lỗi tương tự với (IFS = ',' read -a mảng <<< "$ string") và cách này có vẻ an toàn khi sử dụng.
Dieter Gribnitz

4
trích dẫn ${string//:/ }ngăn chặn sự mở rộng vỏ
Andrew White

1
Tôi đã phải sử dụng như sau trên OSX: array=(${string//:/ })
Mark Thomson

95
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

In ba


8
Tôi thực sự thích phương pháp này. Đơn giản.
Tômwagon

4
Tôi đã sao chép và dán cái này và nó không hoạt động với echo, nhưng đã hoạt động khi tôi sử dụng nó trong một vòng lặp for.
Ben

2
Điều này không hoạt động như đã nêu. @ Jmoney38 hoặc tômwagon nếu bạn có thể dán cái này vào một thiết bị đầu cuối và nhận được đầu ra mong muốn, vui lòng dán kết quả ở đây.
abalter

2
@abalter Làm việc cho tôi với a=($(echo $t | tr ',' "\n")). Kết quả tương tự với a=($(echo $t | tr ',' ' ')).

@procrastinator Tôi vừa thử nó trong VERSION="16.04.2 LTS (Xenial Xerus)"một bashcái vỏ, và cái cuối cùng echochỉ in một dòng trống. Phiên bản nào của Linux và bạn đang sử dụng shell nào? Thật không may, không thể hiển thị phiên cuối trong một bình luận.
abalter

29

Đôi khi, điều đó xảy ra với tôi rằng phương pháp được mô tả trong câu trả lời được chấp nhận không hoạt động, đặc biệt nếu dấu phân cách là trả về vận chuyển.
Trong những trường hợp đó, tôi đã giải quyết theo cách này:

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

for line in "${lines[@]}"
    do
        echo "--> $line"
done

2
+1 Điều này hoàn toàn làm việc cho tôi. Tôi cần đặt nhiều chuỗi, chia cho một dòng mới, vào một mảng và read -a arr <<< "$strings"không hoạt động IFS=$'\n'.
Stefan van den Akker


Điều này không hoàn toàn trả lời câu hỏi ban đầu.
Mike

29

Câu trả lời được chấp nhận hoạt động cho các giá trị trong một dòng.
Nếu biến có vài dòng:

string='first line
        second line
        third line'

Chúng ta cần một lệnh rất khác nhau để có được tất cả các dòng:

while read -r line; do lines+=("$line"); done <<<"$string"

Hoặc đơn giản hơn nhiều bash readarray :

readarray -t lines <<<"$string"

In tất cả các dòng rất dễ dàng tận dụng tính năng printf:

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]

2
Mặc dù không phải mọi giải pháp đều hiệu quả cho mọi tình huống, việc bạn đề cập đến việc đọc lại ... đã thay thế hai giờ cuối cùng của tôi bằng 5 phút ... bạn đã nhận được phiếu bầu của tôi
Tức giận 84


6

Chìa khóa để phân tách chuỗi của bạn thành một mảng là dấu phân cách nhiều ký tự của ", ". Bất kỳ giải pháp sử dụngIFS cho các dấu phân cách nhiều ký tự vốn đã sai vì IFS là một tập hợp các ký tự đó, không phải là một chuỗi.

Nếu bạn gán IFS=", "thì chuỗi sẽ ngắt trên EITHER ","HOẶC " "hoặc bất kỳ kết hợp nào của chúng không phải là biểu diễn chính xác của dấu phân cách hai ký tự của", " .

Bạn có thể sử dụng awkhoặc sedđể tách chuỗi, với quá trình thay thế:

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

Sẽ hiệu quả hơn khi sử dụng regex bạn trực tiếp trong Bash:

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

Với hình thức thứ hai, không có vỏ phụ và nó sẽ nhanh hơn.


Chỉnh sửa bởi bgoldst: Dưới đây là một số điểm chuẩn so sánh readarraygiải pháp của tôi với giải pháp regex của dawg và tôi cũng đưa vào readgiải pháp cho sự tàn phá của nó (lưu ý: Tôi đã sửa đổi một chút giải pháp regex để hài hòa hơn với giải pháp của tôi) bài đăng):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##

Giải pháp rất tuyệt! Tôi chưa bao giờ nghĩ đến việc sử dụng một vòng lặp trên một trận đấu regex, sử dụng tiện lợi $BASH_REMATCH. Nó hoạt động, và thực sự tránh sinh ra các subshells. +1 từ tôi. Tuy nhiên, bằng cách chỉ trích, bản thân regex là một chút không lý tưởng, trong đó có vẻ như bạn bị buộc phải sao chép một phần của mã thông báo dấu phân cách (cụ thể là dấu phẩy) để giải quyết việc thiếu hỗ trợ cho các số nhân không tham lam (cũng nhìn) trong ERE (hương vị regex "mở rộng" được tích hợp vào bash). Điều này làm cho nó ít chung chung và mạnh mẽ.
bgoldst

Thứ hai, tôi đã thực hiện một số điểm chuẩn, và mặc dù hiệu suất tốt hơn các giải pháp khác cho các chuỗi nhỏ, nó trở nên tồi tệ hơn do việc xây dựng lại chuỗi lặp đi lặp lại, trở thành thảm họa đối với các chuỗi rất lớn. Xem chỉnh sửa của tôi để câu trả lời của bạn.
bgoldst

@bgoldst: Thật là một điểm chuẩn tuyệt vời! Để bảo vệ regex, trong 10 hoặc 100 nghìn trường (những gì regex đang phân tách) có thể sẽ có một số dạng bản ghi (như \ncác dòng văn bản được phân tách) bao gồm các trường đó để có thể xảy ra chậm lại thảm khốc. Nếu bạn có một chuỗi với 100.000 trường - có thể Bash không lý tưởng ;-) Cảm ơn vì điểm chuẩn. Tôi đã học được một hoặc hai điều.
dawg

4

Pure bash giải pháp phân cách đa ký tự.

Như những người khác đã chỉ ra trong chủ đề này, câu hỏi của OP đã đưa ra một ví dụ về chuỗi được phân tách bằng dấu phẩy được phân tách thành một mảng, nhưng không cho biết anh ấy / cô ấy chỉ quan tâm đến dấu phân cách dấu phẩy, dấu phân cách ký tự đơn hay đa ký tự phân định

Vì Google có xu hướng xếp hạng câu trả lời này ở hoặc gần đầu kết quả tìm kiếm, tôi muốn cung cấp cho độc giả câu trả lời mạnh mẽ cho câu hỏi về nhiều dấu phân cách ký tự, vì điều đó cũng được đề cập trong ít nhất một câu trả lời.

Nếu bạn đang tìm kiếm giải pháp cho vấn đề phân cách nhiều ký tự, tôi khuyên bạn nên xem lại bài đăng của Mallikarjun M , đặc biệt là phản hồi từ gniourf_gniourf , người cung cấp giải pháp BASH thuần túy thanh lịch này bằng cách sử dụng mở rộng tham số:

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

Liên kết đến bình luận được trích dẫn / bài viết được tham khảo

Liên kết đến câu hỏi được trích dẫn: Làm thế nào để tách một chuỗi trên một dấu phân cách nhiều ký tự trong bash?


1
Xem bình luận của tôi cho một cách tiếp cận tương tự nhưng được cải thiện.
xebeche

3

Điều này hoạt động với tôi trên OSX:

string="1 2 3 4 5"
declare -a array=($string)

Nếu chuỗi của bạn có dấu phân cách khác nhau, chỉ cần thay thế thứ 1 bằng dấu cách:

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

Đơn giản :-)


Hoạt động cho cả Bash và Zsh là một lợi thế!
Elijah W. Gagne

2

Một cách khác để làm điều đó mà không sửa đổi IFS:

read -r -a myarray <<< "${string//, /$IFS}"

Thay vì thay đổi IFS để phù hợp với dấu phân cách mong muốn của chúng tôi, chúng tôi có thể thay thế tất cả các lần xuất hiện của dấu phân cách mong muốn ", "bằng nội dung $IFSthông qua "${string//, /$IFS}".

Có lẽ điều này sẽ chậm cho các chuỗi rất lớn mặc dù?

Điều này dựa trên câu trả lời của Dennis Williamson.


2

Tôi đã xem qua bài đăng này khi tìm cách phân tích một đầu vào như: word1, word2, ...

Không ai ở trên giúp tôi. giải quyết nó bằng cách sử dụng awk. Nếu nó giúp được ai đó:

STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
        echo "This is the word $word"
done

1

Thử cái này

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

Thật đơn giản. Nếu bạn muốn, bạn cũng có thể thêm một khai báo (và cũng xóa dấu phẩy):

IFS=' ';declare -a array=(Paris France Europe)

IFS được thêm vào để hoàn tác ở trên nhưng nó hoạt động mà không có nó trong một ví dụ bash mới


1

Chúng ta có thể sử dụng lệnh tr để tách chuỗi thành đối tượng mảng. Nó hoạt động cả MacOS và Linux

  #!/usr/bin/env bash
  currentVersion="1.0.0.140"
  arrayData=($(echo $currentVersion | tr "." "\n"))
  len=${#arrayData[@]}
  for (( i=0; i<=$((len-1)); i++ )); do 
       echo "index $i - value ${arrayData[$i]}"
  done

Một tùy chọn khác sử dụng lệnh IFS

IFS='.' read -ra arrayData <<< "$currentVersion"
#It is the same as tr
arrayData=($(echo $currentVersion | tr "." "\n"))

#Print the split string
for i in "${arrayData[@]}"
do
    echo $i
done

0

Dùng cái này:

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe

3
Xấu: chịu sự chia tách từ và mở rộng tên đường dẫn. Xin đừng làm sống lại những câu hỏi cũ với câu trả lời tốt để đưa ra câu trả lời xấu.
gniourf_gniourf

2
Đây có thể là một câu trả lời xấu, nhưng nó vẫn là một câu trả lời hợp lệ. Flaggers / người đánh giá: Đối với câu trả lời không chính xác như câu hỏi này, downvote, không xóa!
Scott Weldon

2
@gniourf_gniourf Bạn có thể giải thích tại sao đó là một câu trả lời không? Tôi thực sự không hiểu khi nó thất bại.
George Sovetov

3
@GeorgeSovetov: Như tôi đã nói, nó có thể bị chia tách từ và mở rộng tên đường dẫn. Tổng quát hơn, việc tách một chuỗi thành một mảng như array=( $string )là một antipotype (đáng buồn là rất phổ biến): sự phân tách từ xảy ra : string='Prague, Czech Republic, Europe'; Mở rộng tên đường dẫn xảy ra: string='foo[abcd],bar[efgh]'sẽ thất bại nếu bạn có một tệp có tên, ví dụ, foodhoặc barftrong thư mục của bạn. Việc sử dụng hợp lệ duy nhất của một cấu trúc như vậy là khi toàn stringcầu.
gniourf_gniourf

0

CẬP NHẬT: Đừng làm điều này, do vấn đề với eval.

Với nghi lễ ít hơn một chút:

IFS=', ' eval 'array=($string)'

ví dụ

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar

4
Ác ma là ác! đừng làm điều này
caesarsol

1
Pfft. Không. Nếu bạn viết kịch bản đủ lớn để vấn đề này xảy ra, bạn đã làm sai. Trong mã ứng dụng, eval là xấu xa. Trong kịch bản shell, nó phổ biến, cần thiết và không quan trọng.
dùng1009908

2
đặt một $biến của bạn và bạn sẽ thấy ... Tôi viết nhiều kịch bản và tôi chưa bao giờ phải sử dụng mộteval
caesarsol

2
Bạn nói đúng, điều này chỉ có thể sử dụng được khi đầu vào được biết là sạch. Không phải là một giải pháp mạnh mẽ.
dùng1009908

Lần duy nhất tôi từng phải sử dụng eval, là cho một ứng dụng tự tạo mã / mô-đun của riêng mình ... VÀ điều này không bao giờ có bất kỳ hình thức nhập liệu nào của người dùng ...
Angry 84

0

Đây là hack của tôi!

Chia chuỗi bằng chuỗi là một điều khá nhàm chán khi sử dụng bash. Điều gì xảy ra là chúng tôi có các cách tiếp cận hạn chế chỉ hoạt động trong một vài trường hợp (chia cho ";", "/", "." V.v.) hoặc chúng tôi có nhiều tác dụng phụ trong đầu ra.

Cách tiếp cận dưới đây đã yêu cầu một số thao tác, nhưng tôi tin rằng nó sẽ hoạt động cho hầu hết các nhu cầu của chúng tôi!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: /dba/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split "$STRING_TO_SPLIT" "bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo " > -----------------------------------------"
    echo "${F_SPLIT_R[$i]}"
    echo " < -----------------------------------------"
done

if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo " > -----------------------------------------"
    echo "The strings are the same!"
    echo " < -----------------------------------------"
fi

0

Đối với các yếu tố đa năng, tại sao không phải là một cái gì đó như

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT

-1

Một cách khác sẽ là:

string="Paris, France, Europe"
IFS=', ' arr=(${string})

Bây giờ các phần tử của bạn được lưu trữ trong mảng "mảng". Để lặp qua các phần tử:

for i in ${arr[@]}; do echo $i; done

1
Tôi bao gồm ý tưởng này trong câu trả lời của tôi ; xem câu trả lời sai # 5 (bạn có thể đặc biệt quan tâm đến cuộc thảo luận của tôi về evalmánh khóe). Giải pháp của bạn $IFSđược đặt thành giá trị dấu phẩy sau thực tế.
bgoldst

-1

Vì có rất nhiều cách để giải quyết vấn đề này, chúng ta hãy bắt đầu bằng cách xác định những gì chúng ta muốn thấy trong giải pháp của mình.

  1. Bash cung cấp một nội dung readarraycho mục đích này. Hãy sử dụng nó.
  2. Tránh các thủ thuật xấu xí và không cần thiết như thay đổi IFS, lặp, sử dụng evalhoặc thêm một yếu tố phụ sau đó loại bỏ nó.
  3. Tìm một cách tiếp cận đơn giản, dễ đọc, có thể dễ dàng thích nghi với các vấn đề tương tự.

Các readarraylệnh là đơn giản nhất để sử dụng với dòng mới là dấu phân cách. Với các dấu phân cách khác, nó có thể thêm một phần tử phụ vào mảng. Cách tiếp cận sạch nhất là trước tiên điều chỉnh đầu vào của chúng ta thành một hình thức hoạt động độc đáo readarraytrước khi chuyển nó vào.

Đầu vào trong ví dụ này không có dấu phân cách đa vi khuẩn. Nếu chúng ta áp dụng một chút thông thường, thì tốt nhất là đầu vào được phân tách bằng dấu phẩy mà mỗi phần tử có thể cần được cắt bớt. Giải pháp của tôi là chia đầu vào bằng dấu phẩy thành nhiều dòng, cắt từng phần tử và chuyển tất cả sang readarray.

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'

-2

Một cách tiếp cận khác có thể là:

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

Sau 'mảng' này là một mảng có bốn chuỗi. Điều này không yêu cầu giao dịch IFS hoặc đọc hoặc bất kỳ nội dung đặc biệt nào khác do đó đơn giản và trực tiếp hơn nhiều.


Tương tự (đáng buồn phổ biến) antipotype như các câu trả lời khác: chịu sự chia tách từ và mở rộng tên tệp.
gniourf_gniourf
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.