csift 0.8.0

ripgrep for Claude Code session transcripts: fast regex list/search over ~/.claude/projects/**/*.jsonl
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
//! Per-verb path extraction: rm, cp, mv, sed, tar, git and friends; destination resolution.

use super::*;

#[test]
fn rm_each_non_flag_operand() {
    assert_eq!(
        paths("rm -rf /tmp/a /tmp/b"),
        vec![("/tmp/a".to_string(), "rm"), ("/tmp/b".to_string(), "rm")]
    );
}

#[test]
fn mkdir_p_creates_each_dir() {
    assert_eq!(
        paths("mkdir -p /tmp/x /tmp/y"),
        vec![
            ("/tmp/x".to_string(), "mkdir"),
            ("/tmp/y".to_string(), "mkdir")
        ]
    );
}

#[test]
fn touch_and_tee() {
    assert_eq!(paths("touch a.txt"), vec![("a.txt".to_string(), "touch")]);
    assert_eq!(paths("tee out.log"), vec![("out.log".to_string(), "tee")]);
}

#[test]
fn touch_value_flags_skip_their_arguments() {
    // `-r REFFILE`: the reference file is READ-ONLY - only the real target is created.
    assert_eq!(
        paths("touch -r /ref/file /tmp/out.txt"),
        vec![("/tmp/out.txt".to_string(), "touch")],
        "the -r reference file must not be fabricated as a created path"
    );
    // `-d DATE`: the date string must not be emitted as a path.
    assert_eq!(
        paths("touch -d '2020-01-01' /tmp/out.txt"),
        vec![("/tmp/out.txt".to_string(), "touch")]
    );
    // `--reference=REF` inline form.
    assert_eq!(
        paths("touch --reference=/ref/f /tmp/out.txt"),
        vec![("/tmp/out.txt".to_string(), "touch")]
    );
    // `-t STAMP`: the timestamp is dropped (also caught by the digit-noise filter, but
    // the explicit skip is the precise reason).
    assert_eq!(
        paths("touch -t 202001010000 /tmp/out.txt"),
        vec![("/tmp/out.txt".to_string(), "touch")]
    );
}

#[test]
fn tee_append_is_not_a_create() {
    // `tee -a` / `tee --append` do not truncate (mirrors `>>` vs `>`): verb `tee-a`.
    assert_eq!(
        paths("echo hi | tee -a /tmp/a.log"),
        vec![("/tmp/a.log".to_string(), "tee-a")]
    );
    assert_eq!(
        paths("echo hi | tee --append /tmp/b.log"),
        vec![("/tmp/b.log".to_string(), "tee-a")]
    );
    // Plain `tee` (truncate) stays a `tee` create.
    assert_eq!(
        paths("echo hi | tee /tmp/c.log"),
        vec![("/tmp/c.log".to_string(), "tee")]
    );
}

#[test]
fn ln_target_directory_emits_dest_not_source() {
    // `ln -s -t DIR target`: the real destination is DIR; `target` is the read source.
    assert_eq!(
        paths("ln -s -t /tmp/linkdir /target"),
        vec![("/tmp/linkdir".to_string(), "ln")],
        "the -t DIR is the link destination; the source target must not be reported"
    );
    // `--target-directory=DIR` inline form.
    assert_eq!(
        paths("ln --target-directory=/tmp/d /src"),
        vec![("/tmp/d".to_string(), "ln")]
    );
    // Plain `ln -s target linkname`: last positional (the link) is the destination.
    assert_eq!(
        paths("ln -s /target /tmp/link"),
        vec![("/tmp/link".to_string(), "ln")]
    );
}

#[test]
fn tar_create_emits_archive() {
    // `tar czf` creates the archive (verb `tar`); the source dir is not a written path.
    assert_eq!(
        paths("tar czf /tmp/a.tar.gz src/"),
        vec![("/tmp/a.tar.gz".to_string(), "tar")]
    );
}

#[test]
fn cp_records_only_destination() {
    assert_eq!(
        paths("cp src.txt dst.txt"),
        vec![("dst.txt".to_string(), "cp")]
    );
    // With flags, the LAST non-flag operand is the destination.
    assert_eq!(
        paths("cp -r a/ b/ /dest/"),
        vec![("/dest/".to_string(), "cp")]
    );
}

