IFS(内部字段分隔符)可以用作多个连续定界符char的单个分隔符吗?


10

使用具有非空格值的IFS解析数组会创建空元素。
即使使用tr -s将多个delims缩小到一个delim是不够的。
有一个示例可以更清楚地说明问题。.
是否有一种方法可以通过对IFS进行调整来实现“正常”结果(是否有相关的设置来更改IFS的行为?....即,与默认空白相同) 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>"

这是输出


============== 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>

对于同一问题,有一个更好的答案(我认为):stackoverflow.com/a/14789518/1765658
F. Hauri

Answers:


3

要删除多个(非空格)连续定界符,可以使用两个(字符串/数组)参数扩展。技巧是将IFS变量设置为空字符串,以扩展数组参数。

man bash在分词中记录

删除因无值的参数扩展而导致的无引号的隐式空参数。

(
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
)

好!一种简单有效的方法-无需bash循环,也无需调用实用程序-BTW。正如您提到的“(non-space)”,为清楚起见,我指出它可以与定界符char的任何组合(包括空格)一起正常工作。
Peter.O 2015年

在我的测试中,设置IFS=' '(即空白)的行为相同。我发现它比的显式null参数(“”或“”)更容易混淆IFS
Micha Wiedenmann 2015年

如果您的数据包含嵌入式空格,那将是一个糟糕的解决方案。如果您的数据是'a bc'而不是'abc',则IFS =“”会将'a'拆分为与'bc'分开的元素。
Dejay Clayton 2015年

5

bash联机帮助页:

IFS中不是IFS空格的任何字符,以及任何相邻的IFS空格字符,都将分隔字段。IFS空格字符序列也被视为定界符。

这意味着IFS空格(空格,制表符和换行符)不会像其他分隔符那样被对待。如果您希望使用替代分隔符获得完全相同的行为,则可以借助tr或进行一些分隔符交换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

%#%#%#%#%是替换字段中可能存在的空格的不可思议的价值,它应该是“唯一的”(或非常不相关)。如果您确定该字段中将不再有空格,请删除此部分)。


