IFS (Dấu tách trường nội bộ) có thể hoạt động như một dấu tách đơn cho nhiều ký tự phân cách liên tiếp không?


10

Phân tích cú pháp một mảng bằng IFS với các giá trị không gian trắng không tạo ra các phần tử trống.
Ngay cả việc sử dụng tr -sđể thu nhỏ nhiều lần chuyển sang một lần phân định là không đủ.
Một ví dụ có thể giải thích vấn đề rõ ràng hơn ..
Có cách nào để đạt được kết quả "bình thường" thông qua điều chỉnh IFS (có cài đặt liên quan để thay đổi hành vi của IFS không? .... tức là. Để hoạt động giống như khoảng trắng mặc định IFS.

var=" abc  def   ghi    "
echo "============== IFS=<default>"
arr=($var)
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
#
sfi="$IFS" ; IFS=':'
set -f # Disable file name generation (globbing)
       # (This  data won't "glob", but unless globbing     
       #  is actually needed, turn if off, because   
       #  unusual/unexpected combinations of data can glob!
       #  and they can do it in the most obscure ways...  
       #  With IFS, "you're not in Kansas any more! :)  
var=":abc::def:::ghi::::"
echo "============== IFS=$IFS"
arr=($var)
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
echo "============== IFS=$IFS and tr"
arr=($(echo -n "$var"|tr -s "$IFS"))
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
set +f     # enable globbing 
IFS="$sfi" # re-instate original IFS val
echo "============== IFS=<default>"

Đây là đầu ra


============== IFS=<default>
# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"
============== IFS=:
# arr[0] ""
# arr[1] "abc"
# arr[2] ""
# arr[3] "def"
# arr[4] ""
# arr[5] ""
# arr[6] "ghi"
# arr[7] ""
# arr[8] ""
# arr[9] ""
============== IFS=: and tr
# arr[0] ""
# arr[1] "abc"
# arr[2] "def"
# arr[3] "ghi"
============== IFS=<default>

Có một câu trả lời tốt hơn (tôi nghĩ) cho cùng một câu hỏi: stackoverflow.com/a/14789518/1765658
F. Hauri

Câu trả lời:


3

Để loại bỏ nhiều ký tự phân cách liên tiếp (không phải khoảng trắng), có thể sử dụng hai khai triển tham số (chuỗi / mảng). Thủ thuật là đặt IFSbiến thành chuỗi rỗng để mở rộng tham số mảng.

Đây là tài liệu ở man bashdưới Lời Chia :

Các đối số null ẩn không được trích dẫn, dẫn đến việc mở rộng các tham số không có giá trị, sẽ bị xóa.

(
set -f
str=':abc::def:::ghi::::'
IFS=':'
arr=(${str})
IFS=""
arr=(${arr[@]})

echo ${!arr[*]}

for ((i=0; i < ${#arr[@]}; i++)); do 
   echo "${i}: '${arr[${i}]}'"
done
)

Tốt Một phương pháp đơn giản và hiệu quả - không cần vòng lặp bash và không cần gọi ứng dụng tiện ích - BTW. Như bạn đã đề cập "(không phải không gian)" , tôi muốn chỉ ra, rõ ràng, nó hoạt động tốt với bất kỳ sự kết hợp nào của các ký tự phân cách, bao gồm cả không gian.
Peter.O

Trong cài đặt thử nghiệm của tôi IFS=' '(tức là một khoảng trắng) hoạt động giống nhau. Tôi thấy điều này ít gây nhầm lẫn hơn một đối số null rõ ràng ("" hoặc '') của IFS.
Micha Wiedenmann

Đó là một giải pháp tồi tệ nếu dữ liệu của bạn chứa khoảng trắng được nhúng. Điều này, nếu dữ liệu của bạn là 'a bc' thay vì 'abc', IFS = "" sẽ chia 'a' thành một phần tử riêng biệt từ 'bc'.
Dejay Clayton

5

Từ bashtrang web:

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.

Điều đó có nghĩa là khoảng trắng IFS (không gian, tab và dòng mới) không được xử lý như các dấu phân cách khác. Nếu bạn muốn có chính xác hành vi tương tự với một dấu phân cách thay thế, bạn có thể thực hiện một số hoán đổi dấu tách với sự trợ giúp của trhoặc sed:

var=":abc::def:::ghi::::"
arr=($(echo -n $var | sed 's/ /%#%#%#%#%/g;s/:/ /g'))
for x in ${!arr[*]} ; do
   el=$(echo -n $arr | sed 's/%#%#%#%#%/ /g')
   echo "# arr[$x] \"$el\""
done

Thứ %#%#%#%#%này là một giá trị kỳ diệu để thay thế các không gian có thể bên trong các trường, nó được dự kiến ​​là "duy nhất" (hoặc rất không liên kết). Nếu bạn chắc chắn rằng sẽ không còn chỗ trống trong các trường, chỉ cần bỏ phần này).


@FussyS ... Cảm ơn (xem modificaton trong câu hỏi của tôi) ... Bạn có thể đã cho tôi câu trả lời cho câu hỏi dự định của mình .. và câu trả lời đó có thể là (có lẽ là) "Không có cách nào để IFS cư xử trong theo cách tôi muốn "... Tôi dự định các trví dụ để hiển thị vấn đề ... Tôi muốn tránh một cuộc gọi hệ thống, vì vậy tôi sẽ xem xét một tùy chọn bash ngoài cái ${var##:}mà tôi đã đề cập trong bình luận của mình để xem lại ansewer .... Tôi sẽ đợi một lúc .. có lẽ có một cách để dỗ IFS, nếu không thì phần đầu tiên trong câu trả lời của bạn là sau ....
Peter.O

Cách xử lý đó IFSlà giống nhau trong tất cả các kiểu vỏ Bourne, được chỉ định trong POSIX .
Gilles 'SO- ngừng trở nên xấu xa'

Hơn 4 năm kể từ khi tôi hỏi câu hỏi này - tôi đã tìm thấy câu trả lời của @ nazad (được đăng cách đây một năm) là cách đơn giản nhất để đưa IFS tạo ra một mảng với bất kỳ số và tổ hợp IFSký tự nào dưới dạng chuỗi ký tự. Câu hỏi của tôi đã được trả lời tốt nhất bởi jon_d, nhưng câu trả lời của @ nazad cho thấy một cách sử dụng tiện lợi IFSkhông có vòng lặp và không có ứng dụng tiện ích.
Peter.O

2

Vì bash IFS không cung cấp một cách nội bộ để coi các ký tự phân cách liên tiếp là một dấu phân cách duy nhất (đối với các dấu phân cách không phải khoảng trắng), tôi đã kết hợp một phiên bản bash (so với sử dụng một cuộc gọi bên ngoài, ví dụ: tr, awk, sed )

Nó có thể xử lý IFS nhiều char ..

Dưới đây là thời gian thực hiện của nó, cùng với các thử nghiệm tương tự cho trawkcác tùy chọn được hiển thị trên trang Hỏi / Đáp này ... Các thử nghiệm dựa trên 10000 lần lặp chỉ xây dựng mảng (không có I / O) ...

pure bash     3.174s (28 char IFS)
call (awk) 0m32.210s  (1 char IFS) 
call (tr)  0m32.178s  (1 char IFS) 

Đây là đầu ra

# dlm_str  = :.~!@#$%^&()_+-=`}{][ ";></,
# original = :abc:.. def:.~!@#$%^&()_+-=`}{][ ";></,'single*quote?'..123:
# unified  = :abc::::def::::::::::::::::::::::::::::'single*quote?'::123:
# max-w 2^ = ::::::::::::::::
# shrunk.. = :abc:def:'single*quote?':123:
# arr[0] "abc"
# arr[1] "def"
# arr[2] "'single*quote?'"
# arr[3] "123"

Đây là kịch bản

#!/bin/bash

# Note: This script modifies the source string. 
#       so work with a copy, if you need the original. 
# also: Use the name varG (Global) it's required by 'shrink_repeat_chars'
#
# NOTE: * asterisk      in IFS causes a regex(?) issue,     but  *  is ok in data. 
# NOTE: ? Question-mark in IFS causes a regex(?) issue,     but  ?  is ok in data. 
# NOTE: 0..9 digits     in IFS causes empty/wacky elements, but they're ok in data.
# NOTE: ' single quote  in IFS; don't know yet,             but  '  is ok in data.
# 
function shrink_repeat_chars () # A 'tr -s' analog
{
  # Shrink repeating occurrences of char
  #
  # $1: A string of delimiters which when consecutively repeated and are       
  #     considered as a shrinkable group. A example is: "   " whitespace delimiter.
  #
  # $varG  A global var which contains the string to be "shrunk".
  #
# echo "# dlm_str  = $1" 
# echo "# original = $varG" 
  dlms="$1"        # arg delimiter string
  dlm1=${dlms:0:1} # 1st delimiter char  
  dlmw=$dlm1       # work delimiter  
  # More than one delimiter char
  # ============================
  # When a delimiter contains more than one char.. ie (different byte` values),    
  # make all delimiter-chars in string $varG the same as the 1st delimiter char.
  ix=1;xx=${#dlms}; 
  while ((ix<xx)) ; do # Where more than one delim char, make all the same in varG  
    varG="${varG//${dlms:$ix:1}/$dlm1}"
    ix=$((ix+1))
  done
# echo "# unified  = $varG" 
  #
  # Binary shrink
  # =============
  # Find the longest required "power of 2' group needed for a binary shrink
  while [[ "$varG" =~ .*$dlmw$dlmw.* ]] ; do dlmw=$dlmw$dlmw; done # double its length
# echo "# max-w 2^ = $dlmw"
  #
  # Shrik groups of delims to a single char
  while [[ ! "$dlmw" == "$dlm1" ]] ; do
    varG=${varG//${dlmw}$dlm1/$dlm1}
    dlmw=${dlmw:$((${#dlmw}/2))}
  done
  varG=${varG//${dlmw}$dlm1/$dlm1}
# echo "# shrunk.. = $varG"
}

# Main
  varG=':abc:.. def:.~!@#$%^&()_+-=`}{][ ";></,'\''single*quote?'\''..123:' 
  sfi="$IFS"; IFS=':.~!@#$%^&()_+-=`}{][ ";></,' # save original IFS and set new multi-char IFS
  set -f                                         # disable globbing
  shrink_repeat_chars "$IFS" # The source string name must be $varG
  arr=(${varG:1})    # Strip leading dlim;  A single trailing dlim is ok (strangely
  for ix in ${!arr[*]} ; do  # Dump the array
     echo "# arr[$ix] \"${arr[ix]}\""
  done
  set +f     # re-enable globbing   
  IFS="$sfi" # re-instate the original IFS
  #
exit

Công việc tuyệt vời, +1 thú vị!
F. Hauri

1

Bạn cũng có thể làm điều đó với gawk, nhưng nó không đẹp:

var=":abc::def:::ghi::::"
out=$( gawk -F ':+' '
  {
    # strip delimiters from the ends of the line
    sub("^"FS,"")
    sub(FS"$","")
    # then output in a bash-friendly format
    for (i=1;i<=NF;i++) printf("\"%s\" ", $i)
    print ""
  }
' <<< "$var" )
eval arr=($out)
for x in ${!arr[*]} ; do
  echo "# arr[$x] \"${arr[x]}\""
done

đầu ra

# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"

Cảm ơn ... Tôi dường như chưa rõ ràng trong yêu cầu chính của mình (câu hỏi đã được sửa đổi) ... Thật dễ dàng để làm điều đó bằng cách thay đổi $varthành ${var##:}... Tôi thực sự sau một cách để tự điều chỉnh IFS .. Tôi muốn để thực hiện việc này mà không cần một cuộc gọi bên ngoài (tôi có cảm giác rằng bash có thể thực hiện việc này hiệu quả hơn bất kỳ cuộc gọi bên ngoài nào .. vì vậy tôi sẽ tiếp tục theo dõi đó) ... phương pháp của bạn hoạt động (+1) .... Cho đến nay khi sửa đổi đầu vào, tôi muốn thử với bash hơn là awk hoặc tr (nó sẽ tránh một cuộc gọi hệ thống), nhưng tôi thực sự đang chờ đợi một tinh chỉnh IFS ...
Peter.O

@fred, như đã đề cập, IFS chỉ tạo ra nhiều dấu phân cách liên tiếp cho giá trị khoảng trắng mặc định. Mặt khác, các dấu phân cách liên tiếp dẫn đến các trường trống bên ngoài. Tôi hy vọng một hoặc hai cuộc gọi bên ngoài cực kỳ khó có thể ảnh hưởng đến hiệu suất theo bất kỳ cách thực tế nào.
glenn jackman

@glen .. (Bạn nói rằng câu trả lời của bạn không "đẹp" .. Tôi nghĩ là vậy! :) Tuy nhiên, tôi đã kết hợp một phiên bản bash (so với một cuộc gọi bên ngoài) và dựa trên 10000 lần lặp chỉ xây dựng mảng ( không I / O) ... bash 1.276s... call (awk) 0m32.210s,,, call (tr) 0m32.178s... Làm điều đó một vài lần và bạn có thể nghĩ bash là chậm! ... Có phải awk dễ dàng hơn trong trường hợp này? ... không phải nếu bạn đã có đoạn trích :) ... Tôi sẽ đăng nó sau; phải đi ngay bây giờ.
Peter.O

Nhân tiện, đây là kịch bản gawk của bạn ... về cơ bản tôi chưa từng sử dụng awk trước đây, vì vậy tôi đã xem xét nó (và những người khác) một cách chi tiết ... Tôi không thể chọn tại sao, nhưng tôi sẽ đề cập đến dù sao đi nữa, khi được cung cấp dữ liệu được trích dẫn, nó sẽ mất các trích dẫn và phân tách tại các khoảng trắng giữa các trích dẫn .. và gặp sự cố đối với số lượng trích dẫn lẻ ... Đây là dữ liệu thử nghiệm:var="The \"X\" factor:::A single '\"' crashes:::\"One Two\""
Peter.O

-1

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 -fcũ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 -schỉ 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 commandkhông hợp lệ trong Bourne.
Trong zsh, commandchỉ 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.
commandlà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 commandlàm cho mã chạy trên nhiều shell hơn. Nhưng: loại bỏ commandsẽ 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 commandtừ 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 commandlàm cho zsh cố gắng tìm evalnhư 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 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 commandvà 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.


Vâng, tôi đã viết rằng cho cái vỏ mà OP yêu cầu: Bash. Trong đó vỏ IFS không được giữ. Và vâng, không phải là xách tay, ví dụ như zsh. @ StéphaneChazelas
Isaac

Trong trường hợp bash và zsh, chúng hoạt động như POSIX chỉ định khi được gọi là sh
Stéphane Chazelas

@ StéphaneChazelas Đã thêm (nhiều) ghi chú về các hạn chế của mỗi vỏ.
Isaac

@ StéphaneChazelas Tại sao downvote?
Isaac

Đừng biết, không phải tôi. BTW, tôi nghĩ rằng có một câu hỏi và trả lời riêng về command evalIIRC của Gilles
Stéphane Chazelas
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.