usage-argv 6.5.0

Zero-allocation argv parser for usage specs
Documentation
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
//! Do the generated scripts work in the shells they are written for?
//!
//! A completion script is the one artifact here that no amount of Rust testing reaches: it is text
//! handed to another program, and the way it fails is at a prompt, silently, for a user. So these
//! run the real shells — every one available on the machine — against a stand-in binary that
//! answers the way the derive's hidden command does.
//!
//! The stand-in matters as much as the script: it is what makes this a test of the *protocol* and
//! not of the parser. What the binary would have computed is already checked to the last byte
//! against usage-lib elsewhere.

#![cfg(feature = "complete")]

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
#[cfg(unix)]
use std::process::Stdio;
use usage_argv::complete::Shell;
use usage_argv::script::script;
#[cfg(unix)]
use usage_argv::script::script_for;

/// A directory of this test's own, with the generated script and a stand-in `ex` binary in it.
struct Fixture {
    dir: PathBuf,
}

impl Fixture {
    /// `answer` is what the stand-in prints for a completion request, verbatim.
    fn new(name: &str, shell: Shell, answer: &str) -> Self {
        let dir =
            std::env::temp_dir().join(format!("usage-argv-scripts-{name}-{}", std::process::id()));
        let _ = fs::remove_dir_all(&dir);
        fs::create_dir_all(&dir).expect("a directory to work in");

        fs::write(dir.join("script"), script("ex", shell)).expect("writing the script");

        // The stand-in answers a completion request and nothing else, so a script that called it
        // for anything else would be caught rather than quietly working.
        let stand_in = format!(
            "#!/usr/bin/env bash\nif [[ ${{1:-}} != __complete_word__ ]]; then\n  \
             echo \"the script called the binary for something else: $*\" >&2\n  exit 1\nfi\n\
             printf '%s' {}\n",
            shell_quote(answer)
        );
        let bin = dir.join("ex");
        fs::write(&bin, stand_in).expect("writing the stand-in");
        make_executable(&bin);

        // Files for the path cases to find, and a directory among them so `dirs` can differ.
        fs::write(dir.join("alpha.txt"), "").expect("a file");
        fs::write(dir.join("beta.txt"), "").expect("another");
        fs::write(dir.join("manifest.toml"), "").expect("a filtered file");
        fs::write(dir.join("settings.yaml"), "").expect("another filtered file");
        fs::create_dir(dir.join("gamma")).expect("a directory");

        Self { dir }
    }

    /// A fixture whose stand-in reports the line it was handed, rather than answering.
    ///
    /// What the scripts do with the cursor is the part most likely to be silently wrong — every
    /// shell counts in its own units — so it is worth asserting on what the binary *receives*
    /// and not only on what comes back.
    fn echoing(name: &str, shell: Shell) -> Self {
        let fixture = Self::new(name, shell, "");
        let stand_in = "#!/usr/bin/env bash\n             line=\nwhile [[ $# -gt 0 ]]; do\n               if [[ $1 == --line ]]; then line=$2; shift; fi\n  shift\ndone\n             line=${line//$'\\t'/\\\\t}\n             printf 'line=[%s]\\n' \"$line\"\n";
        let bin = fixture.dir.join("ex");
        fs::write(&bin, stand_in).expect("writing the stand-in");
        make_executable(&bin);
        fixture
    }

    /// Run `code` in `shell`, with the fixture's directory as the working directory and on `PATH`.
    fn run(&self, shell: &str, code: &str) -> String {
        let out = Command::new(shell)
            .arg("-c")
            .arg(code)
            .current_dir(&self.dir)
            .env(
                "PATH",
                format!(
                    "{}:{}",
                    self.dir.display(),
                    std::env::var("PATH").unwrap_or_default()
                ),
            )
            .output()
            .unwrap_or_else(|e| panic!("running {shell}: {e}"));
        assert!(
            out.status.success(),
            "{shell} failed: {}\n{}",
            String::from_utf8_lossy(&out.stderr),
            String::from_utf8_lossy(&out.stdout)
        );
        String::from_utf8_lossy(&out.stdout).into_owned()
    }
}

impl Drop for Fixture {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.dir);
    }
}

