Como posso aplicar 'cut' a vários arquivos e depois 'colar' os resultados?

7

Costumo fazer operações como

paste <(cut -d, -f1 file1.csv) <(cut -d, -f1 file2.csv)

que é muito entediante com mais de alguns arquivos.

Posso automatizar esse processo? com globbing? Eu posso salvar os resultados cut com

typeset -A cut_results
for f in file*.csv; do
    cut_results[$f]="$(cut -d, -f1 $f)"
done

mas não sei como proceder a partir daí.

    
por shadowtalker 17.10.2015 / 22:19

7 respostas

4

Você pode automatizar isso com globbing, especificamente o e qualificador glob , mais eval , mas não é bonito e a citação é complicada:

eval paste *.csv(e\''REPLY="<(cut -d, -f1 $REPLY)"'\')
  • A parte entre \'…\' é algum código a ser executado para cada correspondência do glob. Ele é executado com a variável REPLY definida para a correspondência e pode modificá-la.
  • Eu coloco o código entre aspas simples para que ele não seja expandido quando o glob é analisado.
  • O código REPLY="<(cut -d, -f1 $REPLY)" gera a string <(cut -d, -f1 file1.csv) se a correspondência for file1.csv . As aspas duplas são necessárias para que a parte após o sinal de igual não seja expandida quando o código e for executado, além de substituir o valor de REPLY .
  • Como cada arquivo globbed é substituído por uma string,

Seria melhor esconder a complexidade de uma função. Minimamente testado.

function map {
  emulate -LR zsh
  local cmd pre
  cmd=()
  while [[ $# -ne 0 && $1 != "--" ]]; do
    cmd+=($1)
    shift
  done
  if ((!$#)); then
    echo >&2 "Usage: $0: COMMAND [ARGS...] -- PREPROCESSOR [ARGS...] -- FILES..."
    return 125
  fi
  shift
  while [[ $# -ne 0 && $1 != "--" ]]; do
    pre+="${(q)1} "
    shift
  done
  if ((!$#)); then
    echo >&2 "Usage: $0: COMMAND [ARGS...] -- PREPROCESSOR [ARGS...] -- FILES..."
    return 125
  fi
  shift
  eval "${(@q)cmd}" "<($pre${(@q)^@})"
}

Uso de amostra (a sintaxe é remanescente de zargs ):

map paste -- cut -d, -f1 -- *.csv
    
por 18.10.2015 / 00:13
3

Experimente o awk

awk '{L[FNR]=L[FNR] $1 "\t"}END{for(i=1;i<=FNR;i++)print L[i]}' *.csv

ou colar com sed

paste *.csv | sed 's/ [^\t]*//g'
    
por 17.10.2015 / 22:46
3

Eu acho que sua primeira linha é tão boa quanto uma simples linha de uma frase.

Se houver vários arquivos com nomes diferentes, você poderá reduzir um pouco a digitação repetitiva com uma simples expansão de histórico:

Primeira execução <(cut -d, -f1

Observe o espaço à direita. Observe também que este comando lhe dará um prompt secundário; apenas pressione Ctrl - C . O único ponto é adicioná-lo ao histórico.

Próxima execução paste !!file1.csv) !!file2.csv)

O !! expandirá para o conteúdo completo da execução do comando anterior, incluindo o espaço à direita. Observe que, se você esquecer os parênteses de fechamento à direita, receberá um prompt secundário; você pode simplesmente digitar Ctrl - C e tentar novamente se isso acontecer.

Isso é um pouco hacky, mas bom o suficiente para um uso único. Se você está fazendo muito, você pode escrever uma função bash.

    
por 17.10.2015 / 23:20
1

Estou estudando bash scripting no momento, e isso pareceu uma excelente tarefa simples para praticar, então escrevi o seguinte. (Minha outra resposta é a simples hack de expansão de histórico, mas este é um script completo e eu achei digno de fazer uma resposta adicional.) acredito que é compatível com POSIX e deve funcionar com #!/bin/sh , mas não 100% de certeza. EDIT: Na verdade, o =~ não é compatível com POSIX. Você pode retirar esse check-out e deixar cut retornar o erro.

#!/bin/bash

fieldtocut=1
delimiter=','

usage () {
    cat << EOF
usage: $0 [-f FIELD] [-d DELIMITER] file1..
Cuts field FIELD from each file and pastes it.
Default field is 1, default delimiter is ','
EOF
    exit $1
}

while getopts ':f:d:' opt ; do
    case $opt in
        f)
            if [[ $OPTARG =~ ^[0-9]+$ ]] ; then
                fieldtocut="$OPTARG"
            else
                usage 1
            fi
            ;;
        d)
            delimiter=$OPTARG
            ;;
        *)
            usage 1
            ;;
    esac
done
shift $((OPTIND-1))

[ $# -eq 0 ] && usage 0

pasteargs=''

for file in "$@" ; do
    pasteargs=$(printf '%s' "$pasteargs" '<(cut -d$delimiter -f$fieldtocut ' "$file" ') ')
done

eval paste $pasteargs
    
por 18.10.2015 / 00:00
1

Supondo que seus argumentos estejam em "$@" , acredito em algo como:

eval "paste $(printf "<( cut -d, -f1 %q ) " "$@")"

deve fazer isso.

    
por 19.10.2015 / 03:58
0

Aqui está outra maneira de fazer isso que é muito semelhante à resposta do Curinga :

files=( file1.csv file2.csv)
eval paste "<( cut -d, -f1 ${^files[@]} )"

Em vez de um loop for , isso usa a expansão ${^ ... } , que é específica do Zsh.

O motivo files deve ser atribuído primeiro é que globbing é sempre feito por último, portanto, se files precisar ser gerado automaticamente (como em files=( *.csv ) ), algo como ${^:-( *.csv )} se expandirá somente depois de todos os outros expansões ocorreram. Queremos expandir primeiro .

A expansão ${^ ... } faz com que a matriz resultante aja como o resultado da expansão da chave. Por exemplo, atribua x=(a b) e compare echo ${x}y a echo ${^x}y .

A citação é necessária para induzir Zsh a tratar o texto ao redor como uma string literal. Caso contrário, ele dividiria a linha de comando nos espaços, portanto, nossa expansão ${^ ... } reduziria para ""${^ ... }"" ; ou seja, cada elemento seria cercado apenas por uma string vazia. Isto é,

echo "<( cut -d, -f1 ${^files[@]} )"

e

echo "<( cut -d, -f1 "\
${^files[@]}\
" )"

são equivalentes, mas não são o mesmo que

echo <( cut -d, -f1 ${^files[@]} )

Mas a citação introduz um novo problema: a linha de comando é analisada e dividida sem considerar a expansão em andamento. Ou seja, mesmo que tenhamos entrado efetivamente

paste <( cut -d, -f1 file1.csv ) <( cut -d, -f1 file2.csv )

como desejado, isso é de fato analisado como

paste '<( cut -d, -f1 file1.csv )' '<( cut -d, -f1 file2.csv )'

Portanto, precisamos de eval para analisar novamente a expressão corretamente formada. Para ver isso em ação, compare

setopt noxtrace
eval paste "<( cut -d, -f1 ${^files[@]} )" 1>/dev/null 2>&1

para

setopt xtrace
eval paste "<( cut -d, -f1 ${^files[@]} )" 1>/dev/null 2>&1

Eu esperava que alguma combinação de expansões aninhadas, a expansão ${ ... :- ... } e os sinalizadores de expansão de parâmetro Q , z e / ou s levassem a uma reavaliação sem eval , mas evidentemente Esse não é o caso. Eu também gostaria que houvesse uma maneira de forçar a globalização, mas novamente isso parece impossível.

    
por 19.10.2015 / 02:23
0

Você pode obter awk para percorrer os arquivos no lockstep e relatar o campo de interesse de cada arquivo. Coloque este código em um arquivo, digamos cut_files.awk

NR == FNR{printf "%s%s",$1, FS;
for (k=2; k<ARGC; ++k)
    {getline < ARGV[k]; printf "%s%s", $1, k==ARGC-1?"\n":FS}; next};
NR != FNR{for (k=2; k<ARGC; ++k) close(ARGV[k]); exit}

E depois ligue assim

awk -F',' -f cut_files.awk file1 file2 file3 file4 ....
    
por 19.10.2015 / 17:28

Tags