Script de shell para substituir cadeia em vários arquivos nos diretórios selecionados

3

Eu criei o script abaixo, que pega o caminho de um único diretório e substitui a string de pesquisa em todos os arquivos desse diretório. Gostaria de aprimorar esse script de forma que ele possa pesquisar e substituir a sequência em vários diretórios listados em um arquivo de entrada externo.

Conteúdo do arquivo de entrada externo:

/var/start/system1/dir1
/var/start/system2/dir2
/var/start/system3/dir3
/var/start/system4/dir4

Script com um diretório:

filepath="/var/start/system/dir1"
searchstring="test"
replacestring="test01"

i=0; 

for file in $(grep -l -R $searchstring $filepath)
do
  cp $file $file.bak
  sed -e "s/$searchstring/$replacestring/ig" $file > tempfile.tmp
  mv tempfile.tmp $file

  let i++;

  echo "Modified: " $file
done
    
por user68775 28.05.2014 / 05:02

3 respostas

3

Com ferramentas GNU

< dir.list xargs -rd '\n' grep -rlZ -- "$searchstring" |
  xargs -r0 sed -i -e "s/$searchstring/$replacestring/ig" --

(Não se esqueça de citar suas variáveis, deixando uma variável sem aspas é o operador split + glob)

    
por 28.05.2014 / 07:55
2

Primeiro de tudo, a dança tmpfile pode ser evitada usando sed -i com o GNU sed ou sed -i '' com o FreeBSD (substituição no local).

grep -R pode ter vários caminhos na linha de comando, portanto, se você tiver certeza de que nenhum dos caminhos contém espaços, poderá substituir $(grep -l -Re "$searchstring" "$filepath") por $(grep -l -R "$searchstring" $(cat path_list)) .

Isso falhará se algum dos caminhos contiver espaços, tabulações ou qualquer caractere globbing, mas também o original.

Uma abordagem muito mais robusta usa find e apenas aplica sed a todos os arquivos, confiando que não modifique arquivos sem correspondência (assumindo aqui um bash shell):

# Read newline-separated path list into array from file 'path_list'
IFS=$'\n' read -d '' -r -a paths path_list

# Run sed on everything
find "${paths[@]}" \
  -exec sed -i -r -e "s/$searchstring/$replacestring/ig" '{}' ';'

Mas isso não fornece comentários sobre quais arquivos estão sendo modificados.

Uma versão mais longa que fornece o feedback:

# Read newline-separated path list into array from file 'path_list'
IFS=$'\n' read -d '' -r -a paths path_list

grep -l -R "$searchstring" "${paths[@]}" | while IFS= read -r file; do
  sed -r -i -e "s/$searchstring/$replacestring/ig" "$file"
  echo "Modified: $file"
done
    
por 28.05.2014 / 05:34
1

Esta é a maneira mais portátil que posso pensar em fazer isso, embora ainda dependa do principalmente portátil /dev/fd/0 para .dot . Sem isso, você poderia usar um único arquivo. Em qualquer caso, depende principalmente desta função de shell que escrevi no outro dia:

_sed_cesc_qt() { 
    sed -n ':n;\|^'"$1"'|!{H;$!{n;bn}};{$l;x;l}' |
    sed -n '\|^'"$1"'|{:n;\|[$]$|!{
            N;s|.\n||;bn};s|||
            \|\([^\]\)\\([0-9]\)|{
            s||\0|g;}'"
            s|'"'|&"&"&|g;'"s|.*|'&'|p}"

}

Primeiro mostrarei o trabalho, depois explicarei como. Então, vou criar uma base de arquivos de teste:

printf 'f=%d
    echo "$f" >./"$f"
    echo "$f" >./"$f\n$f"
    echo "$f" >./"$f\n$f\n$f"
' $(seq 10) | . /dev/fd/0

Isso cria um monte de arquivos, cada um nomeado para o número 1-10 que ele contém:

ls -qm 
1, 1?1, 1?1?1, 10, 10?10, 10?10?10, 2, 2?2, 2?2?2, 3, 3?3, 3?3?3, 4, 4?4, 4?4?4, 5, 5?5, 5?5?5, 6, 6?6, 6?6?6, 7,
7?7, 7?7?7, 8, 8?8, 8?8?8, 9, 9?9, 9?9?9

Essa é uma lista delimitada por vírgulas dos arquivos no meu diretório de teste, cada ? representando uma nova linha.

cat ./1*