fn shell_quote(s: &str) -> String {
    format!("'{}'", s.replace('\'', "'\\''"))
}

#[cfg(unix)]
fn make_executable(path: &Path) {
    use std::os::unix::fs::PermissionsExt;
    let mut perms = fs::metadata(path).expect("metadata").permissions();
    perms.set_mode(0o755);
    fs::set_permissions(path, perms).expect("making it executable");
}

#[cfg(not(unix))]
fn make_executable(_path: &Path) {}

/// Whether a program is on `PATH`, so a missing shell is skipped rather than failing the suite.
fn available(program: &str) -> bool {
    Command::new(program)
        .arg("--version")
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

/// Whether a program can check a script this test would hand it, so a shell that cannot is
/// skipped rather than failing the suite.
///
/// Not `--version`. On Windows the executable search order puts the system directory ahead of
/// `PATH`, and installing WSL puts `bash.exe` there — a launcher that answers `--version`
/// perfectly well and then cannot open the file it is given, which is how this test came to
/// fail rather than skip. The precondition is the whole invocation, so that is what gets tried:
/// the same program and flags, on a script known to be valid.
fn can_check_a_script(program: &str, args: &[&str]) -> bool {
    let dir = std::env::temp_dir().join(format!(
        "usage_argv_shell_probe_{}_{program}",
        std::process::id()
    ));
    if fs::create_dir_all(&dir).is_err() {
        return false;
    }
    let probe = dir.join("probe");
    let usable = fs::write(&probe, "echo ok\n").is_ok()
        && Command::new(program)
            .args(args)
            .arg(&probe)
            .output()
            .is_ok_and(|out| out.status.success());
    let _ = fs::remove_dir_all(&dir);
    usable
}

#[test]
fn every_script_is_valid_in_its_own_shell() {
    // A syntax error in generated text is the failure mode that reaches a user as a broken
    // prompt, so it is worth checking even where the shell cannot be driven any further.
    let checks: &[(Shell, &str, &[&str])] = &[
        (Shell::Bash, "bash", &["-n"]),
        (Shell::Zsh, "zsh", &["-n"]),
        (Shell::Fish, "fish", &["--no-execute"]),
    ];

    let mut ran = 0;
    for (shell, program, args) in checks {
        if !can_check_a_script(program, args) {
            continue;
        }
        let fixture = Fixture::new(&format!("syntax-{program}"), *shell, "");
        let out = Command::new(program)
            .args(*args)
            .arg(fixture.dir.join("script"))
            .output()
            .expect("running the shell");
        assert!(
            out.status.success(),
            "the {program} script does not parse:\n{}\n--- script ---\n{}",
            String::from_utf8_lossy(&out.stderr),
            script("ex", *shell)
        );
        ran += 1;
    }
    // Named rather than silently skipped: a test that checks nothing should say so.
    //
    // Unix only. CI installs zsh and fish for the Linux job, so none of the three being usable
    // there is a configuration bug worth stopping for. Nothing installs them on Windows, and a
    // bare `bash` is the WSL launcher that cannot open the file it is handed — so having no
    // shell to check against is the expected state there rather than a fault.
    if cfg!(unix) {
        assert!(
            ran > 0,
            "no shell was available to check a script against — bash, zsh and fish were all missing"
        );
    } else if ran == 0 {
        println!("no shell here could check a script; this verified nothing");
    }
    if ran < checks.len() {
        println!(
            "checked {ran} of {} shells; the rest are not installed",
            checks.len()
        );
    }
}

#[cfg(unix)]
#[test]
fn elvish_offers_what_the_binary_answered() {
    use std::io::Write;

    // Elvish's noninteractive compiler does not define the interactive `edit:` namespace, so
    // loading this as an RC file in a real pseudoterminal is both the syntax and behavior check.
    if !available("elvish") || !available("script") {
        println!("elvish or the script PTY driver is not installed; skipping");
        return;
    }
    let fixture = Fixture::new(
        "elvish-candidates",
        Shell::Elvish,
        "install\tInstall a package\tINSTALL\n",
    );
    let mut rc = fs::read_to_string(fixture.dir.join("script")).expect("reading the Elvish RC");
    rc.push_str(&script_for("ex", "ex-alias", Shell::Elvish));
    fs::write(fixture.dir.join("script"), rc).expect("writing both Elvish completers");
    let rc = fixture.dir.join("script");
    let command = format!("elvish -rc {}", shell_quote(&rc.to_string_lossy()));
    let mut child = Command::new("script")
        .args(["-qfc", &command, "/dev/null"])
        .current_dir(&fixture.dir)
        .env(
            "PATH",
            format!(
                "{}:{}",
                fixture.dir.display(),
                std::env::var("PATH").unwrap_or_default()
            ),
        )
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("starting Elvish in a pseudoterminal");
    let mut stdin = child.stdin.take().expect("PTY stdin");
    std::thread::sleep(std::time::Duration::from_millis(500));
    stdin.write_all(b"ex i\t").expect("requesting completion");
    stdin.flush().expect("flushing completion request");
    std::thread::sleep(std::time::Duration::from_millis(500));
    stdin.write_all(b"\x03").expect("cancelling the input line");
    stdin.flush().expect("flushing cancellation");
    std::thread::sleep(std::time::Duration::from_millis(100));
    stdin.write_all(b"\x04").expect("exiting Elvish");
    let out = child.wait_with_output().expect("waiting for Elvish");
    assert!(
        out.status.success(),
        "Elvish failed:\n{}\n{}",
        String::from_utf8_lossy(&out.stderr),
        String::from_utf8_lossy(&out.stdout)
    );
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.contains("nstall") && !stdout.contains("no candidates"),
        "candidate was not displayed: {stdout:?}"
    );
}