#[test]
fn mv_destination_and_sources() {
    // `mv a b c`: c is the destination (mv), a + b are moved-from sources (mv-from).
    assert_eq!(
        paths("mv a b c"),
        vec![
            ("c".to_string(), "mv"),
            ("a".to_string(), "mv-from"),
            ("b".to_string(), "mv-from")
        ]
    );
    // Two-operand mv: one destination, one source.
    assert_eq!(
        paths("mv old new"),
        vec![("new".to_string(), "mv"), ("old".to_string(), "mv-from")]
    );
    // Single operand (degenerate) → just a destination, no source.
    assert_eq!(paths("mv onlyone"), vec![("onlyone".to_string(), "mv")]);
}

#[test]
fn sed_in_place_vs_streaming() {
    // sed WITHOUT -i mutates nothing.
    assert!(paths("sed s/a/b/ file.txt").is_empty());
    // sed -i mutates the file.
    assert_eq!(
        paths("sed -i s/a/b/ file.txt"),
        vec![("file.txt".to_string(), "sed-i")]
    );
    // sed -i.bak (a backup-suffix in-place flag).
    assert_eq!(
        paths("sed -i.bak s/a/b/ file.txt"),
        vec![("file.txt".to_string(), "sed-i")]
    );
    // sed --in-place long form.
    assert_eq!(
        paths("sed --in-place s/a/b/ file.txt"),
        vec![("file.txt".to_string(), "sed-i")]
    );
}

#[test]
fn sed_bsd_empty_suffix_in_place() {
    // BSD/macOS in-place spelling `sed -i '' '<script>' file`: the `''` is the (empty)
    // backup suffix, NOT the script. The real script must not be fabricated as a file,
    // and the real file must still be recorded.
    assert_eq!(
        paths("sed -i '' 's/a/b/' /real/x.txt"),
        vec![("/real/x.txt".to_string(), "sed-i")]
    );
    assert_eq!(
        paths("sed -i \"\" '/pattern/d' /real/del.txt"),
        vec![("/real/del.txt".to_string(), "sed-i")]
    );
}

#[test]
fn sed_multiple_expression_flags() {
    // Multi `-e` scripts: every `-e`'s VALUE is a script, so the 2nd-and-later must NOT
    // be fabricated as files; only the real file is recorded. Both GNU `-i` and BSD
    // `-i ''` forms.
    assert_eq!(
        paths("sed -i -e 's/a/b/' -e 's/c/d/' /real/m.txt"),
        vec![("/real/m.txt".to_string(), "sed-i")]
    );
    assert_eq!(
        paths("sed -i '' -e 's/a/b/' -e 's/c/d/' /real/bm.txt"),
        vec![("/real/bm.txt".to_string(), "sed-i")]
    );
    // The long `--expression=` inline form, and `-f` script-file form.
    assert_eq!(
        paths("sed -i --expression='s/a/b/' /real/e.txt"),
        vec![("/real/e.txt".to_string(), "sed-i")]
    );
    assert_eq!(
        paths("sed -i -f script.sed /real/f.txt"),
        vec![("/real/f.txt".to_string(), "sed-i")]
    );
}

#[test]
fn sed_in_place_equals_suffix() {
    // GNU `--in-place=SUFFIX` long backup form must be recognized as in-place (was a
    // recall miss - the helper only matched `--in-place` / `-i…`).
    assert_eq!(
        paths("sed --in-place=.bak 's/a/b/' /real/s.txt"),
        vec![("/real/s.txt".to_string(), "sed-i")]
    );
}

#[test]
fn git_mutating_vs_readonly_subcommand() {
    assert_eq!(paths("git add ."), vec![("git:add".to_string(), "git")]);
    assert_eq!(
        paths("git commit -m wip"),
        vec![("git:commit".to_string(), "git")]
    );
    // Read-only subcommands record nothing.
    assert!(paths("git status").is_empty());
    assert!(paths("git log --oneline").is_empty());
    assert!(paths("git diff HEAD").is_empty());
}

#[test]
fn git_skips_leading_dash_c_option() {
    // `git -c key=val commit` → the subcommand is the first non-flag token.
    assert_eq!(
        paths("git -c user.name=x commit"),
        vec![("git:commit".to_string(), "git")]
    );
    // A bare global option (`--no-pager`) is skipped to find the subcommand.
    assert_eq!(
        paths("git --no-pager add ."),
        vec![("git:add".to_string(), "git")]
    );
    // `-C <path>` also consumes its value before the subcommand.
    assert_eq!(
        paths("git -C /repo commit"),
        vec![("git:commit".to_string(), "git")]
    );
    // git with ONLY options and no subcommand → git_subcommand returns None.
    assert!(paths("git --version").is_empty());
    assert!(paths("git -c a=b").is_empty());
}

