aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruce Hill <bruce@bruce-hill.com>2025-12-22 16:32:40 -0500
committerBruce Hill <bruce@bruce-hill.com>2025-12-22 16:32:40 -0500
commit83e6cc9197bd8e7a19834d291fe4c5e62639db38 (patch)
treee1fad62f8e579427470e40ead166ea0e90745665
parent0ee53cd5a79d41b124413d5da3e4279d06b17bfc (diff)
Add Path.writer() and Path.byte_writer()
-rw-r--r--CHANGES.md4
-rw-r--r--api/api.md52
-rw-r--r--api/paths.md52
-rw-r--r--api/paths.yaml76
-rw-r--r--man/man3/tomo-Path.318
-rw-r--r--man/man3/tomo-Path.byte_writer.342
-rw-r--r--man/man3/tomo-Path.writer.342
-rw-r--r--src/environment.c4
-rw-r--r--src/stdlib/paths.c83
-rw-r--r--src/stdlib/paths.h2
10 files changed, 368 insertions, 7 deletions
diff --git a/CHANGES.md b/CHANGES.md
index e609509b..72c4cef5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,9 @@
# Version History
+## v2025-12-22
+
+- Added `Path.writer()` and `Path.byte_writer()` for multiple successive writes
+
## v2025-12-21.6
- Add smarter default behavior if run without any args (REPL-like script runner)
diff --git a/api/api.md b/api/api.md
index 2be5e4e9..d035a3fe 100644
--- a/api/api.md
+++ b/api/api.md
@@ -2646,6 +2646,32 @@ for line in (/dev/stdin).by_line()!
say(line.upper())
```
+## Path.byte_writer
+
+```tomo
+Path.byte_writer : func(path: Path, append: Bool = no, permissions: Int32 = Int32(0o644) -> func(bytes:[Byte], close:Bool=no -> Result))
+```
+
+Returns a function that can be used to repeatedly write bytes to the same file.
+
+The file writer will keep its file descriptor open after each write (unless the `close` argument is set to `yes`). If the file writer is never closed, it will be automatically closed when the file writer is garbage collected.
+
+Argument | Type | Description | Default
+---------|------|-------------|---------
+path | `Path` | The path of the file to write to. | -
+append | `Bool` | If set to `yes`, writes to the file will append. If set to `no`, then the first write to the file will overwrite its contents and subsequent calls will append. | `no`
+permissions | `Int32` | The permissions to set on the file if it is created. | `Int32(0o644)`
+
+**Return:** Returns a function that can repeatedly write bytes to the same file. If `close` is set to `yes`, then the file will be closed after writing. If this function is called again after closing, the file will be reopened for appending.
+
+
+**Example:**
+```tomo
+write := (./file.txt).byte_writer()
+write("Hello\n".utf8())!
+write("world\n".utf8(), close=yes)!
+
+```
## Path.can_execute
```tomo
@@ -3464,6 +3490,32 @@ assert created.read() == [1, 2, 3]
created.remove()
```
+## Path.writer
+
+```tomo
+Path.writer : func(path: Path, append: Bool = no, permissions: Int32 = Int32(0o644) -> func(text:Text, close:Bool=no -> Result))
+```
+
+Returns a function that can be used to repeatedly write to the same file.
+
+The file writer will keep its file descriptor open after each write (unless the `close` argument is set to `yes`). If the file writer is never closed, it will be automatically closed when the file writer is garbage collected.
+
+Argument | Type | Description | Default
+---------|------|-------------|---------
+path | `Path` | The path of the file to write to. | -
+append | `Bool` | If set to `yes`, writes to the file will append. If set to `no`, then the first write to the file will overwrite its contents and subsequent calls will append. | `no`
+permissions | `Int32` | The permissions to set on the file if it is created. | `Int32(0o644)`
+
+**Return:** Returns a function that can repeatedly write to the same file. If `close` is set to `yes`, then the file will be closed after writing. If this function is called again after closing, the file will be reopened for appending.
+
+
+**Example:**
+```tomo
+write := (./file.txt).writer()
+write("Hello\n")!
+write("world\n", close=yes)!
+
+```
# Table
## Table.clear
diff --git a/api/paths.md b/api/paths.md
index 435932e3..8c08b45b 100644
--- a/api/paths.md
+++ b/api/paths.md
@@ -118,6 +118,32 @@ for line in (/dev/stdin).by_line()!
say(line.upper())
```
+## Path.byte_writer
+
+```tomo
+Path.byte_writer : func(path: Path, append: Bool = no, permissions: Int32 = Int32(0o644) -> func(bytes:[Byte], close:Bool=no -> Result))
+```
+
+Returns a function that can be used to repeatedly write bytes to the same file.
+
+The file writer will keep its file descriptor open after each write (unless the `close` argument is set to `yes`). If the file writer is never closed, it will be automatically closed when the file writer is garbage collected.
+
+Argument | Type | Description | Default
+---------|------|-------------|---------
+path | `Path` | The path of the file to write to. | -
+append | `Bool` | If set to `yes`, writes to the file will append. If set to `no`, then the first write to the file will overwrite its contents and subsequent calls will append. | `no`
+permissions | `Int32` | The permissions to set on the file if it is created. | `Int32(0o644)`
+
+**Return:** Returns a function that can repeatedly write bytes to the same file. If `close` is set to `yes`, then the file will be closed after writing. If this function is called again after closing, the file will be reopened for appending.
+
+
+**Example:**
+```tomo
+write := (./file.txt).byte_writer()
+write("Hello\n".utf8())!
+write("world\n".utf8(), close=yes)!
+
+```
## Path.can_execute
```tomo
@@ -936,3 +962,29 @@ assert created.read() == [1, 2, 3]
created.remove()
```
+## Path.writer
+
+```tomo
+Path.writer : func(path: Path, append: Bool = no, permissions: Int32 = Int32(0o644) -> func(text:Text, close:Bool=no -> Result))
+```
+
+Returns a function that can be used to repeatedly write to the same file.
+
+The file writer will keep its file descriptor open after each write (unless the `close` argument is set to `yes`). If the file writer is never closed, it will be automatically closed when the file writer is garbage collected.
+
+Argument | Type | Description | Default
+---------|------|-------------|---------
+path | `Path` | The path of the file to write to. | -
+append | `Bool` | If set to `yes`, writes to the file will append. If set to `no`, then the first write to the file will overwrite its contents and subsequent calls will append. | `no`
+permissions | `Int32` | The permissions to set on the file if it is created. | `Int32(0o644)`
+
+**Return:** Returns a function that can repeatedly write to the same file. If `close` is set to `yes`, then the file will be closed after writing. If this function is called again after closing, the file will be reopened for appending.
+
+
+**Example:**
+```tomo
+write := (./file.txt).writer()
+write("Hello\n")!
+write("world\n", close=yes)!
+
+```
diff --git a/api/paths.yaml b/api/paths.yaml
index 02b8fbe8..a659ffbc 100644
--- a/api/paths.yaml
+++ b/api/paths.yaml
@@ -838,6 +838,82 @@ Path.write:
example: |
(./file.txt).write("Hello, world!")
+Path.writer:
+ short: create a file writer
+ description: >
+ Returns a function that can be used to repeatedly write to the same file.
+ note: >
+ The file writer will keep its file descriptor open after each write (unless
+ the `close` argument is set to `yes`). If the file writer is never closed,
+ it will be automatically closed when the file writer is garbage collected.
+ return:
+ type: 'func(text:Text, close:Bool=no -> Result)'
+ description: >
+ Returns a function that can repeatedly write to the same file. If `close`
+ is set to `yes`, then the file will be closed after writing. If this
+ function is called again after closing, the file will be reopened for
+ appending.
+ args:
+ path:
+ type: 'Path'
+ description: >
+ The path of the file to write to.
+ append:
+ type: 'Bool'
+ default: 'no'
+ description: >
+ If set to `yes`, writes to the file will append. If set to `no`, then
+ the first write to the file will overwrite its contents and subsequent
+ calls will append.
+ permissions:
+ type: 'Int32'
+ default: 'Int32(0o644)'
+ description: >
+ The permissions to set on the file if it is created.
+ example: |
+ write := (./file.txt).writer()
+ write("Hello\n")!
+ write("world\n", close=yes)!
+
+Path.byte_writer:
+ short: create a byte-based file writer
+ description: >
+ Returns a function that can be used to repeatedly write bytes to the same
+ file.
+ note: >
+ The file writer will keep its file descriptor open after each write (unless
+ the `close` argument is set to `yes`). If the file writer is never closed,
+ it will be automatically closed when the file writer is garbage collected.
+ return:
+ type: 'func(bytes:[Byte], close:Bool=no -> Result)'
+ description: >
+ Returns a function that can repeatedly write bytes to the same file. If
+ `close` is set to `yes`, then the file will be closed after writing. If
+ this function is called again after closing, the file will be reopened
+ for appending.
+ args:
+ path:
+ type: 'Path'
+ description: >
+ The path of the file to write to.
+ append:
+ type: 'Bool'
+ default: 'no'
+ description: >
+ If set to `yes`, writes to the file will append. If set to `no`, then
+ the first write to the file will overwrite its contents and subsequent
+ calls will append.
+ permissions:
+ type: 'Int32'
+ default: 'Int32(0o644)'
+ description: >
+ The permissions to set on the file if it is created.
+ example: |
+ write := (./file.txt).byte_writer()
+ write("Hello\n".utf8())!
+ write("world\n".utf8(), close=yes)!
+
+
Path.write_bytes:
short: write bytes to a file
description: >
diff --git a/man/man3/tomo-Path.3 b/man/man3/tomo-Path.3
index ae9b6d51..b916005a 100644
--- a/man/man3/tomo-Path.3
+++ b/man/man3/tomo-Path.3
@@ -2,7 +2,7 @@
.\" Copyright (c) 2025 Bruce Hill
.\" All rights reserved.
.\"
-.TH Path 3 2025-12-07 "Tomo man-pages"
+.TH Path 3 2025-12-22 "Tomo man-pages"
.SH NAME
Path \- a Tomo type
.SH LIBRARY
@@ -51,6 +51,14 @@ For more, see:
.TP
+.BI Path.byte_writer\ :\ func(path:\ Path,\ append:\ Bool\ =\ no,\ permissions:\ Int32\ =\ Int32(0o644)\ ->\ func(bytes:[Byte],\ close:Bool=no\ ->\ Result))
+Returns a function that can be used to repeatedly write bytes to the same file.
+
+For more, see:
+.BR Tomo-Path.byte_writer (3)
+
+
+.TP
.BI Path.can_execute\ :\ func(path:\ Path\ ->\ Bool)
Returns whether or not a file can be executed by the current user/group.
@@ -345,3 +353,11 @@ Writes the given bytes to a unique file path based on the specified path. The fi
For more, see:
.BR Tomo-Path.write_unique_bytes (3)
+
+.TP
+.BI Path.writer\ :\ func(path:\ Path,\ append:\ Bool\ =\ no,\ permissions:\ Int32\ =\ Int32(0o644)\ ->\ func(text:Text,\ close:Bool=no\ ->\ Result))
+Returns a function that can be used to repeatedly write to the same file.
+
+For more, see:
+.BR Tomo-Path.writer (3)
+
diff --git a/man/man3/tomo-Path.byte_writer.3 b/man/man3/tomo-Path.byte_writer.3
new file mode 100644
index 00000000..595f0156
--- /dev/null
+++ b/man/man3/tomo-Path.byte_writer.3
@@ -0,0 +1,42 @@
+'\" t
+.\" Copyright (c) 2025 Bruce Hill
+.\" All rights reserved.
+.\"
+.TH Path.byte_writer 3 2025-12-22 "Tomo man-pages"
+.SH NAME
+Path.byte_writer \- create a byte-based file writer
+.SH LIBRARY
+Tomo Standard Library
+.SH SYNOPSIS
+.nf
+.BI Path.byte_writer\ :\ func(path:\ Path,\ append:\ Bool\ =\ no,\ permissions:\ Int32\ =\ Int32(0o644)\ ->\ func(bytes:[Byte],\ close:Bool=no\ ->\ Result))
+.fi
+.SH DESCRIPTION
+Returns a function that can be used to repeatedly write bytes to the same file.
+
+
+.SH ARGUMENTS
+
+.TS
+allbox;
+lb lb lbx lb
+l l l l.
+Name Type Description Default
+path Path The path of the file to write to. -
+append Bool If set to \fByes\fR, writes to the file will append. If set to \fBno\fR, then the first write to the file will overwrite its contents and subsequent calls will append. no
+permissions Int32 The permissions to set on the file if it is created. Int32(0o644)
+.TE
+.SH RETURN
+Returns a function that can repeatedly write bytes to the same file. If `close` is set to `yes`, then the file will be closed after writing. If this function is called again after closing, the file will be reopened for appending.
+
+.SH NOTES
+The file writer will keep its file descriptor open after each write (unless the `close` argument is set to `yes`). If the file writer is never closed, it will be automatically closed when the file writer is garbage collected.
+
+.SH EXAMPLES
+.EX
+write := (./file.txt).byte_writer()
+write("Hello\[rs]n".utf8())!
+write("world\[rs]n".utf8(), close=yes)!
+.EE
+.SH SEE ALSO
+.BR Tomo-Path (3)
diff --git a/man/man3/tomo-Path.writer.3 b/man/man3/tomo-Path.writer.3
new file mode 100644
index 00000000..8b3d53d8
--- /dev/null
+++ b/man/man3/tomo-Path.writer.3
@@ -0,0 +1,42 @@
+'\" t
+.\" Copyright (c) 2025 Bruce Hill
+.\" All rights reserved.
+.\"
+.TH Path.writer 3 2025-12-22 "Tomo man-pages"
+.SH NAME
+Path.writer \- create a file writer
+.SH LIBRARY
+Tomo Standard Library
+.SH SYNOPSIS
+.nf
+.BI Path.writer\ :\ func(path:\ Path,\ append:\ Bool\ =\ no,\ permissions:\ Int32\ =\ Int32(0o644)\ ->\ func(text:Text,\ close:Bool=no\ ->\ Result))
+.fi
+.SH DESCRIPTION
+Returns a function that can be used to repeatedly write to the same file.
+
+
+.SH ARGUMENTS
+
+.TS
+allbox;
+lb lb lbx lb
+l l l l.
+Name Type Description Default
+path Path The path of the file to write to. -
+append Bool If set to \fByes\fR, writes to the file will append. If set to \fBno\fR, then the first write to the file will overwrite its contents and subsequent calls will append. no
+permissions Int32 The permissions to set on the file if it is created. Int32(0o644)
+.TE
+.SH RETURN
+Returns a function that can repeatedly write to the same file. If `close` is set to `yes`, then the file will be closed after writing. If this function is called again after closing, the file will be reopened for appending.
+
+.SH NOTES
+The file writer will keep its file descriptor open after each write (unless the `close` argument is set to `yes`). If the file writer is never closed, it will be automatically closed when the file writer is garbage collected.
+
+.SH EXAMPLES
+.EX
+write := (./file.txt).writer()
+write("Hello\[rs]n")!
+write("world\[rs]n", close=yes)!
+.EE
+.SH SEE ALSO
+.BR Tomo-Path (3)
diff --git a/src/environment.c b/src/environment.c
index cf662749..a82274e7 100644
--- a/src/environment.c
+++ b/src/environment.c
@@ -341,6 +341,10 @@ env_t *global_env(bool source_mapping) {
{"subdirectories", "Path$children", "func(path:Path, include_hidden=no -> [Path])"}, //
{"unique_directory", "Path$unique_directory", "func(path:Path -> Path)"}, //
{"write", "Path$write", "func(path:Path, text:Text, permissions=Int32(0o644) -> Result)"}, //
+ {"writer", "Path$writer",
+ "func(path:Path, append=no, permissions=Int32(0o644) -> func(text:Text, close=no -> Result))"}, //
+ {"byte_writer", "Path$byte_writer",
+ "func(path:Path, append=no, permissions=Int32(0o644) -> func(bytes:[Byte], close=no -> Result))"}, //
{"write_bytes", "Path$write_bytes", "func(path:Path, bytes:[Byte], permissions=Int32(0o644) -> Result)"}, //
{"write_unique", "Path$write_unique", "func(path:Path, text:Text -> Path?)"}, //
{"write_unique_bytes", "Path$write_unique_bytes", "func(path:Path, bytes:[Byte] -> Path?)"}),
diff --git a/src/stdlib/paths.c b/src/stdlib/paths.c
index ed8383fd..9c74f4c1 100644
--- a/src/stdlib/paths.c
+++ b/src/stdlib/paths.c
@@ -324,6 +324,77 @@ Result_t Path$append_bytes(Path_t path, List_t bytes, int permissions) {
return _write(path, bytes, O_WRONLY | O_APPEND | O_CREAT, permissions);
}
+typedef struct {
+ const char *path_str;
+ int fd;
+ int mode;
+ int permissions;
+} writer_data_t;
+
+static Result_t _write_bytes_to_fd(List_t bytes, bool close_file, void *userdata) {
+ writer_data_t *data = userdata;
+ if (bytes.length > 0) {
+ int fd = open(data->path_str, data->mode, data->permissions);
+ if (fd == -1) {
+ if (errno == EMFILE || errno == ENFILE) {
+ // If we hit file handle limits, run GC collection to try to clean up any lingering file handles that
+ // will be closed by GC finalizers.
+ GC_gcollect();
+ fd = open(data->path_str, data->mode, data->permissions);
+ }
+ if (fd == -1) return FailureResult("Could not write to file: ", data->path_str, " (", strerror(errno), ")");
+ }
+ data->fd = fd;
+
+ if (bytes.stride != 1) List$compact(&bytes, 1);
+ ssize_t written = write(data->fd, bytes.data, (size_t)bytes.length);
+ if (written != (ssize_t)bytes.length)
+ return FailureResult("Could not write to file: ", data->path_str, " (", strerror(errno), ")");
+ }
+ // After first successful write, all writes are appends
+ data->mode = (O_WRONLY | O_CREAT | O_APPEND);
+
+ if (close_file && data->fd != -1) {
+ if (close(data->fd) == -1)
+ return FailureResult("Failed to close file: ", data->path_str, " (", strerror(errno), ")");
+ data->fd = -1;
+ }
+ return SuccessResult;
+}
+
+static Result_t _write_text_to_fd(Text_t text, bool close_file, void *userdata) {
+ return _write_bytes_to_fd(Text$utf8(text), close_file, userdata);
+}
+
+static void _writer_cleanup(writer_data_t *data) {
+ if (data && data->fd != -1) {
+ close(data->fd);
+ data->fd = -1;
+ }
+}
+
+public
+Closure_t Path$byte_writer(Path_t path, bool append, int permissions) {
+ path = Path$expand_home(path);
+ const char *path_str = Path$as_c_string(path);
+ int mode = append ? (O_WRONLY | O_CREAT | O_APPEND) : (O_WRONLY | O_CREAT | O_TRUNC);
+ writer_data_t *userdata =
+ new (writer_data_t, .fd = -1, .path_str = path_str, .mode = mode, .permissions = permissions);
+ GC_register_finalizer(userdata, (void *)_writer_cleanup, NULL, NULL, NULL);
+ return (Closure_t){.fn = _write_bytes_to_fd, .userdata = userdata};
+}
+
+public
+Closure_t Path$writer(Path_t path, bool append, int permissions) {
+ path = Path$expand_home(path);
+ const char *path_str = Path$as_c_string(path);
+ int mode = append ? (O_WRONLY | O_CREAT | O_APPEND) : (O_WRONLY | O_CREAT | O_TRUNC);
+ writer_data_t *userdata =
+ new (writer_data_t, .fd = -1, .path_str = path_str, .mode = mode, .permissions = permissions);
+ GC_register_finalizer(userdata, (void *)_writer_cleanup, NULL, NULL, NULL);
+ return (Closure_t){.fn = _write_text_to_fd, .userdata = userdata};
+}
+
public
OptionalList_t Path$read_bytes(Path_t path, OptionalInt_t count) {
path = Path$expand_home(path);
@@ -331,8 +402,8 @@ OptionalList_t Path$read_bytes(Path_t path, OptionalInt_t count) {
int fd = open(path_str, O_RDONLY);
if (fd == -1) {
if (errno == EMFILE || errno == ENFILE) {
- // If we hit file handle limits, run GC collection to try to clean up any lingering file handles that will
- // be closed by GC finalizers.
+ // If we hit file handle limits, run GC collection to try to clean up any lingering file handles that
+ // will be closed by GC finalizers.
GC_gcollect();
fd = open(path_str, O_RDONLY);
}
@@ -705,8 +776,8 @@ OptionalClosure_t Path$by_line(Path_t path) {
FILE *f = fopen(path_str, "r");
if (f == NULL) {
if (errno == EMFILE || errno == ENFILE) {
- // If we hit file handle limits, run GC collection to try to clean up any lingering file handles that will
- // be closed by GC finalizers.
+ // If we hit file handle limits, run GC collection to try to clean up any lingering file handles that
+ // will be closed by GC finalizers.
GC_gcollect();
f = fopen(path_str, "r");
}
@@ -726,8 +797,8 @@ OptionalList_t Path$lines(Path_t path) {
FILE *f = fopen(path_str, "r");
if (f == NULL) {
if (errno == EMFILE || errno == ENFILE) {
- // If we hit file handle limits, run GC collection to try to clean up any lingering file handles that will
- // be closed by GC finalizers.
+ // If we hit file handle limits, run GC collection to try to clean up any lingering file handles that
+ // will be closed by GC finalizers.
GC_gcollect();
f = fopen(path_str, "r");
}
diff --git a/src/stdlib/paths.h b/src/stdlib/paths.h
index 881a3c78..c272314c 100644
--- a/src/stdlib/paths.h
+++ b/src/stdlib/paths.h
@@ -39,6 +39,8 @@ Result_t Path$write(Path_t path, Text_t text, int permissions);
Result_t Path$write_bytes(Path_t path, List_t bytes, int permissions);
Result_t Path$append(Path_t path, Text_t text, int permissions);
Result_t Path$append_bytes(Path_t path, List_t bytes, int permissions);
+Closure_t Path$byte_writer(Path_t path, bool append, int permissions);
+Closure_t Path$writer(Path_t path, bool append, int permissions);
OptionalText_t Path$read(Path_t path);
OptionalList_t Path$read_bytes(Path_t path, OptionalInt_t limit);
Result_t Path$set_owner(Path_t path, OptionalText_t owner, OptionalText_t group, bool follow_symlinks);