#[test]
fn bash_offers_what_the_binary_answered() {
    if !available("bash") {
        println!("bash is not installed; skipping");
        return;
    }
    let fixture = Fixture::new("bash-candidates", Shell::Bash, "install\nuninstall\n");
    // Driven the way bash drives it: the line, the cursor, the split words, and the index — then
    // the function, then whatever it left in COMPREPLY.
    let out = fixture.run(
        "bash",
        r#"source ./script
COMP_LINE='ex ins'
COMP_POINT=6
COMP_WORDS=(ex ins)
COMP_CWORD=1
_usage_complete_ex
printf '%s\n' "${COMPREPLY[@]}"
"#,
    );
    assert_eq!(out, "install\nuninstall\n");
}

#[test]
fn bash_asks_the_shell_for_paths_when_the_marker_says_so() {
    if !available("bash") {
        println!("bash is not installed; skipping");
        return;
    }
    // A candidate *and* the marker, which is the interesting shape: the answer this CLI knows
    // about, plus the paths it does not.
    let fixture = Fixture::new(
        "bash-files",
        Shell::Bash,
        &format!("keep\n{}\n", usage_argv::complete::FILES_MARKER),
    );
    let out = fixture.run(
        "bash",
        r#"source ./script
COMP_LINE='ex '
COMP_POINT=3
COMP_WORDS=(ex '')
COMP_CWORD=1
_usage_complete_ex
printf '%s\n' "${COMPREPLY[@]}"
"#,
    );
    let offered: Vec<&str> = out.lines().collect();
    assert!(offered.contains(&"keep"), "{offered:?}");
    assert!(offered.contains(&"alpha.txt"), "{offered:?}");
    assert!(offered.contains(&"beta.txt"), "{offered:?}");
    // The marker itself is a message to the script, not a candidate.
    assert!(
        !offered.iter().any(|o| o.contains('\u{1}')),
        "the marker reached the user: {offered:?}"
    );

    // And `dirs` narrows it to directories.
    let fixture = Fixture::new(
        "bash-dirs",
        Shell::Bash,
        &format!("{}\n", usage_argv::complete::DIRS_MARKER),
    );
    let out = fixture.run(
        "bash",
        r#"source ./script
COMP_LINE='ex '
COMP_POINT=3
COMP_WORDS=(ex '')
COMP_CWORD=1
_usage_complete_ex
printf '%s\n' "${COMPREPLY[@]}"
"#,
    );
    let offered: Vec<&str> = out.lines().filter(|l| !l.is_empty()).collect();
    assert_eq!(offered, ["gamma"], "only the directory");
}

