Substitua uma sequência em um arquivo por nodejs


166

Eu uso a tarefa md5 grunt para gerar nomes de arquivos MD5. Agora, quero renomear as fontes no arquivo HTML com o novo nome de arquivo no retorno de chamada da tarefa. Gostaria de saber qual é a maneira mais fácil de fazer isso.


1
Eu gostaria que houvesse uma combinação de renomeador e substituir em arquivo, que renomeiasse os arquivos e pesquisasse / substituísse qualquer referência para esses arquivos.
Brain2000

Respostas:


302

Você pode usar regex simples:

var result = fileAsString.replace(/string to be replaced/g, 'replacement');

Assim...

var fs = require('fs')
fs.readFile(someFile, 'utf8', function (err,data) {
  if (err) {
    return console.log(err);
  }
  var result = data.replace(/string to be replaced/g, 'replacement');

  fs.writeFile(someFile, result, 'utf8', function (err) {
     if (err) return console.log(err);
  });
});

Claro, mas eu tenho que ler o arquivo, substituir o texto e depois escrever o arquivo novamente, ou existe uma maneira mais fácil, desculpe, eu sou mais um cara de front-end.
Andreas Köberle

Talvez haja um módulo de nó para conseguir isso, mas não estou ciente disso. Adicionado um exemplo completo btw.
Asgoth

4
@Zax: Obrigado, estou surpreso que esse 'bug' possa sobreviver por tanto tempo;)
asgoth

1
pena quanto eu sei utf-8 suporte muitos linguagem como: vietnamita, chinês ...
vuhung3990

Se a sua string aparecer várias vezes no seu texto, ela substituirá apenas a primeira string encontrada.
eltongonc

79

Como o replace não estava funcionando para mim, criei um simples pacote npm replace-in-file para substituir rapidamente o texto em um ou mais arquivos. É parcialmente baseado na resposta de @ asgoth.

Editar (3 de outubro de 2016) : O pacote agora oferece suporte a promessas e globs, e as instruções de uso foram atualizadas para refletir isso.

Editar (16 de março de 2018) : O pacote acumulou mais de 100 mil downloads mensais agora e foi estendido com recursos adicionais e uma ferramenta CLI.

Instalar:

npm install replace-in-file

Exigir módulo

const replace = require('replace-in-file');

Especificar opções de substituição

const options = {

  //Single file
  files: 'path/to/file',

  //Multiple files
  files: [
    'path/to/file',
    'path/to/other/file',
  ],

  //Glob(s) 
  files: [
    'path/to/files/*.html',
    'another/**/*.path',
  ],

  //Replacement to make (string or regex) 
  from: /Find me/g,
  to: 'Replacement',
};

Substituição assíncrona com promessas:

replace(options)
  .then(changedFiles => {
    console.log('Modified files:', changedFiles.join(', '));
  })
  .catch(error => {
    console.error('Error occurred:', error);
  });

Substituição assíncrona com retorno de chamada:

replace(options, (error, changedFiles) => {
  if (error) {
    return console.error('Error occurred:', error);
  }
  console.log('Modified files:', changedFiles.join(', '));
});

Substituição síncrona:

try {
  let changedFiles = replace.sync(options);
  console.log('Modified files:', changedFiles.join(', '));
}
catch (error) {
  console.error('Error occurred:', error);
}

3
Ótimo e fácil de usar módulo chave na mão. Usado com async / await e uma bola sobre uma grande pasta bastante e foi muito rápido
Matt Fletcher

Será que vai ser capaz de trabalhar com tamanhos de arquivo maior do que 256 Mb desde que eu li em algum lugar que limite corda em js nó é 256 Mb
Alien128

Acredito que sim, mas também há um trabalho em andamento para implementar a substituição de streaming para arquivos maiores.
Adam Reis

1
bom, encontrei e usei este pacote (para sua ferramenta CLI) antes de ler esta resposta do SO. adoro isso
Russell Chisholm

