From 0c3032a55122213884f08b974c21c25608e92078 Mon Sep 17 00:00:00 2001 From: Seb Aebischer Date: Sat, 6 May 2023 22:28:14 +0100 Subject: [PATCH] feat(file): `withFile` --- README.md | 14 +++++++++++++- src/file.ts | 33 +++++++++++++++++++++++++++++++++ src/index.ts | 2 ++ src/text.ts | 20 ++++++++++++++++++++ tests/.eslintrc.yml | 3 +++ tests/file.spec.ts | 37 +++++++++++++++++++++++++++++++++++++ tests/text.spec.ts | 25 +++++++++++++++++++++++++ 7 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/file.ts create mode 100644 src/text.ts create mode 100644 tests/file.spec.ts create mode 100644 tests/text.spec.ts diff --git a/README.md b/README.md index b0b7373..4763941 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Scripting tools -Useful tools for automating stuff with Node. +Useful tools for automating stuff with Node, like making small changes to config files or updating a dependency. ## Usage @@ -38,3 +38,15 @@ If you also need to inspect `stderr`, use `exec` which is the promisified versio const output = await exec('echo hello'); // => { stdout: 'hello\n', stderr: '' } ``` + +### File manipulation + +Work with text files using the `withFile` function. It opens the file, passes the content to a callback for editing, then saves it back if any changes were made. + +```ts +await withFile('example.txt', (f) => { + // ... +}); +``` + +For all available methods see the [`Text`](src/text.ts) class. diff --git a/src/file.ts b/src/file.ts new file mode 100644 index 0000000..dc33a27 --- /dev/null +++ b/src/file.ts @@ -0,0 +1,33 @@ +import { readFile, writeFile } from 'fs/promises'; +import { EOL } from 'os'; + +import { Text } from './text'; + +/** + * Work with a text file. + * + * Opens the file, passes the content to `callback`, then saves it back if any + * changes were made. + * + * Line endings in the content are normalised to `\n`, and returned to the + * OS-specific line ending on save (as defined by `os.EOL`). + * + * @param path File path. + * @param callback Function that does something with the file content. + */ +export async function withFile( + path: string, + callback: (f: Text) => void | Promise, +): Promise { + const rawContent = (await readFile(path)).toString(); + const originalContent = rawContent + .replace(/\r\n/g, '\r') + .replace(/\r/g, '\n'); + + const f = new Text(originalContent); + await callback(f); + + if (f.content !== originalContent) { + await writeFile(path, f.content.replace(/\n/g, EOL)); + } +} diff --git a/src/index.ts b/src/index.ts index 033bc73..240a291 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,3 @@ +export * from './file'; export * from './shell'; +export * from './text'; diff --git a/src/text.ts b/src/text.ts new file mode 100644 index 0000000..2130c1e --- /dev/null +++ b/src/text.ts @@ -0,0 +1,20 @@ +/** + * Class for manipulating a block of text. + */ +export class Text { + /** + * The text content. + */ + content: string; + + constructor(content: string) { + this.content = content; + } + + /** + * Returns the text split into lines. + */ + lines(): string[] { + return this.content.split('\n'); + } +} diff --git a/tests/.eslintrc.yml b/tests/.eslintrc.yml index 621a33f..32b6dce 100644 --- a/tests/.eslintrc.yml +++ b/tests/.eslintrc.yml @@ -1,2 +1,5 @@ extends: - '@comicrelief/eslint-config/mixins/mocha' + +rules: + no-param-reassign: off diff --git a/tests/file.spec.ts b/tests/file.spec.ts new file mode 100644 index 0000000..6dd5694 --- /dev/null +++ b/tests/file.spec.ts @@ -0,0 +1,37 @@ +import { readFile, unlink, writeFile } from 'fs/promises'; + +import { expect } from 'chai'; + +import { withFile } from '@/src'; + +const TEST_FILE = 'test.tmp'; + +describe('withFile', () => { + before('create test file', async () => { + await writeFile(TEST_FILE, 'hello world'); + }); + + after('delete test file', async () => { + await unlink(TEST_FILE); + }); + + it('should pass file content to callback', async () => { + let wasCalled = false; + + await withFile(TEST_FILE, (f) => { + expect(f.content).to.equal('hello world'); + wasCalled = true; + }); + + expect(wasCalled, 'callback was not called').to.be.true; + }); + + it('should write back any changes made', async () => { + await withFile(TEST_FILE, (f) => { + f.content = 'goodbye'; + }); + + const newContent = (await readFile(TEST_FILE)).toString(); + expect(newContent).to.equal('goodbye'); + }); +}); diff --git a/tests/text.spec.ts b/tests/text.spec.ts new file mode 100644 index 0000000..83a94da --- /dev/null +++ b/tests/text.spec.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; + +import { Text } from '@/src'; + +describe('Text', () => { + describe('content', () => { + it('should get content', () => { + const text = new Text('test'); + expect(text.content).to.equal('test'); + }); + + it('should set content', () => { + const text = new Text('foo'); + text.content = 'bar'; + expect(text.content).to.equal('bar'); + }); + }); + + describe('lines', () => { + it('should return an array of lines', () => { + const text = new Text('one\ntwo\nthree'); + expect(text.lines()).to.deep.equal(['one', 'two', 'three']); + }); + }); +});