#[test]
fn bash_filters_files_by_the_extensions_the_binary_declared() {
    if !available("bash") {
        println!("bash is not installed; skipping");
        return;
    }
    let fixture = Fixture::new(
        "bash-extensions",
        Shell::Bash,
        "\u{1}extensions\ttoml\tyaml\n",
    );
    let out = fixture.run(
        "bash",
        r#"source ./script
COMP_LINE='ex '
COMP_POINT=3
COMP_WORDS=(ex '')
COMP_CWORD=1
_usage_complete_ex
printf '%s\n' "${COMPREPLY[@]}"
"#,
    );
    let offered: Vec<&str> = out.lines().filter(|line| !line.is_empty()).collect();
    assert!(offered.contains(&"manifest.toml"), "{offered:?}");
    assert!(offered.contains(&"settings.yaml"), "{offered:?}");
    assert!(
        offered.contains(&"gamma"),
        "directories remain traversable: {offered:?}"
    );
    assert!(!offered.contains(&"alpha.txt"), "{offered:?}");
}

#[test]
fn fish_prints_the_candidates_it_was_given() {
    if !available("fish") {
        println!("fish is not installed; skipping");
        return;
    }
    // fish's completion functions print `value\tdescription` lines, which is exactly what the
    // binary hands over — so the function can be called directly and its output read.
    let fixture = Fixture::new(
        "fish-candidates",
        Shell::Fish,
        "install\tInstall a tool\nuninstall\tRemove it\n",
    );
    let out = fixture.run("fish", "source ./script; __usage_complete_ex");
    assert_eq!(out, "install\tInstall a tool\nuninstall\tRemove it\n");
}

/// A candidate that looks like an option to fish's own printer is still offered.
///
/// `echo -n` prints nothing at all: the flag is read as fish's, not as the data it was handed. So a
/// CLI with a `-n` flag had that candidate vanish on the way to the prompt.
///
/// Undescribed candidates, which is the case that breaks and the common one — the renderer writes a
/// description column only when something in the answer has one, so a spec whose flags carry no
/// help sends the bare spellings. With a description attached the argument is `-n\tDry run`, which
/// is nobody's option and survives either way; a test written that way passes against the bug.
#[test]
fn fish_prints_a_candidate_that_looks_like_an_option() {
    if !available("fish") {
        println!("fish is not installed; skipping");
        return;
    }
    let fixture = Fixture::new("fish-optionlike", Shell::Fish, "-n\n-e\n-E\n--dry-run\n");
    let out = fixture.run("fish", "source ./script; __usage_complete_ex");
    assert_eq!(
        out, "-n\n-e\n-E\n--dry-run\n",
        "every option-like candidate should arrive as itself"
    );
}

#[test]
fn fish_adds_paths_and_never_the_marker() {
    if !available("fish") {
        println!("fish is not installed; skipping");
        return;
    }
    let fixture = Fixture::new(
        "fish-files",
        Shell::Fish,
        &format!("keep\n{}\n", usage_argv::complete::FILES_MARKER),
    );
    let out = fixture.run("fish", "source ./script; __usage_complete_ex");
    let offered: Vec<&str> = out.lines().map(|l| l.split('\t').next().unwrap()).collect();
    assert!(offered.contains(&"keep"), "{offered:?}");
    assert!(offered.contains(&"alpha.txt"), "{offered:?}");
    assert!(
        !out.contains('\u{1}'),
        "the marker reached the user: {out:?}"
    );
}

/// zsh's completion builtins, stubbed so the script can be driven outside a completion context.
///
/// What is under test is the part of the script this crate wrote: the parsing of the three
/// columns, the aligned display, the menu decision, and whether paths are asked for. zsh's own
/// `compadd` and `_files` are the parts that need no testing here — but they are also what makes
/// the function impossible to call directly, so they are replaced by stubs that report what they
/// were handed. The arrays arrive by name, and zsh's dynamic scoping lets a stub read them.
const ZSH_STUBS: &str = r#"typeset -A compstate
compdef() { }
compadd() {
    local entry
    for entry in "${display[@]}"; do print -r -- "display:$entry"; done
    for entry in "${inserts[@]}"; do print -r -- "insert:$entry"; done
    return 0
}
_files() { print -r -- "_files:$*"; return 0 }
"#;

