Skip to main content

yash_builtin/typeset/
print_variables.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2023 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17use super::*;
18use std::fmt::Write;
19use yash_env::variable::{Value, VariableSet};
20
21impl PrintVariables {
22    /// Executes the command.
23    pub fn execute(
24        self,
25        variables: &VariableSet,
26        context: &PrintContext,
27    ) -> Result<String, Vec<ExecuteError>> {
28        let mut output = String::new();
29        let mut errors = Vec::new();
30
31        if self.variables.is_empty() {
32            let mut variables = variables.iter(self.scope.into()).collect::<Vec<_>>();
33            // TODO Honor the collation order in the locale.
34            variables.sort_unstable_by_key(|&(name, _)| name);
35            for (name, var) in variables {
36                print_one(name, var, &self.attrs, context, &mut output);
37            }
38        } else {
39            for name in self.variables {
40                match variables.get_scoped(&name.value, self.scope.into()) {
41                    Some(var) => print_one(&name.value, var, &self.attrs, context, &mut output),
42                    None => errors.push(ExecuteError::PrintUnsetVariable(name)),
43                }
44            }
45        }
46
47        if errors.is_empty() {
48            Ok(output)
49        } else {
50            Err(errors)
51        }
52    }
53}
54
55/// Formats a variable for printing.
56fn print_one(
57    name: &str,
58    var: &Variable,
59    filter_attrs: &[(VariableAttr, State)],
60    context: &PrintContext,
61    output: &mut String,
62) {
63    // Skip if the variable name contains `=`.
64    // Such variables should not be printed since the output would be
65    // re-interpreted as a variable with a different name.
66    if name.contains('=') {
67        return;
68    }
69
70    // Skip if the variable does not match the filter.
71    if filter_attrs
72        .iter()
73        .any(|&(attr, state)| attr.test(var) != state)
74    {
75        return;
76    }
77
78    // Do the formatting.
79    let options = AttributeOption {
80        var,
81        options_allowed: context.options_allowed,
82    };
83    let separator = if name.starts_with('-') { "-- " } else { "" };
84    let quoted_name = yash_quote::quoted(name);
85    match &var.value {
86        Some(value @ Value::Scalar(_)) => writeln!(
87            output,
88            "{} {}{}{}={}",
89            context.builtin_name,
90            options,
91            separator,
92            quoted_name,
93            value.quote()
94        )
95        .unwrap(),
96
97        Some(value @ Value::Array(_)) => {
98            writeln!(output, "{}={}", quoted_name, value.quote()).unwrap();
99
100            let options = options.to_string();
101            if !options.is_empty() || context.builtin_is_significant {
102                writeln!(
103                    output,
104                    "{} {}{}{}",
105                    context.builtin_name, options, separator, quoted_name
106                )
107                .unwrap();
108            }
109        }
110
111        None => writeln!(
112            output,
113            "{} {}{}{}",
114            context.builtin_name, options, separator, quoted_name
115        )
116        .unwrap(),
117    }
118}
119
120/// `Display` implementor for printing command line options that reproduce the
121/// variable attributes.
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123struct AttributeOption<'a> {
124    /// Variable to be printed
125    var: &'a Variable,
126    /// Options that are printed if they are set
127    options_allowed: &'a [OptionSpec<'a>],
128}
129
130impl std::fmt::Display for AttributeOption<'_> {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        for option in self.options_allowed {
133            if let Some(attr) = option.attr {
134                if let Ok(attr) = VariableAttr::try_from(attr) {
135                    if attr.test(self.var).into() {
136                        write!(f, "-{} ", option.short)?;
137                    }
138                }
139            }
140        }
141        Ok(())
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use yash_env::option::{Off, On};
149    use yash_env::variable::Context;
150
151    #[test]
152    fn printing_one_variable() {
153        let mut vars = VariableSet::new();
154        vars.get_or_new("foo", Scope::Global.into())
155            .assign("value", None)
156            .unwrap();
157        let pv = PrintVariables {
158            variables: Field::dummies(["foo"]),
159            attrs: vec![],
160            scope: Scope::Global,
161        };
162
163        let output = pv.execute(&vars, &PRINT_CONTEXT).unwrap();
164        assert_eq!(output, "typeset foo=value\n")
165    }
166
167    #[test]
168    fn printing_multiple_variables() {
169        let mut vars = VariableSet::new();
170        vars.get_or_new("first", Scope::Global.into())
171            .assign("1", None)
172            .unwrap();
173        vars.get_or_new("second", Scope::Global.into())
174            .assign("2", None)
175            .unwrap();
176        vars.get_or_new("third", Scope::Global.into())
177            .assign("3", None)
178            .unwrap();
179        let pv = PrintVariables {
180            variables: Field::dummies(["first", "second", "third"]),
181            attrs: vec![],
182            scope: Scope::Global,
183        };
184
185        assert_eq!(
186            pv.execute(&vars, &PRINT_CONTEXT).unwrap(),
187            "typeset first=1\n\
188             typeset second=2\n\
189             typeset third=3\n",
190        );
191    }
192
193    #[test]
194    fn printing_array_variable() {
195        let mut vars = VariableSet::new();
196        vars.get_or_new("a", Scope::Global.into())
197            .assign(Value::array(["1", "2  2", "3"]), None)
198            .unwrap();
199        let pv = PrintVariables {
200            variables: Field::dummies(["a"]),
201            attrs: vec![],
202            scope: Scope::Global,
203        };
204
205        let result = pv.execute(&vars, &PRINT_CONTEXT).unwrap();
206        assert_eq!(result, "a=(1 '2  2' 3)\n");
207    }
208
209    #[test]
210    fn printing_valueless_variable() {
211        let mut vars = VariableSet::new();
212        vars.get_or_new("x", Scope::Global.into());
213        let pv = PrintVariables {
214            variables: Field::dummies(["x"]),
215            attrs: vec![],
216            scope: Scope::Global,
217        };
218
219        let result = pv.execute(&vars, &PRINT_CONTEXT).unwrap();
220        assert_eq!(result, "typeset x\n");
221    }
222
223    #[test]
224    fn quoting_variable_names_and_values() {
225        let mut vars = VariableSet::new();
226        vars.get_or_new("valueless$", Scope::Global.into());
227        vars.get_or_new("scalar$", Scope::Global.into())
228            .assign("=;", None)
229            .unwrap();
230        vars.get_or_new("array$", Scope::Global.into())
231            .assign(Value::array(["~", "'", "*?"]), None)
232            .unwrap();
233        let pv = PrintVariables {
234            variables: Field::dummies(["valueless$", "scalar$", "array$"]),
235            attrs: vec![],
236            scope: Scope::Global,
237        };
238
239        assert_eq!(
240            pv.execute(&vars, &PRINT_CONTEXT).unwrap(),
241            "typeset 'valueless$'\n\
242             typeset 'scalar$'='=;'\n\
243             'array$'=('~' \"'\" '*?')\n",
244        );
245    }
246
247    #[test]
248    fn printing_global_and_local_variables_at_once() {
249        let mut outer = VariableSet::new();
250        outer
251            .get_or_new("global", Scope::Global.into())
252            .assign("global value", None)
253            .unwrap();
254        let mut inner = outer.push_context(Context::default());
255        inner
256            .get_or_new("local", Scope::Local.into())
257            .assign("local value", None)
258            .unwrap();
259        let pv = PrintVariables {
260            variables: Field::dummies(["global", "local"]),
261            attrs: vec![],
262            scope: Scope::Global,
263        };
264
265        assert_eq!(
266            pv.execute(&inner, &PRINT_CONTEXT).unwrap(),
267            "typeset global='global value'\n\
268             typeset local='local value'\n",
269        );
270    }
271
272    #[test]
273    fn printing_local_variables_only() {
274        let mut outer = VariableSet::new();
275        outer
276            .get_or_new("global", Scope::Global.into())
277            .assign("global value", None)
278            .unwrap();
279        let mut inner = outer.push_context(Context::default());
280        inner
281            .get_or_new("local", Scope::Local.into())
282            .assign("local value", None)
283            .unwrap();
284
285        let pv = PrintVariables {
286            variables: Field::dummies(["local"]),
287            attrs: vec![],
288            scope: Scope::Local,
289        };
290        let output = pv.execute(&inner, &PRINT_CONTEXT).unwrap();
291        assert_eq!(output, "typeset local='local value'\n");
292
293        let pv = PrintVariables {
294            variables: Field::dummies(["global"]),
295            attrs: vec![],
296            scope: Scope::Local,
297        };
298        assert_eq!(
299            pv.execute(&inner, &PRINT_CONTEXT).unwrap_err(),
300            [ExecuteError::PrintUnsetVariable(Field::dummy("global"))]
301        );
302    }
303
304    #[test]
305    fn printing_all_global_and_local_variables() {
306        let mut outer = VariableSet::new();
307        outer
308            .get_or_new("one", Scope::Global.into())
309            .assign("1", None)
310            .unwrap();
311        let mut inner = outer.push_context(Context::default());
312        inner
313            .get_or_new("two", Scope::Local.into())
314            .assign("2", None)
315            .unwrap();
316        inner
317            .get_or_new("three", Scope::Local.into())
318            .assign("3", None)
319            .unwrap();
320        let pv = PrintVariables {
321            variables: vec![],
322            attrs: vec![],
323            scope: Scope::Global,
324        };
325
326        assert_eq!(
327            pv.execute(&inner, &PRINT_CONTEXT).unwrap(),
328            // sorted by name
329            "typeset one=1\n\
330             typeset three=3\n\
331             typeset two=2\n",
332        );
333    }
334
335    #[test]
336    fn printing_all_local_variables() {
337        let mut outer = VariableSet::new();
338        outer
339            .get_or_new("one", Scope::Global.into())
340            .assign("1", None)
341            .unwrap();
342        let mut inner = outer.push_context(Context::default());
343        inner
344            .get_or_new("two", Scope::Local.into())
345            .assign("2", None)
346            .unwrap();
347        inner
348            .get_or_new("three", Scope::Local.into())
349            .assign("3", None)
350            .unwrap();
351        let pv = PrintVariables {
352            variables: vec![],
353            attrs: vec![],
354            scope: Scope::Local,
355        };
356
357        assert_eq!(
358            pv.execute(&inner, &PRINT_CONTEXT).unwrap(),
359            // sorted by name
360            "typeset three=3\n\
361             typeset two=2\n",
362        );
363    }
364
365    #[test]
366    fn printing_attributes_of_valueless_variables() {
367        let mut vars = VariableSet::new();
368        let mut x = vars.get_or_new("x", Scope::Global.into());
369        x.export(true);
370        let mut y = vars.get_or_new("y", Scope::Global.into());
371        y.make_read_only(Location::dummy("y location"));
372        let mut z = vars.get_or_new("z", Scope::Global.into());
373        z.export(true);
374        z.make_read_only(Location::dummy("z location"));
375        let pv = PrintVariables {
376            variables: Field::dummies(["x", "y", "z"]),
377            attrs: vec![],
378            scope: Scope::Global,
379        };
380
381        assert_eq!(
382            pv.execute(&vars, &PRINT_CONTEXT).unwrap(),
383            "typeset -x x\n\
384             typeset -r y\n\
385             typeset -r -x z\n",
386        );
387    }
388
389    #[test]
390    fn printing_attributes_of_scalar_variables() {
391        let mut vars = VariableSet::new();
392        let mut x = vars.get_or_new("x", Scope::Global.into());
393        x.assign("X", None).unwrap();
394        x.export(true);
395        let mut y = vars.get_or_new("y", Scope::Global.into());
396        y.assign("Y", None).unwrap();
397        y.make_read_only(Location::dummy("y location"));
398        let mut z = vars.get_or_new("z", Scope::Global.into());
399        z.assign("Z", None).unwrap();
400        z.export(true);
401        z.make_read_only(Location::dummy("z location"));
402        let pv = PrintVariables {
403            variables: Field::dummies(["x", "y", "z"]),
404            attrs: vec![],
405            scope: Scope::Global,
406        };
407
408        assert_eq!(
409            pv.execute(&vars, &PRINT_CONTEXT).unwrap(),
410            "typeset -x x=X\n\
411             typeset -r y=Y\n\
412             typeset -r -x z=Z\n",
413        );
414    }
415
416    #[test]
417    fn printing_attributes_of_array_variables() {
418        let mut vars = VariableSet::new();
419        let mut x = vars.get_or_new("x", Scope::Global.into());
420        x.assign(Value::array(["X"]), None).unwrap();
421        x.export(true);
422        let mut y = vars.get_or_new("y", Scope::Global.into());
423        y.assign(Value::array(["Y"]), None).unwrap();
424        y.make_read_only(Location::dummy("y location"));
425        let mut z = vars.get_or_new("z", Scope::Global.into());
426        z.assign(Value::array(["Z"]), None).unwrap();
427        z.export(true);
428        z.make_read_only(Location::dummy("z location"));
429        let pv = PrintVariables {
430            variables: Field::dummies(["x", "y", "z"]),
431            attrs: vec![],
432            scope: Scope::Global,
433        };
434
435        assert_eq!(
436            pv.execute(&vars, &PRINT_CONTEXT).unwrap(),
437            "x=(X)\n\
438             typeset -x x\n\
439             y=(Y)\n\
440             typeset -r y\n\
441             z=(Z)\n\
442             typeset -r -x z\n",
443        );
444    }
445
446    #[test]
447    fn printing_valueless_variable_name_starting_with_hyphen() {
448        let mut vars = VariableSet::new();
449        let mut var = vars.get_or_new("-v", Scope::Global.into());
450        var.export(true);
451        let pv = PrintVariables {
452            variables: Field::dummies(["-v"]),
453            attrs: vec![],
454            scope: Scope::Global,
455        };
456
457        let output = pv.execute(&vars, &PRINT_CONTEXT).unwrap();
458        assert_eq!(output, "typeset -x -- -v\n");
459    }
460
461    #[test]
462    fn printing_scalar_variable_name_starting_with_hyphen() {
463        let mut vars = VariableSet::new();
464        let mut var = vars.get_or_new("-v", Scope::Global.into());
465        var.assign("value", None).unwrap();
466        var.export(true);
467        let pv = PrintVariables {
468            variables: Field::dummies(["-v"]),
469            attrs: vec![],
470            scope: Scope::Global,
471        };
472
473        let output = pv.execute(&vars, &PRINT_CONTEXT).unwrap();
474        assert_eq!(output, "typeset -x -- -v=value\n");
475    }
476
477    #[test]
478    fn printing_array_variable_name_starting_with_hyphen() {
479        let mut vars = VariableSet::new();
480        let mut var = vars.get_or_new("-v", Scope::Global.into());
481        var.assign(Value::array(["1", "2"]), None).unwrap();
482        var.export(true);
483        let pv = PrintVariables {
484            variables: Field::dummies(["-v"]),
485            attrs: vec![],
486            scope: Scope::Global,
487        };
488
489        let output = pv.execute(&vars, &PRINT_CONTEXT).unwrap();
490        assert_eq!(output, "-v=(1 2)\ntypeset -x -- -v\n");
491    }
492
493    fn variables_with_different_attributes() -> VariableSet {
494        let mut vars = VariableSet::new();
495        let mut a = vars.get_or_new("a", Scope::Global.into());
496        a.export(true);
497        let mut b = vars.get_or_new("b", Scope::Global.into());
498        b.make_read_only(Location::dummy("b location"));
499        let mut c = vars.get_or_new("c", Scope::Global.into());
500        c.export(true);
501        c.make_read_only(Location::dummy("c location"));
502        vars.get_or_new("d", Scope::Global.into());
503        vars
504    }
505
506    #[test]
507    fn selecting_readonly_variables() {
508        let vars = variables_with_different_attributes();
509        let pv = PrintVariables {
510            variables: vec![],
511            attrs: vec![(VariableAttr::ReadOnly, On)],
512            scope: Scope::Global,
513        };
514
515        assert_eq!(
516            pv.execute(&vars, &PRINT_CONTEXT).unwrap(),
517            "typeset -r b\n\
518             typeset -r -x c\n",
519        );
520    }
521
522    #[test]
523    fn selecting_non_readonly_variables() {
524        let vars = variables_with_different_attributes();
525        let pv = PrintVariables {
526            variables: vec![],
527            attrs: vec![(VariableAttr::ReadOnly, Off)],
528            scope: Scope::Global,
529        };
530
531        assert_eq!(
532            pv.execute(&vars, &PRINT_CONTEXT).unwrap(),
533            "typeset -x a\n\
534             typeset d\n",
535        );
536    }
537
538    #[test]
539    fn selecting_exported_variables() {
540        let vars = variables_with_different_attributes();
541        let pv = PrintVariables {
542            variables: vec![],
543            attrs: vec![(VariableAttr::Export, On)],
544            scope: Scope::Global,
545        };
546
547        assert_eq!(
548            pv.execute(&vars, &PRINT_CONTEXT).unwrap(),
549            "typeset -x a\n\
550             typeset -r -x c\n",
551        );
552    }
553
554    #[test]
555    fn selecting_non_exported_variables() {
556        let vars = variables_with_different_attributes();
557        let pv = PrintVariables {
558            variables: vec![],
559            attrs: vec![(VariableAttr::Export, Off)],
560            scope: Scope::Global,
561        };
562
563        assert_eq!(
564            pv.execute(&vars, &PRINT_CONTEXT).unwrap(),
565            "typeset -r b\n\
566             typeset d\n",
567        );
568    }
569
570    #[test]
571    fn selecting_with_multiple_filtering_attributes() {
572        let vars = variables_with_different_attributes();
573        let pv = PrintVariables {
574            variables: vec![],
575            attrs: vec![(VariableAttr::ReadOnly, On), (VariableAttr::Export, Off)],
576            scope: Scope::Global,
577        };
578
579        let result = pv.execute(&vars, &PRINT_CONTEXT).unwrap();
580        assert_eq!(result, "typeset -r b\n");
581    }
582
583    #[test]
584    fn variable_not_found() {
585        let foo = Field::dummy("foo");
586        let bar = Field::dummy("bar");
587        let pv = PrintVariables {
588            variables: vec![foo.clone(), bar.clone()],
589            attrs: vec![],
590            scope: Scope::Global,
591        };
592
593        assert_eq!(
594            pv.execute(&VariableSet::new(), &PRINT_CONTEXT).unwrap_err(),
595            [
596                ExecuteError::PrintUnsetVariable(foo),
597                ExecuteError::PrintUnsetVariable(bar)
598            ]
599        );
600    }
601
602    #[test]
603    fn variable_name_containing_equal_is_ignored() {
604        let mut vars = VariableSet::new();
605        vars.get_or_new("first", Scope::Global.into())
606            .assign("1", None)
607            .unwrap();
608        vars.get_or_new("second=!", Scope::Global.into())
609            .assign("2", None)
610            .unwrap();
611        vars.get_or_new("third", Scope::Global.into())
612            .assign("3", None)
613            .unwrap();
614        let pv = PrintVariables {
615            variables: Field::dummies(["first", "second=!", "third"]),
616            attrs: vec![],
617            scope: Scope::Global,
618        };
619
620        assert_eq!(
621            pv.execute(&vars, &PRINT_CONTEXT).unwrap(),
622            "typeset first=1\n\
623             typeset third=3\n",
624        );
625    }
626
627    mod non_default_context {
628        use super::*;
629        use crate::typeset::syntax::{EXPORT_OPTION, READONLY_OPTION};
630
631        #[test]
632        fn builtin_name() {
633            let mut vars = VariableSet::new();
634            vars.get_or_new("foo", Scope::Global.into())
635                .assign("value", None)
636                .unwrap();
637            let bar = &mut vars.get_or_new("bar", Scope::Global.into());
638            bar.assign(Value::array(["1", "2"]), None).unwrap();
639            bar.make_read_only(Location::dummy("bar location"));
640            vars.get_or_new("baz", Scope::Global.into());
641            let pv = PrintVariables {
642                variables: Field::dummies(["foo", "bar", "baz"]),
643                attrs: vec![],
644                scope: Scope::Global,
645            };
646            let context = PrintContext {
647                builtin_name: "export",
648                ..PRINT_CONTEXT
649            };
650
651            assert_eq!(
652                pv.execute(&vars, &context).unwrap(),
653                "export foo=value\n\
654                 bar=(1 2)\n\
655                 export -r bar\n\
656                 export baz\n"
657            );
658        }
659
660        #[test]
661        fn builtin_is_significant() {
662            let mut vars = VariableSet::new();
663            vars.get_or_new("a", Scope::Global.into())
664                .assign(Value::array(["foo", "bar"]), None)
665                .unwrap();
666            let pv = PrintVariables {
667                variables: Field::dummies(["a"]),
668                attrs: vec![],
669                scope: Scope::Global,
670            };
671
672            let context = PrintContext {
673                builtin_is_significant: false,
674                ..PRINT_CONTEXT
675            };
676            assert_eq!(
677                pv.clone().execute(&vars, &context).unwrap(),
678                "a=(foo bar)\n"
679            );
680
681            let context = PrintContext {
682                builtin_is_significant: true,
683                ..PRINT_CONTEXT
684            };
685            assert_eq!(
686                pv.clone().execute(&vars, &context).unwrap(),
687                "a=(foo bar)\ntypeset a\n"
688            );
689        }
690
691        #[test]
692        fn insignificant_builtin_with_attributed_array() {
693            let mut vars = VariableSet::new();
694            let a = &mut vars.get_or_new("a", Scope::Global.into());
695            a.assign(Value::array(["foo", "bar"]), None).unwrap();
696            a.make_read_only(Location::dummy("a location"));
697            let pv = PrintVariables {
698                variables: Field::dummies(["a"]),
699                attrs: vec![],
700                scope: Scope::Global,
701            };
702
703            let context = PrintContext {
704                builtin_is_significant: false,
705                ..PRINT_CONTEXT
706            };
707            assert_eq!(
708                pv.clone().execute(&vars, &context).unwrap(),
709                "a=(foo bar)\ntypeset -r a\n"
710            );
711        }
712
713        #[test]
714        fn options_allowed() {
715            let mut vars = VariableSet::new();
716            let mut a = vars.get_or_new("a", Scope::Global.into());
717            a.assign("A", None).unwrap();
718            let mut b = vars.get_or_new("b", Scope::Global.into());
719            b.assign("B", None).unwrap();
720            b.export(true);
721            let mut c = vars.get_or_new("c", Scope::Global.into());
722            c.assign("C", None).unwrap();
723            c.make_read_only(Location::dummy("c location"));
724            let mut d = vars.get_or_new("d", Scope::Global.into());
725            d.assign("D", None).unwrap();
726            d.export(true);
727            d.make_read_only(Location::dummy("d location"));
728            let pv = PrintVariables {
729                variables: vec![],
730                attrs: vec![],
731                scope: Scope::Global,
732            };
733
734            let context = PrintContext {
735                options_allowed: &[READONLY_OPTION],
736                ..PRINT_CONTEXT
737            };
738            assert_eq!(
739                pv.clone().execute(&vars, &context).unwrap(),
740                "typeset a=A\n\
741                 typeset b=B\n\
742                 typeset -r c=C\n\
743                 typeset -r d=D\n",
744            );
745
746            let context = PrintContext {
747                options_allowed: &[EXPORT_OPTION],
748                ..PRINT_CONTEXT
749            };
750            assert_eq!(
751                pv.clone().execute(&vars, &context).unwrap(),
752                "typeset a=A\n\
753                 typeset -x b=B\n\
754                 typeset c=C\n\
755                 typeset -x d=D\n",
756            );
757
758            let context = PrintContext {
759                options_allowed: &[],
760                ..PRINT_CONTEXT
761            };
762            assert_eq!(
763                pv.execute(&vars, &context).unwrap(),
764                "typeset a=A\n\
765                 typeset b=B\n\
766                 typeset c=C\n\
767                 typeset d=D\n",
768            );
769        }
770    }
771}