38

Talvez o módulo "replace" ( www.npmjs.org/package/replace ) também funcione para você. Não seria necessário ler e gravar o arquivo.

Adaptado da documentação:

// install:

npm install replace 

// require:

var replace = require("replace");

// use:

replace({
    regex: "string to be replaced",
    replacement: "replacement string",
    paths: ['path/to/your/file'],
    recursive: true,
    silent: true,
});

Você sabe como pode filtrar por extensão de arquivo nos caminhos? algo como caminhos: [ '/ para / seu arquivo / caminho / * js.'] -> Ele não funciona
Kalamarico

Você pode usar o node-glob para expandir os padrões glob para uma matriz de caminhos e, em seguida, iterar sobre eles.
RobW

3
Isso é legal, mas foi abandonado. Consulte stackoverflow.com/a/31040890/1825390 para obter um pacote mantido, se você desejar uma solução pronta para uso.
Xavdid

1
Há também uma versão mantida chamada node-replace ; No entanto, olhando para a base de código nem esta nem substituir-in-file realmente substituir o texto no arquivo, eles usam readFile()e writeFile()como a resposta aceita.
C1moore 8/02

26

Você também pode usar a função 'sed' que faz parte do ShellJS ...

 $ npm install [-g] shelljs


 require('shelljs/global');
 sed('-i', 'search_pattern', 'replace_pattern', file);

Visite ShellJs.org para mais exemplos.


esta parece ser a solução mais limpa :)
Yerken

1
shxpermite executar a partir de scripts npm, o ShellJs.org recomendou. github.com/shelljs/shx
Joshua Robinson

Eu também gosto disso. Melhor um oneliner, de NPM-módulo, mas as linhas de código surveral ^^
Suther

Importar uma dependência de terceiros não é a solução mais limpa.
four43

Isso não fará multilinhas.
chovy

5

Você pode processar o arquivo enquanto está sendo lido usando fluxos. É como usar buffers, mas com uma API mais conveniente.

