如何通过逗号而不是空格来分割列表

58

我想用逗号,而不是空格切分文本,使用for foo in list循环。假设我有一个CSV文件CSV_File,里面包含以下文本:

Hello,World,Questions,Answers,bash shell,script
...
我使用以下代码将其分割成几个单词:
for word in $(cat CSV_File | sed -n 1'p' | tr ',' '\n')
do echo $word
done

它会输出:

Hello
World
Questions
Answers
bash
shell
script

但我希望它按逗号而不是空格拆分文本:

Hello
World
Questions
Answers
bash shell
script

如何在 bash 中实现这个?


4
当然,下面是使用 awk 的一个示例:假设我们有一个名为 example.txt 的文本文件,其中包含以下内容:John 35 Emily 27 Michael 41 Samantha 23我们可以使用以下命令来打印这些行中所有人的年龄总和:awk '{sum += $2} END {print sum}' example.txt输出结果将是 126。现在让我解释一下这个命令:
  • awk 是一个用于处理文本文件的命令行工具。
  • '{sum += $2} END {print sum}' 是一个 awk 脚本,它对每一行执行一次,将第二列的值添加到一个名为 sum 的变量中。
  • END 关键字告诉 awk 在处理完所有行后执行一次操作。在本例中,我们打印了 sum 变量的值。
  • example.txt 是输入文件的名称。
希望这个例子能帮助你更好地理解如何使用 awk
- Eng.Fouad
9个回答

62

IFS设置为 ,:

sorin@sorin:~$ IFS=',' ;for i in `echo "Hello,World,Questions,Answers,bash shell,script"`; do echo $i; done
Hello
World
Questions
Answers
bash shell
script
sorin@sorin:~$ 

不错!我完全忘记了IFS环境变量! - chown
2
要在脚本中使用它,您应该将IFS变量恢复为先前的值。请参见Andrew Newdigate的答案。 - clime
1
@Sorin: "在脚本中使用" 意味着需要更多的代码来重置 IFS,以避免任何意外行为。IFS 的影响似乎相当广泛,因此最好懒惰一点而不是不清楚。顺便说一下,如果你像你的回答中那样运行命令,它将为当前环境更改 IFS,很容易忘记,然后想知道为什么你的 shell 表现得那么奇怪。 - clime

61

使用一个子shell替换来解析单词,会取消你所做的所有将空格放在一起的工作。

尝试改用:

cat CSV_file | sed -n 1'p' | tr ',' '\n' | while read word; do
    echo $word
done

这还增加了并行性。像你问题中的子shell那样使用会强制整个子shell进程完成,然后才能开始遍历答案。将其管道传输到子shell(如我的答案中所示)可以让它们并行工作。当然,这仅在文件中有许多行的情况下才会产生影响。


1
是的,这比我建议的要好得多。赞一个给mkj的l33t bash技能 :) - chown
1
甚至不需要 while 循环。 - Martin York
1
不需要保留 while 循环,但是我理解 echo 的调用是代理了一些更有趣的命令;也就是说,OP 希望将多个单词的 CSV 内容存储到 shell 变量中,并将其用于其他任意命令。这就是为什么我使用了 read 来演示如何将内容转换成 shell 变量。 - mkj
请注意,如果输入包含换行符,则此方法将无法按预期工作(它将在逗号和原始输入中出现的换行符上进行拆分,即 a,b\nc,d 将被拆分为 4 个字段而不是所需的 3 个)。对于 Bash,我建议使用单命令范围的 IFS 设置与 read -aread -d 结合使用(参见 Bash 中正确的 IFS 设置),但对于 POSIX shell,我发现子字符串处理是唯一干净且可靠的解决方案。 - desseim

23

我认为规范的方法是:

while IFS=, read field1 field2 field3 field4 field5 field6; do 
  do stuff
done < CSV.file
如果您不知道或不关心有多少字段:
IFS=,
while read line; do
  # split into an array
  field=( $line )
  for word in "${field[@]}"; do echo "$word"; done

  # or use the positional parameters
  set -- $line
  for word in "$@"; do echo "$word"; done

done < CSV.file

