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 $IFS
biế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à read
chia 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 $IFS
có 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> và <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> và <đượ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> và <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 $IFS
cù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 $string
biến xảy ra có chứa bất kỳ LF nào, thì read
sẽ dừng xử lý một khi nó gặp LF đầu tiên. Nội dung read
chỉ 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ỉ để read
tuyê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 read
nộ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à read
nộ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 read
nộ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à read
luô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 $IFS
biế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 read
vì 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 $IFS
ký 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 -f
trướ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 ( tr
hoặ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 tr
và sed
, 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 echo
lệ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 -f
và 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 và # 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 -f
và set +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 và # 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 -f
và set +f
sau đó, 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 $IFS
thà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ư eval
cuộ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 eval
theo 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 $IFS
chỉ để 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 $OIFS
biế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 $IFS
phé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 $IFS
bà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 $array
chuyể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 eval
chạ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 $array
phép gán bên trong eval
đối số để nó có hiệu lực trong môi trường shell, trong khi $IFS
gán tiền tố được thêm tiền tố vào eval
lệnh sẽ không tồn tại lâu hơn eval
lệ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 $IFS
biế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, $IFS
biế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 read
nộ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áp và thự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ề $IFS
biế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à, $IFS
khô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 đó read
là 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 read
theo 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 read
nó, 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 read
là truyền các đối số NAME không . Trong trường hợp này, read
sẽ 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 $REPLY
và, 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ẽ read
mà 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 -d
tù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à read
trả 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 -d
tù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) read
lỗi trả về và (2) $REPLY
trống, nghĩa read
là 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 -O
khô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 read
khi thực hiện phân tích trường, readarray
bỏ 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 -d
tù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 read
giả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 readarray
bả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 callback
tù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")
,
(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/