Bash: заглавные буквы поля csv

Bash: заглавные буквы поля csv

У меня есть этот входной файл в системе 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

Не используйте цикл оболочки для обработки текста. Используйте утилиту для обработки текста.

Здесь, чтобы писать имена с заглавной буквы в 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

Немного проще с GNU awkи его 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

Если вам приходится использовать цикл оболочки, по крайней мере используйте оболочку с оператором заглавных букв:

#! /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на \u\Lи в awk, применив tolower()ко второй части каждого слова.

Если бы вам пришлось использовать только bashвстроенные команды и их, как вы указали в комментариях, это было бы гораздо более обременительно (помимо того, что неэффективно), поскольку bash имеет очень ограниченное количество операторов по сравнению, например, с zsh или ksh93, а его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будут заглавными как , Jean_pierreа другие будут заглавными как Jean_Pierre. Вам может потребоваться адаптироваться к вашему конкретному вводу (также рассмотрите возможность объединения символов, что также добавит гаечный ключ в работу здесь). Смотрите такжеLingua::EN::NameCase perlмодуль для обработки еще большего количества особых случаев.

Что касается того, какие команды устанавливаются по умолчанию на каких системах. Большинство систем будут иметь perl(возможно Text::CSV, модуль, но, скорее всего, не Lingua::EN::NameCaseтот) и POSIX-совместимые awkреализации sh, многие (даже некоторые не-GNU системы) имеют bash(оболочку GNU), несколько имеют GNU awk (хотя не некоторые основанные на GNU системы, такие как Ubuntu, которые по крайней мере в некоторых версиях предпочитают mawk). В настоящее время немногие установили zshпо умолчанию.

CentOS, будучи системой GNU, должна иметь bashи gawkустановленными по умолчанию в дополнение к perl. bashи gawkдаже предоставлять shи awkтам.

решение2

Если все ваши входные данные представляют собой простые двухсловные имена, состоящие только из английских букв, без заглавных букв в середине слова, как в опубликованном вами примере, то с помощью любого awk в любой оболочке на каждой машине Unix:

$ 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 -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выбирает 5-й ( e) столбец из каждого объекта и извлекает каждое слово в нем. Каждое слово имеет свой первый символ, преобразованный в верхний регистр с помощью функции ascii_upcase, jqа затем результат выводится как правильно заключенные в кавычки данные CSV.

Учитывая данные в вопросе, это приведет к

1,,,,"Ivan Petrov",,67,
2,2,,,"Vasia Pupkin","director",8,
3,,,,"John Lenon",,,

Это также справится с полями CSV, содержащими встроенные запятые и символы новой строки.

Связанный контент