#[test]
fn mv_with_only_flags_has_no_paths() {
    // `mv -f` (only a flag, no operands) → emit_mv's split_last None arm.
    assert!(paths("mv -f").is_empty());
}

#[test]
fn cp_with_only_flags_emits_nothing() {
    // `cp -r` (no operands) → emit_last_operand finds no paths.
    assert!(paths("cp -r").is_empty());
}

#[test]
fn sudo_and_env_prefixes_are_stripped() {
    // sudo rm → still detected.
    assert_eq!(paths("sudo rm /etc/x"), vec![("/etc/x".to_string(), "rm")]);
    // env VAR=1 touch → the real verb is `touch` after stripping the env assignment.
    assert_eq!(
        paths("env FOO=1 BAR=2 touch out.txt"),
        vec![("out.txt".to_string(), "touch")]
    );
}

// ── R1: cp/mv/install `-t DIR` correctness (source-vs-dest inversion) ──

#[test]
fn cp_target_directory_flag_dest_is_the_t_value_not_a_source() {
    // `cp -t DIR src…`: the DEST is DIR; the sources are reads, never reported as cp.
    assert_eq!(
        paths("cp -t /tmp/destdir /home/a.txt /home/b.txt"),
        vec![("/tmp/destdir".to_string(), "cp")]
    );
    // `--target-directory=DIR` inline form.
    assert_eq!(
        paths("cp --target-directory=/tmp/d /home/a.txt"),
        vec![("/tmp/d".to_string(), "cp")]
    );
    // install behaves the same.
    assert_eq!(
        paths("install -m755 -t /usr/local/bin a b"),
        vec![("/usr/local/bin".to_string(), "install")]
    );
}

#[test]
fn mv_target_directory_flag_dest_and_sources() {
    // `mv -t DIR a b`: DIR is the destination (mv); a + b are mv-from sources; DIR is
    // NOT also listed as a source.
    let got = paths("mv -t /tmp/destdir /home/a.txt /home/b.txt");
    assert!(
        got.contains(&("/tmp/destdir".to_string(), "mv")),
        "got: {got:?}"
    );
    assert!(
        got.contains(&("/home/a.txt".to_string(), "mv-from")),
        "got: {got:?}"
    );
    assert!(
        got.contains(&("/home/b.txt".to_string(), "mv-from")),
        "got: {got:?}"
    );
    assert!(
        !got.iter()
            .any(|(p, v)| p == "/tmp/destdir" && *v == "mv-from"),
        "the -t DIR must not also be a source: {got:?}"
    );
}

// ── R1: recall - ln / install / rsync / >| ──

#[test]
fn ln_install_rsync_destinations_caught() {
    assert_eq!(
        paths("ln -s /src /tmp/link"),
        vec![("/tmp/link".to_string(), "ln")]
    );
    assert_eq!(
        paths("install -m755 bin /usr/local/bin/tool"),
        vec![("/usr/local/bin/tool".to_string(), "install")]
    );
    assert_eq!(
        paths("rsync -a src/ /tmp/dest/"),
        vec![("/tmp/dest/".to_string(), "rsync")]
    );
}

// ── R7: a trailing OUTPUT redirect must not poison a positional-dest verb ──
// collect_redirections now REMOVES the redirect tokens (operator + its spaced path) from
// the operand stream before verb dispatch - symmetric to strip_input_redirects for `<`.
// Without this, the surviving `2>&1` / `2>/dev/null` / spaced `> file` token displaced the
// real cp/mv/ln/install/rsync destination (RECALL MISS), got mislabeled as a source
// (SEMANTIC LEAK), or double-emitted the redirect path (DOUBLE-EMIT).

#[test]
fn cp_dest_survives_trailing_fd_dup_redirect() {
    // `2>&1` is an fd-dup (no path); it must NOT become cp's positional.last() dest. The
    // REAL dest `/tmp/CP_DEST.txt` is emitted exactly once, the source is a read.
    assert_eq!(
        paths("cp src.txt /tmp/CP_DEST.txt 2>&1"),
        vec![("/tmp/CP_DEST.txt".to_string(), "cp")]
    );
}

