Skip to main content

yash_builtin/alias/
semantics.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
17//! Core runtime behavior of the alias built-in
18
19use super::Command;
20use thiserror::Error;
21use yash_env::Env;
22use yash_env::alias::Alias;
23use yash_env::alias::HashEntry;
24use yash_env::semantics::Field;
25use yash_env::source::pretty::{Report, ReportType, Snippet};
26use yash_quote::quoted;
27
28/// Error in executing the alias built-in
29#[derive(Clone, Debug, Eq, Error, PartialEq)]
30pub enum Error {
31    /// Printing a non-existent alias
32    #[error("alias {name} not found")]
33    NonExistentAlias { name: Field },
34}
35
36impl Error {
37    /// Converts this error to a [`Report`].
38    #[must_use]
39    pub fn to_report(&self) -> Report<'_> {
40        let mut report = Report::new();
41        report.r#type = ReportType::Error;
42        report.title = "cannot print alias definition".into();
43        report.snippets = match self {
44            Self::NonExistentAlias { name } => {
45                Snippet::with_primary_span(&name.origin, self.to_string().into())
46            }
47        };
48        report
49    }
50}
51
52impl<'a> From<&'a Error> for Report<'a> {
53    #[inline]
54    fn from(error: &'a Error) -> Self {
55        error.to_report()
56    }
57}
58
59/// Defines an alias.
60///
61/// If `name_value` is of the form `name=value`, defines an alias named `name`
62/// that expands to `value`. Otherwise, returns `Err(name_value)`.
63fn define<S>(env: &mut Env<S>, name_value: Field) -> Result<(), Field> {
64    let Some(equal) = name_value.value.find('=') else {
65        return Err(name_value);
66    };
67    let replacement = name_value.value[equal + 1..].to_owned();
68    let name = {
69        let mut name = name_value.value;
70        name.truncate(equal);
71        // TODO Reject invalid name
72        name.shrink_to_fit();
73        name
74    };
75    // TODO Support global aliases
76    let global = false;
77
78    env.aliases
79        .replace(HashEntry::new(name, replacement, global, name_value.origin));
80
81    Ok(())
82}
83
84/// Prints the definition of an alias.
85///
86/// This function appends a string of the form `name=value\n` to `result`.
87/// If the named alias does not exist, returns an error.
88fn find_and_print<S>(env: &Env<S>, name: Field, result: &mut String) -> Result<(), Error> {
89    let alias = env
90        .aliases
91        .get(name.value.as_str())
92        .ok_or(Error::NonExistentAlias { name })?;
93
94    print(&alias.0, result);
95
96    Ok(())
97}
98
99/// Prints the definition of an alias.
100/// This function appends a string of the form `name=value\n` to `result`.
101fn print(alias: &Alias, result: &mut String) {
102    use std::fmt::Write as _;
103    writeln!(
104        result,
105        "{}={}",
106        quoted(&alias.name),
107        quoted(&alias.replacement),
108    )
109    .unwrap();
110}
111
112impl Command {
113    /// Executes the alias built-in
114    ///
115    /// Returns a string that contains the alias definitions to be printed and a
116    /// list of errors that occurred during the execution.
117    pub async fn execute<S>(self, env: &mut Env<S>) -> (String, Vec<Error>) {
118        let mut output = String::new();
119        let mut errors = Vec::new();
120
121        if self.operands.is_empty() {
122            // Make a temporary vector to sort the aliases by name
123            let mut aliases = env.aliases.iter().collect::<Vec<_>>();
124            // TODO Locale-aware sorting
125            aliases.sort_unstable_by_key(|alias| &alias.0.name);
126            for alias in aliases {
127                print(&alias.0, &mut output);
128            }
129        } else {
130            for operand in self.operands {
131                if let Err(operand) = define(env, operand) {
132                    let result = find_and_print(env, operand, &mut output);
133                    errors.extend(result.err());
134                }
135            }
136        }
137
138        (output, errors)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use futures_util::FutureExt as _;
146    use yash_env::source::Location;
147
148    #[test]
149    fn defining_alias() {
150        let mut env = Env::new_virtual();
151        let origin = Location::dummy("definition location");
152
153        let result = define(
154            &mut env,
155            Field {
156                value: "foo=bar".into(),
157                origin: origin.clone(),
158            },
159        );
160
161        assert_eq!(result, Ok(()));
162        assert_eq!(
163            *env.aliases.get("foo").unwrap().0,
164            Alias {
165                name: "foo".into(),
166                replacement: "bar".into(),
167                global: false,
168                origin,
169            }
170        );
171    }
172
173    #[test]
174    fn defining_alias_without_value() {
175        let mut env = Env::new_virtual();
176        let field = Field::dummy("valueless");
177        let result = define(&mut env, field.clone());
178        assert_eq!(result, Err(field));
179        assert_eq!(env.aliases.len(), 0);
180    }
181
182    #[test]
183    fn finding_and_printing_alias() {
184        let mut env = Env::new_virtual();
185        env.aliases.insert(HashEntry::new(
186            "foo".into(),
187            "bar".into(),
188            false,
189            Location::dummy("definition location"),
190        ));
191        let mut result = String::new();
192
193        let return_value = find_and_print(&env, Field::dummy("foo"), &mut result);
194
195        assert_eq!(return_value, Ok(()));
196        assert_eq!(result, "foo=bar\n");
197    }
198
199    #[test]
200    fn finding_non_existent_alias() {
201        let name = Field::dummy("foo");
202        let mut result = String::new();
203
204        let return_value = find_and_print(&Env::new_virtual(), name.clone(), &mut result);
205
206        assert_eq!(return_value, Err(Error::NonExistentAlias { name }));
207        assert_eq!(result, "");
208    }
209
210    #[test]
211    fn printing_quoted_alias_name() {
212        let alias = Alias {
213            name: "foo bar".into(),
214            replacement: "x".into(),
215            global: false,
216            origin: Location::dummy("definition location"),
217        };
218        let mut result = String::new();
219
220        print(&alias, &mut result);
221
222        assert_eq!(result, "'foo bar'=x\n");
223    }
224
225    #[test]
226    fn printing_quoted_alias_value() {
227        let alias = Alias {
228            name: "ll".into(),
229            replacement: "ls -l".into(),
230            global: false,
231            origin: Location::dummy("definition location"),
232        };
233        let mut result = String::new();
234
235        print(&alias, &mut result);
236
237        assert_eq!(result, "ll='ls -l'\n");
238    }
239
240    #[test]
241    fn executing_with_operands() {
242        let mut env = Env::new_virtual();
243        let operands = Field::dummies(["foo=bar", "bar", "foo"]);
244        let command = Command { operands };
245
246        let (output, errors) = command.execute(&mut env).now_or_never().unwrap();
247
248        assert_eq!(output, "foo=bar\n");
249        assert_eq!(
250            errors,
251            [Error::NonExistentAlias {
252                name: Field::dummy("bar")
253            }]
254        );
255    }
256
257    #[test]
258    fn executing_with_no_operands() {
259        let mut env = Env::new_virtual();
260        env.aliases.insert(HashEntry::new(
261            "foo".into(),
262            "bar".into(),
263            false,
264            Location::dummy("foo location"),
265        ));
266        env.aliases.insert(HashEntry::new(
267            "ll".into(),
268            "ls -l".into(),
269            false,
270            Location::dummy("ll location"),
271        ));
272        env.aliases.insert(HashEntry::new(
273            "ls".into(),
274            "ls --color".into(),
275            false,
276            Location::dummy("ls location"),
277        ));
278        env.aliases.insert(HashEntry::new(
279            "cat".into(),
280            "cat".into(),
281            false,
282            Location::dummy("cat location"),
283        ));
284
285        let command = Command { operands: vec![] };
286
287        let (output, errors) = command.execute(&mut env).now_or_never().unwrap();
288        // The output is sorted by name
289        assert_eq!(output, "cat=cat\nfoo=bar\nll='ls -l'\nls='ls --color'\n");
290        assert_eq!(errors, []);
291    }
292}