cut 命令是从文本文件中删除“列”的规范工具。 在此上下文中,“列”可以定义为由其在行上的物理位置标识的字符或字节范围,或由分隔符分隔的字段范围。
我之前写过关于使用 AWK 命令的文章。 在这个详细的指南中,我将解释 Linux 中的四个基本实用的 cut 命令示例,它们将对您有所帮助。
4 Linux中Cut命令的实例
如果您愿意,可以观看此视频,它解释了我在文章中列出的相同剪切命令的实际示例。
1. 使用字符范围
当调用 -c
命令行选项,剪切命令将删除 特点 范围。
与任何其他过滤器一样,cut 命令不会更改输入文件,但会将修改后的数据复制到其标准输出。 您有责任将命令输出重定向到文件以保存结果或使用管道将其作为输入发送到另一个命令。
如果你已经下载了 样本测试文件 在上面的视频中使用,你可以看到 BALANCE.txt
数据文件,直接来自我妻子在工作中使用的会计软件:
sh$ head BALANCE.txt
ACCDOC ACCDOCDATE ACCOUNTNUM ACCOUNTLIB ACCDOCLIB DEBIT CREDIT
4 1012017 623477 TIDE SCHEDULE ALNEENRE-4701-LOC 00000001615,00
4 1012017 445452 VAT BS/ENC ALNEENRE-4701-LOC 00000000323,00
4 1012017 4356 PAYABLES ALNEENRE-4701-LOC 00000001938,00
5 1012017 623372 ACCOMODATION GUIDE ALNEENRE-4771-LOC 00000001333,00
5 1012017 445452 VAT BS/ENC ALNEENRE-4771-LOC 00000000266,60
5 1012017 4356 PAYABLES ALNEENRE-4771-LOC 00000001599,60
6 1012017 4356 PAYABLES FACT FA00006253 - BIT QUIROBEN 00000001837,20
6 1012017 445452 VAT BS/ENC FACT FA00006253 - BIT QUIROBEN 00000000306,20
6 1012017 623795 TOURIST GUIDE BOOK FACT FA00006253 - BIT QUIROBEN 00000001531,00
这是一个固定宽度的文本文件,因为数据字段填充了可变数量的空格,以确保它们显示为对齐良好的表格。
作为推论,数据列总是在每一行的相同字符位置开始和结束。 但是有一个小陷阱:尽管它的名字, cut
命令实际上要求您指定要保留的数据范围,而不是要删除的范围。 所以,如果我只需要 ACCOUNTNUM
和 ACCOUNTLIB
上面数据文件中的列,我会这样写:
sh$ cut -c 25-59 BALANCE.txt | head
ACCOUNTNUM ACCOUNTLIB
623477 TIDE SCHEDULE
445452 VAT BS/ENC
4356 /accountPAYABLES
623372 ACCOMODATION GUIDE
445452 VAT BS/ENC
4356 PAYABLES
4356 PAYABLES
445452 VAT BS/ENC
623795 TOURIST GUIDE BOOK
什么是范围?
正如我们刚刚看到的,cut 命令要求我们指定要保留的数据范围。 所以,让我们更正式地介绍一下什么是范围:对于 cut
命令,范围由连字符分隔的开始和结束位置定义。 范围从 1 开始,即该行的第一项是项目编号 1,而不是 0。范围包括在内:开始和结束将保留在输出中,以及它们之间的所有字符。 指定结束位置在其起始位置之前(“低于”)的范围是错误的。 作为一种快捷方式,您可以省略起始值或结束值,如下表所述:
a-b
: a 和 b 之间的范围(包括)a
: 相当于范围a-a
-b
: 相当于1-a
b-
: 相当于b-∞
cut 命令允许您通过用逗号分隔多个范围来指定它们。 这里有几个例子:
# Keep characters from 1 to 24 (inclusive)
cut -c -24 BALANCE.txt
# Keep characters from 1 to 24 and 36 to 59 (inclusive)
cut -c -24,36-59 BALANCE.txt
# Keep characters from 1 to 24, 36 to 59 and 93 to the end of the line (inclusive)
cut -c -24,36-59,93- BALANCE.txt
一个限制(或功能,取决于你看到它的方式) cut
命令是它永远不会重新排序数据。 因此,以下命令将产生与前一个命令完全相同的结果,尽管范围以不同的顺序指定:
cut -c 93-,-24,36-59 BALANCE.txt
您可以使用 diff
命令:
diff -s <(cut -c -24,36-59,93- BALANCE.txt)
<(cut -c 93-,-24,36-59 BALANCE.txt)
Files /dev/fd/63 and /dev/fd/62 are identical
同样, cut
命令从不重复数据:
# One might expect that could be a way to repeat
# the first column three times, but no...
cut -c -10,-10,-10 BALANCE.txt | head -5
ACCDOC
4
4
4
5
值得一提的是,有一个提案 -o
取消最后两个限制的选项,允许 cut
用于重新排序或复制数据的实用程序。 但这是 被 POSIX 委员会拒绝“因为这种类型的增强超出了 IEEE P1003.2b 草案标准的范围。”
就我自己而言,我不知道有任何删减版本将该提案作为扩展来实现。 但是,如果您这样做,请使用评论部分与我们分享!
2. 使用字节范围
当调用 -b
命令行选项,剪切命令将删除 字节 范围。
乍一看,字符范围和字节范围没有明显区别:
sh$ diff -s <(cut -b -24,36-59,93- BALANCE.txt)
<(cut -c -24,36-59,93- BALANCE.txt)
Files /dev/fd/63 and /dev/fd/62 are identical
那是因为我的示例数据文件正在使用 US-ASCII 字符编码 (“字符集”)作为 file -i
命令可以正确猜到:
sh$ file -i BALANCE.txt
BALANCE.txt: text/plain; charset=us-ascii
在该字符编码中,字符和字节之间存在一对一的映射。 仅使用一个字节,理论上您最多可以编码 256 个不同的字符(数字、字母、标点符号、符号……)实际上,这个数字要低得多,因为字符编码提供了一些特殊值(如 32 或 65 控制字符 普遍存在)。 无论如何,即使我们可以使用完整的字节范围,也远远不足以存储人类写作的多样性。 因此,今天,字符和字节之间的一对一映射比规范更加例外,并且几乎总是被无处不在的 UTF-8 多字节编码所取代。 现在让我们看看 cut 命令如何处理这个问题。
使用多字节字符
正如我之前所说,用作该文章示例的示例数据文件来自我妻子使用的会计软件。 它附加了她最近更新了该软件,之后导出的文本文件略有不同。 我让您尝试自己发现差异:
sh$ head BALANCE-V2.txt
ACCDOC ACCDOCDATE ACCOUNTNUM ACCOUNTLIB ACCDOCLIB DEBIT CREDIT
4 1012017 623477 TIDE SCHEDULE ALNÉENRE-4701-LOC 00000001615,00
4 1012017 445452 VAT BS/ENC ALNÉENRE-4701-LOC 00000000323,00
4 1012017 4356 PAYABLES ALNÉENRE-4701-LOC 00000001938,00
5 1012017 623372 ACCOMODATION GUIDE ALNÉENRE-4771-LOC 00000001333,00
5 1012017 445452 VAT BS/ENC ALNÉENRE-4771-LOC 00000000266,60
5 1012017 4356 PAYABLES ALNÉENRE-4771-LOC 00000001599,60
6 1012017 4356 PAYABLES FACT FA00006253 - BIT QUIROBEN 00000001837,20
6 1012017 445452 VAT BS/ENC FACT FA00006253 - BIT QUIROBEN 00000000306,20
6 1012017 623795 TOURIST GUIDE BOOK FACT FA00006253 - BIT QUIROBEN 00000001531,00
本节的标题可能会帮助您找出发生了哪些变化。 但是,无论发现与否,现在让我们看看这种变化的后果:
sh$ cut -c 93-,-24,36-59 BALANCE-V2.txt
ACCDOC ACCDOCDATE ACCOUNTLIB DEBIT CREDIT
4 1012017 TIDE SCHEDULE 00000001615,00
4 1012017 VAT BS/ENC 00000000323,00
4 1012017 PAYABLES 00000001938,00
5 1012017 ACCOMODATION GUIDE 00000001333,00
5 1012017 VAT BS/ENC 00000000266,60
5 1012017 PAYABLES 00000001599,60
6 1012017 PAYABLES 00000001837,20
6 1012017 VAT BS/ENC 00000000306,20
6 1012017 TOURIST GUIDE BOOK 00000001531,00
19 1012017 SEMINAR FEES 00000000080,00
19 1012017 PAYABLES 00000000080,00
28 1012017 MAINTENANCE 00000000746,58
28 1012017 VAT BS/ENC 00000000149,32
28 1012017 PAYABLES 00000000895,90
31 1012017 PAYABLES 00000000240,00
31 1012017 VAT BS/DEBIT 00000000040,00
31 1012017 S 00000000200,00
32 1012017 WATER 00000000202,20
32 1012017 VAT BS/DEBIT 00000000020,22
32 1012017 WATER 00000000170,24
32 1012017 VAT BS/DEBIT 00000000009,37
32 1012017 PAYABLES 00000000402,03
34 1012017 RENTAL COSTS 00000000018,00
34 1012017 PAYABLES 00000000018,00
35 1012017 MISCELLANEOUS CHARGES 00000000015,00
35 1012017 VAT BS/DEBIT 00000000003,00
35 1012017 PAYABLES 00000000018,00
36 1012017 LANDLINE TELEPHONE 00000000069,14
36 1012017 VAT BS/ENC 00000000013,83
我已经复制了上面的扩展命令输出,所以很明显列对齐出了点问题。
解释是原始数据文件仅包含 US-ASCII 字符(符号、标点符号、数字和拉丁字母,没有任何变音符号)
但是,如果您仔细查看软件更新后生成的文件,您会发现新的导出数据文件现在保留了重音字母。 为了 example,名称为“ALNEENRE”的公司现在拼写正确,而之前导出为“ALNEENRE”(无重音)
这 file -i
实用程序没有错过该更改,因为它现在将文件报告为 UTF-8 编码:
sh$ file -i BALANCE-V2.txt
BALANCE-V2.txt: text/plain; charset=utf-8
要查看如何在 UTF-8 文件中编码重音字母,我们可以使用 hexdump
实用程序允许我们直接查看文件中的字节:
# To reduce clutter, let's focus only on the second line of the file
sh$ sed '2!d' BALANCE-V2.txt
4 1012017 623477 TIDE SCHEDULE ALNÉENRE-4701-LOC 00000001615,00
sh$ sed '2!d' BALANCE-V2.txt | hexdump -C
00000000 34 20 20 20 20 20 20 20 20 20 31 30 31 32 30 31 |4 101201|
00000010 37 20 20 20 20 20 20 20 36 32 33 34 37 37 20 20 |7 623477 |
00000020 20 20 20 54 49 44 45 20 53 43 48 45 44 55 4c 45 | TIDE SCHEDULE|
00000030 20 20 20 20 20 20 20 20 20 20 20 41 4c 4e c3 89 | ALN..|
00000040 45 4e 52 45 2d 34 37 30 31 2d 4c 4f 43 20 20 20 |ENRE-4701-LOC |
00000050 20 20 20 20 20 20 20 20 20 20 20 20 20 30 30 30 | 000|
00000060 30 30 30 30 31 36 31 35 2c 30 30 20 20 20 20 20 |00001615,00 |
00000070 20 20 20 20 20 20 20 20 20 20 20 0a | .|
0000007c
在行 00000030 的 hexdump
输出,经过一堆空格(字节 20
),你可以看到:
- 信
A
被编码为字节41
, - 信
L
被编码为字节4c
, - 和信
N
被编码为字节4e
.
但是,大写 带有 ACUTE 的拉丁文大写字母 E (因为它是 Unicode 标准中字母 É 的正式名称)使用两个字节进行编码 c3 89
这就是问题所在:使用 cut
范围表示为字节位置的命令适用于固定长度编码,但不适用于可变长度编码,如 UTF-8 或 移位 JIS. 这在下面有清楚的解释 POSIX 标准的非规范摘录:
cut 实用程序的早期版本在字节和字符被认为是等效的环境中工作(在某些实现中模
嘿,等一下! 我没有使用 -b
“故障”中的选项 example 上面,但是 -c
选项。 所以,这不应该奏效吗?!?
是的,它应该:很不幸,但我们在 2018 年,尽管如此,从 GNU Coreutils 8.30 开始,cut 实用程序的 GNU 实现仍然不能正确处理多字节字符。 引用 GNU 文档, 这 -c
选项是“现在与 -b 相同,但国际化将改变这一点[… ]” —— 10 多年来一直存在的提及!
另一方面, OpenBSD cut 实用程序的实现是 POSIX 兼容的,并且将遵循当前的语言环境设置以正确处理多字节字符:
# Ensure subseauent commands will know we are using UTF-8 encoded
# text files
openbsd-6.3$ export LC_CTYPE=en_US.UTF-8
# With the `-c` option, cut works properly with multi-byte characters
openbsd-6.3$ cut -c -24,36-59,93- BALANCE-V2.txt
ACCDOC ACCDOCDATE ACCOUNTLIB DEBIT CREDIT
4 1012017 TIDE SCHEDULE 00000001615,00
4 1012017 VAT BS/ENC 00000000323,00
4 1012017 PAYABLES 00000001938,00
5 1012017 ACCOMODATION GUIDE 00000001333,00
5 1012017 VAT BS/ENC 00000000266,60
5 1012017 PAYABLES 00000001599,60
6 1012017 PAYABLES 00000001837,20
6 1012017 VAT BS/ENC 00000000306,20
6 1012017 TOURIST GUIDE BOOK 00000001531,00
19 1012017 SEMINAR FEES 00000000080,00
19 1012017 PAYABLES 00000000080,00
28 1012017 MAINTENANCE 00000000746,58
28 1012017 VAT BS/ENC 00000000149,32
28 1012017 PAYABLES 00000000895,90
31 1012017 PAYABLES 00000000240,00
31 1012017 VAT BS/DEBIT 00000000040,00
31 1012017 S 00000000200,00
32 1012017 WATER 00000000202,20
32 1012017 VAT BS/DEBIT 00000000020,22
32 1012017 WATER 00000000170,24
32 1012017 VAT BS/DEBIT 00000000009,37
32 1012017 PAYABLES 00000000402,03
34 1012017 RENTAL COSTS 00000000018,00
34 1012017 PAYABLES 00000000018,00
35 1012017 MISCELLANEOUS CHARGES 00000000015,00
35 1012017 VAT BS/DEBIT 00000000003,00
35 1012017 PAYABLES 00000000018,00
36 1012017 LANDLINE TELEPHONE 00000000069,14
36 1012017 VAT BS/ENC 00000000013,83
正如所料,当使用 -b
字节模式,而不是 -c
字符模式,OpenBSD 剪切实现的行为类似于传统 cut
:
openbsd-6.3$ cut -b -24,36-59,93- BALANCE-V2.txt
ACCDOC ACCDOCDATE ACCOUNTLIB DEBIT CREDIT
4 1012017 TIDE SCHEDULE 00000001615,00
4 1012017 VAT BS/ENC 00000000323,00
4 1012017 PAYABLES 00000001938,00
5 1012017 ACCOMODATION GUIDE 00000001333,00
5 1012017 VAT BS/ENC 00000000266,60
5 1012017 PAYABLES 00000001599,60
6 1012017 PAYABLES 00000001837,20
6 1012017 VAT BS/ENC 00000000306,20
6 1012017 TOURIST GUIDE BOOK 00000001531,00
19 1012017 SEMINAR FEES 00000000080,00
19 1012017 PAYABLES 00000000080,00
28 1012017 MAINTENANCE 00000000746,58
28 1012017 VAT BS/ENC 00000000149,32
28 1012017 PAYABLES 00000000895,90
31 1012017 PAYABLES 00000000240,00
31 1012017 VAT BS/DEBIT 00000000040,00
31 1012017 S 00000000200,00
32 1012017 WATER 00000000202,20
32 1012017 VAT BS/DEBIT 00000000020,22
32 1012017 WATER 00000000170,24
32 1012017 VAT BS/DEBIT 00000000009,37
32 1012017 PAYABLES 00000000402,03
34 1012017 RENTAL COSTS 00000000018,00
34 1012017 PAYABLES 00000000018,00
35 1012017 MISCELLANEOUS CHARGES 00000000015,00
35 1012017 VAT BS/DEBIT 00000000003,00
35 1012017 PAYABLES 00000000018,00
36 1012017 LANDLINE TELEPHONE 00000000069,14
36 1012017 VAT BS/ENC 00000000013,83
3. 使用字段
在某种意义上,使用分隔文本文件中的字段对 cut
实用程序,因为它只需要在每一行上定位(一个字节)字段分隔符,然后逐字复制字段内容到输出,而无需担心任何编码问题。
这是一个示例分隔文本文件:
sh$ head BALANCE.csv
ACCDOC;ACCDOCDATE;ACCOUNTNUM;ACCOUNTLIB;ACCDOCLIB;DEBIT;CREDIT
4;1012017;623477;TIDE SCHEDULE;ALNEENRE-4701-LOC;00000001615,00;
4;1012017;445452;VAT BS/ENC;ALNEENRE-4701-LOC;00000000323,00;
4;1012017;4356;PAYABLES;ALNEENRE-4701-LOC;;00000001938,00
5;1012017;623372;ACCOMODATION GUIDE;ALNEENRE-4771-LOC;00000001333,00;
5;1012017;445452;VAT BS/ENC;ALNEENRE-4771-LOC;00000000266,60;
5;1012017;4356;PAYABLES;ALNEENRE-4771-LOC;;00000001599,60
6;1012017;4356;PAYABLES;FACT FA00006253 - BIT QUIROBEN;;00000001837,20
6;1012017;445452;VAT BS/ENC;FACT FA00006253 - BIT QUIROBEN;00000000306,20;
6;1012017;623795;TOURIST GUIDE BOOK;FACT FA00006253 - BIT QUIROBEN;00000001531,00;
您可能知道该文件格式为 CSV (用于逗号分隔值),即使字段分隔符并不总是逗号。 为了 example分号 (;
) 经常被用作字段分隔符,在已经使用逗号作为分隔符的国家/地区将数据导出为“CSV”时,它通常是默认选择 小数分隔符 (就像我们在法国所做的那样——因此在我的示例文件中选择了该字符)。 另一种流行的变体使用 制表符 作为字段分隔符,产生有时称为 制表符分隔值 文件。 最后,在 Unix 和 Linux 世界中,冒号 (:
) 是您可能会发现的另一个相对常见的字段分隔符,因为 example在标准 /etc/passwd
和 /etc/group
文件。
使用分隔文本文件格式时,您向 cut 命令提供要继续使用的字段范围 -f
选项,并且您必须使用 -d
选项(没有 -d
选项,cut 实用程序默认使用制表符作为分隔符):
sh$ cut -f 5- -d';' BALANCE.csv | head
ACCDOCLIB;DEBIT;CREDIT
ALNEENRE-4701-LOC;00000001615,00;
ALNEENRE-4701-LOC;00000000323,00;
ALNEENRE-4701-LOC;;00000001938,00
ALNEENRE-4771-LOC;00000001333,00;
ALNEENRE-4771-LOC;00000000266,60;
ALNEENRE-4771-LOC;;00000001599,60
FACT FA00006253 - BIT QUIROBEN;;00000001837,20
FACT FA00006253 - BIT QUIROBEN;00000000306,20;
FACT FA00006253 - BIT QUIROBEN;00000001531,00;
处理不包含分隔符的行
但是如果输入文件中的某些行不包含分隔符怎么办? 很容易将其想象为仅包含第一个字段的行。 但这不是 cut 实用程序所做的。
默认情况下,当使用 -f
选项,cut 实用程序将始终逐字输出不包含分隔符的行(可能假设这是一个非数据行,如某种标题或注释):
sh$ (echo "# 2018-03 BALANCE"; cat BALANCE.csv) > BALANCE-WITH-HEADER.csv
sh$ cut -f 6,7 -d';' BALANCE-WITH-HEADER.csv | head -5
# 2018-03 BALANCE
DEBIT;CREDIT
00000001615,00;
00000000323,00;
;00000001938,00
使用 -s
选项,你可以扭转这种行为,所以 cut
将始终忽略这样的行:
sh$ cut -s -f 6,7 -d';' BALANCE-WITH-HEADER.csv | head -5
DEBIT;CREDIT
00000001615,00;
00000000323,00;
;00000001938,00
00000001333,00;
如果您心情不好,您可以利用该功能作为一种相对模糊的方式来仅保留包含给定字符的行:
# Keep lines containing a `e`
sh$ printf "%sn" {mighty,bold,great}-{condor,monkey,bear} | cut -s -f 1- -d'e'
更改输出分隔符
作为扩展,cut 的 GNU 实现允许使用不同的字段分隔符进行输出,使用 --output-delimiter
选项:
sh$ cut -f 5,6- -d';' --output-delimiter="*" BALANCE.csv | head
ACCDOCLIB*DEBIT*CREDIT
ALNEENRE-4701-LOC*00000001615,00*
ALNEENRE-4701-LOC*00000000323,00*
ALNEENRE-4701-LOC**00000001938,00
ALNEENRE-4771-LOC*00000001333,00*
ALNEENRE-4771-LOC*00000000266,60*
ALNEENRE-4771-LOC**00000001599,60
FACT FA00006253 - BIT QUIROBEN**00000001837,20
FACT FA00006253 - BIT QUIROBEN*00000000306,20*
FACT FA00006253 - BIT QUIROBEN*00000001531,00*
请注意,在这种情况下,所有出现的字段分隔符都会被替换,而不仅仅是那些在命令行参数中指定的范围的边界处。
4. 非 POSIX GNU 扩展
说到非 POSIX GNU 扩展,其中一些特别有用。 值得一提的是,以下扩展同样适用于字节、字符(对于当前 GNU 实现中的含义)或字段范围:--complement
将该选项想象为 sed 地址中的感叹号(!
); 而不是保持与给定范围匹配的数据, cut
将保留与范围不匹配的数据
# Keep only field 5
sh$ cut -f 5 -d';' BALANCE.csv |head -3
ACCDOCLIB
ALNEENRE-4701-LOC
ALNEENRE-4701-LOC
# Keep all but field 5
sh$ cut --complement -f 5 -d';' BALANCE.csv |head -3
ACCDOC;ACCDOCDATE;ACCOUNTNUM;ACCOUNTLIB;DEBIT;CREDIT
4;1012017;623477;TIDE SCHEDULE;00000001615,00;
4;1012017;445452;VAT BS/ENC;00000000323,00;
--zero-terminated
(-z
)
使用 NUL 字符 作为行终止符而不是 换行符. 这 -z
当您的数据可能包含嵌入的换行符时,选项特别有用,例如在处理文件名时(因为换行符是文件名中的有效字符,但 NUL 不是)。
向您展示如何 -z
选项有效,让我们做一个小实验。 首先,我们将创建一个文件,其名称包含嵌入的新行:
bash$ touch
现在假设我要显示每个字符的前 5 个字符 *.txt
文件名。 一个天真的解决方案将在这里惨败:
sh$ ls -1 *.txt | cut -c 1-5
BALAN
BALAN
EMPTY
FILE
WITH
NAME.
你可能已经读过 ls
专为 人类消费,并且在命令管道中使用它是一种反模式(确实如此)。 所以让我们使用 find
命令改为:
sh$ find . -name '*.txt' -printf "%fn" | cut -c 1-5
BALAN
EMPTY
FILE
WITH
NAME.
BALAN
和……产生与以前基本相同的错误结果(尽管顺序不同,因为 ls
隐式排序文件名, find
命令不这样做)。
问题是在这两种情况下, cut
命令无法区分作为数据字段(文件名)一部分的换行符和用作记录结束标记的换行符。 但是,使用 NUL 字节(