能够按名称引用特定字段非常方便。 - HXCaine
@glenn-jackman 您是正确的,规范的UNIX将使用您的第一种方法。第二种方法仅适用于现代实现的bash或zsh。 - Dwight Spencer
1
Bash的read命令有一个-a选项,可以将行中的单词读入数组中:while read -a words; do for word in "${words[@]}" ... - glenn jackman
至少在我使用的版本中,当一个记录有额外逗号时,读取n个字段不会出错,而是将最后一个字段中间带有逗号的两个值放入其中。 - zsalya
是的,IFS=, read a b <<<"1,2,3"将会把变量b设置为字符串2,3 - glenn jackman

12
kent$  echo "Hello,World,Questions,Answers,bash shell,script"|awk -F, '{for (i=1;i<=NF;i++)print $i}'
Hello
World
Questions
Answers
bash shell
script

1
我假设 echo $word 不是实际需要对 $word 进行的操作。在这种情况下,您的 awk 表达式是在原始问题中执行 sed 和 tr 的另一种方式。我认为 Eng.Fouad 想要将带有空格的值存储在 shell 变量中以便进行其他操作。 - mkj
@mkj 这个解决方案可以作为shell变量进一步使用,例如: FOO="Hello,World,Questions,Answers,bash shell,script"; BOO=$(echo $FOO | awk -F, '{for (i=1;i<=NF;i++)print $i}'); for B in $BOO; do echo "<$B>"; done - Roman Chernyatchik
@RomanChernyatchik 在 $BOO 的循环中,会为 "bash" 和 "shell" 分别产生不同的变量,因此不会按照 OP 的意图工作。 - Peter Berg

11
创建一个Bash函数
split_on_commas() {
  local IFS=,
  local WORD_LIST=($1)
  for word in "${WORD_LIST[@]}"; do
    echo "$word"
  done
}

split_on_commas "this,is a,list" | while read item; do
  # Custom logic goes here
  echo Item: ${item}
done

... 这将生成以下输出:

Item: this
Item: is a
Item: list

(请注意,根据一些反馈,本答案已经更新。)

奇怪,有任何想法“为什么”会发生这种情况吗? - Andrew Newdigate
副作用在这里解释了http://superuser.com/questions/781766/ifs-separated-items-in-loop - Val
1
为了避免“副作用”,首先将IFS变量存储在某个地方,例如OLDIFS=$IFS,然后执行IFS=, sentences1=($sentences),最后恢复IFS:IFS=$OLDIFS。否则,这就是我正在寻找的答案。谢谢。 - clime
@clime 和 Val,我已经更新了我的答案以考虑到你们的反馈。它似乎运行良好,但请让我知道你们的想法。 - Andrew Newdigate
我认为你的帖子现在太复杂了。修复原始代码片段并在最后加上一个小注释给评论者以荣誉就足够了 ;)。但无论如何,没有什么是完美的。 - clime

5

阅读:http://linuxmanpages.com/man1/sh.1.php & http://www.gnu.org/s/hello/manual/autoconf/Special-Shell-Variables.html

IFS是内部字段分隔符,用于扩展后的单词拆分和使用read内置命令将行拆分为单词。默认值为“”。

IFS是一个Shell环境变量,因此在Shell脚本的上下文中保持不变,但除非您导出它,否则在其他情况下不会保持不变。还要注意,IFS很可能根本不会从您的环境继承:请参见GNU文章以了解更多关于IFS的原因和信息。

您编写的代码应该像这样:

IFS=","
for word in $(cat tmptest | sed -n 1'p' | tr ',' '\n'); do echo $word; done;

应该可以正常工作,我在命令行上测试过了。

sh-3.2#IFS=","
sh-3.2#for word in $(cat tmptest | sed -n 1'p' | tr ',' '\n'); do echo $word; done;
World
Questions
Answers
bash shell
script

1

您可以使用:

cat f.csv | sed 's/,/ /g' |  awk '{print $1 " / " $4}'

或者

echo "Hello,World,Questions,Answers,bash shell,script" | sed 's/,/ /g' |  awk '{print $1 " / " $4}'

这是将逗号替换为空格的部分

sed 's/,/ /g'

0
使用readarray(mapfile):
$ cat csf
Hello,World,Questions,Answers,bash shell,script

$ readarray -td, arr < csf

$ printf '%s\n' "${arr[@]}"
Hello
World
Questions
Answers
bash shell
script

0

对我来说,使用数组分割更简单 ref

IN="bla@some.com;john@home.com"
arrIN=(${IN//;/ })
echo ${arrIN[1]}  

但是这个 ...,bash shell,... 也会被分割,而这正是 OP 想要避免的。 - Ivan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接