Skip to main content

yash_builtin/ulimit/
syntax.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2024 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//! Command-line argument parser for the `ulimit` built-in
18
19use super::{Command, ResourceExt as _, SetLimitType, SetLimitValue, ShowLimitType};
20use crate::common::syntax::{Mode, OptionSpec, ParseError, parse_arguments};
21use std::num::ParseIntError;
22use std::str::FromStr;
23use thiserror::Error;
24use yash_env::Env;
25use yash_env::semantics::Field;
26use yash_env::source::Location;
27use yash_env::source::pretty::{Report, ReportType, Snippet, Span, SpanRole, add_span};
28use yash_env::system::resource::Resource;
29
30#[derive(Clone, Debug, Eq, Error, PartialEq)]
31#[non_exhaustive]
32pub enum Error {
33    /// An error occurred in the common syntax parser.
34    #[error(transparent)]
35    CommonError(#[from] ParseError<'static>),
36
37    /// The `-a` option is given with a resource limit operand.
38    #[error("cannot set limit for -a")]
39    AllWithOperand(Field),
40
41    /// Both the `-H` and `-S` options are given without a resource limit
42    /// operand.
43    #[error("cannot show both hard and soft limits at once")]
44    ShowingBoth { soft: Location, hard: Location },
45
46    /// More than one resource is specified.
47    #[error("cannot specify more than one resource")]
48    TooManyResources(Location),
49
50    /// More than one operand is given.
51    ///
52    /// The vector contains *all* the operands, including the first proper one.
53    #[error("too many operands")]
54    TooManyOperands(Vec<Field>),
55
56    /// An operand is not a valid limit.
57    #[error("invalid limit")]
58    InvalidLimit(Field, ParseIntError),
59}
60
61impl Error {
62    /// Converts the error to a report.
63    #[must_use]
64    pub fn to_report(&self) -> Report<'_> {
65        let snippets = match self {
66            Self::CommonError(e) => return e.to_report(),
67            Self::AllWithOperand(field) => Snippet::with_primary_span(
68                &field.origin,
69                format!("{field}: unexpected operand").into(),
70            ),
71            Self::ShowingBoth { soft, hard } => {
72                let mut snippets =
73                    Snippet::with_primary_span(soft, "soft limit requested here".into());
74                add_span(
75                    &hard.code,
76                    Span {
77                        range: hard.byte_range(),
78                        role: SpanRole::Primary {
79                            label: "hard limit requested here".into(),
80                        },
81                    },
82                    &mut snippets,
83                );
84                snippets
85            }
86            Self::TooManyResources(location) => {
87                Snippet::with_primary_span(location, "unexpected option".into())
88            }
89            Self::TooManyOperands(fields) => Snippet::with_primary_span(
90                &fields[1].origin,
91                format!("{}: unexpected operand", fields[1].value).into(),
92            ),
93            Self::InvalidLimit(operand, parse_int_error) => Snippet::with_primary_span(
94                &operand.origin,
95                format!("{operand}: invalid limit ({parse_int_error})").into(),
96            ),
97        };
98        let mut report = Report::new();
99        report.r#type = ReportType::Error;
100        report.title = self.to_string().into();
101        report.snippets = snippets;
102        report
103    }
104}
105
106impl<'a> From<&'a Error> for Report<'a> {
107    #[inline]
108    fn from(error: &'a Error) -> Self {
109        error.to_report()
110    }
111}
112
113/// Result of parsing command line arguments
114pub type Result = std::result::Result<Command, Error>;
115
116/// Command-line options for the `ulimit` built-in
117const OPTION_SPECS: &[OptionSpec] = &[
118    OptionSpec::new().short('H').long("hard"),
119    OptionSpec::new().short('S').long("soft"),
120    OptionSpec::new().short('a').long("all"),
121    OptionSpec::new().short('v').long("as"),
122    OptionSpec::new().short('c').long("core"),
123    OptionSpec::new().short('t').long("cpu"),
124    OptionSpec::new().short('d').long("data"),
125    OptionSpec::new().short('f').long("fsize"),
126    OptionSpec::new().short('k').long("kqueues"),
127    OptionSpec::new().short('x').long("locks"),
128    OptionSpec::new().short('l').long("memlock"),
129    OptionSpec::new().short('q').long("msgqueue"),
130    OptionSpec::new().short('e').long("nice"),
131    OptionSpec::new().short('n').long("nofile"),
132    OptionSpec::new().short('u').long("nproc"),
133    OptionSpec::new().short('m').long("rss"),
134    OptionSpec::new().short('r').long("rtprio"),
135    OptionSpec::new().short('R').long("rttime"),
136    OptionSpec::new().short('b').long("sbsize"),
137    OptionSpec::new().short('i').long("sigpending"),
138    OptionSpec::new().short('s').long("stack"),
139    OptionSpec::new().short('w').long("swap"),
140];
141
142/// Parses command line arguments.
143pub fn parse<S>(env: &Env<S>, args: Vec<Field>) -> Result {
144    let (options, operands) = parse_arguments(OPTION_SPECS, Mode::with_env(env), args)?;
145
146    let mut resource_option = None;
147    let mut hard = None;
148    let mut soft = None;
149
150    for option in options {
151        match option.spec.get_short().unwrap() {
152            'H' => hard = Some(option.location),
153            'S' => soft = Some(option.location),
154            c => {
155                if resource_option.is_some_and(|c2| c2 != c) {
156                    return Err(Error::TooManyResources(option.location));
157                }
158                resource_option = Some(c);
159            }
160        }
161    }
162
163    let resource = match resource_option {
164        Some('a') => {
165            return if let Some(operand) = operands.into_iter().next() {
166                Err(Error::AllWithOperand(operand))
167            } else {
168                Ok(Command::ShowAll(show_limit_type(hard, soft)?))
169            };
170        }
171
172        Some(option_char) => Resource::ALL
173            .iter()
174            .copied()
175            .find(|r| r.option() == option_char)
176            .unwrap(),
177
178        None => Resource::FSIZE,
179    };
180
181    if operands.len() > 1 {
182        return Err(Error::TooManyOperands(operands));
183    }
184
185    if let Some(operand) = { operands }.pop() {
186        let limit_type = set_limit_type(hard, soft);
187        let value = parse_value(operand)?;
188        return Ok(Command::Set(resource, limit_type, value));
189    }
190
191    Ok(Command::ShowOne(resource, show_limit_type(hard, soft)?))
192}
193
194fn show_limit_type(
195    hard: Option<Location>,
196    soft: Option<Location>,
197) -> std::result::Result<ShowLimitType, Error> {
198    match (hard, soft) {
199        (None, _) => Ok(ShowLimitType::Soft),
200        (Some(_), None) => Ok(ShowLimitType::Hard),
201        (Some(hard), Some(soft)) => Err(Error::ShowingBoth { soft, hard }),
202    }
203}
204
205fn set_limit_type(hard: Option<Location>, soft: Option<Location>) -> SetLimitType {
206    match (hard, soft) {
207        (None, Some(_)) => SetLimitType::Soft,
208        (Some(_), None) => SetLimitType::Hard,
209        (None, None) | (Some(_), Some(_)) => SetLimitType::Both,
210    }
211}
212
213fn parse_value(operand: Field) -> std::result::Result<SetLimitValue, Error> {
214    operand
215        .value
216        .parse()
217        .map_err(|e| Error::InvalidLimit(operand, e))
218}
219
220impl FromStr for SetLimitValue {
221    type Err = ParseIntError;
222
223    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
224        match s {
225            "unlimited" => Ok(Self::Unlimited),
226            "soft" => Ok(Self::CurrentSoft),
227            "hard" => Ok(Self::CurrentHard),
228            _ => Ok(Self::Number(s.parse()?)),
229        }
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn show_default_soft_default_fsize() {
239        let env = Env::new_virtual();
240        let result = parse(&env, vec![]);
241        assert_eq!(
242            result,
243            Ok(Command::ShowOne(Resource::FSIZE, ShowLimitType::Soft))
244        );
245    }
246
247    #[test]
248    fn show_explicit_soft_default_fsize() {
249        let env = Env::new_virtual();
250        let result = parse(&env, Field::dummies(["-S"]));
251        assert_eq!(
252            result,
253            Ok(Command::ShowOne(Resource::FSIZE, ShowLimitType::Soft))
254        );
255    }
256
257    #[test]
258    fn show_explicit_hard_default_fsize() {
259        let env = Env::new_virtual();
260        let result = parse(&env, Field::dummies(["-H"]));
261        assert_eq!(
262            result,
263            Ok(Command::ShowOne(Resource::FSIZE, ShowLimitType::Hard))
264        );
265    }
266
267    #[test]
268    fn show_cpu_default_soft() {
269        let env = Env::new_virtual();
270        let result = parse(&env, Field::dummies(["-t"]));
271        assert_eq!(
272            result,
273            Ok(Command::ShowOne(Resource::CPU, ShowLimitType::Soft))
274        );
275    }
276
277    #[test]
278    fn show_cpu_explicit_hard() {
279        let env = Env::new_virtual();
280        let result = parse(&env, Field::dummies(["-t", "-H"]));
281        assert_eq!(
282            result,
283            Ok(Command::ShowOne(Resource::CPU, ShowLimitType::Hard))
284        );
285    }
286
287    #[test]
288    fn show_all_default_soft() {
289        let env = Env::new_virtual();
290        let result = parse(&env, Field::dummies(["-a"]));
291        assert_eq!(result, Ok(Command::ShowAll(ShowLimitType::Soft)));
292    }
293
294    #[test]
295    fn show_all_explicit_soft() {
296        let env = Env::new_virtual();
297        let result = parse(&env, Field::dummies(["-Sa"]));
298        assert_eq!(result, Ok(Command::ShowAll(ShowLimitType::Soft)));
299    }
300
301    #[test]
302    fn show_all_explicit_hard() {
303        let env = Env::new_virtual();
304        let result = parse(&env, Field::dummies(["-aH"]));
305        assert_eq!(result, Ok(Command::ShowAll(ShowLimitType::Hard)));
306    }
307
308    #[test]
309    fn set_default_both_default_fsize() {
310        let env = Env::new_virtual();
311        let result = parse(&env, Field::dummies(["0"]));
312        assert_eq!(
313            result,
314            Ok(Command::Set(
315                Resource::FSIZE,
316                SetLimitType::Both,
317                SetLimitValue::Number(0)
318            ))
319        );
320    }
321
322    #[test]
323    fn set_explicit_soft_default_fsize() {
324        let env = Env::new_virtual();
325        let result = parse(&env, Field::dummies(["-S", "0"]));
326        assert_eq!(
327            result,
328            Ok(Command::Set(
329                Resource::FSIZE,
330                SetLimitType::Soft,
331                SetLimitValue::Number(0)
332            ))
333        );
334    }
335
336    #[test]
337    fn set_explicit_hard_default_fsize() {
338        let env = Env::new_virtual();
339        let result = parse(&env, Field::dummies(["-H", "0"]));
340        assert_eq!(
341            result,
342            Ok(Command::Set(
343                Resource::FSIZE,
344                SetLimitType::Hard,
345                SetLimitValue::Number(0)
346            ))
347        );
348    }
349
350    #[test]
351    fn set_default_both_explicit_data() {
352        let env = Env::new_virtual();
353        let result = parse(&env, Field::dummies(["-d", "0"]));
354        assert_eq!(
355            result,
356            Ok(Command::Set(
357                Resource::DATA,
358                SetLimitType::Both,
359                SetLimitValue::Number(0)
360            ))
361        );
362    }
363
364    #[test]
365    fn set_explicit_soft_explicit_data() {
366        let env = Env::new_virtual();
367        let result = parse(&env, Field::dummies(["-Sd", "0"]));
368        assert_eq!(
369            result,
370            Ok(Command::Set(
371                Resource::DATA,
372                SetLimitType::Soft,
373                SetLimitValue::Number(0)
374            ))
375        );
376    }
377
378    #[test]
379    fn set_explicit_hard_explicit_data() {
380        let env = Env::new_virtual();
381        let result = parse(&env, Field::dummies(["-Hd", "0"]));
382        assert_eq!(
383            result,
384            Ok(Command::Set(
385                Resource::DATA,
386                SetLimitType::Hard,
387                SetLimitValue::Number(0)
388            ))
389        );
390    }
391
392    #[test]
393    fn set_unlimited() {
394        let env = Env::new_virtual();
395        let result = parse(&env, Field::dummies(["unlimited"]));
396        assert_eq!(
397            result,
398            Ok(Command::Set(
399                Resource::FSIZE,
400                SetLimitType::Both,
401                SetLimitValue::Unlimited
402            ))
403        );
404    }
405
406    #[test]
407    fn set_all() {
408        let env = Env::new_virtual();
409        let result = parse(&env, Field::dummies(["-a", "0"]));
410        assert_eq!(result, Err(Error::AllWithOperand(Field::dummy("0"))));
411    }
412
413    #[test]
414    fn show_hard_and_soft() {
415        let env = Env::new_virtual();
416        let result = parse(&env, Field::dummies(["-H", "-S"]));
417        assert_eq!(
418            result,
419            Err(Error::ShowingBoth {
420                soft: Location::dummy("-S"),
421                hard: Location::dummy("-H")
422            })
423        );
424    }
425
426    #[test]
427    fn set_hard_and_soft() {
428        let env = Env::new_virtual();
429        let result = parse(&env, Field::dummies(["-HS", "0"]));
430        assert_eq!(
431            result,
432            Ok(Command::Set(
433                Resource::FSIZE,
434                SetLimitType::Both,
435                SetLimitValue::Number(0)
436            ))
437        );
438    }
439
440    #[test]
441    fn redundant_limit_type_options() {
442        let env = Env::new_virtual();
443        let result = parse(&env, Field::dummies(["-H", "-H", "0"]));
444        assert_eq!(
445            result,
446            Ok(Command::Set(
447                Resource::FSIZE,
448                SetLimitType::Hard,
449                SetLimitValue::Number(0)
450            ))
451        );
452    }
453
454    #[test]
455    fn more_than_one_resource() {
456        let env = Env::new_virtual();
457        let result = parse(&env, Field::dummies(["-d", "-f"]));
458        assert_eq!(result, Err(Error::TooManyResources(Location::dummy("-f"))));
459    }
460
461    #[test]
462    fn redundant_resource_options() {
463        let env = Env::new_virtual();
464        let result = parse(&env, Field::dummies(["-dd", "-d", "0"]));
465        assert_eq!(
466            result,
467            Ok(Command::Set(
468                Resource::DATA,
469                SetLimitType::Both,
470                SetLimitValue::Number(0)
471            ))
472        );
473    }
474
475    #[test]
476    fn too_many_operands() {
477        let env = Env::new_virtual();
478        let args = Field::dummies(["0", "1"]);
479        let result = parse(&env, args.clone());
480        assert_eq!(result, Err(Error::TooManyOperands(args)));
481    }
482
483    #[test]
484    fn set_limit_value_from_str_number() {
485        assert_eq!("0".parse(), Ok(SetLimitValue::Number(0)));
486        assert_eq!("1".parse(), Ok(SetLimitValue::Number(1)));
487        assert_eq!("100".parse(), Ok(SetLimitValue::Number(100)));
488    }
489
490    #[test]
491    fn set_limit_value_from_str_unlimited() {
492        assert_eq!("unlimited".parse(), Ok(SetLimitValue::Unlimited));
493    }
494
495    #[test]
496    fn set_limit_value_from_str_soft() {
497        assert_eq!("soft".parse(), Ok(SetLimitValue::CurrentSoft));
498    }
499
500    #[test]
501    fn set_limit_value_from_str_hard() {
502        assert_eq!("hard".parse(), Ok(SetLimitValue::CurrentHard));
503    }
504}