我在 CentOS 系统上有这个输入文件:
1,,,,ivan petrov,,67,
2,2,,,Vasia pupkin,director,8,
3,,,,john Lenon,,,
任务是将其更改为:
1,,,,Ivan Petrov,,67,
2,2,,,Vasia Pupkin,director,8,
3,,,,John Lenon,,,
姓名应以大写字母开头
#!/bin/bash
while IFS="," read line
do
ns=$(echo $line | awk -F, '{print $5}')
name=$(echo $ns | awk '{print $1}')
surname=$(echo $ns | awk '{print $2}')
ns=$(echo ${name^} ${surname^})
awk -v nm="$ns" 'BEGIN{FS=OFS=","}{$5=nm}1' accnew.csv
done < <(tail -n +2 accnew.csv) > 1new.csv
这是我的脚本,但它不能正常工作。
答案1
不要使用 shell 循环来处理文本。使用文本处理实用程序。
这里,要将第 5个字段中的名称大写,如果Lingua::EN::NameCase
perl
模块可用:
perl -Mopen=locale -MLingua::EN::NameCase -F, -ae '
$F[4] = nc $F[4] unless @F < 5;
print join ",", @F' < your-file
如果不是,作为近似值,您可以将一个或多个字母数字字符的每个序列的第一个字符转换为大写:
perl -Mopen=locale -F, -ae '
$F[4] =~ s/\w+/\u$&/g unless @F < 5;
print join ",", @F' < your-file
然而,这将无法正确处理诸如McGregor
, van Dike
... 或带有组合字符的名称。
(perl 还具有适当的 CSV 解析模块,以防您的输入不仅仅是简单的 csv,而无需在示例中引用)。
使用标准语法也可以完成同样的操作awk
,但要麻烦得多:
awk -F, -v OFS=, '
NF >= 5 {
r = $5; $5 = ""
while (match(r, "[[:alnum:]]+")) {
$5 = $5 substr(r, 1, RSTART - 1) \
toupper(substr(r, RSTART, 1)) \
substr(r, RSTART + 1, RLENGTH - 1)
r = substr(r, RSTART + RLENGTH)
}
$5 = $5 r
}
{print}' < your-file
使用 GNUawk
及其patsplit()
函数会稍微容易一些:
gawk -F, -v OFS=, '
NF >= 5 {
n = patsplit($5, f, /[[:alnum:]]+/, s)
$5 = s[0]
for (i = 1; i <= n; i++)
$5 = $5 toupper(substr(f[i], 1, 1)) \
substr(f[i], 2) s[i]
}
{print}' < your-file
如果必须使用 shell 循环,至少使用带有大写运算符的 shell:
#! /bin/zsh -
while IFS=, read -ru3 -A fields; do
(( $#fields < 5 )) || fields[5]=${(C)fields[5]}
print -r -- ${(j[,])fields} || exit
done 3< your-file
请注意,其中一个(以及Lingua::EN::NameCase
基于它的)与其他的不同之处在于,它变成éric serRA
了实例Éric Serra
而不是Éric SerRA
实例。perl
通过将\u
to\u\L
和 inawk
应用于tolower()
每个单词的第二部分,您可以获得相同的结果。
如果您必须只使用bash
及其内置命令(如您在注释中指出的那样),那会更加麻烦(除了效率低之外),因为与 zsh 或 ksh93 相比,bash 的运算符非常有限,而且它的read -a
无法读取分隔值。
那必须是这样的(这里假设${var^}
操作符是 bash 4.0+):
#! /bin/bash -
set -o noglob -o nounset
IFS=,
re='^([^[:alnum:]]*)([[:alnum:]]+)(.*)$'
while IFS= read -ru3 line; do
fields=( $line'' )
if (( ${#fields[@]} >= 5 )); then
rest="${fields[4]}" fields[4]=
while [[ "$rest" =~ $re ]]; do
fields[4]="${fields[4]}${BASH_REMATCH[1]}${BASH_REMATCH[2]^}"
rest="${BASH_REMATCH[3]}"
done
fi
printf '%s\n' "${fields[*]}" || exit
done 3< your-file
这些假设输入是在用户区域设置字符集中编码的有效文本(例如,在 UTF-8 区域设置中,上面的内容é
以 UTF-8(0xc3 0xa9 字节)编码,而不是 iso8859-1 或其他字符集)。 bash(可能还有 awk)会因 NUL 字节而阻塞。
由于perl
's\w
是 alnums + 下划线,因此您还会发现字符串之间的区别,jean_pierre
其中的perl
字符串大写为 as,Jean_pierre
而其他字符串则大写为Jean_Pierre
。您可能需要适应您的特定输入(还可以考虑组合字符,这也会在此处的工作中添加扳手)。另请参阅Lingua::EN::NameCase
perl
模块来处理更多特殊情况。
至于默认安装在什么系统上的命令。大多数系统都会有perl
(可能是Text::CSV
模块,但可能不是那个Lingua::EN::NameCase
)和 POSIX 兼容awk
和sh
实现,许多(甚至一些非 GNU 系统)有bash
(GNU shell),一些有 GNU awk(尽管不是一些基于 GNU 的系统)例如 Ubuntu,至少在某些版本中更喜欢 mawk)。目前很少有zsh
默认安装的。
CentOS 作为 GNU 系统bash
,gawk
除了perl
.bash
甚至gawk
提供sh
和awk
那里。
答案2
如果您的所有输入都是所有英文字母的简单 2 个单词名称,没有中间单词大写,就像您发布的示例中那样,那么在每个 Unix 机器上的任何 shell 中使用任何 awk:
$ awk '
BEGIN { FS=OFS="," }
{ split($5,ns," "); $5 = uc(ns[1]) " " uc(ns[2]) }
{ print }
function uc(str) { return toupper(substr(str,1,1)) substr(str,2) }
' file
1,,,,Ivan Petrov,,67,
2,2,,,Vasia Pupkin,director,8,
3,,,,John Lenon,,,
答案3
另一种 bash 方式:
while IFS=, read -ra fields; do
read -ra name <<<"${fields[4]}"
fields[4]=${name[*]^}
(IFS=,; echo "${fields[*]}")
done < file
1,,,,Ivan Petrov,,67
2,2,,,Vasia Pupkin,director,8
3,,,,John Lenon,,
和 Perl
perl -F, -lane '
$F[4] = join " ", map {ucfirst} split " ", $F[4];
print join ",", @F;
' file
答案4
使用csvjson
来自csvkit将 CSV 文件转换为 JSON,然后使用jq
在将修改后的数据输出为 CSV 之前:
csvjson -H file |
jq -r '
.[].e |= gsub(
"(?<a>[[:alnum:]]+)";
.a | sub("(?<b>.)"; .b | ascii_upcase)) |
.[] | map(.) | @csv'
该csvjson
命令将 CSV 文件转换为 JSON 文档,其中数组中的每一列均按字母键排列,每个原始 CSV 行有一个对象。该表达式从每个对象中jq
挑选出第五 ( ) 列并提取其中的每个单词。使用的函数e
将每个单词的第一个字符转换为大写,然后将结果输出为正确引用的 CSV 数据。ascii_upcase
jq
鉴于问题中的数据,这将导致
1,,,,"Ivan Petrov",,67,
2,2,,,"Vasia Pupkin","director",8,
3,,,,"John Lenon",,,
这也可以处理包含嵌入逗号和换行符的 CSV 字段。