diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-13 02:31:32 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-13 02:31:32 +0200 |
| commit | 26c94eb3db62ec6eebbf8d22c11afe691d9520c4 (patch) | |
| tree | 165e5bf69234b4f96c9b74deb4898d7143ddf120 /man | |
| parent | a6e442a645902011b2081c216daaec052cdc6ce6 (diff) | |
| download | krino-0.0.1.tar.gz krino-0.0.1.zip | |
krino: release 0.0.1 — man pages, install, examples, cross and release, README, changelogv0.0.1
Also: undo removes the directories its run created; a hardlink is never a
duplicate of its own other name; a flag written before "undo" is honoured;
--version prints no leading v. Duplicate conditions with different scopes
not sharing an original is documented as a known limitation.
Diffstat (limited to 'man')
| -rw-r--r-- | man/krino.1 | 348 | ||||
| -rw-r--r-- | man/krino.conf.5 | 696 |
2 files changed, 1044 insertions, 0 deletions
diff --git a/man/krino.1 b/man/krino.1 new file mode 100644 index 0000000..627a2ee --- /dev/null +++ b/man/krino.1 @@ -0,0 +1,348 @@ +.\" SPDX-License-Identifier: GPL-3.0-or-later +.Dd September 13, 2026 +.Dt KRINO 1 +.Os +.Sh NAME +.Nm krino +.Nd sort files in a directory by rules +.Sh SYNOPSIS +.Nm +.Op Fl y | Fl n +.Op Fl v +.Op Fl -json +.Op Fl c Ar file +.Op Ar name ... +.Pp +.Nm +.Cm init +.Pp +.Nm +.Cm new +.Ar name path +.Pp +.Nm +.Cm check +.Op Ar name ... +.Pp +.Nm +.Cm explain +.Ar file +.Pp +.Nm +.Cm log +.Op Fl n Ar count +.Pp +.Nm +.Cm undo +.Op Ar run +.Sh DESCRIPTION +.Nm +sorts the files in one or more configured directories according to rules +kept in +.Xr krino.conf 5 . +For each directory in turn it scans the files, evaluates every rule against +every file, and builds a plan: the chain of actions +.Pq Ic copy , Ic move , Ic rename , Ic delete +that the file's matching rules add up to. +.Pp +Every rule that matches a file adds its actions to that file's chain, in +the order the rules are written; +.Sy (stop) +on a matching rule ends the chain for that file, so a later rule is never +even evaluated against it. +A rule with no actions besides +.Sy (stop) +is an exclusion. +.Pp +Every test sees the file as it was when the directory was scanned: name, +path, size, modification time and content. +Actions already added to a file's chain by an earlier rule never change +what a later rule's tests see, so the whole plan for a directory can be +computed, shown, and approved before anything is touched. +.Pp +With no +.Fl y +or +.Fl n , +.Nm +prints the plan and asks how to proceed +.Pq Sx REVIEW . +.Fl y +applies the plan without asking; +.Fl n +prints it and changes nothing. +If standard input is not a terminal and neither is given, +.Nm +refuses rather than guess. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl y +Apply the plan without asking. +Cannot be combined with +.Fl n . +.It Fl n +Dry run: print the plan and exit without changing anything. +.It Fl v +Also list files that were skipped as unmatched, ignored or busy, and show +the full reason a test matched or not. +.It Fl -json +With +.Fl n , +print the plan as JSON instead of the table. +Refused unless +.Fl n +is also given. +The document's own +.Ic note +field says its shape is unstable before krino 1.0; a script that reads it +should expect it to change before then. +.It Fl c Ar file +Use +.Ar file +in place of +.Pa $XDG_CONFIG_HOME/krino/krino.conf . +.It Fl h , Fl -help +Print usage and exit. +.It Fl -version +Print +.Dq krino Ar version +and exit. +.El +.Sh SUBCOMMANDS +.Bl -tag -width Ds +.It Ic init +Create the config directory with a commented +.Pa krino.conf +and +.Pa template.conf . +Refuses if +.Pa krino.conf +already exists, and leaves an existing +.Pa template.conf +alone. +.It Ic new Ar name path +Copy +.Pa template.conf +to +.Pa dirs/ Ns Ar name Ns Pa .conf , +fill in +.Ar path , +and append +.Ar name +to +.Ic include +in +.Pa krino.conf , +keeping its comments. +.It Ic check Op Ar name ... +Validate the configuration, list each included directory's rules, and list +which content-extraction tools +.Pq Xr krino.conf 5 , Sx CONTENT EXTRACTION +are available. +With no +.Ar name , +checks every included directory. +.It Ic explain Ar file +Evaluate every rule of +.Ar file Ns 's +directory against it and show each test's result, so a rule that should +match but does not +.Pq or the reverse +can be diagnosed test by test. +See +.Sx KNOWN LIMITATIONS . +.It Ic log Op Fl n Ar count +List the most recent runs, newest first +.Pq Ar count No defaults to 10 : +the run id, its start time, the directories it touched, and what it did. +A run that a later +.Ic undo +has reversed at least one file of is marked +.Pq undone . +.It Ic undo Op Ar run +Reverse +.Ar run . +With no +.Ar run , +reverse the most recent run; if that run is itself an undo, it is refused. +An older run is undone by naming it. +See +.Sx UNDO . +.El +.Sh REVIEW +With neither +.Fl y +nor +.Fl n , +after showing one directory's plan +.Nm +asks: +.Bd -literal -offset indent +[a] apply all [c] choose per file [s] skip this directory [q] quit +.Ed +.Pp +.Ic a +applies every step of every chain shown. +.Ic s +applies nothing in this directory and moves on to the next one. +.Ic q +stops +.Nm +entirely; directories already applied earlier in this run stay applied and +can be reversed with +.Ic undo . +.Pp +.Ic c +asks about each file in turn: +.Bd -literal -offset indent + [y] yes [n] no [a] yes to this and all remaining [d] done, apply chosen so far [q] quit, apply nothing +.Ed +.Pp +Approval is per file: a file's whole chain runs, or none of it. +.Ic d +stops asking and applies whatever was already chosen, declining the rest. +.Ic q +here aborts the review for this directory entirely, discarding even a file +already marked +.Ic yes , +and folds into the top-level +.Ic q +above. +.Sh UNDO +.Ic krino undo +reverses a run's steps, last step first within each file, after building +and showing a plan of its own +.Pq shown and approved the same way as a sort plan; Fl y No and Fl n No apply . +One undo plan covers every directory the run touched, so its menu has no +per-directory skip; +.Ic s +and +.Ic q +both apply nothing: +.Bd -literal -offset indent +[a] apply all [c] choose per file [s] skip [q] quit +.Ed +.Pp +An undo run cannot itself be undone: naming it to +.Ic undo +is refused. +.Pp +A step's reversal is refused when the world has moved on since the step ran +.Pq its target is gone or has changed, or the original path is occupied again ; +a refused file has nothing of its chain reversed, so no file is left half +undone. +An exception is a created directory found non-empty at its own turn: that +alone does not refuse the rest of the file's reversal, since it means +another file still lives there, not that something unexpected changed +underfoot. +Every created directory still empty once every file in the run has had its +turn is then removed, deepest path first, so an undo does not leave the +empty directories it made behind. +.Pp +A permanent delete, +.Sy (delete permanent) , +is never undoable; it is shown in the plan with that reason and nothing is +attempted for it. +.Pp +.Ic krino log +marks a run +.Pq undone +once its undo run has reversed at least one file's chain \(em not that +every file in it was restored: a run left partly reversed, because some +files were declined during the undo's own review, is still shown as +.Pq undone +in full. +.Sh ENVIRONMENT +.Bl -tag -width Ds +.It Ev XDG_CONFIG_HOME +Base of the configuration directory +.Pq Pa $XDG_CONFIG_HOME/krino ; +default +.Pa ~/.config . +.It Ev XDG_STATE_HOME +Base of the log and the per-directory lock files +.Pq Pa $XDG_STATE_HOME/krino ; +default +.Pa ~/.local/state . +.It Ev XDG_DATA_HOME +Base of the trash a +.Sy (delete) +step uses +.Pq Pa $XDG_DATA_HOME/Trash ; +default +.Pa ~/.local/share . +.It Ev PAGER +Used to show a plan taller than the terminal; default +.Dq less -FRX . +Never used for +.Fl -json +output. +.It Ev NO_COLOR +When set, disables the ANSI colour +.Nm +otherwise uses on a terminal for a permanent delete and a refused undo +step. +.El +.Sh FILES +.Bl -tag -width Ds +.It Pa $XDG_CONFIG_HOME/krino/krino.conf +The main configuration file: which directories run, defaults, the log +path. +.It Pa $XDG_CONFIG_HOME/krino/template.conf +Copied by +.Ic krino new +for each new directory. +.It Pa $XDG_CONFIG_HOME/krino/dirs/ Ns Ar name Ns Pa .conf +One file per configured directory. +.It Pa $XDG_STATE_HOME/krino/krino.log +The append-only log every run writes to. +.It Pa $XDG_STATE_HOME/krino/ Ns Ar name Ns Pa .lock +Held for the duration of a run against directory +.Ar name , +so a second +.Nm +against the same directory waits, or fails immediately with +.Fl y . +.It Pa $XDG_DATA_HOME/Trash +The freedesktop.org trash a +.Sy (delete) +step moves files into. +.El +.Sh EXIT STATUS +.Bl -tag -width Ds +.It 0 +Success, including a run that found nothing to do, or one whose plan was +entirely declined at review. +.It 1 +A step failed, a directory's lock was already held by another +.Nm , +or a configured directory could not be read. +.It 2 +A usage mistake or a configuration error. +Flags must come before any directory name on the command line: a flag +found after a directory name is a usage error rather than a guess. +.It 130 +Interrupted +.Pq Ic Ctrl-C +or terminated; the current step, if any, is finished and logged first. +.El +.Sh KNOWN LIMITATIONS +.Ic krino explain +evaluates every test of every rule's condition against the file, without +the cost-ordered short-circuiting a real run uses within an +.Sy and/or . +It can therefore run a content-extraction tool, and report a +.Dq content unreadable +warning, for a test that a real run would never reach because an earlier, +cheaper test in the same condition already decided the rule did not match. +.Ic explain +is a full trace of every test, not a cost-faithful preview of what +.Fl n +would actually spend time on. +.Sh SEE ALSO +.Xr krino.conf 5 +.Pp +.Pa docs/sexp-primer.md +in the source repository +.Pq installed at Pa $PREFIX/share/doc/krino/sexp-primer.md +is a short tutorial on the configuration syntax. diff --git a/man/krino.conf.5 b/man/krino.conf.5 new file mode 100644 index 0000000..fbd1c06 --- /dev/null +++ b/man/krino.conf.5 @@ -0,0 +1,696 @@ +.\" SPDX-License-Identifier: GPL-3.0-or-later +.Dd September 13, 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 . +This page is 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 and rule names must be strings; 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 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)) +.Ed +.Bl -tag -width Ds +.It Ic (include Ar name No ...) +The directories to sort, in the order given. +Each +.Ar name +has its rules in +.Pa dirs/ Ns Ar name Ns Pa .conf . +.It Ic (log Ar path ) +Where the log goes. +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 . +.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 + +(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. +.Ar ~ +expands. +.It Ic (ignore Ar pattern No ...) +May repeat; patterns accumulate in order. +Gitignore syntax: +.Ql * , +.Ql ** , +.Ql \&? , +.Ql [...] ; +a leading +.Ql / +anchors to the root; 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. +.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 an integer plus +.Ql s m h d w ; +sizes an integer plus an optional +.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 . +Built-in: +.Sy yes . +.It Ic recursive +.Sy yes +or +.Sy no . +Built-in: +.Sy no . +.It Ic max-depth +An integer; +.Sy 1 +means the root only. +Built-in: unlimited. +.It Ic min-age +A duration. +Files modified more recently are skipped as busy. +Built-in: +.Sy 2m . +.It Ic max-read +A size. +No content is extracted from a file above this size. +Built-in: +.Sy 50M . +.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. +.Pa report.pdf.part +beside +.Pa report.pdf . +Built-in: +.Sy .part .aria2 .crdownload . +.It Ic on-conflict +.Pq rule . +.Sy suffix , skip +or +.Sy overwrite : +see +.Sx Conflicts . +Built-in: +.Sy suffix . +.El +.Sh RULES +.Bd -literal -offset indent +(rule NAME ITEM...) +.Ed +.Pp +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 / . +.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) +is an exclusion: files it matches receive no actions from later rules. +.Pp +.Ar dest +is a directory: a relative path is relative to the root, +.Ql ~ +expands, an absolute path is allowed, and it is created if missing. +.Ar dest +and +.Ar name +take placeholders; see +.Sx Placeholders . +.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. +.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 . +.It Ic (name Ar re No ...) +True when the file name matches any of the regexes. +.It Ic (path Ar re No ...) +True when the path relative to the root matches any of the regexes. +.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. +.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 . +.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 +Known text extensions, or detected from the first 8 KiB: valid UTF-8, or +UTF-16 with a BOM, and no NUL bytes. +.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 +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 +No content. +.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. +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 , +and runs of whitespace collapsed to one space, 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. +.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 . +.Pp +The plan warns when a chain moves a file more than once; that is usually +a missing +.Ic (stop) . +.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 any step fails the source is untouched and the temporary file is +removed. +.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 still exists with +the size and modification time recorded in the plan; 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. +.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. +.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. +.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. +.It Ic {mtime:FMT} +The file's modification time. +.It Ic {now:FMT} +The start of the run. +.It Ic {{ No and Ic }} +A literal +.Ql { +or +.Ql } . +.El +.Pp +.Ar FMT +is a strftime subset: +.Ql %Y %m %d %H %M %S %j %% . +.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. +.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, 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 +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. +.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, +then the name that sorts first. +.Ic (duplicate) +is true for every other scanned file in the group. +.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. +Pairing +.Ic (duplicate) +with +.Ic (delete) +over a directory that may hold intentional copies +.Pq a mirror, a staging queue, anything another tool manages +will delete things the user meant to keep. +Keep such a rule on a directory scoped narrowly enough that every +byte-identical pair in it really is an accident. +.Pp +.Sy Warning: +each +.Ic (duplicate) +or +.Ic (duplicate Ar dir ) +condition elects its own original from its own candidates; two conditions +with different scopes can therefore elect +.Em different +originals for the same content. +Within one directory's rules +.Pq one rule combining conditions with Ic or , or two separate rules , +this can make every copy in a group selected by some condition, and with +.Ic (delete permanent) +every copy selected is gone for good. +.Ic krino +plans and applies one directory at a time, so a directory already applied +in the run stays applied while a later directory is planned; a +.Fl n +run only ever plans, so it shows every directory's plan as if none of the +others had been applied. +Two files in different directories that a +.Fl n +plan each lists as a duplicate of the other are therefore not necessarily +both deleted when the same run is applied with +.Fl y : +deleting the first can remove the very original the second file was a +duplicate of, leaving the second no longer a duplicate by the time its own +directory is planned. +Use one duplicate scope for rules that delete within a directory; prefer +.Ic (delete) +to +.Ic (delete permanent) +with +.Ic (duplicate) ; +and within one directory's rules, two files each listed as a duplicate of +the other in the +.Fl n +plan means both would be deleted. +.Sh KNOWN LIMITATIONS +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 (duplicate) +conditions with different scopes do not share an original: two conditions +electing from different candidate sets can each treat a different file as +the original of the same content +.Pq Sx DUPLICATES . +.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 . |
