使用cut命令重新排列列

173

我有一个文件,格式如下:

Column1    Column2
str1       1
str2       2
str3       3

我想要重新排列这些列。我尝试了以下命令:

cut -f2,1 file.txt

该命令无法重新排列列。有任何想法为什么它不起作用吗?

9个回答

199

针对cut(1)命令手册:

使用-b、-c或-f参数中的一个且仅一个。每个列表由一个范围或多个由逗号分隔的范围组成。所选输入按照读取顺序写入,并且仅写入一次。

它首先到达字段1,因此打印字段1,然后是字段2。

请改用awk:

awk '{ print $2 " " $1}' file.txt

24
很遗憾,“cut”命令不支持这种直观的重新排序命令。不管怎样,另一个提示是:您可以使用awk-FS-OFS选项来使用自定义输入和输出字段分隔符(就像cut-d--output-delimiter一样)。 - malana
23
抱歉,FS是一个选项,OFS则是一个变量。例如:awk -v OFS=";" -F"\t" '{print $2,$1}' - malana
4
针对 Git Bash 的 Windows 用户,如果你在执行上述命令时发现输出结果奇怪,例如列重叠等情况,那是回车符的问题所致。请将文件的行尾格式从CRLF更改为LF。注意不要改变原意。 - jakub.g
1
或者,如果您不想更改输入文件,可以在将其传输到awk之前通过 | sed 's/\r//' | 进行管道传输。 - jakub.g
3
这个很简单,但对某些人可能很有用。只需将空格替换为 \t 以通过制表符重新排序,如果您想要更多列,可以像这样执行:awk '{print $4 "\t" $2 "\t" $6 "\t" $7}' file - FatihSarigol
所以它不能工作的原因是 cut 无法重新排序。手册中应该只说明这一点。 - CervEd

77

您也可以组合使用剪切粘贴

paste <(cut -f2 file.txt) <(cut -f1 file.txt)

通过评论:可以避免使用进程替换并通过以下方式去除一个 cut 实例:

paste file.txt file.txt | cut -f2,3

3
不确定这是否算得上“巧妙”,但是:f=file.txt paste <(cut -f2 $f) <(cut -f1 $f)。此外,我注意到当你有许多列并且想移动大块列时,这种方法是最简单的。 - Michael Rusch
无法处理同一列中长度不同的单元格。 - kraymer
2
@kraymer 你是什么意思?只要有唯一的列分隔符,cut 对于可变长度的列可以正常工作。 - tripleee
1
为了消除冗余文件,你可能可以使用tee命令: - JJW5432
3
可以避免使用 bashisms 并通过以下方式删除一个 cut 实例:
paste file.txt file.txt | cut -f2,3
- agc
显示剩余4条评论

9
使用 join
join -t $'\t' -o 1.2,1.1 file.txt file.txt

注意事项:

  • -t $'\t' In GNU join the more intuitive -t '\t' without the $ fails, (coreutils v8.28 and earlier?); it's probably a bug that a workaround like $ should be necessary. See: unix join separator char.

  • Even though there's just one file being worked on, join syntax requires two filenames. Repeating the file name allows join to perform the desired action.

  • For systems with low resources join offers a smaller footprint than some of the tools used in other answers:

     wc -c $(realpath `which cut join sed awk perl`) | head -n -1
       43224 /usr/bin/cut
       47320 /usr/bin/join
      109840 /bin/sed
      658072 /usr/bin/gawk
     2093624 /usr/bin/perl
    

公平地说,如果您想要低占用空间,您可以使用C语言编写自己的定制工具...除了Perl之外,列出的所有实用程序都是核心实用程序。 - user4945014

7

仅使用shell,

while read -r col1 col2
do
  echo $col2 $col1
done <"file"

这通常非常低效。例如,通常您会发现相应的Awk脚本要快得多。您还应小心引用值"$col2""$col1" - 数据中可能有shell元字符或其他诡计。 - tripleee

7
你可以使用Perl来实现这一点:
perl -ane 'print "$F[1] $F[0]\n"' < file.txt
  • -e选项表示执行该命令后面的内容
  • -n选项表示逐行读取(打开文件,这里是STDOUT,并循环处理每一行)
  • -a选项表示将这些行拆分为一个名为@F(“F”代表Field)的向量。Perl中的向量索引从0开始,而cut从1开始。
  • 您可以添加-F pattern(-F和pattern之间没有空格)以在读取文件时使用pattern作为字段分隔符,而不是默认的空格