#[test]
fn zsh_presents_the_three_columns_it_was_given() {
    if !available("zsh") {
        println!("zsh is not installed; skipping");
        return;
    }
    let fixture = Fixture::new(
        "zsh-columns",
        Shell::Zsh,
        "install\tInstall a tool\tinstall\nrm\tRemove it\trm\n",
    );
    let out = fixture.run(
        "zsh",
        &format!(
            "{ZSH_STUBS}\nsource ./script\nBUFFER='ex i'\nCURSOR=4\n_ex\n\
             print -r -- \"insert_mode=${{compstate[insert]:-none}}\"\n"
        ),
    );

    // Descriptions aligned to the longest value, which is what makes a list of candidates read
    // as a table rather than as ragged text.
    assert!(out.contains("display:install  -- Install a tool"), "{out}");
    assert!(out.contains("display:rm       -- Remove it"), "{out}");
    // And what would actually be typed, which is the third column's whole purpose.
    assert!(out.contains("insert:install"), "{out}");
    // Nothing needed quoting, so no menu is forced.
    assert!(out.contains("insert_mode=none"), "{out}");
}

#[test]
fn zsh_hands_paths_to_files_and_forces_a_menu_when_a_value_needs_quoting() {
    if !available("zsh") {
        println!("zsh is not installed; skipping");
        return;
    }
    // A value with a space in it: shown as it is, typed quoted — and zsh should offer a menu
    // rather than silently inserting one of several possibilities.
    let fixture = Fixture::new(
        "zsh-quoted",
        Shell::Zsh,
        &format!(
            "with space\tOdd\t'with space'\n{}\n",
            usage_argv::complete::FILES_MARKER
        ),
    );
    let out = fixture.run(
        "zsh",
        &format!(
            "{ZSH_STUBS}\nsource ./script\nBUFFER='ex '\nCURSOR=3\n_ex\n\
             print -r -- \"insert_mode=${{compstate[insert]:-none}}\"\n"
        ),
    );
    assert!(out.contains("display:with space  -- Odd"), "{out}");
    assert!(out.contains("insert:'with space'"), "{out}");
    assert!(out.contains("insert_mode=menu"), "{out}");
    // And the marker asked zsh's own file completion, rather than becoming a candidate.
    assert!(out.contains("_files:"), "{out}");
    assert!(!out.contains('\u{1}'), "the marker reached the user: {out}");

    // Directories only, when that is what the position takes.
    let fixture = Fixture::new(
        "zsh-dirs",
        Shell::Zsh,
        &format!("{}\n", usage_argv::complete::DIRS_MARKER),
    );
    let out = fixture.run(
        "zsh",
        &format!("{ZSH_STUBS}\nsource ./script\nBUFFER='ex '\nCURSOR=3\n_ex\n"),
    );
    assert!(out.contains("_files:-/"), "{out}");
}

#[test]
fn zsh_passes_extension_filters_to_its_native_file_completer() {
    if !available("zsh") {
        println!("zsh is not installed; skipping");
        return;
    }
    let fixture = Fixture::new(
        "zsh-extensions",
        Shell::Zsh,
        "\u{1}extensions\ttoml\tyaml\n",
    );
    let out = fixture.run(
        "zsh",
        &format!("{ZSH_STUBS}\nsource ./script\nBUFFER='ex '\nCURSOR=3\n_ex\n"),
    );
    assert!(out.contains("_files:-g *.(toml|yaml)"), "{out}");
}

