diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-15 22:18:06 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-15 22:18:06 +0200 |
| commit | 1a1dc05b5250c6a281736fa6b706f74721f83847 (patch) | |
| tree | e9dda722cebf25d1cb98894c82ad24045a5a8ea4 | |
| parent | 88cbe4e623eab9adb608566ac636d36eed72e551 (diff) | |
| download | krino-1a1dc05b5250c6a281736fa6b706f74721f83847.tar.gz krino-1a1dc05b5250c6a281736fa6b706f74721f83847.zip | |
krino.conf(5): WRITING RULES guide; reference corrected against the code
| -rw-r--r-- | CHANGELOG.md | 8 | ||||
| -rw-r--r-- | docs/design.md | 12 | ||||
| -rw-r--r-- | man/krino.1 | 4 | ||||
| -rw-r--r-- | man/krino.conf.5 | 605 |
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 |
