Zastąp ciąg w pliku nodejs

167

Używam zadania md5 grunt do generowania nazw plików MD5. Teraz chcę zmienić nazwy źródeł w pliku HTML na nową nazwę pliku w wywołaniu zwrotnym zadania. Zastanawiam się, jak to zrobić najłatwiej.

Andreas Köberle
źródło
1
Chciałbym, żeby istniała kombinacja zmiany nazwy i zamiany w pliku, która zarówno zmieniłaby nazwy plików, jak i wyszukałaby / zastąpiła wszelkie odniesienia do tych plików.
Brain2000

Odpowiedzi:

303

Możesz użyć prostego wyrażenia regularnego:

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

Więc...

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);
  });
});
asgoth
źródło
Jasne, ale czy muszę przeczytać plik, zamienić tekst, a następnie napisać plik ponownie, czy jest łatwiejszy sposób, przepraszam, jestem bardziej facetem od frontendu.
Andreas Köberle
Może istnieje moduł węzłowy, który to osiągnie, ale nie jestem tego świadomy. Przy okazji dodano pełny przykład.
asgoth
4
@Zax: Dzięki, jestem zaskoczony, że ten 'błąd' mógł przetrwać tak długo;)
asgoth
1
sorry as i know utf-8 obsługuje wiele języków, takich jak: vietnamese, chinese ...
vuhung3990
Jeśli twój ciąg pojawia się wiele razy w twoim tekście, zastąpi tylko pierwszy znaleziony ciąg.
eltongonc
79

Ponieważ funkcja zastępowania nie działała, utworzyłem prosty pakiet npm do zamiany w pliku, aby szybko zastąpić tekst w jednym lub wielu plikach. Jest częściowo oparty na odpowiedzi @ asgoth.

Edycja (3 października 2016 r.) : Pakiet obsługuje teraz obietnice i elementy globalne, a instrukcje użytkowania zostały zaktualizowane, aby to odzwierciedlić.

Edycja (16 marca 2018 r.) : Pakiet zgromadził teraz ponad 100 tys. Miesięcznych pobrań i został rozszerzony o dodatkowe funkcje, a także narzędzie CLI.

Zainstalować:

npm install replace-in-file

Wymagaj modułu

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

Określ opcje wymiany

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

Asynchroniczna wymiana z obietnicami:

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

Asynchroniczna zamiana z wywołaniem zwrotnym:

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

Synchroniczna wymiana:

try {
  let changedFiles = replace.sync(options);
  console.log('Modified files:', changedFiles.join(', '));
}
catch (error) {
  console.error('Error occurred:', error);
}
Adam Reis
źródło
3
Świetny i łatwy w obsłudze moduł pod klucz. Użyłem go z async / await i globem na dość dużym folderze i był błyskawiczny
Matt Fletcher
Czy będzie w stanie pracować z rozmiarami plików większymi niż 256 Mb, ponieważ gdzieś przeczytałem, że limit ciągów w węźle js wynosi 256 MB
Alien128
Wierzę, że tak, ale trwają również prace nad wdrożeniem zastępowania przesyłania strumieniowego dla większych plików.
Adam Reis,
1
fajnie, znalazłem i użyłem tego pakietu (jako narzędzia CLI), zanim przeczytałem tę odpowiedź. uwielbiam to
Russell Chisholm
38

Być może moduł „replace” ( www.npmjs.org/package/replace ) również zadziała. Nie wymagałoby to czytania, a następnie zapisywania pliku.

Na podstawie dokumentacji:

// 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,
});
Slack Undertow
źródło
Czy wiesz, jak można filtrować według rozszerzenia pliku w ścieżkach? coś takiego jak ścieżki: ['ścieżka / do / twojego / pliku / *. js'] -> to nie działa
Kalamarico
Możesz użyć node-glob, aby rozszerzyć wzorce glob do tablicy ścieżek, a następnie iterować po nich.
RobW
3
To miłe, ale zostało porzucone. Zobacz stackoverflow.com/a/31040890/1825390, aby uzyskać pakiet, który jest obsługiwany, jeśli chcesz mieć gotowe rozwiązanie.
xavdid
1
Istnieje również utrzymywana wersja zwana zastępowaniem węzłów ; Jednak patrząc na podstawę kodu, ani to, ani zamiana w pliku, nie zastępują tekstu w pliku, używają readFile()i writeFile()tak jak zaakceptowana odpowiedź.
c1moore
26

