Câu trả lời đơn giản là: thu gọn tất cả các dấu phân cách thành một (đầu tiên).
Điều đó đòi hỏi một vòng lặp (chạy ít hơn log(N)
lần):
var=':a bc::d ef:#$%_+$$% ^%&*(*&*^
$#,.::ghi::*::' # a long test string.
d=':@!#$%^&*()_+,.' # delimiter set
f=${d:0:1} # first delimiter
v=${var//["$d"]/"$f"}; # convert all delimiters to
: # the first of the delimiter set.
tmp=$v # temporal variable (v).
while
tmp=${tmp//["$f"]["$f"]/"$f"}; # collapse each two delimiters to one
[[ "$tmp" != "$v" ]]; # If there was a change
do
v=$tmp; # actualize the value of the string.
done
Tất cả những gì còn lại phải làm là phân chia chính xác chuỗi trên một dấu phân cách và in nó:
readarray -td "$f" arr < <(printf '%s%s' "$v"'' "$f")
printf '<%s>' "${arr[@]}" ; echo
Không cần set -f
cũng không phải thay đổi IFS.
Đã thử nghiệm với không gian, dòng mới và ký tự toàn cầu. Tất cả công việc. Khá chậm (như một vòng lặp shell nên được dự kiến).
Nhưng chỉ dành cho bash (bash 4.4+ vì tùy chọn -d
để đọc lại).
sh
Một phiên bản shell không thể sử dụng một mảng, mảng duy nhất có sẵn là các tham số vị trí.
Việc sử dụng tr -s
chỉ là một dòng (IFS không thay đổi trong tập lệnh):
set -f; IFS=$f command eval set -- '$(echo "$var" | tr -s "$d" "[$f*]" )""'
Và in nó:
printf '<%s>' "$@" ; echo
Vẫn chậm, nhưng không nhiều nữa.
Lệnh command
không hợp lệ trong Bourne.
Trong zsh, command
chỉ gọi các lệnh bên ngoài và làm cho eval thất bại nếu command
được sử dụng.
Trong ksh, ngay cả với command
, giá trị của IFS được thay đổi trong phạm vi toàn cầu.
Và command
làm cho sự phân tách thất bại trong các shell liên quan đến mksh (mksh, lksh, posh) Việc xóa lệnh command
làm cho mã chạy trên nhiều shell hơn. Nhưng: loại bỏ command
sẽ làm cho IFS giữ lại giá trị của nó trong hầu hết các shell (eval là một nội dung đặc biệt) ngoại trừ trong bash (không có chế độ posix) và zsh ở chế độ mặc định (không mô phỏng). Khái niệm này không thể được thực hiện để làm việc trong zsh mặc định có hoặc không command
.
IFS nhiều ký tự
Đúng, IFS có thể là nhiều ký tự, nhưng mỗi ký tự sẽ tạo một đối số:
set -f; IFS="$d" command eval set -- '$(echo "$var" )""'
printf '<%s>' "$@" ; echo
Sẽ xuất:
<><a bc><><d ef><><><><><><><><>< ><><><><><><><><><
><><><><><><ghi><><><><><>
Với bash, bạn có thể bỏ qua command
từ nếu không trong mô phỏng sh / POSIX. Lệnh sẽ thất bại trong ksh93 (IFS giữ giá trị thay đổi). Trong zsh, lệnh command
làm cho zsh cố gắng tìm eval
như một lệnh bên ngoài (mà nó không tìm thấy) và thất bại.
Điều xảy ra là các ký tự IFS duy nhất được tự động thu gọn thành một dấu phân cách là khoảng trắng IFS.
Một không gian trong IFS sẽ thu gọn tất cả các không gian liên tiếp thành một. Một tab sẽ thu gọn tất cả các tab. Một không gian và một tab sẽ thu gọn các khoảng trống và / hoặc các tab thành một dấu phân cách. Lặp lại ý tưởng với dòng mới.
Để thu gọn một số dấu phân cách, một số tung hứng xung quanh là bắt buộc.
Giả sử ASCII 3 (0x03) không được sử dụng trong đầu vào var
:
var=${var// /$'\3'} # protect spaces
var=${var//["$d"]/ } # convert all delimiters to spaces
set -f; # avoid expanding globs.
IFS=" " command eval set -- '""$var""' # split on spaces.
set -- "${@//$'\3'/ }" # convert spaces back.
Hầu hết các ý kiến về ksh, zsh và bash (about command
và IFS) vẫn được áp dụng ở đây.
Giá trị $'\0'
sẽ ít có xác suất hơn trong nhập văn bản, nhưng các biến bash không thể chứa NULs ( 0x00
).
Không có lệnh nội bộ nào trong sh để thực hiện các hoạt động chuỗi giống nhau, vì vậy tr là giải pháp duy nhất cho các tập lệnh sh.