@FussyS ...谢谢(请参阅我的问题的修改内容)...您可能已经给了我预期的问题的答案..而这个答案可能是(可能是)“没有办法让IFS在我想要的方式” ...我打算 tr举一些例子来说明问题...我想避免系统调用,因此,我将讨论bash选项,${var##:}它超出了我在对glen的答案的评论中提到的选项。我会等待一段时间..也许有一种方法可以哄骗IFS,否则您的答案的第一部分是在……之后
Peter.O 2011年

IFS在所有Bourne样式的shell中,对的处理都是相同的,它在POSIX中指定
吉尔(Gilles)“所以

自问这个问题以来已有 4年了-我发现@nazad的答案(发布于一年多以前)是最简单的方法,可以处理IFS创建具有任意数字和IFS字符组合作为定界字符串的数组。我的问题最好用回答jon_d,但@nazad的回答显示了一种IFS没有循环且没有实用程序的漂亮方法。
Peter.O 2015年

2

由于bash IFS不能提供内部方法来将连续的定界符char视为单个定界符(对于非空白定界符),因此我将所有bash版本放在一起(使用外部调用,例如tr,awk,sed )

它可以处理多字符IFS。

这是它的执行时间结果,以及此Q / A页面上显示的trawk选项的类似测试...这些测试基于仅建立错误(没有I / O)的10000次操作。

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

这是输出

# 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"

这是脚本

#!/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

很棒的工作,有趣的+1!
F. Hauri 2013年

1

您也可以使用gawk来做到这一点,但这并不漂亮:

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

输出

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

谢谢...我的主要要求(修改的问题)似乎还不清楚。...只需将我更改$var${var##:}... 就可以很容易地做到这一点...我真的在想办法调整IFS本身。在没有外部调用的情况下执行此操作(我感觉bash可以比任何外部调用都更有效地执行此操作。所以我会继续努力)...您的方法有效(+1)....到目前为止随着修改输入的进行,我宁愿使用bash尝试它,而不是awk或tr(这样可以避免系统调用),但是我真的很想进行IFS调整...
Peter.O

如前所述,@ fred,IFS仅吸取多个连续的距离作为默认的空白值。否则,连续的定界符将导致多余的空字段。我预计一个或两个外部呼叫极不可能以任何实际方式影响性能。
glenn jackman 2011年

@glen ..(您说您的回答不是“漂亮”。。我认为是!:)但是,我将所有bash版本(与外部调用相比)组合在一起,并且基于10000个仅建立了错误的提示(没有I / O)... bash 1.276s... call (awk) 0m32.210s,,, call (tr) 0m32.178s......这样做了几次,你可能会认为是bash慢!...在这种情况下,awk更容易吗?...如果您已经有了摘要,则不会:) ...稍后再发布;现在必须走。
Peter.O 2011年

顺便说一句,重新输入您的gawk脚本...我以前基本上没有使用过awk,所以我一直在详细研究它(以及其他内容)...我无法选择原因,但是我会提到问题无论如何..给定带引号的数据时,它会松开引号,并在引号之间分隔开..并导致奇数个引号崩溃...这是测试数据:var="The \"X\" factor:::A single '\"' crashes:::\"One Two\""
Peter.O 2011年

-1

简单的答案是:将所有定界符折叠为一个(第一个)。
这需要一个循环(运行少于log(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

剩下要做的就是在一个定界符上正确分割字符串,然后打印它:

 readarray -td "$f" arr < <(printf '%s%s' "$v"'' "$f")
 printf '<%s>' "${arr[@]}" ; echo

无需set -f更改IFS。
使用空格,换行符和glob字符进行测试。所有的工作。相当慢(因为应该会出现shell循环)。
但仅适用于bash(bash 4.4+,因为可以选择-dreadarray)。


SH

Shell版本不能使用数组,唯一可用的数组是位置参数。
使用tr -s仅一行(IFS在脚本中不变):

 set -f; IFS=$f command eval set -- '$(echo "$var" | tr -s "$d" "[$f*]" )""'

并打印:

 printf '<%s>' "$@" ; echo

仍然很慢,但是没有更多。

该命令command在Bourne中无效。
在zsh中,command仅调用外部命令,如果command使用eval,则使eval失败。
在ksh中,即使使用command,IFS的值也会在全局范围内更改。
command使得劈mksh相关壳(mksh,lksh,豪华)拆除命令失败,command使得更多的炮弹的代码运行。但是:删除command将使IFS在大多数shell(eval是一个特殊的内置)中保留其值,除了在bash(无posix模式)和zsh(默认(无仿真)模式)中之外。无论有没有,都不能使此概念在默认zsh中工作command


多字符IFS

是的,IFS可以是多个字符,但是每个字符都会生成一个参数:

 set -f; IFS="$d" command eval set -- '$(echo "$var" )""'
 printf '<%s>' "$@" ; echo

将输出:

 <><a bc><><d ef><><><><><><><><><      ><><><><><><><><><
 ><><><><><><ghi><><><><><>

使用bash,command如果不在sh / POSIX仿真中,则可以省略单词。该命令将在ksh93中失败(IFS保留更改后的值)。在zsh中,该命令command使zsh尝试查找eval为外部命令(找不到),但失败。

发生的情况是,自动折叠到一个定界符的唯一IFS字符是IFS空白。
IFS中的一个空格会将所有连续的空格折叠为一个。一个标签页将折叠所有标签页。一个空格一个制表符会将空格和/或制表符的行折叠为一个定界符。用换行符重复该想法。

要折叠多个定界符,需要进行一些调整。
假设在输入中未使用ASCII 3(0x03)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.

关于ksh,zsh和bash(关于command和IFS)的大多数评论仍然适用于此。

$'\0'在文本输入中不太可能出现,但是bash变量不能包含NUL(0x00)。

sh中没有内部命令可以执行相同的字符串操作,因此tr是sh脚本的唯一解决方案。


是的,我在OP中要求外壳程序要求:Bash。在该外壳中,不保留IFS。是的,例如,对于zsh来说是不可移植的。@StéphaneChazelas–
以撒

在bash和zsh中的情况下,它们表现为POSIX指定调用时以sh
斯特凡Chazelas

@StéphaneChazelas添加了(许多)关于每个外壳的限制的注释。
艾萨克(Isaac)

@StéphaneChazelas为什么要投票?
艾萨克

不知道,不是我。顺便说一句,我认为有一个专门的Q&A在这里约command evalIIRC的吉尔斯
斯特凡Chazelas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.