#[test]
fn zsh_expands_a_local_alias_before_asking_the_binary() {
    if !available("zsh") {
        println!("zsh is not installed; skipping");
        return;
    }
    let fixture = Fixture::echoing("zsh-alias", Shell::Zsh);
    let out = fixture.run(
        "zsh",
        &format!(
            "{ZSH_STUBS}\nalias true=false\nsource ./script\nalias xa='ex deploy'\nalias empty=''\n\
             BUFFER='xa feature'\nCURSOR=10\n_ex\n\
             BUFFER='  xa feature'\nCURSOR=12\n_ex\n\
             BUFFER=$'\\txa feature'\nCURSOR=11\n_ex\n\
             BUFFER='empty feature'\nCURSOR=13\n_ex\n\
             BUFFER='FOO=1 xa feature'\nCURSOR=16\n_ex\n\
             BUFFER='FOO+=1 xa feature'\nCURSOR=17\n_ex\n\
             BUFFER=\"FOO='a b' xa feature\"\nCURSOR=20\n_ex\n"
        ),
    );
    assert!(out.contains("display:line=[ex deploy feature]"), "{out}");
    assert!(out.contains("display:line=[  ex deploy feature]"), "{out}");
    assert!(out.contains("display:line=[\\tex deploy feature]"), "{out}");
    assert!(out.contains("display:line=[ feature]"), "{out}");
    assert!(
        out.contains("display:line=[FOO=1 ex deploy feature]"),
        "{out}"
    );
    assert!(
        out.contains("display:line=[FOO+=1 ex deploy feature]"),
        "{out}"
    );
    assert!(
        out.contains("display:line=[FOO='a b' ex deploy feature]"),
        "{out}"
    );
}

#[test]
fn the_line_a_shell_hands_over_is_cut_at_the_cursor_in_its_own_units() {
    // The cursor is counted differently by every shell — characters in a UTF-8 locale for bash
    // and zsh, characters for fish, an offset into the whole input buffer for PowerShell — so
    // no script passes one. Each cuts the line with its own offset, where the units cancel out.
    // A non-ASCII character before the cursor is what makes the difference visible: passed as a
    // number, `6` would have landed mid-character and completed the wrong word.
    if available("bash") {
        let fixture = Fixture::echoing("bash-cursor", Shell::Bash);
        let out = fixture.run(
            "bash",
            r#"source ./script
COMP_LINE='ex ünicode here'
COMP_POINT=6
COMP_WORDS=(ex ünicode here)
COMP_CWORD=1
_usage_complete_ex
printf '%s\n' "${COMPREPLY[@]}"
"#,
        );
        assert_eq!(out.trim(), "line=[ex üni]", "bash");
    }

    if available("zsh") {
        let fixture = Fixture::echoing("zsh-cursor", Shell::Zsh);
        let out = fixture.run(
            "zsh",
            &format!("{ZSH_STUBS}\nsource ./script\nBUFFER='ex ünicode here'\nCURSOR=6\n_ex\n"),
        );
        // Reported as a candidate by the stand-in, so it comes back through `display`.
        assert!(out.contains("display:line=[ex üni]"), "zsh: {out}");
    }

    if available("fish") {
        // fish is handed a line already cut at the cursor, so what it passes on is that line —
        // which is why its script says nothing about a cursor at all.
        let fixture = Fixture::echoing("fish-cursor", Shell::Fish);
        let out = fixture.run("fish", "source ./script; __usage_complete_ex");
        assert!(out.starts_with("line=["), "fish: {out}");
    }
}

#[test]
fn the_zsh_script_works_however_it_was_installed() {
    if !available("zsh") {
        println!("zsh is not installed; skipping");
        return;
    }
    // Two installs, and a script has to survive both. Dropped in `$fpath` as `_ex`, compinit
    // autoloads the file and calls the function *named after it* — which is why the function is
    // `_ex` rather than something tidier, and what a mismatched name would silently break.
    // Sourced from a config instead, nothing has called it yet, so it has to register itself.
    let fixture = Fixture::new("zsh-install", Shell::Zsh, "install\tInstall\tinstall\n");

    // Sourced: the tail should register, not complete.
    let out = fixture.run(
        "zsh",
        &format!("{ZSH_STUBS}\ncompdef() {{ print -r -- \"compdef:$*\" }}\nsource ./script\n"),
    );
    assert!(
        out.contains("compdef:_ex ex"),
        "sourcing should register: {out}"
    );

    // Autoloaded: the file defines `_ex`, and calling it by that name completes.
    let out = fixture.run(
        "zsh",
        &format!("{ZSH_STUBS}\nsource ./script\nBUFFER='ex i'\nCURSOR=4\n_ex\n"),
    );
    assert!(out.contains("display:install"), "autoload path: {out}");

    // And the name in the `#compdef` line is the one compinit will look for.
    let script_text = script("ex", Shell::Zsh);
    assert!(script_text.contains("#compdef ex"), "{script_text}");
    assert!(script_text.contains("_ex() {"), "{script_text}");
}