Skip to content

Commit

Permalink
feat(file): withFile
Browse files Browse the repository at this point in the history
  • Loading branch information
seb-cr committed May 6, 2023
1 parent a68ade7 commit 0c3032a
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 1 deletion.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.
33 changes: 33 additions & 0 deletions src/file.ts
Original file line number Diff line number Diff line change
@@ -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<void>,
): Promise<void> {
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));
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './file';
export * from './shell';
export * from './text';
20 changes: 20 additions & 0 deletions src/text.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
3 changes: 3 additions & 0 deletions tests/.eslintrc.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
extends:
- '@comicrelief/eslint-config/mixins/mocha'

rules:
no-param-reassign: off
37 changes: 37 additions & 0 deletions tests/file.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
25 changes: 25 additions & 0 deletions tests/text.spec.ts
Original file line number Diff line number Diff line change
@@ -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']);
});
});
});

0 comments on commit 0c3032a

Please sign in to comment.