Możesz także użyć funkcji 'sed', która jest częścią ShellJS ...

 $ npm install [-g] shelljs


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

Wizyta ShellJs.org więcej przykładów.

Tony O'Hagan
źródło
to wydaje się być najczystszym rozwiązaniem :)
Yerken
1
shxpozwala na uruchamianie ze skryptów npm, zaleca ShellJs.org. github.com/shelljs/shx
Joshua Robinson
Ja też to lubię. Lepszy oneliner niż moduł npm, ale kilka wierszy kodu ^^
suther
Importowanie zależności innej firmy nie jest najczystszym rozwiązaniem.
cztery43
To nie będzie działać na wielu liniach.
chovy
5

Możesz przetwarzać plik podczas odczytu za pomocą strumieni. To tak, jak używanie buforów, ale z wygodniejszym 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');
sanbor
źródło
3
Ale ... co się stanie, jeśli fragment podzieli ciąg wyrażenia regularnego Znajdź? Czy wtedy zamiar nie zawodzi?
Jaakko Karhu
To bardzo dobra uwaga. Zastanawiam się, czy ustawiając bufferSizedłuższy niż ciąg, który zastępujesz, zapisując ostatni fragment i łącząc z bieżącym, możesz uniknąć tego problemu.
sanbor
1
Prawdopodobnie ten fragment powinien również zostać ulepszony poprzez zapisanie zmodyfikowanego pliku bezpośrednio do systemu plików zamiast tworzenia dużej zmiennej, ponieważ plik może być większy niż dostępna pamięć.
sanbor
1

Napotkałem problemy podczas zastępowania małego symbolu zastępczego dużym ciągiem kodu.

Ja robiłem:

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

Odkryłem, że problem dotyczy specjalnych wzorców zastępowania JavaScript, opisanych tutaj . Ponieważ kod, którego użyłem jako zastępujący ciąg, zawierał trochę $w sobie, zakłócał wynik.

Moim rozwiązaniem było użycie opcji zamiany funkcji, która NIE WYKONUJE żadnej specjalnej zamiany:

var replaced = original.replace('PLACEHOLDER', function() {
    return largeStringVar;
});
anderspitman
źródło
1

ES2017 / 8 dla węzła 7.6+ z tymczasowym plikiem zapisu do zastąpienia atomowego.

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
}

Uwaga, tylko dla małych plików, ponieważ zostaną one odczytane do pamięci.

Matt
źródło
Nie ma potrzeby bluebird, użyj natywnego Promisei util.promisify .
Francisco Mateo
1
@FranciscoMateo True, ale poza 1 lub 2 funkcjami promisifyAll jest nadal bardzo przydatne.
Matt
1

W systemie Linux lub Mac Keep jest prosty i po prostu używaj seda z powłoką. Nie są wymagane żadne biblioteki zewnętrzne. Poniższy kod działa w systemie Linux.

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

Składnia seda jest trochę inna na Macu. Nie mogę tego teraz przetestować, ale wydaje mi się, że wystarczy dodać pusty ciąg po „-i”:

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

Litera „g” po ostatnim „!” sprawia, że ​​sed zastępuje wszystkie instancje w linii. Usuń go, a tylko pierwsze wystąpienie w wierszu zostanie zastąpione.

777
źródło
1

Rozwijając odpowiedź @ Sanbor, najskuteczniejszym sposobem na to jest odczytanie oryginalnego pliku jako strumienia, a następnie strumieniowanie każdej porcji do nowego pliku, a następnie zastąpienie oryginalnego pliku nowym plikiem.

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);
  }
})()
dusza sudo
źródło
0

Zamiast tego użyłbym strumienia dupleksowego. jak udokumentowano tutaj nodejs doc dupleksowe strumienie

Strumień Transform to strumień Duplex, w którym dane wyjściowe są w jakiś sposób obliczane na podstawie danych wejściowych.

pungggi
źródło
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' })

z pewnością możesz ulepszyć funkcję szablonu odczytu, aby czytać jako strumień i komponować bajty po linii, aby była bardziej wydajna

Ezzabuzaid
źródło