#[test]
fn install_ln_rsync_dest_survive_trailing_redirect() {
    // install with `2>&1`: the real dest /etc/DEST.conf survives.
    assert_eq!(
        paths("install -m 644 s /etc/DEST.conf 2>&1"),
        vec![("/etc/DEST.conf".to_string(), "install")]
    );
    // ln -s with `2>/dev/null`: the link name survives (the dev-sink emits no row).
    assert_eq!(
        paths("ln -s t /tmp/LINKNAME 2>/dev/null"),
        vec![("/tmp/LINKNAME".to_string(), "ln")]
    );
    // rsync with `2>&1`: the dest dir survives.
    assert_eq!(
        paths("rsync -a src/ /tmp/RSYNC_DEST/ 2>&1"),
        vec![("/tmp/RSYNC_DEST/".to_string(), "rsync")]
    );
}

#[test]
fn mv_into_dir_not_leaked_as_source_by_trailing_redirect() {
    // `mv /a /b /tmp/DESTDIR/ 2>/dev/null`: the spaced/attached `2>/dev/null` is a dev-sink
    // (emits no row) AND is removed from operands, so DESTDIR stays the dest (verb `mv`) and
    // /a,/b stay sources (mv-from). Before the fix, `2>/dev/null` became the dest_tok
    // (rejected as a sink → NO mv dest) and ALL of /a,/b,DESTDIR leaked as mv-from reads.
    assert_eq!(
        paths("mv /a /b /tmp/DESTDIR/ 2>/dev/null"),
        vec![
            ("/tmp/DESTDIR/".to_string(), "mv"),
            ("/a".to_string(), "mv-from"),
            ("/b".to_string(), "mv-from")
        ]
    );
}

#[test]
fn cp_dest_survives_trailing_append_redirect_no_double_emit() {
    // `cp a b /tmp/CDEST/ >> /tmp/log.txt`: the `>>` and its spaced path are both consumed,
    // so /tmp/log.txt is emitted ONCE (by the redirect collector, verb `>>`) and the real
    // dest /tmp/CDEST/ stays cp's last positional - no double-emit, no dropped dest.
    assert_eq!(
        paths("cp a b /tmp/CDEST/ >> /tmp/log.txt"),
        vec![
            ("/tmp/log.txt".to_string(), ">>"),
            ("/tmp/CDEST/".to_string(), "cp")
        ]
    );
}

#[test]
fn cp_dest_survives_spaced_truncate_redirect() {
    // The spaced `> /tmp/log` form (operator + path are two tokens): both consumed.
    assert_eq!(
        paths("cp src /tmp/CP2/ > /tmp/log"),
        vec![
            ("/tmp/log".to_string(), ">"),
            ("/tmp/CP2/".to_string(), "cp")
        ]
    );
}

#[test]
fn target_directory_value_all_forms() {
    // `-t DIR`, `--target-directory DIR`, `--target-directory=DIR`, and absent.
    assert_eq!(target_directory_value(&["-t", "/d", "a"]), Some("/d"));
    assert_eq!(
        target_directory_value(&["--target-directory", "/d", "a"]),
        Some("/d")
    );
    assert_eq!(
        target_directory_value(&["--target-directory=/d", "a"]),
        Some("/d")
    );
    assert_eq!(target_directory_value(&["a", "b"]), None);
    // `-t` at the very end with no value → None (the `get(i+1)` None arm).
    assert_eq!(target_directory_value(&["a", "-t"]), None);
}

#[test]
fn cp_install_without_t_flag_uses_last_operand() {
    // The non-`-t` path of emit_copy_like (last positional is the dest).
    assert_eq!(paths("cp a b /dest"), vec![("/dest".to_string(), "cp")]);
    assert_eq!(
        paths("install -m644 src /etc/conf"),
        vec![("/etc/conf".to_string(), "install")]
    );
    // `-T`/`--no-target-directory` (forces 2-operand) → default last-operand path.
    assert_eq!(
        paths("cp -T src /dest/file"),
        vec![("/dest/file".to_string(), "cp")]
    );
}

#[test]
fn mv_target_dir_when_dir_token_repeats_in_sources() {
    // The `src == dir` skip arm of emit_mv's `-t` path: the `-t` value is excluded
    // from the mv-from sources even though it is also a non-flag positional.
    let got = paths("mv -t /tmp/d /tmp/a /tmp/b");
    // /tmp/d is the destination (mv); a + b are sources; /tmp/d is NOT a source.
    assert!(got.contains(&("/tmp/d".to_string(), "mv")));
    assert!(got.contains(&("/tmp/a".to_string(), "mv-from")));
    assert!(got.contains(&("/tmp/b".to_string(), "mv-from")));
    assert_eq!(
        got.iter().filter(|(p, _)| p == "/tmp/d").count(),
        1,
        "the -t DIR appears exactly once (as mv), never as a source: {got:?}"
    );
}

