1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
|
.\" 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, and a symbolic link or anything else
that is not a regular file is counted and listed as
.Dq symlink
or
.Dq not a regular file .
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.
Several mistakes in one file are reported together, but only the first in
each rule: fix it 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 left with no steps is counted and listed as excluded, with the rule
that stopped it; but 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 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 whose rules use
.Ic (duplicate) ,
a file that is a duplicate under any of the duplicate scopes those rules use
\(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 .
|