aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-15 22:18:06 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-15 22:18:06 +0200
commit1a1dc05b5250c6a281736fa6b706f74721f83847 (patch)
treee9dda722cebf25d1cb98894c82ad24045a5a8ea4
parent88cbe4e623eab9adb608566ac636d36eed72e551 (diff)
downloadkrino-1a1dc05b5250c6a281736fa6b706f74721f83847.tar.gz
krino-1a1dc05b5250c6a281736fa6b706f74721f83847.zip
krino.conf(5): WRITING RULES guide; reference corrected against the code
-rw-r--r--CHANGELOG.md8
-rw-r--r--docs/design.md12
-rw-r--r--man/krino.14
-rw-r--r--man/krino.conf.5605
4 files changed, 566 insertions, 63 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1cdf8b5..e597678 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,14 @@
expand (`{foo}`, `{mtime}` without a format, `%B`, `{0}`, `{10}`, an
unclosed `{`) at the action's file:line:col, instead of skipping the step
at plan time.
+- krino.conf(5) gains WRITING RULES: what happens to a file from scan to
+ execution, how to make and test a configuration, a worked example with
+ real output, and the mistakes `krino check` reports. Its reference
+ sections were checked statement by statement against the code and
+ corrected where they were wrong or left things to guess (claims across
+ directories, `busy` values, `ignore` anchoring and case, capture order,
+ `{now}`, max-read, text detection, overwrite and execution-time conflicts,
+ unscanned destinations, and more).
- `make ci` passes on OpenBSD 7.9 (OpenBSD make) and FreeBSD 15.0 (bmake)
with Go 1.26.8; the design notes that OpenBSD's `go` package needs
`GOTOOLCHAIN=auto` to use it.
diff --git a/docs/design.md b/docs/design.md
index bf8eca0..9479914 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -274,8 +274,12 @@ UTF-8 is replaced by U+FFFD before folding.
rule matches only when its condition is true; an unknown exclude holds
(§4.6). While no earlier rule has matched a file but one could not be
decided, `(matched)` is unknown too, so a catch-all
- `(not (matched))` does not take the file. `explain` shows an unknown test as `?`. A format with no text (§6,
- "anything else") is not a failure: `content` is false, silently.
+ `(not (matched))` does not take the file, and a rule with `(stop)` whose
+ condition is unknown ends the search for the file. A `duplicate` test
+ whose lookup fails is unknown in the same way (an exclude holds, marked
+ "(duplicate check failed)"). `explain` shows an unknown test as `?` and an
+ undecided rule as "undecided". A format with no text (§6, "anything
+ else") is not a failure: `content` is false, silently.
### 5.5 Duplicates
@@ -450,7 +454,9 @@ Captures come from the first `name` test, not inside a `not`, that was true
while evaluating this rule. Tests of equal cost keep their written order, so
"first" is well defined. `krino check` rejects a rule that uses `{N}` unless
every such `name` test in it has at least N groups, and a rule that uses
-`{N}` with no `name` test at all.
+`{N}` with no `name` test at all. It also rejects any placeholder that
+could never expand: an unknown name, `{mtime}` or `{now}` without a format,
+an unknown format code, `{0}`, `{10}` and up, an unclosed `{`.
With `fold` on, a `name` test matches the folded name, but its captures are
the original name's characters: `(name "^(.+)-faktura")` on
diff --git a/man/krino.1 b/man/krino.1
index be370cd..88d841a 100644
--- a/man/krino.1
+++ b/man/krino.1
@@ -200,7 +200,9 @@ directory against it and show each test's result
.Ql no ,
or
.Ql \&?
-for a result that depends on content krino could not read
+for a result that depends on content krino could not read or a duplicate
+check that failed; a rule left undecided is shown as
+.Dq undecided
.Pc ,
so a rule that should match but does not
.Pq or the reverse
diff --git a/man/krino.conf.5 b/man/krino.conf.5
index fda7649..14e473f 100644
--- a/man/krino.conf.5
+++ b/man/krino.conf.5
@@ -1,5 +1,5 @@
.\" SPDX-License-Identifier: GPL-3.0-or-later
-.Dd September 14, 2026
+.Dd September 15, 2026
.Dt KRINO.CONF 5
.Os
.Sh NAME
@@ -13,7 +13,9 @@ A short tutorial is
.Pa docs/sexp-primer.md
in the source repository, installed at
.Pa $PREFIX/share/doc/krino/sexp-primer.md .
-This page is the reference for every form.
+.Sx WRITING RULES
+explains how a configuration is used, with a worked example; the sections
+after it are the reference for every form.
.Pp
.Bl -bullet -compact
.It
@@ -49,8 +51,10 @@ to end of line.
Encoding is UTF-8 only.
There is no other syntax: no quote characters, no dotted pairs, no block
comments.
-Paths, keywords, regexes and rule names must be strings; settings values,
-type names, operators, sizes and durations are symbols.
+Paths, keywords, regexes, rule names and
+.Ic busy
+suffixes must be strings; other settings values, type names, operators,
+sizes and durations are symbols.
.Pp
Every list and atom's byte offset, line and column is recorded, so
.Ic krino new
@@ -59,6 +63,285 @@ can splice a name into
without disturbing anything else in the file, and a syntax error is
reported as
.Ar file : Ns Ar line : Ns Ar col .
+.Sh WRITING RULES
+.Ss What happens to a file
+When
+.Nm krino
+sorts a directory, every file goes through these stages, in this order:
+.Bl -enum
+.It
+.Sy Scan .
+The files directly in
+.Ic path ,
+or with
+.Ic recursive
+.Sy yes
+also those in subdirectories down to
+.Ic max-depth .
+Symbolic links are never followed and anything that is not a regular file
+is left out.
+The walk does not enter a copy or move destination that lies inside the
+directory
+.Pq Sx Unscanned destinations ,
+the Trash, or the configuration directory.
+A file is then skipped as
+.Dq ignored
+.Pq Ic ignore ,
+.Dq busy
+.Pq Ic busy ,
+.Dq too new
+.Pq Ic min-age
+or
+.Dq too big
+.Pq Ic max-size ,
+in that order.
+A skipped file is counted in the plan and listed by
+.Fl v ;
+no exclusion or rule sees it.
+.It
+.Sy Exclusions .
+The
+.Ic exclude
+forms of
+.Pa krino.conf ,
+then the directory's, in the order written.
+The first that holds sets the file aside
+.Pq Sx EXCLUSIONS .
+.It
+.Sy Rules ,
+top to bottom.
+A rule whose condition is true adds its actions to the file's chain, and if
+it has
+.Ic (stop) ,
+no later rule is looked at.
+A rule that cannot be decided
+.Pq Sx Operators
+adds nothing; if it has
+.Ic (stop) ,
+it ends the search as well.
+A file no rule matched is
+.Dq unmatched
+and left alone.
+.It
+.Sy The chain .
+The actions become steps in order: placeholders are expanded, conflicts are
+resolved
+.Pq Sx Conflicts ,
+and each step starts from where the one before left the file.
+Files are planned in path order.
+.It
+.Sy The plan .
+.Ic krino -n
+prints it and stops.
+Otherwise every file is approved, chosen file by file, or skipped
+.Pq Xr krino 1 ;
+.Fl y
+approves everything.
+.It
+.Sy Execution .
+Each approved step runs and is logged
+.Pq Sx How each action runs ;
+a failed step skips the rest of that file's chain.
+.Ic krino undo
+reverses a run.
+.El
+.Ss Making and testing a configuration
+.Bd -literal -offset indent
+krino init # krino.conf and template.conf
+krino new dl ~/dl # dirs/dl.conf from the template, "dl" added to include
+.Ed
+.Pp
+Then write rules in
+.Pa dirs/dl.conf
+and, before anything is applied, repeat:
+.Bl -tag -width Ds
+.It Ic krino check Ar name
+Reads the whole configuration and reports every mistake as
+.Ar file : Ns Ar line : Ns Ar col
+.Pq Sx Mistakes check reports ;
+lists each directory's exclusions and rules, and the extraction tools found.
+A configuration with any mistake is refused as a whole: no run starts.
+.It Ic krino -n -v Ar name
+The plan, without changing any file: each file krino would act on with its
+steps, the rule and the tests that made it match, then the files excluded,
+unmatched and skipped.
+Copy and move steps show the destination directory; the final file names
+are in
+.Fl -json .
+.Fl -min-age Ar 0
+includes files newer than
+.Ic min-age .
+.It Ic krino explain Ar file
+Why one file gets what it gets: whether the scan would skip it, every
+exclusion and every rule with each test's result
+.Pf ( Sy yes , no ,
+or
+.Sy \&?
+for undecided), which rule stopped the search, and whether a delete would
+be skipped for a duplicate.
+It shows no destinations.
+.El
+.Pp
+When the plan is right,
+.Ic krino Ar name
+reviews and applies it,
+.Ic krino log
+lists runs and
+.Ic krino undo
+reverses the last one.
+.Ss A worked example
+A directory
+.Pa ~/dl
+holds
+.Pa invoice-17.txt
+.Pq containing Dq acme ltd ,
+.Pa Screenshot_2026-09-01_10-00.png ,
+.Pa holiday.jpg ,
+.Pa tool_1.2.deb
+.Pq modified six months ago ,
+.Pa draft-notes.txt ,
+.Pa cache.tmp ,
+and
+.Pa report.pdf
+beside a
+.Pa report.pdf.part
+still downloading.
+.Pa dirs/dl.conf :
+.Bd -literal -offset indent
+(path "~/dl")
+(ignore "*.tmp")
+(exclude (name "^draft-"))
+
+(rule "invoices"
+ (when (type document)
+ (content "acme ltd"))
+ (move "Invoices/{mtime:%Y}")
+ (stop))
+
+(rule "screenshots"
+ (when (name "^Screenshot_(\ed{4})-(\ed{2})"))
+ (move "Pictures/Screenshots/{1}-{2}")
+ (stop))
+
+(rule "old-installers"
+ (when (type package) (age > 90d))
+ (delete))
+
+(rule "images"
+ (when (type image))
+ (move "Pictures"))
+.Ed
+.Pp
+.Ic krino -n -v dl
+prints
+.Pq abridged :
+.Bd -literal -offset indent
+8 scanned \(pc 4 to act on \(pc 0 warnings \(pc 0.00s
+
+ 1 Screenshot_2026-09-01_10-00.png
+ move \(-> Pictures/Screenshots/2026-09/
+ rule screenshots
+ because name "^Screenshot_(\ed{4})-(\ed{2})"
+
+ 2 holiday.jpg
+ move \(-> Pictures/
+ rule images
+ because type jpg
+
+ 3 invoice-17.txt
+ move \(-> Invoices/2026/
+ rule invoices
+ because type txt, content "acme ltd"
+
+ 4 tool_1.2.deb
+ trash
+ rule old-installers
+ because type deb, age > 90d
+
+not acted on: 1 ignored \(pc 1 busy \(pc 1 excluded \(pc 1 unmatched
+
+excluded
+ draft-notes.txt (exclude (name "^draft-"))
+
+not matched
+ report.pdf.part
+
+skipped
+ cache.tmp ignored
+ report.pdf busy
+.Ed
+.Pp
+The screenshot matched
+.Dq screenshots ,
+whose
+.Ic (stop)
+kept
+.Dq images
+from moving it a second time;
+.Ic {1}
+and
+.Ic {2}
+are the regex's groups.
+.Ic (delete)
+shows as
+.Dq trash .
+The busy file's
+.Pa .part
+companion is itself scanned: add
+.Ql (ignore \(dq*.part\(dq)
+to leave it alone, as the template does.
+.Ic krino explain ~/dl/Screenshot_2026-09-01_10-00.png
+shows the reasoning:
+.Bd -literal -offset indent
+(exclude (name "^draft-")): no
+ no name "^draft-"
+rule invoices: no
+ no and
+ no type document
+ no content "acme ltd"
+rule screenshots: MATCH
+ yes name "^Screenshot_(\ed{4})-(\ed{2})"
+rule old-installers: not evaluated, stopped by rule screenshots
+rule images: not evaluated, stopped by rule screenshots
+.Ed
+.Ss Mistakes check reports
+Every form is checked when the configuration is read, by
+.Ic krino check
+and by every run; nothing is scanned until all of it is right.
+Some of the messages, as printed (long ones wrapped here):
+.Bd -literal -offset indent
+dl.conf: no (path ...): say which directory this file sorts
+dl.conf:2:11: rule "a": (when) needs a condition; leave it out
+ to match every file
+dl.conf:2:1: rule "a" does nothing: give it an action like
+ (move "Somewhere") or (stop)
+dl.conf:2:38: rule "a": (move "Out") after delete would never run
+dl.conf:2:17: unknown test (nmae ...); tests are type, name, path,
+ content, size, age, duplicate, matched, and, or, not
+dl.conf:2:23: name: bad regex "[x": missing closing ]
+dl.conf:2:25: size: bad size "10k": want a whole number with an
+ optional K, M, G or T, like 50M
+dl.conf:2:24: age: bad duration "3": want a whole number followed
+ by s, m, h, d or w, like 30d
+dl.conf:2:35: rule "a": move takes a string: write (move "Out")
+dl.conf:2:11: rule "a": {1} needs a name test to capture from
+dl.conf:2:32: rule "a": {2} but a name test has only 1 capture group
+dl.conf:2:29: rule "a": unknown time format %B in {mtime:...}
+dl.conf:2:37: rule "a": rename gives a new name, not a path; use
+ move to change directory
+dl.conf:2:11: rule "a": min-age cannot be set in a rule, only case,
+ fold and on-conflict
+dl.conf:2:7: busy takes strings, like ".part"; got .part
+dl.conf:2:24: rule "a" defined twice (first at line 2)
+.Ed
+.Pp
+A rule combining
+.Ic (duplicate)
+with a delete action, a placeholder that could never expand
+.Pq Ic {foo} , Ic {mtime} No without a format, Ic {0} , Ic {10} , No an unclosed Ic {
+and a
+.Ic {N}
+beyond the name test's groups are refused the same way.
.Sh FILES
.Ss krino.conf
The main file, read first:
@@ -71,13 +354,25 @@ The main file, read first:
.Ed
.Bl -tag -width Ds
.It Ic (include Ar name No ...)
-The directories to sort, in the order given.
+The directories to sort, in the order given; directory names given to
+.Xr krino 1
+on the command line run in the command line's order instead.
Each
.Ar name
has its rules in
-.Pa dirs/ Ns Ar name Ns Pa .conf .
+.Pa dirs/ Ns Ar name Ns Pa .conf ,
+beside the main file
+.Pq also one given with Fl c .
+A name starts with a letter or digit followed by letters, digits,
+.Ql \&. ,
+.Ql _
+or
+.Ql - ,
+appears once, and is not one of krino's commands
+.Pq init, new, check, explain, log, undo .
.It Ic (log Ar path )
-Where the log goes.
+Where the log goes, an absolute path or one starting with
+.Ql ~/ .
Optional; defaults to
.Pa $XDG_STATE_HOME/krino/krino.log .
.It Ic (defaults Ar setting No ...)
@@ -112,9 +407,11 @@ order written.
.Bl -tag -width Ds
.It Ic (path Ar dir )
Required.
-The directory this file sorts.
-.Ar ~
-expands.
+The directory this file sorts: an absolute path, or one starting with
+.Ql ~/ ,
+which expands to the home directory; a form like
+.Ql ~user
+is refused.
.It Ic (ignore Ar pattern No ...)
May repeat; patterns accumulate in order.
Gitignore syntax:
@@ -122,9 +419,13 @@ Gitignore syntax:
.Ql ** ,
.Ql \&? ,
.Ql [...] ;
-a leading
+a pattern with a
.Ql /
-anchors to the root; a trailing
+anywhere but at its end is anchored to the root, so
+.Ql docs/*.txt
+does not ignore
+.Pa sub/docs/b.txt ;
+a trailing
.Ql /
matches directories only;
.Ql \&!
@@ -132,6 +433,11 @@ re-includes; a pattern with no
.Ql /
matches at any depth; the last matching pattern wins.
An ignored directory is not descended into.
+Patterns are case-sensitive:
+.Ic case
+and
+.Ic fold
+do not apply to them.
.It Ic (exclude Ar condition No ...)
May repeat.
Sets matching files aside in this directory; see
@@ -150,9 +456,16 @@ inside a
form.
The most specific wins: rule, then directory, then defaults, then the
built-in default below.
-Durations are an integer plus
-.Ql s m h d w ;
-sizes an integer plus an optional
+Durations are a whole number followed by one of
+.Ql s m h d w
+.Po
+.Ql 0s ,
+not
+.Ql 0 ;
+.Ql m
+is minutes
+.Pc ;
+sizes a whole number with an optional capital
.Ql K M G T
.Pq powers of 1024 .
.Bl -tag -width Ds
@@ -178,6 +491,14 @@ a with an ogonek becomes plain a, an l with a stroke becomes plain l,
e acute becomes plain e, u with diaeresis becomes plain u,
and so on for every Latin letter
.Pc .
+Letters of other scripts lose their combining marks too
+.Pq a Cyrillic short i matches a plain Cyrillic i .
+Folding applies to patterns as well, where a letter that folds to two
+.Pq \[ss] to ss, \[ae] to ae
+changes the regex:
+.Ql ^x\[ss]+y$
+becomes
+.Ql ^xss+y$ .
Built-in:
.Sy yes .
.It Ic recursive
@@ -187,13 +508,17 @@ or
Built-in:
.Sy no .
.It Ic max-depth
-An integer;
+A whole number from 1;
.Sy 1
means the root only.
+It applies only with
+.Ic recursive
+.Sy yes .
Built-in: unlimited.
.It Ic min-age
A duration.
-Files modified more recently are skipped as too new.
+Files modified more recently are skipped as too new; a file dated in the
+future counts as just modified.
.Xr krino 1 Ns 's
.Fl -min-age
overrides it for one run.
@@ -201,7 +526,10 @@ Built-in:
.Sy 2m .
.It Ic max-read
A size.
-No content is extracted from a file above this size.
+No content is extracted from a file above this size, and a document or
+tool whose text grows past it is content unreadable.
+.Sy 0
+means unlimited.
Built-in:
.Sy 50M .
.It Ic max-size
@@ -212,14 +540,18 @@ means unlimited, so a directory can lift a limit set in
.Ic defaults .
Built-in: unlimited.
.It Ic busy
-One or more suffixes.
-A file is skipped when its own name with one of these suffixes appended
-also exists beside it, e.g.
+Suffixes, as strings.
+A file is skipped when an entry with its name plus one of these suffixes
+exists beside it, a file or a directory, e.g.
.Pa report.pdf.part
beside
.Pa report.pdf .
+Suffixes are case-sensitive; the companion file itself is scanned like any
+other unless ignored;
+.Ic (busy)
+with no suffix turns the check off.
Built-in:
-.Sy .part .aria2 .crdownload .
+.Ql \&".part\&" \&".aria2\&" \&".crdownload\&" .
.It Ic on-conflict
.Pq rule .
.Sy suffix , skip
@@ -266,6 +598,8 @@ No rule has run yet, so
.Ic (matched)
is never true inside an
.Ic exclude .
+An excluded file still takes part in other files' duplicate checks, and can
+be their original.
Unlike
.Ic ignore ,
which never opens a file, an
@@ -277,8 +611,11 @@ An
fails closed: when its value depends on a content test that cannot read the
file
.Pq over Ic max-read , No a tool missing, failing or timing out, or a document read only in part ,
-the exclude holds and the file is set aside as
-.Dq (content unreadable) ,
+or on a duplicate test whose lookup fails, the exclude holds and the file is
+set aside as
+.Dq (content unreadable)
+or
+.Dq (duplicate check failed) ,
with a warning.
An exclude that is false whatever the text holds does not
.Pq see Sx Operators .
@@ -320,7 +657,9 @@ Move the file into directory
Rename in place.
.Ar name
must not contain
-.Ql / .
+.Ql / ,
+not even through a placeholder such as
+.Ic {mtime:%Y/%m} .
.It Ic (delete)
Move to the trash.
.It Ic (delete permanent)
@@ -332,7 +671,13 @@ Once this rule matches, evaluate no further rules for this file.
.Pp
A rule with only
.Ic (stop)
-is an exclusion: files it matches receive no actions from later rules.
+keeps the files it matches from every later rule, and so does one whose
+condition cannot be decided
+.Pq Sx Operators .
+It does not undo what earlier rules planned and is not listed as excluded;
+an
+.Ic exclude
+sets a file aside from every rule.
.Pp
A rule whose condition contains
.Ic (duplicate)
@@ -349,14 +694,51 @@ and every run refuse it
.Pq Sx DUPLICATES .
.Pp
.Ar dest
-is a directory: a relative path is relative to the root,
+is a directory: a relative path is relative to the root, a leading
.Ql ~
-expands, an absolute path is allowed, and it is created if missing.
+is the home directory, an absolute path is allowed, and it is created if
+missing.
.Ar dest
and
.Ar name
take placeholders; see
.Sx Placeholders .
+A step is skipped, with the reason in the plan, when its destination's
+placeholders would take it out of the directory written before the first
+placeholder
+.Pq Dq destination ... leaves ... through a placeholder ,
+or when a
+.Ic rename Ns 's
+placeholders produce an empty name,
+.Ql \&.
+or
+.Ql \&.. .
+.Ss Unscanned destinations
+The part of every
+.Ic copy
+and
+.Ic move
+destination before its first placeholder, cut back to the last
+.Ql / ,
+is not scanned when it lies inside the directory, so files
+.Nm krino
+has filed are not sorted again:
+.Ql Work/Acme/{mtime:%Y}
+leaves out
+.Pa Work/Acme ,
+and
+.Ql Work/Acme-{mtime:%Y}
+all of
+.Pa Work .
+Files placed there by hand are not seen either, whether or not the rule ever
+matches.
+.Ic {{
+and
+.Ic }}
+are literal text here, not placeholders.
+.Fl v
+lists every such directory that exists under
+.Dq not scanned (a rule's destination) .
.Sh CONDITIONS
.Ss Operators
.Ic (and Ar cond No ...) ,
@@ -374,13 +756,17 @@ A
test that cannot read its file is unknown, with a warning; a document read
only in part answers the keywords found in what was read and leaves the
others unknown.
-Operators combine unknowns by three-valued logic:
+A
+.Ic duplicate
+test whose lookup fails is unknown too.
+Operators combine unknowns by three-valued logic: an
.Ic and
-is false if any argument is false,
+is false if any of its arguments is false, even when another is unknown;
+an
.Ic or
-true if any is true, and
+is true if any of its arguments is true; the
.Ic not
-of unknown is unknown.
+of an unknown is unknown.
A rule matches only when its condition is true, so
.Ql (not (content \(dqx\(dq))
never acts on a file krino could not read; an exclusion holds when its
@@ -389,7 +775,33 @@ While no earlier rule has matched a file but one could not be decided,
.Ic (matched)
is unknown as well, so a catch-all
.Ql (not (matched))
-does not take the file either.
+does not take the file either; and a rule with
+.Ic (stop)
+that cannot be decided ends the search for the file.
+.Pp
+The arguments of
+.Ic and
+and
+.Ic or ,
+and several conditions in one
+.Ic when ,
+are evaluated cheapest first:
+.Ic type , size , age
+and
+.Ic matched ,
+then
+.Ic name
+and
+.Ic path ,
+then
+.Ic duplicate ,
+then
+.Ic content ;
+tests of equal cost keep their written order, and evaluation stops as soon
+as the answer is known.
+The order never changes whether a condition is true, but it decides which
+.Ic name
+test supplies the captures and whether a slow test runs at all.
.Ss Tests
.Bl -tag -width Ds
.It Ic (type Ar t No ...)
@@ -408,10 +820,22 @@ see
.Pc
or a multi-part suffix like
.Ic tar.gz .
+Write
+.Ar t
+without its dot:
+.Ic (type .pdf)
+matches only names ending in
+.Ql ..pdf .
.It Ic (name Ar re No ...)
-True when the file name matches any of the regexes.
+True when any of the regexes matches anywhere in the file name; use
+.Ql ^
+and
+.Ql $
+to match the whole name.
.It Ic (path Ar re No ...)
-True when the path relative to the root matches any of the regexes.
+The same for the path relative to the root, with
+.Ql /
+between directories.
.It Ic (content Ar keyword No ...)
True when the extracted text contains any of the keywords; see
.Sx CONTENT EXTRACTION .
@@ -421,6 +845,11 @@ is one of
.Ql > >= < <= = .
.It Ic (age Ar op duration )
Age by modification time.
+A file dated in the future has a negative age:
+.Ql (age < 1m)
+is true for it and
+.Ql (age >= 0s)
+false.
.It Ic (duplicate)
True when another scanned file has identical content and this one is not
the chosen original; see
@@ -428,6 +857,10 @@ the chosen original; see
.It Ic (duplicate Ar dir No ...)
The same, also comparing against every regular file under
.Ar dir .
+A
+.Ar dir
+that does not exist is reported as a warning, and the test goes on with
+the others.
.It Ic (matched)
True when an earlier rule already matched this file.
.El
@@ -521,16 +954,15 @@ Groups overlap; a file can belong to several.
.Sh CONTENT EXTRACTION
.Bl -tag -width "presentation"
.It Sy text
-Known text extensions, or detected from the first 8 KiB: valid UTF-8, or
-UTF-16 with a BOM, and no NUL bytes.
-A file that holds a NUL or invalid UTF-8 further on, such as a
-self-extracting installer, has no content.
+txt md log csv tsv json yaml yml toml ini conf cfg rtf tex go c h cpp hpp
+py sh js ts rs java rb pl lua css sql: read as text; bytes that are not
+valid UTF-8 are read as Latin-1.
.It Sy pdf
.Ic pdftotext
.Pq 30 second timeout .
.It Sy docx, xlsx, pptx, odt, ods, odp, epub
Zip plus streaming XML, Go standard library only.
-.It Sy html, xml
+.It Sy html, htm, xhtml, xml, svg
Tags removed, entities decoded.
.It Sy doc
.Ic antiword ,
@@ -542,7 +974,10 @@ or
.Ic catppt
.Pq both from catdoc .
.It Sy anything else
-No content.
+Detected from its first 8 KiB: valid UTF-8 with no NUL byte, or UTF-16 with a
+byte-order mark, is read as text, if the rest of the file agrees.
+Otherwise it has no content: images, audio, archives and other binary files,
+and a file that turns binary further on, such as a self-extracting installer.
.El
.Pp
External tools are optional and looked up once per run;
@@ -550,7 +985,9 @@ External tools are optional and looked up once per run;
lists which were found.
A file above
.Ic max-read
-is not extracted.
+is not extracted, and extraction that yields more text than
+.Ic max-read
+stops as content unreadable.
Tools run without a shell and are given absolute paths only, so a file
name starting with
.Ql -
@@ -560,8 +997,8 @@ Before matching, text and keywords are normalised the same way: case
.Pq if Sy ignore ,
folding
.Pq if Sy yes ,
-and runs of whitespace collapsed to one space, so a keyword split across
-lines in a PDF still matches.
+runs of whitespace collapsed to one space and whitespace at either end
+removed, so a keyword split across lines in a PDF still matches.
Matching is substring:
.Ql \&"acme\&"
matches
@@ -595,6 +1032,11 @@ A file edited in place with its size and modification time preserved keeps
its old answers, as does a new file reusing a deleted file's inode with the
same size, extension and preserved modification time; deleting the cache
resets everything.
+.Ic krino -n
+writes the cache too;
+.Ic krino explain
+only reads it.
+A run of a directory with no content test left deletes its cache.
.Sh ACTIONS
.Ss Chains
A file's chain is the actions of every matching rule, in rule order, and
@@ -617,6 +1059,7 @@ and does not end the chain.
The plan warns when a chain moves a file more than once; that is usually
a missing
.Ic (stop) .
+When a step fails at execution, the rest of that file's chain is skipped.
.Ss How each action runs
.Bl -tag -width Ds
.It Ic move
@@ -624,8 +1067,9 @@ a missing
when source and destination share a filesystem; otherwise a copy to a
temporary file in the destination, fsync, rename into place, then the
source is removed.
-If any step fails the source is untouched and the temporary file is
-removed.
+If the copy fails the source is untouched and the temporary file is
+removed; if the copy lands but the source cannot be removed, both remain and
+the step fails.
.It Ic copy
To a temporary file in the destination, then renamed into place.
Mode and modification time are preserved.
@@ -643,27 +1087,34 @@ or a move.
.Xr unlink 2 .
.El
.Pp
-Before each step, the executor checks that the source still exists with
-the size and modification time recorded in the plan; if not, the step
-fails as
+Before each step, the executor checks that the source is still the file
+planned: a regular file with the same inode, size and modification time; if
+not, the step fails as
.Dq changed since plan
and the rest of that file's chain is skipped.
.Ss Placeholders
.Bl -tag -width "{mtime:FMT}"
.It Ic {name}
-The current file name.
+The file's current name: after a
+.Ic rename
+step, the new one.
.It Ic {stem}
The name without its last extension.
.It Ic {ext}
The last extension with its dot, e.g.
.Ql .pdf ;
empty if none.
+A name whose only dot is its first character, like
+.Pa .bashrc ,
+has no extension.
.It Ic {1} No ... Ic {9}
Capture groups of the first
.Ic name
test, not inside a
.Ic not ,
-that matched while evaluating this rule.
+that was true while this rule was evaluated, in evaluation order
+.Pq Sx Operators ;
+that can be a test in a branch that did not match in the end.
.Ic krino check
refuses a rule that uses
.Ic { Ns Ar n Ns Ic }
@@ -676,10 +1127,15 @@ groups, and refuses one that uses
with no
.Ic name
test at all.
+A rule whose condition was decided without reaching its
+.Ic name
+test has no captures; the step is then skipped as
+.Dq no capture group Ar n .
.It Ic {mtime:FMT}
-The file's modification time.
+The file's modification time when it was scanned, in local time.
.It Ic {now:FMT}
-The start of the run.
+The time the directory is planned; in a run of several directories, later
+ones can get a later time.
.It Ic {{ No and Ic }}
A literal
.Ql {
@@ -704,6 +1160,13 @@ the name's own
.Ar FMT
is a strftime subset:
.Ql %Y %m %d %H %M %S %j %% .
+A placeholder that could never expand is refused by
+.Ic krino check
+and every run
+.Pq Sx Mistakes check reports ;
+a lone
+.Ql }
+is literal.
.Ss Conflicts
When the computed target already exists:
.Bl -tag -width Ds
@@ -718,6 +1181,11 @@ Skip this step; the chain continues from the file's current path.
Move the existing target to the trash first
.Pq logged, so undo restores it ,
then proceed.
+A target that is another file of the same plan, matched or excluded, is
+never displaced: a free name is taken as with
+.Ic suffix .
+A target that is not a regular file is skipped.
+The trash must be on the target's filesystem, or the step fails.
.El
.Pp
A
@@ -733,12 +1201,23 @@ as well; a directory whose destination's first path component is a
placeholder relies on this to avoid re-filing its own output \(em see
.Sx KNOWN LIMITATIONS .
.Pp
-Conflicts between files in the same plan are resolved when planning, so
-the plan shows final names; an in-plan claim is never displaced.
-The claim set spans the whole run, so two configured directories cannot
+Conflicts between files in the same plan are resolved when planning, file
+by file in path order; a target another step of the plan has claimed is
+never displaced.
+In a dry run the claims span every directory, so two directories cannot
plan the same final name.
-The executor re-checks at execution time; if the name has to change, the
-log records the actual name used.
+A real run applies each directory before planning the next, which sees the
+result on disk; only where the earlier directory's files ended up stays
+claimed.
+The plan shows a copy or move destination as its directory; the final name,
+with any suffix, is in
+.Fl -json
+and in the log.
+At execution the target is checked again: if one has appeared since
+planning, a free name is taken whatever
+.Ic on-conflict
+says, the log records it, and a move or rename that got another name ends
+that file's chain.
.Sh DUPLICATES
Candidates are the scanned files plus, for
.Ic (duplicate Ar dir ) ,
@@ -761,9 +1240,12 @@ The
in each group is, in order of preference: a file under one of the given
.Ar dir
arguments, then the oldest by modification time, then the shortest name,
-then the name that sorts first.
+then the name that sorts first, then the full path that sorts first.
.Ic (duplicate)
is true for every other scanned file in the group.
+Excluded files are candidates too, and can be the original.
+A lookup that fails, because a file cannot be read, makes the test unknown
+.Pq Sx Operators .
.Pp
.Sy Warning:
.Ic (duplicate)
@@ -862,6 +1344,11 @@ Give such a rule its own narrow
pattern, or a
.Ar dest
whose first path component is a literal string, when that matters.
+.Pp
+.Ic krino -n
+over several directories plans each as if the earlier ones had not been
+applied, so it cannot show what a later directory's rules would do with a
+file an earlier directory moves into it.
.Sh SEE ALSO
.Xr krino 1
.Pp