Skip to content

core.line

← Module catalog

soul-sidesoulstack.filesfilesroot required

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
  • Sidesoul-side
  • Collectionsoulstack.files
  • Categoryfiles
Capabilitiesfs_write_root

States

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.

Changes when

A line was added or replaced.

Stays unchanged when

The first match already equals line.

Parameters

ParamTypeRequired / defaultDescription
pathstringrequiredTarget file.
linestringoptionalThe exact line being managed (required for present).
regexpstringoptionalRE2 pattern: the first matching line is replaced with line.
insertafterstringoptionalLiteral anchor or EOF — where to insert (mutually exclusive with insertbefore).
insertbeforestringoptionalLiteral anchor or BOF — where to insert (mutually exclusive with insertafter).
createbooloptionalCreate the file if it does not exist.
modestringoptionalPermissions on create, octal form, e.g. "0644".
ownerstringoptionalOwner on create (user name).
groupstringoptionalOwning 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

FieldTypeDescription
pathstring
matchedbool
replacedbool

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.

See also