var fs = require('fs');
function searchReplaceFile(regexpFind, replace, cssFileName) {
    var file = fs.createReadStream(cssFileName, 'utf8');
    var newCss = '';

    file.on('data', function (chunk) {
        newCss += chunk.toString().replace(regexpFind, replace);
    });

    file.on('end', function () {
        fs.writeFile(cssFileName, newCss, function(err) {
            if (err) {
                return console.log(err);
            } else {
                console.log('Updated!');
            }
    });
});

searchReplaceFile(/foo/g, 'bar', 'file.txt');

3
Mas ... e se o pedaço dividir a sequência regexpFind? A intenção não falha então?
Jaakko Karhu

Esse é um ponto muito bom. Gostaria de saber se, definindo um valor bufferSizemaior que a string que você está substituindo e salvando o último pedaço e concatenando com o atual, você poderá evitar esse problema.
precisa saber é

1
Provavelmente, esse trecho também deve ser aprimorado gravando o arquivo modificado diretamente no sistema de arquivos, em vez de criar uma grande variável, pois o arquivo pode ser maior que a memória disponível.
precisa saber é

1

Encontrei problemas ao substituir um pequeno espaço reservado por uma grande sequência de códigos.

Eu estava fazendo:

var replaced = original.replace('PLACEHOLDER', largeStringVar);

Eu descobri que o problema eram os padrões especiais de substituição do JavaScript, descritos aqui . Como o código que eu estava usando como a string de substituição tinha algum $, ele estava atrapalhando a saída.

Minha solução foi usar a opção de substituição de função, que NÃO faz nenhuma substituição especial:

var replaced = original.replace('PLACEHOLDER', function() {
    return largeStringVar;
});

1

ES2017 / 8 para Nó 7.6+ com um arquivo de gravação temporário para substituição atômica.

const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs'))

async function replaceRegexInFile(file, search, replace){
  let contents = await fs.readFileAsync(file, 'utf8')
  let replaced_contents = contents.replace(search, replace)
  let tmpfile = `${file}.jstmpreplace`
  await fs.writeFileAsync(tmpfile, replaced_contents, 'utf8')
  await fs.renameAsync(tmpfile, file)
  return true
}

Note, apenas para arquivos pequenos, pois eles serão lidos na memória.


Não há necessidade bluebird, use native Promisee util.promisify .
Francisco Mateo

1
@FranciscoMateo True, mas além de 1 ou 2 funções promisifyAll ainda é super útil.
Matt

1

No Linux ou Mac, o keep é simples e apenas use sed com o shell. Nenhuma biblioteca externa necessária. O código a seguir funciona no Linux.

const shell = require('child_process').execSync
shell(`sed -i "s!oldString!newString!g" ./yourFile.js`)

A sintaxe sed é um pouco diferente no Mac. Não posso testá-lo agora, mas acredito que você só precisa adicionar uma string vazia após o "-i":

const shell = require('child_process').execSync
shell(`sed -i "" "s!oldString!newString!g" ./yourFile.js`)

O "g" após a final "!" faz com que sed substitua todas as instâncias em uma linha. Remova-o e somente a primeira ocorrência por linha será substituída.


1

Expandindo a resposta do @ Sanbor, a maneira mais eficiente de fazer isso é ler o arquivo original como um fluxo e, em seguida, também transmitir cada pedaço em um novo arquivo e, por fim, substituir o arquivo original pelo novo.

async function findAndReplaceFile(regexFindPattern, replaceValue, originalFile) {
  const updatedFile = `${originalFile}.updated`;

  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(originalFile, { encoding: 'utf8', autoClose: true });
    const writeStream = fs.createWriteStream(updatedFile, { encoding: 'utf8', autoClose: true });

    // For each chunk, do the find & replace, and write it to the new file stream
    readStream.on('data', (chunk) => {
      chunk = chunk.toString().replace(regexFindPattern, replaceValue);
      writeStream.write(chunk);
    });

    // Once we've finished reading the original file...
    readStream.on('end', () => {
      writeStream.end(); // emits 'finish' event, executes below statement
    });

    // Replace the original file with the updated file
    writeStream.on('finish', async () => {
      try {
        await _renameFile(originalFile, updatedFile);
        resolve();
      } catch (error) {
        reject(`Error: Error renaming ${originalFile} to ${updatedFile} => ${error.message}`);
      }
    });

    readStream.on('error', (error) => reject(`Error: Error reading ${originalFile} => ${error.message}`));
    writeStream.on('error', (error) => reject(`Error: Error writing to ${updatedFile} => ${error.message}`));
  });
}

async function _renameFile(oldPath, newPath) {
  return new Promise((resolve, reject) => {
    fs.rename(oldPath, newPath, (error) => {
      if (error) {
        reject(error);
      } else {
        resolve();
      }
    });
  });
}

// Testing it...
(async () => {
  try {
    await findAndReplaceFile(/"some regex"/g, "someReplaceValue", "someFilePath");
  } catch(error) {
    console.log(error);
  }
})()

0

Eu usaria um fluxo duplex. como documentado aqui nodejs doc fluxos duplex

Um fluxo de transformação é um fluxo duplex em que a saída é calculada de alguma forma a partir da entrada.


0

<p>Please click in the following {{link}} to verify the account</p>


function renderHTML(templatePath: string, object) {
    const template = fileSystem.readFileSync(path.join(Application.staticDirectory, templatePath + '.html'), 'utf8');
    return template.match(/\{{(.*?)\}}/ig).reduce((acc, binding) => {
        const property = binding.substring(2, binding.length - 2);
        return `${acc}${template.replace(/\{{(.*?)\}}/, object[property])}`;
    }, '');
}
renderHTML(templateName, { link: 'SomeLink' })

com certeza você pode melhorar a função do modelo de leitura para ler como fluxo e compor os bytes por linha para torná-la mais eficiente

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.