运行Perl的优点是(如果您了解Perl),您可以对F进行更多计算,而不仅仅是重新排列列。


perlrun(1)声称 -a 隐式设置 -n,但如果我不设置 -n 运行,它似乎不会循环。奇怪。 - Trenton
什么版本?对我来说,perl -ae print的作用就像cat一样。 - pwes

3

我曾经做过类似的工作,虽然不是专家,但我想分享一下我所使用的命令。我有一个多列csv文件,我只需要其中4个列,然后我需要重新排序它们。

我的文件使用竖线“|”作为分隔符,但可以更改。

LC_ALL=C cut -d$'|' -f1,2,3,8,10 ./file/location.txt | sed -E "s/(.*)\|(.*)\|(.*)\|(.*)\|(.*)/\3\|\5\|\1\|\2\|\4/" > ./newcsv.csv

诚然,这可能有些粗糙,但可以进行微调以适应需要!


这并没有回答所提出的问题。请在发布帖子之前花时间解决问题,遵循堆栈溢出的精神。 - Bill Gale
谢谢指出这个问题,非常有帮助。希望您喜欢您的徽章。 - Chris Rymer

3
作为对其他建议的补充,建议复制列然后进行cut。对于复制,paste等只适用于文件,而不适用于流。在这种情况下,请改用sed

cat file.txt | sed s/'.*'/'&\t&'/ | cut -f2,3

这适用于文件和流,并且如果您不仅仅使用cat从文件中读取内容,而是在重新排列列之前执行了一些有趣的操作,则会更加有趣。

相比之下,以下内容不起作用:

cat file.txt | paste - - | cut -f2,3

在这里,双stdin占位符paste不会复制stdin,而是读取下一行。


澄清一下,使用sed时,“&”用于打印匹配的文本。因此,在这里,我们将匹配的文本打印两次,由制表符分隔。 - oradwell

1

使用sed

使用基本正则表达式的嵌套子表达式来捕获和重新排列列内容。当需要重新排序的列数有限时,此方法最为适用。

基本思路是用\(\)包围搜索模式中有趣的部分,可以在替换模式中使用\#回放,其中#表示搜索模式中子表达式的顺序位置。

例如:

$ echo "foo bar" | sed "s/\(foo\) \(bar\)/\2 \1/"

产生:

bar foo

子表达式外的文本被扫描,但不会在替换字符串中保留以备播放。

虽然问题没有讨论固定宽度列,但我们将在此讨论,因为这是任何解决方案的有价值的衡量标准。为简单起见,假设文件是空格分隔的,尽管可以扩展该解决方案以适用于其他分隔符。

折叠空格

为了说明最简单的用法,假设多个空格可以折叠成单个空格,并且第二列值以EOL终止(而不是空格填充)。

文件:

bash-3.2$ cat f
Column1    Column2
str1       1
str2       2
str3       3
bash-3.2$ od -a f
0000000    C   o   l   u   m   n   1  sp  sp  sp  sp   C   o   l   u   m
0000020    n   2  nl   s   t   r   1  sp  sp  sp  sp  sp  sp  sp   1  nl
0000040    s   t   r   2  sp  sp  sp  sp  sp  sp  sp   2  nl   s   t   r
0000060    3  sp  sp  sp  sp  sp  sp  sp   3  nl 
0000072

转换:

bash-3.2$ sed "s/\([^ ]*\)[ ]*\([^ ]*\)[ ]*/\2 \1/" f
Column2 Column1
1 str1
2 str2
3 str3
bash-3.2$ sed "s/\([^ ]*\)[ ]*\([^ ]*\)[ ]*/\2 \1/" f | od -a
0000000    C   o   l   u   m   n   2  sp   C   o   l   u   m   n   1  nl
0000020    1  sp   s   t   r   1  nl   2  sp   s   t   r   2  nl   3  sp
0000040    s   t   r   3  nl
0000045

保留列宽

现在我们将这种方法扩展到具有固定宽度列的文件,同时允许列具有不同的宽度。

文件:

bash-3.2$ cat f2
Column1    Column2
str1       1
str2       2
str3       3
bash-3.2$ od -a f2
0000000    C   o   l   u   m   n   1  sp  sp  sp  sp   C   o   l   u   m
0000020    n   2  nl   s   t   r   1  sp  sp  sp  sp  sp  sp  sp   1  sp
0000040   sp  sp  sp  sp  sp  nl   s   t   r   2  sp  sp  sp  sp  sp  sp
0000060   sp   2  sp  sp  sp  sp  sp  sp  nl   s   t   r   3  sp  sp  sp
0000100   sp  sp  sp  sp   3  sp  sp  sp  sp  sp  sp  nl
0000114

转换:

bash-3.2$ sed "s/\([^ ]*\)\([ ]*\) \([^ ]*\)\([ ]*\)/\3\4 \1\2/" f2
Column2 Column1
1       str1      
2       str2      
3       str3      
bash-3.2$ sed "s/\([^ ]*\)\([ ]*\) \([^ ]*\)\([ ]*\)/\3\4 \1\2/" f2 | od -a
0000000    C   o   l   u   m   n   2  sp   C   o   l   u   m   n   1  sp
0000020   sp  sp  nl   1  sp  sp  sp  sp  sp  sp  sp   s   t   r   1  sp
0000040   sp  sp  sp  sp  sp  nl   2  sp  sp  sp  sp  sp  sp  sp   s   t
0000060    r   2  sp  sp  sp  sp  sp  sp  nl   3  sp  sp  sp  sp  sp  sp
0000100   sp   s   t   r   3  sp  sp  sp  sp  sp  sp  nl 
0000114

最后,尽管问题的示例没有不同长度的字符串,但是这个 sed 表达式支持这种情况。

文件:

bash-3.2$ cat f3
Column1    Column2
str1       1      
string2    2      
str3       3      

转换:

bash-3.2$ sed "s/\([^ ]*\)\([ ]*\) \([^ ]*\)\([ ]*\)/\3\4 \1\2/" f3
Column2 Column1   
1       str1      
2       string2   
3       str3    
bash-3.2$ sed "s/\([^ ]*\)\([ ]*\) \([^ ]*\)\([ ]*\)/\3\4 \1\2/" f3 | od -a
0000000    C   o   l   u   m   n   2  sp   C   o   l   u   m   n   1  sp
0000020   sp  sp  nl   1  sp  sp  sp  sp  sp  sp  sp   s   t   r   1  sp
0000040   sp  sp  sp  sp  sp  nl   2  sp  sp  sp  sp  sp  sp  sp   s   t
0000060    r   i   n   g   2  sp  sp  sp  nl   3  sp  sp  sp  sp  sp  sp
0000100   sp   s   t   r   3  sp  sp  sp  sp  sp  sp  nl 
0000114

与shell下其他列重排方法的比较

  • 令人惊讶的是,作为文件操作工具,awk不适合从字段到记录结尾进行切割。在sed中,可以使用正则表达式来实现,例如\(xxx.*$\),其中xxx是要匹配的列。

  • 使用pastecut子shell在实现shell脚本内部时会变得棘手。在命令行中可行的代码,在带入shell脚本时无法解析。至少这是我的经验(这也驱使我采用这种方法)。


0

扩展@Met的答案,也使用Perl:
如果输入和输出都是TAB分隔的:

perl -F'\t' -lane 'print join "\t", @F[1, 0]' in_file

如果输入和输出是以空格分隔的:
perl -lane 'print join " ", @F[1, 0]' in_file

这里,
-e 告诉 Perl 在内联代码中查找,而不是在单独的脚本文件中查找,
-n 逐行读取输入,
-l 在读取行后删除输入记录分隔符(在 *NIX 上为 \n)(类似于 chomp),并将输出记录分隔符(在 *NIX 上为 \n)添加到每个 print 中,
-a 将输入行按空格拆分成数组 @F
-F'\t'-a 结合使用,将输入行拆分为 TAB,而不是空格,进入数组 @F

@F[1, 0] 是由数组 @F 的第二个和第一个元素组成的数组,按此顺序。请记住,在 Perl 中,数组从零开始索引,而在 cut 中的字段从 1 开始索引。因此,@F[0, 1] 中的字段与 cut -f1,2 中的字段相同。

请注意,这种符号表示法比其他一些上面发布的答案(对于简单任务来说是可以的)更灵活地操作输入。例如:
# reverses the order of fields:
perl -F'\t' -lane 'print join "\t", reverse @F' in_file

# prints last and first fields only:
perl -F'\t' -lane 'print join "\t", @F[-1, 0]' in_file

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