1
1
1
10
10
10

Cada arquivo contém apenas um único número.

Agora eu farei o grep replace:

find ././ \! -type d -exec \
        grep -l '[02468]$' \{\} + |
_sed_cesc_qt '\./\./' | 
sed 's|.|\&|g' |
xargs printf 'f=%b
        sed "/[02468]\$/s//CHANGED/" <<-SED >"$f"
        $(cat <"$f")
        SED\n' | 
. /dev/fd/0

Agora, quando eu ...

cat ./1*

1
1
1
1CHANGED
1CHANGED
1CHANGED

Todos os arquivos [2468] são similarmente CHANGED . Ele funciona recursivamente também. Ok, agora vou explicar como.

Primeiro, eu acho, a função:

  1. inicie em :n ext label
  2. \| address | argumento $1 - um marcador
  3. se a linha atual for ! não compatível {
    • anexe-o ao H buffer antigo
    • se a linha atual for ! não $ última linha {
    • sobrescrever linha atual com n ext line
    • b ranch de volta para :n ext label
    • }}
  4. else se a linha atual for $ última linha l ook no espaço padrão
  5. else e x mudam o conteúdo dos buffers de retenção e padrão e ...
  6. l inequivocamente no espaço padrão

Essa é a primeira declaração sed - e é basicamente a carne e batatas dela. Nós nunca p rint o espaço padrão - nós apenas l ook nele. É assim que o POSIX define a função l :

[2addr] l (The letter ell.) Write the pattern space to standard output in a visually unambiguous form. The characters listed in the Base Definitions volume of IEEE Std 1003.1-2001, Table 5-1, Escape Sequences and Associated Actions ( '\', '\a', '\b', '\f', '\r', '\t', '\v' ) shall be written as the corresponding escape sequence; the '\n' in that table is not applicable. Non-printable characters not in that table shall be written as one three-digit octal number (with a preceding \backslash) for each byte in the character (most significant byte first). Long lines shall be folded, with the point of folding indicated by writing a \backslash followed by a \newline; the length at which folding occurs is unspecified, but should be appropriate for the output device. The end of each line shall be marked with a '$'.

Então, se eu fizer isso:

printf '\e%s10\n10\n10' '\' | sed -n 'N;N;l'

Eu recebo:

3\10\n10\n10$

Isso é quase perfeitamente escapado para printf . Ele precisa apenas de um zero extra para o octal e para remover o $ - assim, a próxima instrução sed o limpa.

Eu não vou fazer o mesmo nível de detalhe, mas basicamente a próxima declaração sed :

  1. Se a linha começar com $1 marker ...
  2. Puxa a linha N ext até que a linha atual termine em $
  3. Se tiver que fazer o que foi mencionado acima, ele removerá o caractere de barra invertida \ e \n .
  4. Em seguida, remove o $ final
  5. localiza as barras invertidas \ seguidas por um número que não é precedido por outra barra invertida \ e insere um zero
  6. pesquisa as cotações de ' e aspas duplas
  7. Finalmente, ele envolve toda a string com ' aspas simples

Então agora, quando eu faço:

printf %s\n ././1* |
_sed_cesc_qt '\./\./'

Eu recebo:

'././1'
'././1\n1'
'././1\n1\n1'
'././10'
'././10\n10'
'././10\n10\n10'

O resto é fácil. Depende do fato de que a string ././ será resolvida, mas ocorrerá apenas na saída de find/grep na cabeça de cada nome de caminho - portanto, ela se tornará meu marcador $1 .

Eu -exec grep de find e especifique -l para que ele envie nomes de arquivos para os arquivos que contenham o regex.

Eu chamo a função e obtenho sua saída.

Eu, então, \ barra invertida escape de todos os caracteres em sua saída para xargs .

E com printf eu escrevo um script para o arquivo |pipe - que eu .dot source como /dev/fd/0 . Eu defino a variável f como seu argumento atual - meu nome de caminho - e cat that $f argumento para um << heredocument, que é alimentado para sed e sed escreve sobre o arquivo de origem.

Isso pode envolver arquivos temporários - isso depende do seu shell. bash e zsh escreverão um arquivo temporário para cada documento - mas também os limpam. dash , por outro lado, apenas escreverá o heredocument em anônimo |pipe .

O importante é que o arquivo terá que ser totalmente lido antes de ser escrito - é exatamente como funcionam os documentos e a substituição de comandos.

    
por 28.05.2014 / 13:59