Bash: title-case um campo csv

Bash: title-case um campo csv

Eu tenho este arquivo de entrada em um sistema CentOS:

1,,,,ivan petrov,,67,
2,2,,,Vasia pupkin,director,8,
3,,,,john Lenon,,,

A tarefa é alterá-lo para:

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

Nome e Sobrenome devem começar com letra maiúscula

#!/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

Esse é o meu script, mas não funciona corretamente.

Responder1

Não use um shell loop para processar texto. Use um utilitário de processamento de texto.

Aqui, para colocar os nomes em maiúscula no 5º campo , se oLingua::EN::NameCase perlmódulo está disponível:

perl -Mopen=locale -MLingua::EN::NameCase -F, -ae '
  $F[4] = nc $F[4] unless @F < 5;
  print join ",", @F' < your-file

Caso contrário, como aproximação, você poderia converter para maiúscula o primeiro caractere de cada sequência de um ou mais caracteres alfanuméricos:

perl -Mopen=locale -F, -ae '
  $F[4] =~ s/\w+/\u$&/g unless @F < 5;
  print join ",", @F' < your-file

No entanto, isso não lidaria adequadamente com nomes como McGregor, van Dike... ou aqueles com caracteres combinados.

(perl também possui módulos de análise CSV adequados, caso sua entrada não seja apenas o csv simples sem citar sua amostra).

O mesmo pode ser feito com awka sintaxe padrão, mas é muito mais complicado:

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

Um pouco mais fácil com o GNU awke sua patsplit()função:

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

Se você tiver que usar um shell loop, pelo menos use um shell com um operador de capitalização:

#! /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

Observe que aquele (e o Lingua::EN::NameCasebaseado) difere dos outros porque se transforma éric serRAem, Éric Serraem vez de, Éric SerRApor exemplo. Você pode obter o mesmo resultado perlalterando \upara \u\Le awkaplicando tolower()à segunda parte de cada palavra.

Se você tivesse que usar apenas bashseus comandos internos conforme indicado nos comentários, isso seria muito mais complicado (além de ser ineficiente), pois o bash tem operadores muito limitados em comparação com os do zsh ou ksh93, por exemplo, e seuread -anão consigo ler valores separados.

Isso teria que ser algo como (aqui assumindo o bash 4.0+ para o ${var^}operador):

#! /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

Eles assumem que a entrada é um texto válido codificado no conjunto de caracteres de localidade do usuário (por exemplo, em uma localidade UTF-8, o éacima é codificado em UTF-8 (0xc3 0xa9 bytes), não em iso8859-1 ou outro conjunto de caracteres). Os bash (e possivelmente awk) irão engasgar com bytes NUL.

Como perlé \walnums + sublinhado, você também encontrará uma diferença para strings como jean_pierrewhich perlseria capitalizada como Jean_pierreenquanto as outras seriam capitalizadas como Jean_Pierre. Você pode precisar se adaptar à sua entrada específica (considere também combinar caracteres que também seriam um obstáculo aqui). Veja também oLingua::EN::NameCase perlmódulo para lidar com casos ainda mais especiais.

Quanto a quais comandos são instalados por padrão em quais sistemas. A maioria dos sistemas terá perl(possivelmente o Text::CSVmódulo, mas provavelmente não Lingua::EN::NameCaseaquele) e um compatível com POSIX awke shimplementações, muitos (até mesmo alguns sistemas não-GNU) têm bash(o shell GNU), vários têm GNU awk (embora não alguns sistemas baseados em GNU como o Ubuntu que pelo menos em algumas versões prefere o mawk). Poucos atualmente têm zshinstalado por padrão.

O CentOS, sendo um sistema GNU, deve ter bashe gawkser instalado por padrão, além do perl. bashe gawkaté mesmo fornecer she awkaí.

Responder2

Se todas as suas entradas forem nomes simples de 2 palavras, todas as letras em inglês, sem letras maiúsculas no meio, como no exemplo postado, use qualquer awk em qualquer shell em cada caixa 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,,,

Responder3

Um bash alternativo:

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,,

e perl

perl -F, -lane '
    $F[4] = join " ", map {ucfirst} split " ", $F[4];
    print join ",", @F;
' file

Responder4

Usando csvjsondecsvkitpara transformar seu arquivo CSV em JSON e, em seguida, modificá-lo comjqantes de gerar os dados modificados como CSV:

csvjson -H file |
jq -r '
    .[].e |= gsub(
        "(?<a>[[:alnum:]]+)"; 
        .a | sub("(?<b>.)"; .b | ascii_upcase)) |
    .[] | map(.) | @csv'

O csvjsoncomando converte seu arquivo CSV em um documento JSON com chaves alfabéticas para cada coluna em uma matriz com um objeto por linha CSV original. A jqexpressão seleciona a 5ª ecoluna ( ) de cada objeto e extrai cada palavra dela. Cada palavra tem seu primeiro caractere convertido em maiúsculas usando a ascii_upcasefunção de jqe o resultado é então gerado como dados CSV devidamente citados.

Dados os dados da pergunta, isso resultaria em

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

Isso também funcionaria com campos CSV contendo vírgulas e novas linhas incorporadas.

informação relacionada