// ── tar archive creation recall ──

#[test]
fn tar_create_emits_archive_dest() {
    // `-czf <archive>` (dashed bundle, spaced archive).
    assert_eq!(
        paths("tar -czf /tmp/arch.tar.gz src/"),
        vec![("/tmp/arch.tar.gz".to_string(), "tar")]
    );
    // `czf <archive>` (bundle without a leading dash).
    assert_eq!(
        paths("tar czf backup.tar.gz ."),
        vec![("backup.tar.gz".to_string(), "tar")]
    );
    // `-cf <archive>` (no compression flag).
    assert_eq!(
        paths("tar -cf out.tar a b c"),
        vec![("out.tar".to_string(), "tar")]
    );
    // Long-flag inline + spaced forms.
    assert_eq!(
        paths("tar --create --file=/tmp/x.tar dir/"),
        vec![("/tmp/x.tar".to_string(), "tar")]
    );
    assert_eq!(
        paths("tar --create --file /tmp/y.tar dir/"),
        vec![("/tmp/y.tar".to_string(), "tar")]
    );
    // A glued archive (`-czfARCHIVE`).
    assert_eq!(
        paths("tar -czf/tmp/glued.tgz src/"),
        vec![("/tmp/glued.tgz".to_string(), "tar")]
    );
}

#[test]
fn tar_extract_is_a_marker_and_list_writes_nothing() {
    // Extraction writes the archive MEMBERS, whose names live inside the archive:
    // an `extract:tar` class marker, never invented member paths.
    assert_eq!(just_paths("tar -xzf /tmp/arch.tar.gz"), ["extract:tar"]);
    assert_eq!(just_paths("tar xf archive.tar"), ["extract:tar"]);
    // A pure LIST/test run extracts nothing and emits nothing.
    assert!(just_paths("tar -tzf /tmp/arch.tar.gz").is_empty());
    assert!(just_paths("tar tf archive.tar").is_empty());
}

#[test]
fn tar_create_to_stdout_emits_nothing() {
    // A create bundle with NO `f` (archive → stdout, e.g. piped) writes no named file.
    assert!(just_paths("tar cz src/").is_empty());
    // `--create` long flag with no `--file` likewise has no destination to emit.
    assert!(just_paths("tar --create --gzip dir/").is_empty());
}

#[test]
fn tar_file_without_create_writes_nothing() {
    // `-f <archive>` but NO create flag (`-rf` append is not a create) → no emit, since
    // `has_create` is false.
    assert!(just_paths("tar -rf /tmp/x.tar extra").is_empty());
    // A spaced `--file <archive>` with no `--create` likewise emits nothing.
    assert!(just_paths("tar --list --file /tmp/x.tar").is_empty());
}

#[test]
fn perl_in_place_is_the_sed_twin() {
    // `perl -pi -e '<script>' file…` edits each file operand in place.
    assert_eq!(
        paths("perl -pi -e 's/a/b/' notes.md src/x.rs"),
        vec![
            ("notes.md".to_string(), "perl-i"),
            ("src/x.rs".to_string(), "perl-i"),
        ]
    );
    // The backup-suffix cluster (`-pi.bak`) still carries the in-place `i`.
    assert_eq!(
        paths("perl -pi.bak -e 's/a/b/' f.txt"),
        vec![("f.txt".to_string(), "perl-i")]
    );
    // `-E` is the modern-features sibling of `-e`.
    assert_eq!(
        paths("perl -i -E 's/x/y/' cfg.ini"),
        vec![("cfg.ini".to_string(), "perl-i")]
    );
}

#[test]
fn perl_without_in_place_or_explicit_script_emits_nothing() {
    // No in-place cluster: a stream filter, no file written.
    assert!(paths("perl -pe 's/a/b/' f.txt").is_empty());
    // No `-e`: the first positional is a script FILE and the layout is ambiguous.
    assert!(paths("perl -pi prog.pl data.txt").is_empty());
    // A plain script run is not an in-place edit.
    assert!(paths("perl build.pl").is_empty());
    // The `-e` VALUE is a script, never leaked as a file.
    assert_eq!(
        paths("perl -pi -e 's|old/path|new/path|' one.md"),
        vec![("one.md".to_string(), "perl-i")]
    );
}