.\" SPDX-License-Identifier: GPL-3.0-or-later .Dd September 15, 2026 .Dt KRINO.CONF 5 .Os .Sh NAME .Nm krino.conf .Nd krino's configuration language and files .Sh DESCRIPTION .Xr krino 1 Ns 's configuration is written in s-expressions: each form is a list inside parentheses, holding an operation optionally followed by its arguments. A short tutorial is .Pa docs/sexp-primer.md in the source repository, installed at .Pa $PREFIX/share/doc/krino/sexp-primer.md . .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 .Sy List : .Ql \&( items separated by whitespace .Ql \&) . .It .Sy String : .Ql \&"...\&" . A backslash escapes only .Ql \&" and .Ql \e ; any other backslash is kept literally, so .Ql \&"\ebacme\eb\&" is the regex .Ql \ebacme\eb . Strings may span lines. .It .Sy Symbol : any other run of characters except whitespace, .Ql \&( , .Ql \&) , .Ql \&" , .Ql \&; . .It .Sy Comment : .Ql \&; to end of line. .El .Pp Encoding is UTF-8 only. There is no other syntax: no quote characters, no dotted pairs, no block comments. 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 can splice a name into .Ic include 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: among the entries the walk looks at, a symbolic link, or anything else that is neither a regular file nor a directory, is counted and listed as .Dq symlink or .Dq not a regular file . A subdirectory the walk does not enter is not counted. 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 Op Ar name Reads .Pa krino.conf and the named directory's file .Pq every included directory's, with no name , reports mistakes as .Ar file : Ns Ar line : Ns Ar col .Pq Sx Mistakes check reports , and lists each directory's exclusions and rules and the extraction tools found. A run refuses to start while a file it reads has a mistake. .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, all modified more than two minutes ago .Pq the built-in Ic min-age . .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 a file is read, by .Ic krino check and by every run, and nothing is scanned while a file that run reads has a mistake. The mistakes found in one pass are reported together. A rule's placeholders, capture groups and .Ic (duplicate) with a delete are checked only once its condition has no mistake, and only its first bad action is reported: fix what is shown and check again. Some of the messages, as printed .Pq the path shortened, 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 or 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: .Bd -literal -offset indent (include "downloads" "invoices") ; dirs/\*(Ltname\*(Gt.conf, run in this order (log "~/.local/state/krino/krino.log") ; optional (defaults ; optional; any setting below (min-age 5m)) (exclude (type iso)) ; optional, may repeat .Ed .Bl -tag -width Ds .It Ic (include Ar name No ...) 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 , 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, an absolute path or one starting with .Ql ~/ . Optional; defaults to .Pa $XDG_STATE_HOME/krino/krino.log . .It Ic (defaults Ar setting No ...) Defaults for every directory; a directory's own file, and a rule inside it, can override them. See .Sx SETTINGS . .It Ic (exclude Ar condition No ...) May repeat. Sets matching files aside in every directory; see .Sx EXCLUSIONS . .El .Ss dirs/name.conf One file per configured directory: .Bd -literal -offset indent (path "~/downloads") ; required (recursive no) ; any setting from SETTINGS (ignore "*.part" "*.aria2" ".*") ; may repeat; patterns accumulate in order (exclude (name "^keep-")) ; may repeat (rule "acme" (when (type document) (or (content "acme ltd" "0000000000") (name "(^|[^a-z0-9])acme([^a-z0-9]|$)")) (not (name "^draft"))) (move "Work/Acme/{mtime:%Y}") (stop)) .Ed .Pp Top-level forms may appear in any order, except that rules run in the order written. .Bl -tag -width Ds .It Ic (path Ar dir ) Required. 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: .Ql * , .Ql ** , .Ql \&? , .Ql [...] ; a pattern with a .Ql / 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 \&! 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 .Sx EXCLUSIONS . .It Ic (rule Ar name item No ...) See .Sx RULES . .El .Sh SETTINGS Each setting may appear in .Ic (defaults ...) , at the top of a directory file, or, where marked .Pq rule , inside a .Ic rule form. The most specific wins: rule, then directory, then defaults, then the built-in default below. 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 .It Ic case .Pq rule . .Sy ignore or .Sy strict : case sensitivity for .Ic name , path and .Ic content . Built-in: .Sy ignore . .It Ic fold .Pq rule . .Sy yes or .Sy no : strip diacritics before comparing .Po 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 .Sy yes or .Sy no . Built-in: .Sy no . .It Ic max-depth 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; a file dated in the future counts as just modified. .Xr krino 1 Ns 's .Fl -min-age overrides it for one run. Built-in: .Sy 2m . .It Ic max-read A size. No content is extracted from a file above this size, and a pdf, zip-based document or tool output whose text grows past it is content unreadable. .Sy 0 means unlimited. Built-in: .Sy 50M . .It Ic max-size A size. Files above it are skipped as too big: no rule sees them. .Sy 0 means unlimited, so a directory can lift a limit set in .Ic defaults . Built-in: unlimited. .It Ic busy 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: .Ql \&".part\&" \&".aria2\&" \&".crdownload\&" . .It Ic on-conflict .Pq rule . .Sy suffix , skip or .Sy overwrite : see .Sx Conflicts . Built-in: .Sy suffix . .El .Sh EXCLUSIONS .Bd -literal -offset indent (exclude (type iso img)) ; by extension (exclude (name "^keep-")) ; by name (exclude (type pdf) (content "confidential")) ; by content .Ed .Pp An .Ic exclude form sets files aside before any rule sees them. Its conditions are those of .Sx CONDITIONS , and all of them must hold, as in .Ic when ; a file matching any .Ic exclude form is excluded. Forms in .Pa krino.conf apply to every directory and are tested first, then the directory's own. They use the directory's .Ic case and .Ic fold . .Pp An excluded file is counted as excluded in the plan and listed, with the form that matched, under .Fl v ; .Ic krino explain traces every form, and .Ic krino check lists them. 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 .Ic exclude can test size and content. .Pp An .Ic exclude 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 , 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 . A file whose format has no text at all, such as an image or an archive, is not unreadable: its content tests are false, with no warning. So is a file of no known extension that starts as text and holds binary data further on, such as a self-extracting installer. .Sh RULES .Bd -literal -offset indent (rule NAME ITEM...) .Ed .Pp A rule's name cannot start with .Ql \&( : the log shows choices made in review under the rule name .Sy (review) . Items, in any order except that actions run in the order written: .Bl -tag -width Ds .It Ic (when Ar cond No ...) The condition; see .Sx CONDITIONS . Several conditions means all must hold. A rule with no .Ic when matches every file. An empty .Ic (when) is an error. .It Ic case , Ic fold , Ic on-conflict Rule-level settings; see .Sx SETTINGS . .It Ic (copy Ar dest ) Copy the file into directory .Ar dest . .It Ic (move Ar dest ) Move the file into directory .Ar dest . .It Ic (rename Ar name ) Rename in place. .Ar name must not contain .Ql / , not even through a placeholder such as .Ic {mtime:%Y/%m} . .It Ic (delete) Move to the trash. .It Ic (delete permanent) Unlink. Cannot be undone. .It Ic (stop) Once this rule matches, evaluate no further rules for this file. .El .Pp A rule with only .Ic (stop) keeps the files it matches from every later rule; so does a rule with .Ic (stop) whose condition cannot be decided .Pq Sx Operators . A file a stop rule matched without any action from earlier rules is counted and listed as excluded, with that rule; one stopped by an undecided rule is listed as not matched, with the warning. A stop rule does not undo what earlier rules planned, while an .Ic exclude sets a file aside before any rule. .Pp A rule whose condition contains .Ic (duplicate) anywhere, including inside .Ic or or .Ic not , cannot contain .Ic (delete) or .Ic (delete permanent) ; .Ic krino check and every run refuse it .Pq Sx DUPLICATES . .Pp .Ar dest is a directory: a relative path is relative to the root, .Ql ~ alone or a leading .Ql ~/ is the home directory .Po .Ql ~foo/x is a relative path under the root, though the plan shows it as .Ql ~foo/x/ .Pc , 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 Every .Ic copy and .Ic move destination that lies inside the directory is not scanned, so files .Nm krino has filed are not sorted again: a destination without placeholders is left out whole, so .Ql Pictures leaves out .Pa Pictures ; one with placeholders is left out up to the last .Ql / before its first placeholder: .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 ...) , .Ic (or Ar cond No ...) , .Ic (not Ar cond ) . .Ic and and .Ic or take one or more arguments; .Ic not exactly one. .Pp A .Ic content 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. A .Ic duplicate test whose lookup fails is unknown too. Operators combine unknowns by three-valued logic: an .Ic and is false if any of its arguments is false, even when another is unknown; an .Ic or is true if any of its arguments is true; the .Ic not 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 condition is true or unknown. 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; 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. A test costs, from least to most: .Ic type , size , age and .Ic matched ; then .Ic name and .Ic path ; then .Ic duplicate ; then .Ic content . A nested .Ic (and ...) or .Ic (or ...) costs the sum of its arguments, and a .Ic (not ...) costs what its argument costs. Arguments 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 ...) True when the name ends in .Ql \&. Ns Ar t for any .Ar t , case-insensitively. .Ar t may be a type group .Po e.g.\& .Ic document ; see .Sx TYPE GROUPS .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 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 ...) 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 . .It Ic (size Ar op size ) .Ar op 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 .Sx DUPLICATES . .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 .Pp Regexes use Go's RE2 syntax: POSIX extended regular expressions plus .Ql \ed \ew \es \eb , non-greedy quantifiers, and the inline flags .Ql (?i) and .Ql (?-i) ; there are no backreferences and no lookaround. .Ss Case, folding and word boundaries .Ic case and .Ic fold apply to .Ic name , path and .Ic content , in both the file's data and the pattern; .Ic type is always case-insensitive. A regex can override .Ic case locally with .Ql (?i) or .Ql (?-i) . .Pp .Sy RE2 treats .Ql _ as a word character. .Ql \eb is the boundary between a word character and a non-word character, so .Ql \ebacme\eb does .Em not match .Ar ACME_REPORT_2026.pdf : the .Ql E before the underscore and the underscore itself are both word characters, so there is no boundary there for .Ql \eb to match. Use a character class instead of .Ql \eb when the name may be glued to the rest with an underscore or a digit: .Bd -literal -offset indent (name "(^|[^a-z0-9])acme([^a-z0-9]|$)") .Ed matches .Ar ACME_REPORT_2026.pdf .Pq case-insensitively, by default because it treats anything that is not a lowercase letter or digit, including .Ql _ , as a separator. .Sh TYPE GROUPS A group name in .Ic (type ...) stands for every extension listed for it: .Bl -tag -width "presentation" .It Ic image jpg jpeg png gif webp bmp tif tiff heic heif avif svg ico raw cr2 nef arw dng .It Ic video mp4 mkv webm mov avi m4v mpg mpeg wmv flv 3gp .It Ic audio mp3 flac ogg opus m4a aac wav wma aiff .It Ic archive zip tar gz tgz bz2 tbz2 xz txz zst 7z rar lz lzma cpio .It Ic document pdf doc docx odt rtf txt md tex .It Ic spreadsheet xls xlsx ods csv tsv .It Ic presentation ppt pptx odp .It Ic ebook epub mobi azw azw3 fb2 djvu .It Ic code go c h cpp hpp py sh js ts rs java rb pl lua html css json yaml yml toml xml sql .It Ic text txt md log csv tsv json yaml yml toml xml ini conf .It Ic package deb rpm apk appimage exe msi flatpak snap .It Ic font ttf otf woff woff2 .El .Pp Groups overlap; a file can belong to several. .Sh CONTENT EXTRACTION .Bl -tag -width "presentation" .It Sy text 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. A file starting with a UTF-16 byte-order mark is read as UTF-16; otherwise a file that is not valid UTF-8 as a whole is read as Latin-1, one character per byte, so a single stray byte makes its UTF-8 letters no longer match. .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, htm, xhtml, xml, svg Tags removed, entities decoded. .It Sy doc .Ic antiword , else .Ic catdoc . .It Sy xls / ppt .Ic xls2csv or .Ic catppt .Pq both from catdoc . .It Sy anything else Detected from its first 8 KiB: a UTF-16 byte-order mark is read as text; valid UTF-8 with no NUL byte is read as text if the rest of the file is too. 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; .Ic krino check lists which were found. A file above .Ic max-read is not extracted, and a pdf, zip-based document or tool output whose text grows past .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 - is never read as an option, and each is killed at its timeout. .Pp Before matching, text and keywords are normalised the same way: case .Pq if Sy ignore , folding .Pq if Sy yes , 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 .Ql \&"acmeco\&" . Words hyphenated across lines in a PDF are not rejoined. .Ss Keyword cache For each file it extracts, .Nm krino records in .Pa $XDG_CACHE_HOME/krino/ Ns Ar name Ns Pa .cache which of the directory's content keywords the text contains, and answers later content tests from it while the file is unchanged. No text and no file names are stored; a file is known by device, inode, size, modification time and extension, which a move within one filesystem keeps. The keywords are stored normalised, the way the test compares them, and only those the directory still uses. .Pp A file is extracted again when it changes, or when a test asks about a keyword its entry was never checked against. Failures are never cached, and a file above .Ic max-read is refused before the cache is read. The whole cache is discarded when an extraction tool is installed, removed or replaced, when krino's extraction or normalisation, the Go release or the Unicode tables change, or when the directory's .Ic max-read changes. Each run keeps entries only for files still in the directory. 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 within a rule in the order written. .Ic copy leaves the file where it is; .Ic rename changes its name and .Ic move its directory, and later steps use the new path; .Ic delete ends the chain, and steps after it are shown in the plan as .Dq skipped: deleted by rule Ar x . A .Ic delete of a duplicate is itself skipped .Pq Sx DUPLICATES and does not end the chain. .Pp 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 .Xr rename 2 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 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. .It Ic rename .Xr rename 2 within the same directory. .It Ic delete Into the freedesktop.org trash at .Pa $XDG_DATA_HOME/Trash . A file on a different filesystem from the trash is not trashed: the step fails with a message suggesting .Ic (delete permanent) or a move. .It Ic (delete permanent) .Xr unlink 2 . .El .Pp Before each step, the executor checks that the source is still the file planned: a regular file with the same size and modification time, and, while it is still at its scanned path, the same device and inode; 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 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 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 } unless every such .Ic name test in it has at least .Ar n groups, and refuses one that uses .Ic { Ns Ar n Ns Ic } 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 when it was scanned, in local time. .It Ic {now:FMT} 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 { or .Ql } . .El .Pp With .Ic fold on, the default, a .Ic name test matches the folded name, but its captures are the original name's characters: .Ql (name \(dq^(.+)-faktura\(dq) on .Pa \[/L]\['o]d\[u017A]-faktura.pdf makes .Ic {1} the name's own .Ql \[/L]\['o]d\[u017A] . .Pp .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 .It Ic suffix .Pq default . Use .Pa stem_1.ext , stem_2.ext , and so on. .It Ic skip Skip this step; the chain continues from the file's current path. .It Ic overwrite 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 .Ic copy whose target already has identical content is skipped as .Dq already there , whatever the policy, so a backup rule can run every time. A step whose target is the file itself .Pq a Ar dest No that resolves to the file's own directory, or a Ic rename No to its current name is skipped as .Dq already there 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, 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. 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 ) , every regular file under .Ar dir .Pq symlinks skipped . Files are grouped by size; only groups of two or more are hashed, first the first and last 64 KiB, then SHA-256 of the whole file. Empty files are never duplicates. .Pp Two names for the same file .Pq the same device and inode, as a hard link creates are never duplicates of .Em each other ; they may still both independently be duplicates of a separate file with identical content. .Pp The .Em original 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 in bytes, 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. When the tested file itself cannot be read, the test is unknown .Pq Sx Operators ; another file of the same size that cannot be read is left out of the comparison with a warning, so the test can still be false. .Pp .Sy Warning: .Ic (duplicate) cannot tell a deliberate second copy from an accidental one. Byte-identical content is byte-identical either way; the distinction between .Dq I meant to keep two and .Dq this should not exist twice exists only in the user's intent, not in the files. A .Ic move rule over a directory that holds intentional copies .Pq a mirror, a staging queue, anything another tool manages will move things the user meant to keep there; keep such a rule on a directory scoped narrowly enough that every byte-identical pair in it really is an accident. .Pp .Sy Duplicates are found, never deleted. Deciding which copy to remove is left to the user, or to a tool built for it such as .Xr jdupes 1 . A rule combining .Ic (duplicate) with a delete action is refused .Pq Sx RULES . And in a directory that uses .Ic (duplicate) \(em in a rule or in an .Ic (exclude ...) , since what matters is what krino knows and not which form taught it \(em a file that is a duplicate under any of the duplicate scopes that directory uses \(em each distinct set of .Ar dir arguments, and the plain .Ic (duplicate) , looked up for the file whether or not evaluation reached that test \(em gets no delete step from any rule. The plan shows the step as .Dq skipped: a duplicate is never deleted , or, when that lookup fails, .Dq skipped: duplicate check failed, so not deleted: Ar reason ; the rest of the chain continues from the file's current path. This covers what the refusal cannot see: a later rule deleting through .Ic (matched) or through a condition of its own. .Pp Together they mean no rule can delete every copy of content a duplicate test in that directory can see. A file displaced by .Ic (on-conflict overwrite) is not covered; it goes to the trash, and .Ic krino undo restores it. .Pp The way to deal with duplicates is to move them aside and decide later: .Bd -literal -offset indent (rule "dupes" (when (duplicate "~/docs/Archive")) (move "~/.dupes/") (stop)) .Ed .Pp Every move is logged, so .Ic krino undo puts them back. Duplicate conditions with different scopes do not share an original, so two such rules can between them move every copy of a group aside; nothing is deleted. .Sh KNOWN LIMITATIONS .Xr antiword 1 refuses a legacy Word document with very little text .Pq Dq the text stream of this file is too small to handle ; with only antiword installed such a file is content unreadable, while .Xr catdoc 1 reads it. .Pp A .Ar dest whose .Em first path component is itself a placeholder .Pq e.g. Ic {ext} No or Ic {mtime:%Y} has no static prefix, so nothing under it can be excluded from the walk before scanning starts, and a recursive directory walks into .Ic krino Ns 's own output. A file already sitting at its own computed destination is a no-op, .Dq already there .Pq Sx Conflicts , rather than being re-filed on every run, but the walk still visits it, which a .Ic (duplicate) or .Ic content test in another rule can also see. Give such a rule its own narrow .Ic ignore 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 .Pa docs/sexp-primer.md in the source repository .Pq installed at Pa $PREFIX/share/doc/krino/sexp-primer.md .