core.line
Line-by-line, in-place edits to an existing file (the lineinfile equivalent).
core.line changes individual lines of a file rather than rewriting it whole (as core.file does): present adds a line or replaces the first regexp match, absent removes every match. The write is atomic (temp + rename), and by default the existing file's mode/owner/group are preserved. It is a deliberately narrow, safe MVP: regexp is a partial match (RE2, no backrefs) and replace touches only the first match. Soul-side.
Requirements
- Rootrequired
- Side
soul-side - Collection
soulstack.files - Category
files
fs_write_rootStates
core.line.present — The line is present in the file. With regexp, the first matching line is replaced with line; without regexp, the exact line is appended if it is not already there.
A line was added or replaced.
The first match already equals line.
Parameters
| Param | Type | Required / default | Description |
|---|---|---|---|
path | string | required | Target file. |
line | string | optional | The exact line being managed (required for present). |
regexp | string | optional | RE2 pattern: the first matching line is replaced with line. |
insertafter | string | optional | Literal anchor or EOF — where to insert (mutually exclusive with insertbefore). |
insertbefore | string | optional | Literal anchor or BOF — where to insert (mutually exclusive with insertafter). |
create | bool | optional | Create the file if it does not exist. |
mode | string | optional | Permissions on create, octal form, e.g. "0644". |
owner | string | optional | Owner on create (user name). |
group | string | optional | Owning group on create (group name). |
Example — Replace an existing line by regexp
- name: Ensure PasswordAuthentication is off in sshd_config module: core.line.present params: path: /etc/ssh/sshd_config regexp: '^#?PasswordAuthentication' line: 'PasswordAuthentication no'Output
| Field | Type | Description |
|---|---|---|
path | string | |
matched | bool | |
replaced | bool |
core.line.absent — Removes matching lines: with regexp, every match; without regexp, every exact match of line.
At least one line was removed.
There is nothing to remove or the file does not exist.
Parameters
| Param | Type | Required / default | Description |
|---|---|---|---|
path | string | required | Target file. |
line | string | optional | The exact line to remove (line or regexp required). |
regexp | string | optional | RE2 pattern: every matching line is removed. |
create | bool | optional | Create the file if it does not exist. |
mode | string | optional | Permissions on create, octal form. |
owner | string | optional | Owner on create (user name). |
group | string | optional | Owning group on create (group name). |
Example — Remove every line matching a pattern
- name: Remove legacy include directives module: core.line.absent params: path: /etc/app/app.conf regexp: '^include\s+/etc/app/legacy/'Output
| Field | Type | Description |
|---|---|---|
path | string | |
removed | int |
Notes
- regexp is the main source of drift: it is applied to each logical line without its trailing \n via Go's regexp.MatchString, so it is a partial match (not "the whole line" unless you add ^…$ anchors yourself). CRLF is not normalized — \r stays part of the line and participates in matching. Backrefs are unsupported. An invalid pattern is an error before the run (regexp.Compile at Validate).
- present with regexp and multiple matches replaces only the first matching line and leaves the rest untouched; a warning with the match count is added to the output. This is a deliberate, safe choice, not a bug.
- The insertafter/insertbefore anchors (other than EOF/BOF) are an exact line match (literal), not regexp: the insertion position is predictable. An anchor that is not found falls back to the end of the file (EOF).
- The write is atomic (temp + rename), not an in-place truncate. By default the existing file's mode/owner/group are preserved; explicit mode/owner/group on present override them. absent does not accept them and always keeps the current ones.
- The source file's trailing newline is preserved; an empty file is not turned into a file with one blank line. No subprocesses are spawned — reading, editing, and writing happen in-process, without a shell.
Security & defaults
- regexp edits someone else's file by partial match — the main source of a dangerous edit: without ^…$ it matches a substring, and in absent it deletes every matching line. Too broad a pattern (especially from untrusted interpolation) will remove more than intended; an empty pattern in absent matches every line and deletes the whole file.
- ReDoS is not a threat and backrefs are forbidden: Go's regexp engine (RE2) gives linear-time matching (catastrophic backtracking is impossible), and the pattern is compiled at Validate. Backrefs (substituting regexp groups into line) are unsupported in the MVP — this removes a class of "wrote the wrong thing into the file" mistakes.
- The write is atomic (temp + rename), with no partially written file: an interrupted run (crash, OOM) does not leave a config half-rewritten. On an in-place edit mode/owner/group are preserved — the module does not silently lower the existing file's permissions.
- The pattern should be a fixed anchor set by the task author, not taken from external input. Dangerous: regexp: "${ input.user_pattern }". Safe: regexp: '^include\s+/etc/app/legacy/'.
- Privileges: the manifest declares only fs_write_root (writing outside /var/lib/soul-stack/), not run_as_root; the edit runs with the soul agent process's privileges, which for /etc/... paths in practice requires root.