Skip to main content

shuck_semantic/
glob.rs

1use crate::{OptionValue, ShellBehaviorAt, ShellDialect};
2
3/// Whether unquoted expansion results are subject to field splitting.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum FieldSplittingBehavior {
6    /// Field splitting does not apply.
7    Never,
8    /// Field splitting applies only when the expansion is unquoted.
9    UnquotedOnly,
10    /// Runtime option state may select either behavior.
11    Ambiguous,
12}
13
14impl FieldSplittingBehavior {
15    /// Returns whether an unquoted expansion result may be split into fields.
16    pub fn unquoted_results_can_split(self) -> bool {
17        matches!(self, Self::UnquotedOnly | Self::Ambiguous)
18    }
19}
20
21/// Whether pathname expansion applies to literal globs and substitution results.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum PathnameExpansionBehavior {
24    /// Pathname expansion does not apply.
25    Disabled,
26    /// Literal glob characters are active, but substitution results are not globbed.
27    LiteralGlobsOnly,
28    /// Runtime option state may enable literal glob expansion, but substitution results stay plain.
29    LiteralGlobsOnlyOrDisabled,
30    /// Unquoted substitution results can also trigger pathname expansion.
31    SubstitutionResultsWhenUnquoted,
32    /// Runtime option state may select either behavior family.
33    Ambiguous,
34}
35
36impl PathnameExpansionBehavior {
37    /// Returns whether literal glob characters may trigger pathname expansion.
38    pub fn literal_globs_can_expand(self) -> bool {
39        !matches!(self, Self::Disabled)
40    }
41
42    /// Returns whether unquoted substitution results may be interpreted as globs.
43    pub fn unquoted_substitution_results_can_glob(self) -> bool {
44        matches!(
45            self,
46            Self::SubstitutionResultsWhenUnquoted | Self::Ambiguous
47        )
48    }
49}
50
51/// Whether zsh filename expansion runs before or after parameter expansion.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum FileExpansionOrderBehavior {
54    /// Filename expansion runs before parameter expansion.
55    BeforeParameterExpansion,
56    /// Filename expansion runs after parameter expansion.
57    AfterParameterExpansion,
58    /// Runtime option state may select either order.
59    Ambiguous,
60}
61
62/// How unmatched glob patterns behave.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum GlobFailureBehavior {
65    /// An unmatched glob produces an error.
66    ErrorOnNoMatch,
67    /// An unmatched glob stays literal in argv.
68    KeepLiteralOnNoMatch,
69    /// An unmatched glob is removed from argv.
70    DropUnmatchedPattern,
71    /// Unmatched globs are removed unless every glob in the command misses.
72    CshNullGlob,
73    /// Runtime option state may select either behavior.
74    Ambiguous,
75}
76
77/// Whether glob patterns match dot-prefixed path segments without an explicit dot.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum GlobDotBehavior {
80    /// Dot-prefixed names require an explicit dot in the pattern.
81    ExplicitDotRequired,
82    /// Dot-prefixed names may be matched by ordinary glob metacharacters.
83    DotfilesIncluded,
84    /// Runtime option state may select either behavior.
85    Ambiguous,
86}
87
88/// Whether a zsh pattern-operator family is active.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum PatternOperatorBehavior {
91    /// The operator family is disabled.
92    Disabled,
93    /// The operator family is enabled.
94    Enabled,
95    /// Runtime option state may select either behavior.
96    Ambiguous,
97}
98
99/// Whether zsh brace character-class expansion is active.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum BraceCharacterClassBehavior {
102    /// Brace character classes are not expanded.
103    Disabled,
104    /// Brace character classes are expanded.
105    Enabled,
106    /// Runtime option state may select either behavior.
107    Ambiguous,
108}
109
110impl BraceCharacterClassBehavior {
111    /// Returns whether a brace character class may expand at runtime.
112    pub fn can_expand(self) -> bool {
113        matches!(self, Self::Enabled | Self::Ambiguous)
114    }
115}
116
117/// Option-sensitive pattern operators available to glob and shell-pattern facts.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct GlobPatternBehavior {
120    extended_glob: PatternOperatorBehavior,
121    ksh_glob: PatternOperatorBehavior,
122    sh_glob: PatternOperatorBehavior,
123}
124
125impl GlobPatternBehavior {
126    /// Creates a pattern behavior from its operator-family states.
127    pub const fn from_parts(
128        extended_glob: PatternOperatorBehavior,
129        ksh_glob: PatternOperatorBehavior,
130        sh_glob: PatternOperatorBehavior,
131    ) -> Self {
132        Self {
133            extended_glob,
134            ksh_glob,
135            sh_glob,
136        }
137    }
138
139    /// Returns whether zsh extended glob operators such as `#`, `~`, and `^` are active.
140    pub fn extended_glob(self) -> PatternOperatorBehavior {
141        self.extended_glob
142    }
143
144    /// Returns whether ksh-style glob operators such as `@(...)` are active.
145    pub fn ksh_glob(self) -> PatternOperatorBehavior {
146        self.ksh_glob
147    }
148
149    /// Returns whether sh-compatible glob parsing is active.
150    pub fn sh_glob(self) -> PatternOperatorBehavior {
151        self.sh_glob
152    }
153}
154
155impl ShellBehaviorAt<'_> {
156    /// Returns the field-splitting behavior implied by the shell and runtime option state.
157    pub fn field_splitting(&self) -> FieldSplittingBehavior {
158        if self.shell != ShellDialect::Zsh {
159            return FieldSplittingBehavior::UnquotedOnly;
160        }
161
162        match self
163            .effective_zsh_options()
164            .map(|options| options.sh_word_split)
165        {
166            Some(OptionValue::Off) => FieldSplittingBehavior::Never,
167            Some(OptionValue::Unknown) => FieldSplittingBehavior::Ambiguous,
168            Some(OptionValue::On) | None => FieldSplittingBehavior::UnquotedOnly,
169        }
170    }
171
172    /// Returns the pathname-expansion behavior implied by the shell and runtime option state.
173    pub fn pathname_expansion(&self) -> PathnameExpansionBehavior {
174        if self.shell != ShellDialect::Zsh {
175            return PathnameExpansionBehavior::SubstitutionResultsWhenUnquoted;
176        }
177
178        let Some(options) = self.effective_zsh_options() else {
179            return PathnameExpansionBehavior::LiteralGlobsOnly;
180        };
181
182        match options.glob {
183            OptionValue::Off => PathnameExpansionBehavior::Disabled,
184            OptionValue::Unknown => match options.glob_subst {
185                OptionValue::Off => PathnameExpansionBehavior::LiteralGlobsOnlyOrDisabled,
186                OptionValue::On | OptionValue::Unknown => PathnameExpansionBehavior::Ambiguous,
187            },
188            OptionValue::On => match options.glob_subst {
189                OptionValue::Off => PathnameExpansionBehavior::LiteralGlobsOnly,
190                OptionValue::On => PathnameExpansionBehavior::SubstitutionResultsWhenUnquoted,
191                OptionValue::Unknown => PathnameExpansionBehavior::Ambiguous,
192            },
193        }
194    }
195
196    /// Returns the filename-expansion order implied by the shell and runtime option state.
197    pub fn file_expansion_order(&self) -> FileExpansionOrderBehavior {
198        if self.shell != ShellDialect::Zsh {
199            return FileExpansionOrderBehavior::BeforeParameterExpansion;
200        }
201
202        match self
203            .effective_zsh_options()
204            .map(|options| options.sh_file_expansion)
205        {
206            Some(OptionValue::On) => FileExpansionOrderBehavior::BeforeParameterExpansion,
207            Some(OptionValue::Unknown) => FileExpansionOrderBehavior::Ambiguous,
208            Some(OptionValue::Off) | None => FileExpansionOrderBehavior::AfterParameterExpansion,
209        }
210    }
211
212    /// Returns the unmatched-glob behavior implied by the shell and runtime option state.
213    pub fn glob_failure(&self) -> GlobFailureBehavior {
214        if self.shell != ShellDialect::Zsh {
215            return GlobFailureBehavior::KeepLiteralOnNoMatch;
216        }
217
218        let Some(options) = self.effective_zsh_options() else {
219            return GlobFailureBehavior::ErrorOnNoMatch;
220        };
221
222        match options.glob {
223            OptionValue::Off => GlobFailureBehavior::KeepLiteralOnNoMatch,
224            OptionValue::Unknown => GlobFailureBehavior::Ambiguous,
225            OptionValue::On => match options.csh_null_glob {
226                OptionValue::On => GlobFailureBehavior::CshNullGlob,
227                OptionValue::Unknown => GlobFailureBehavior::Ambiguous,
228                OptionValue::Off => match options.null_glob {
229                    OptionValue::On => GlobFailureBehavior::DropUnmatchedPattern,
230                    OptionValue::Unknown => GlobFailureBehavior::Ambiguous,
231                    OptionValue::Off => match options.nomatch {
232                        OptionValue::On => GlobFailureBehavior::ErrorOnNoMatch,
233                        OptionValue::Off => GlobFailureBehavior::KeepLiteralOnNoMatch,
234                        OptionValue::Unknown => GlobFailureBehavior::Ambiguous,
235                    },
236                },
237            },
238        }
239    }
240
241    /// Returns how glob patterns treat dot-prefixed path segments.
242    pub fn glob_dot_matching(&self) -> GlobDotBehavior {
243        if self.shell != ShellDialect::Zsh {
244            return GlobDotBehavior::ExplicitDotRequired;
245        }
246
247        match self
248            .effective_zsh_options()
249            .map(|options| options.glob_dots)
250        {
251            Some(OptionValue::On) => GlobDotBehavior::DotfilesIncluded,
252            Some(OptionValue::Unknown) => GlobDotBehavior::Ambiguous,
253            Some(OptionValue::Off) | None => GlobDotBehavior::ExplicitDotRequired,
254        }
255    }
256
257    /// Returns the option-sensitive glob/pattern operator families available at this offset.
258    pub fn glob_pattern(&self) -> GlobPatternBehavior {
259        if self.shell != ShellDialect::Zsh {
260            return GlobPatternBehavior::from_parts(
261                PatternOperatorBehavior::Disabled,
262                PatternOperatorBehavior::Disabled,
263                PatternOperatorBehavior::Disabled,
264            );
265        }
266
267        let Some(options) = self.effective_zsh_options() else {
268            return GlobPatternBehavior::from_parts(
269                PatternOperatorBehavior::Disabled,
270                PatternOperatorBehavior::Disabled,
271                PatternOperatorBehavior::Disabled,
272            );
273        };
274
275        GlobPatternBehavior::from_parts(
276            pattern_operator_behavior(options.extended_glob),
277            pattern_operator_behavior(options.ksh_glob),
278            pattern_operator_behavior(options.sh_glob),
279        )
280    }
281
282    /// Returns whether zsh brace character classes such as `{abc}` are active.
283    pub fn brace_character_classes(&self) -> BraceCharacterClassBehavior {
284        if self.shell != ShellDialect::Zsh {
285            return BraceCharacterClassBehavior::Disabled;
286        }
287
288        match self
289            .effective_zsh_options()
290            .map(|options| options.brace_ccl)
291        {
292            Some(OptionValue::On) => BraceCharacterClassBehavior::Enabled,
293            Some(OptionValue::Unknown) => BraceCharacterClassBehavior::Ambiguous,
294            Some(OptionValue::Off) | None => BraceCharacterClassBehavior::Disabled,
295        }
296    }
297}
298
299fn pattern_operator_behavior(value: OptionValue) -> PatternOperatorBehavior {
300    match value {
301        OptionValue::Off => PatternOperatorBehavior::Disabled,
302        OptionValue::On => PatternOperatorBehavior::Enabled,
303        OptionValue::Unknown => PatternOperatorBehavior::Ambiguous,
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use crate::{SemanticBuildOptions, SemanticModel, ShellProfile};
311    use shuck_indexer::Indexer;
312    use shuck_parser::parser::Parser;
313
314    fn model_with_profile(source: &str, profile: ShellProfile) -> SemanticModel {
315        let output = Parser::with_profile(source, profile.clone())
316            .parse()
317            .unwrap();
318        let indexer = Indexer::new(source, &output);
319        SemanticModel::build_with_options(
320            &output.file,
321            source,
322            &indexer,
323            SemanticBuildOptions {
324                shell_profile: Some(profile),
325                ..SemanticBuildOptions::default()
326            },
327        )
328    }
329
330    #[test]
331    fn tracks_field_splitting_by_offset() {
332        for (source, expected) in [
333            ("print $name\n", FieldSplittingBehavior::Never),
334            (
335                "setopt sh_word_split\nprint $name\n",
336                FieldSplittingBehavior::UnquotedOnly,
337            ),
338            (
339                "opt=sh_word_split\nsetopt \"$opt\"\nprint $name\n",
340                FieldSplittingBehavior::Ambiguous,
341            ),
342        ] {
343            let model = model_with_profile(source, ShellProfile::native(ShellDialect::Zsh));
344            let offset = source.find("print").expect("expected print offset");
345
346            assert_eq!(
347                model.shell_behavior_at(offset).field_splitting(),
348                expected,
349                "{source}"
350            );
351        }
352    }
353
354    #[test]
355    fn uses_non_zsh_default_behaviors() {
356        let source = "printf '%s\\n' $name *.txt\n";
357        let model = model_with_profile(source, ShellProfile::native(ShellDialect::Bash));
358        let offset = source.find("printf").expect("expected printf offset");
359        let behavior = model.shell_behavior_at(offset);
360
361        assert_eq!(
362            behavior.field_splitting(),
363            FieldSplittingBehavior::UnquotedOnly
364        );
365        assert_eq!(
366            behavior.pathname_expansion(),
367            PathnameExpansionBehavior::SubstitutionResultsWhenUnquoted
368        );
369        assert_eq!(
370            behavior.glob_failure(),
371            GlobFailureBehavior::KeepLiteralOnNoMatch
372        );
373        assert_eq!(
374            behavior.glob_dot_matching(),
375            GlobDotBehavior::ExplicitDotRequired
376        );
377        let pattern = behavior.glob_pattern();
378        assert_eq!(pattern.extended_glob(), PatternOperatorBehavior::Disabled);
379        assert_eq!(pattern.ksh_glob(), PatternOperatorBehavior::Disabled);
380        assert_eq!(pattern.sh_glob(), PatternOperatorBehavior::Disabled);
381        assert_eq!(
382            behavior.brace_character_classes(),
383            BraceCharacterClassBehavior::Disabled
384        );
385    }
386
387    #[test]
388    fn tracks_brace_character_classes_by_offset() {
389        for (source, expected) in [
390            ("print {app}\n", BraceCharacterClassBehavior::Disabled),
391            (
392                "setopt brace_ccl\nprint {app}\n",
393                BraceCharacterClassBehavior::Enabled,
394            ),
395            (
396                "setopt brace_ccl\nunsetopt brace_ccl\nprint {app}\n",
397                BraceCharacterClassBehavior::Disabled,
398            ),
399            (
400                "opt=brace_ccl\nsetopt \"$opt\"\nprint {app}\n",
401                BraceCharacterClassBehavior::Ambiguous,
402            ),
403        ] {
404            let model = model_with_profile(source, ShellProfile::native(ShellDialect::Zsh));
405            let offset = source.find("print").expect("expected print offset");
406
407            assert_eq!(
408                model.shell_behavior_at(offset).brace_character_classes(),
409                expected,
410                "{source}"
411            );
412        }
413    }
414
415    #[test]
416    fn tracks_pathname_expansion_by_offset() {
417        for (source, expected) in [
418            ("print $name\n", PathnameExpansionBehavior::LiteralGlobsOnly),
419            (
420                "setopt glob_subst\nprint $name\n",
421                PathnameExpansionBehavior::SubstitutionResultsWhenUnquoted,
422            ),
423            (
424                "setopt no_glob\nprint $name\n",
425                PathnameExpansionBehavior::Disabled,
426            ),
427            (
428                "opt=glob_subst\nsetopt \"$opt\"\nprint $name\n",
429                PathnameExpansionBehavior::Ambiguous,
430            ),
431            (
432                "if cond; then setopt no_glob; fi\nprint $name\n",
433                PathnameExpansionBehavior::LiteralGlobsOnlyOrDisabled,
434            ),
435        ] {
436            let model = model_with_profile(source, ShellProfile::native(ShellDialect::Zsh));
437            let offset = source.find("print").expect("expected print offset");
438
439            assert_eq!(
440                model.shell_behavior_at(offset).pathname_expansion(),
441                expected,
442                "{source}"
443            );
444        }
445    }
446
447    #[test]
448    fn pathname_expansion_respects_no_glob_precedence() {
449        for (source, expected) in [
450            (
451                "setopt no_glob glob_subst\nprint $name\n",
452                PathnameExpansionBehavior::Disabled,
453            ),
454            (
455                "setopt glob_subst\nprint $name\n",
456                PathnameExpansionBehavior::SubstitutionResultsWhenUnquoted,
457            ),
458            (
459                "unsetopt glob_subst\nprint $name\n",
460                PathnameExpansionBehavior::LiteralGlobsOnly,
461            ),
462        ] {
463            let model = model_with_profile(source, ShellProfile::native(ShellDialect::Zsh));
464            let offset = source.find("print").expect("expected print offset");
465
466            assert_eq!(
467                model.shell_behavior_at(offset).pathname_expansion(),
468                expected,
469                "{source}"
470            );
471        }
472    }
473
474    #[test]
475    fn file_expansion_order_tracks_sh_file_expansion_by_offset() {
476        let source = "\
477print ~$USER
478setopt sh_file_expansion
479print ~$USER
480unsetopt sh_file_expansion
481print ~$USER
482opt=sh_file_expansion
483setopt \"$opt\"
484print ~$USER
485";
486        let model = model_with_profile(source, ShellProfile::native(ShellDialect::Zsh));
487
488        let first = source.find("print ~$USER").unwrap();
489        let enabled = source.find("print ~$USER\nunsetopt").unwrap();
490        let disabled = source.find("print ~$USER\nopt=").unwrap();
491        let ambiguous = source.rfind("print ~$USER").unwrap();
492
493        assert_eq!(
494            model.shell_behavior_at(first).file_expansion_order(),
495            FileExpansionOrderBehavior::AfterParameterExpansion,
496        );
497        assert_eq!(
498            model.shell_behavior_at(enabled).file_expansion_order(),
499            FileExpansionOrderBehavior::BeforeParameterExpansion,
500        );
501        assert_eq!(
502            model.shell_behavior_at(disabled).file_expansion_order(),
503            FileExpansionOrderBehavior::AfterParameterExpansion,
504        );
505        assert_eq!(
506            model.shell_behavior_at(ambiguous).file_expansion_order(),
507            FileExpansionOrderBehavior::Ambiguous,
508        );
509    }
510
511    #[test]
512    fn pathname_expansion_keeps_glob_subst_off_when_glob_is_flow_merged() {
513        let source = "\
514if cond; then
515  setopt no_glob
516fi
517print $name
518";
519        let model = model_with_profile(source, ShellProfile::native(ShellDialect::Zsh));
520        let offset = source.find("print").expect("expected print offset");
521        let behavior = model.shell_behavior_at(offset).pathname_expansion();
522
523        assert_eq!(
524            behavior,
525            PathnameExpansionBehavior::LiteralGlobsOnlyOrDisabled
526        );
527        assert!(behavior.literal_globs_can_expand());
528        assert!(!behavior.unquoted_substitution_results_can_glob());
529    }
530
531    #[test]
532    fn tracks_function_leaked_option_effects_by_offset() {
533        let source = "\
534fn() {
535  setopt sh_word_split glob_subst
536}
537fn
538print $name
539";
540        let model = model_with_profile(source, ShellProfile::native(ShellDialect::Zsh));
541        let offset = source.rfind("print").expect("expected print offset");
542
543        assert_eq!(
544            model.shell_behavior_at(offset).field_splitting(),
545            FieldSplittingBehavior::UnquotedOnly
546        );
547        assert_eq!(
548            model.shell_behavior_at(offset).pathname_expansion(),
549            PathnameExpansionBehavior::SubstitutionResultsWhenUnquoted
550        );
551        assert_eq!(
552            model.shell_behavior_at(offset).glob_failure(),
553            GlobFailureBehavior::ErrorOnNoMatch
554        );
555    }
556
557    #[test]
558    fn tracks_glob_failure_modes_by_offset() {
559        for (source, expected) in [
560            ("print *\n", GlobFailureBehavior::ErrorOnNoMatch),
561            (
562                "setopt no_nomatch\nprint *\n",
563                GlobFailureBehavior::KeepLiteralOnNoMatch,
564            ),
565            (
566                "setopt null_glob\nprint *\n",
567                GlobFailureBehavior::DropUnmatchedPattern,
568            ),
569            (
570                "setopt csh_null_glob\nprint *\n",
571                GlobFailureBehavior::CshNullGlob,
572            ),
573            (
574                "opt=no_nomatch\nsetopt \"$opt\"\nprint *\n",
575                GlobFailureBehavior::Ambiguous,
576            ),
577        ] {
578            let model = model_with_profile(source, ShellProfile::native(ShellDialect::Zsh));
579            let offset = source.find("print").expect("expected print offset");
580
581            assert_eq!(
582                model.shell_behavior_at(offset).glob_failure(),
583                expected,
584                "{source}"
585            );
586        }
587    }
588
589    #[test]
590    fn glob_failure_respects_option_precedence() {
591        for (source, expected) in [
592            (
593                "setopt no_glob null_glob csh_null_glob\nprint *\n",
594                GlobFailureBehavior::KeepLiteralOnNoMatch,
595            ),
596            (
597                "setopt null_glob csh_null_glob\nprint *\n",
598                GlobFailureBehavior::CshNullGlob,
599            ),
600            (
601                "setopt null_glob\nunsetopt csh_null_glob\nprint *\n",
602                GlobFailureBehavior::DropUnmatchedPattern,
603            ),
604            (
605                "setopt no_nomatch null_glob csh_null_glob\nprint *\n",
606                GlobFailureBehavior::CshNullGlob,
607            ),
608        ] {
609            let model = model_with_profile(source, ShellProfile::native(ShellDialect::Zsh));
610            let offset = source.find("print").expect("expected print offset");
611
612            assert_eq!(
613                model.shell_behavior_at(offset).glob_failure(),
614                expected,
615                "{source}"
616            );
617        }
618    }
619
620    #[test]
621    fn tracks_glob_dot_matching_by_offset() {
622        for (source, expected) in [
623            ("print *\n", GlobDotBehavior::ExplicitDotRequired),
624            (
625                "setopt glob_dots\nprint *\n",
626                GlobDotBehavior::DotfilesIncluded,
627            ),
628            (
629                "setopt glob_dots\nunsetopt glob_dots\nprint *\n",
630                GlobDotBehavior::ExplicitDotRequired,
631            ),
632            (
633                "opt=glob_dots\nsetopt \"$opt\"\nprint *\n",
634                GlobDotBehavior::Ambiguous,
635            ),
636        ] {
637            let model = model_with_profile(source, ShellProfile::native(ShellDialect::Zsh));
638            let offset = source.find("print").expect("expected print offset");
639
640            assert_eq!(
641                model.shell_behavior_at(offset).glob_dot_matching(),
642                expected,
643                "{source}"
644            );
645        }
646    }
647
648    #[test]
649    fn tracks_glob_pattern_operator_families_by_offset() {
650        let source = "\
651print *
652setopt extended_glob ksh_glob sh_glob
653print *
654unsetopt extended_glob
655print *
656unsetopt ksh_glob
657print *
658unsetopt sh_glob
659print *
660opt=extended_glob
661unsetopt \"$opt\"
662print *
663";
664        let model = model_with_profile(source, ShellProfile::native(ShellDialect::Zsh));
665        let mut print_offsets = source.match_indices("print").map(|(offset, _)| offset);
666
667        let native = model
668            .shell_behavior_at(print_offsets.next().expect("expected native print"))
669            .glob_pattern();
670        assert_eq!(native.extended_glob(), PatternOperatorBehavior::Disabled);
671        assert_eq!(native.ksh_glob(), PatternOperatorBehavior::Disabled);
672        assert_eq!(native.sh_glob(), PatternOperatorBehavior::Disabled);
673
674        let enabled = model
675            .shell_behavior_at(print_offsets.next().expect("expected enabled print"))
676            .glob_pattern();
677        assert_eq!(enabled.extended_glob(), PatternOperatorBehavior::Enabled);
678        assert_eq!(enabled.ksh_glob(), PatternOperatorBehavior::Enabled);
679        assert_eq!(enabled.sh_glob(), PatternOperatorBehavior::Enabled);
680
681        let only_ksh_and_sh = model
682            .shell_behavior_at(print_offsets.next().expect("expected partial print"))
683            .glob_pattern();
684        assert_eq!(
685            only_ksh_and_sh.extended_glob(),
686            PatternOperatorBehavior::Disabled
687        );
688        assert_eq!(only_ksh_and_sh.ksh_glob(), PatternOperatorBehavior::Enabled);
689        assert_eq!(only_ksh_and_sh.sh_glob(), PatternOperatorBehavior::Enabled);
690
691        let only_sh = model
692            .shell_behavior_at(print_offsets.next().expect("expected sh print"))
693            .glob_pattern();
694        assert_eq!(only_sh.extended_glob(), PatternOperatorBehavior::Disabled);
695        assert_eq!(only_sh.ksh_glob(), PatternOperatorBehavior::Disabled);
696        assert_eq!(only_sh.sh_glob(), PatternOperatorBehavior::Enabled);
697
698        let disabled_again = model
699            .shell_behavior_at(print_offsets.next().expect("expected disabled print"))
700            .glob_pattern();
701        assert_eq!(
702            disabled_again.extended_glob(),
703            PatternOperatorBehavior::Disabled
704        );
705        assert_eq!(disabled_again.ksh_glob(), PatternOperatorBehavior::Disabled);
706        assert_eq!(disabled_again.sh_glob(), PatternOperatorBehavior::Disabled);
707
708        let ambiguous = model
709            .shell_behavior_at(print_offsets.next().expect("expected ambiguous print"))
710            .glob_pattern();
711        assert_eq!(
712            ambiguous.extended_glob(),
713            PatternOperatorBehavior::Ambiguous
714        );
715        assert_eq!(ambiguous.ksh_glob(), PatternOperatorBehavior::Ambiguous);
716        assert_eq!(ambiguous.sh_glob(), PatternOperatorBehavior::Ambiguous);
717    }
718}