ChatGPT解决这个技术问题 Extra ChatGPT

Replace a string in a file with nodejs

I use the md5 grunt task to generate MD5 filenames. Now I want to rename the sources in the HTML file with the new filename in the callback of the task. I wonder what's the easiest way to do this.

I wish there was a renamer and replace-in-file combination, which would both rename the files, and search/replace any reference for those files as well.
@Brain2000 Check out gulp-rev-all at: github.com/smysnk/gulp-rev-all (it does the hash versioning all in one step... easy to use)

J
Jonathan Lin

You could use simple regex:

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

So...

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);
  });
});

Sure, but do I have to read the file replace the text and then write the file again, or is there an easier way, sorry I'm more of a frontend guy.
Maybe there is a node module to achieve this, but i'm not aware of it. Added a full example btw.
@Zax: Thanks, I'm surprised this 'bug' could survive so long ;)
sorry as i know utf-8 support many language like: vietnamese, chinese...
If your string appearance multiple times in your text it will replace only the first string it finds.
A
Adam Reis

Since replace wasn't working for me, I've created a simple npm package replace-in-file to quickly replace text in one or more files. It's partially based on @asgoth's answer.

Edit (3 October 2016): The package now supports promises and globs, and the usage instructions have been updated to reflect this.

Edit (16 March 2018): The package has amassed over 100k monthly downloads now and has been extended with additional features as well as a CLI tool.

Install:

npm install replace-in-file

Require module

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

Specify replacement options

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',
};

Asynchronous replacement with promises:

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

Asynchronous replacement with callback:

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

Synchronous replacement:

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

Great and easy to use turn-key module. Used it with async/await and a glob over quite a large folder and it was lightning fast
Will it be able to work with file sizes greater then 256 Mb since i read somewhere that string limit in node js is 256 Mb
nice, I found and used this package (for its CLI tool) before I ever read this SO answer. love it
Excellent! This works super fast and easy!
Absolutely wonderful code, and fittings explanations to boot!
G
Gabriel

Perhaps the "replace" module (www.npmjs.org/package/replace) also would work for you. It would not require you to read and then write the file.

Adapted from the documentation:

// 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,
});

Do you know how can filter by file extension in paths? something like paths: ['path/to/your/file/*.js'] --> it doesn't work
You can use node-glob to expand glob patterns to an array of paths, and then iterate over them.
This is nice, but has been abandoned. See stackoverflow.com/a/31040890/1825390 for a maintained package if you want an out-of-the-box solution.
There's also a maintained version called node-replace; however, looking at the code base neither this nor replace-in-file actually replace text in the file, they use readFile() and writeFile() just like the accepted answer.
The library works fine but it does not have Typescript support
T
Tony O'Hagan

You can also use the 'sed' function that's part of ShellJS ...

 $ npm install [-g] shelljs


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

Full documentation ...

ShellJS - sed()

ShellJS


this seems to be the cleanest solution :)
shx lets you run from npm scripts, ShellJs.org recommended it. github.com/shelljs/shx
I like this too. Better an oneliner, than npm-module, but surveral lines of code ^^
Importing a third party dependency is not the cleanest solution.
This won't do multilines.
S
SridharKritha

If someone wants to use promise based 'fs' module for the task.

const fs = require('fs').promises;

// Below statements must be wrapped inside the 'async' function:
const data = await fs.readFile(someFile, 'utf8');
const result = data.replace(/string to be replaced/g, 'replacement');
await fs.writeFile(someFile, result,'utf8');

s
sanbor

You could process the file while being read by using streams. It's just like using buffers but with a more convenient API.

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');

But... what if the chunk splits the regexpFind string? Doesn't the intention fail then?
That's a very good point. I wonder if by setting a bufferSize longer than the string that you're replacing and saving the last chunk and concatenating with the current one you could avoid that problem.
Probably this snippet should also be improved by writing the modified file directly to the filesystem rather than creating a big variable as the file might be larger than available memory.
@JaakkoKarhu I made an npm package that keeps old chunks in memory in case the string spans multiple chunks. It's called stream-replace-string. It doesn't work with regexs, but it is an efficient solution when just finding strings.
7
777

On Linux or Mac, keep is simple and just use sed with the shell. No external libraries required. The following code works on Linux.

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

The sed syntax is a little different on Mac. I can't test it right now, but I believe you just need to add an empty string after the "-i":

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

The "g" after the final "!" makes sed replace all instances on a line. Remove it, and only the first occurrence per line will be replaced.


h
hellatan

Expanding on @Sanbor's answer, the most efficient way to do this is to read the original file as a stream, and then also stream each chunk into a new file, and then lastly replace the original file with the new file.

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);
  }
})()

This does not handle the case where the text that regexFindPattern matches is split between two chunks.
V
Vivek Molkar

I ran into issues when replacing a small placeholder with a large string of code.

I was doing:

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

I figured out the problem was JavaScript's special replacement patterns, described here. Since the code I was using as the replacing string had some $ in it, it was messing up the output.

My solution was to use the function replacement option, which DOES NOT do any special replacement:

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

M
Matt

ES2017/8 for Node 7.6+ with a temporary write file for atomic replacement.

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, only for smallish files as they will be read into memory.


No need for bluebird, use native Promise and util.promisify.
@FranciscoMateo True, but beyond 1 or 2 functions promisifyAll is still super useful.
N
Natdrip

This may help someone:

This is a little different than just a global replace

from the terminal we run
node replace.js

replace.js:

function processFile(inputFile, repString = "../") {
var fs = require('fs'),
    readline = require('readline'),
    instream = fs.createReadStream(inputFile),
    outstream = new (require('stream'))(),
    rl = readline.createInterface(instream, outstream);
    formatted = '';   

const regex = /<xsl:include href="([^"]*)" \/>$/gm;

rl.on('line', function (line) {
    let url = '';
    let m;
    while ((m = regex.exec(line)) !== null) {
        // This is necessary to avoid infinite loops with zero-width matches
        if (m.index === regex.lastIndex) {
            regex.lastIndex++;
        }
        
        url = m[1];
    }

    let re = new RegExp('^.* <xsl:include href="(.*?)" \/>.*$', 'gm');

    formatted += line.replace(re, `\t<xsl:include href="${repString}${url}" />`);
    formatted += "\n";
});

rl.on('close', function (line) {
    fs.writeFile(inputFile, formatted, 'utf8', function (err) {
        if (err) return console.log(err);
    });

});
}


// path is relative to where your running the command from
processFile('build/some.xslt');

This is what this does. We have several file that have xml:includes

However in development we need the path to move down a level.

From this

<xsl:include href="common/some.xslt" />

to this

<xsl:include href="../common/some.xslt" />

So we end up running two regx patterns one to get the href and the other to write there is probably a better way to do this but it work for now.

Thanks


p
pungggi

I would use a duplex stream instead. like documented here nodejs doc duplex streams

A Transform stream is a Duplex stream where the output is computed in some way from the input.


E
Ezzabuzaid

<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' })

for sure you can improve the reading template function to read as stream and compose the bytes by line to make it more efficient


u
user9987636

Nomaly, I use tiny-replace-files to replace texts in file or files. This pkg is smaller and lighter... https://github.com/Rabbitzzc/tiny-replace-files

import { replaceStringInFilesSync } from 'tiny-replace-files'

const options = {
  files: 'src/targets/index.js',
  from: 'test-plugin',
  to: 'self-name',
}

# await
const result = replaceStringInFilesSync(options)
console.info(result)

While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From Review