Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move GetoptsState definition to yash-builtin and store it in Env::any #445

Merged
merged 4 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions yash-builtin/src/getopts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,16 @@
//!
//! # Implementation notes
//!
//! This implementation uses the `getopts_state` field in the [`Env`] to check
//! if the built-in is invoked with the same arguments and `$OPTIND` as the
//! previous invocation.
//! This implementation uses the `any` field in the [`Env`] to check if the
//! built-in is invoked with the same arguments and `$OPTIND` as the previous
//! invocation.

use crate::common::report_error;
use crate::common::report_simple_error;
use crate::common::syntax::parse_arguments;
use crate::common::syntax::Mode;
use either::Either::{Left, Right};
use std::num::NonZeroUsize;
use yash_env::builtin::getopts::Origin;
use yash_env::semantics::ExitStatus;
use yash_env::semantics::Field;
use yash_env::variable::OPTIND;
Expand Down Expand Up @@ -239,11 +238,11 @@ pub async fn main(env: &mut Env, args: Vec<Field>) -> crate::Result {
// Get the arguments to parse
let (args, arg_origin) = if operands.len() > 2 {
let iter = operands[2..].iter().map(|f| f.value.as_str());
(Left(iter), Origin::DirectArgs)
(Left(iter), verify::Origin::DirectArgs)
} else {
let params = &env.variables.positional_params().values;
let iter = params.iter().map(|v| v.as_str());
(Right(iter), Origin::PositionalParams)
(Right(iter), verify::Origin::PositionalParams)
};

// Get the `$OPTIND` value
Expand All @@ -256,18 +255,18 @@ pub async fn main(env: &mut Env, args: Vec<Field>) -> crate::Result {
origin: arg_origin,
optind,
};
if let Some(previous) = &env.getopts_state {
match current.verify(previous) {
if let Some(state) = env.any.get_mut::<verify::GetoptsState>() {
match current.verify(&*state) {
Ok(None) => {}
Ok(Some(current)) => env.getopts_state = Some(current.into_state()),
Ok(Some(current)) => *state = current.into_state(),
Err(e) => return report_simple_error(env, &e.to_string()).await,
}
} else {
if optind != "1" {
let message = format!("unexpected $OPTIND value `{optind}`");
return report_simple_error(env, &message).await;
}
env.getopts_state = Some(current.into_state());
env.any.insert(Box::new(current.into_state()));
}

// Parse the next option
Expand Down
12 changes: 6 additions & 6 deletions yash-builtin/src/getopts/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use super::indexes_to_optind;
use super::model;
use super::verify::GetoptsState;
use thiserror::Error;
use yash_env::semantics::Field;
use yash_env::variable::AssignError;
Expand Down Expand Up @@ -205,7 +206,7 @@ impl model::Result {
.assign(optind.clone(), location)
.map_err(|e| Error::with_name_and_assign_error(OPTIND.to_string(), e))?;

if let Some(state) = &mut env.getopts_state {
if let Some(state) = env.any.get_mut::<GetoptsState>() {
state.optind = optind;
}

Expand All @@ -215,11 +216,10 @@ impl model::Result {

#[cfg(test)]
mod tests {
use super::super::verify::Origin;
use super::*;
use assert_matches::assert_matches;
use std::num::NonZeroUsize;
use yash_env::builtin::getopts::GetoptsState;
use yash_env::builtin::getopts::Origin;
use yash_env::stack::Builtin;
use yash_env::stack::Frame;
use yash_env::variable::Value;
Expand Down Expand Up @@ -355,11 +355,11 @@ mod tests {
#[test]
fn report_updates_optind_of_getopts_state() {
let mut env = env_with_dummy_arg0_and_optarg();
env.getopts_state = Some(GetoptsState {
env.any.insert(Box::new(GetoptsState {
args: vec!["-a".to_string(), "-b".to_string()],
origin: Origin::DirectArgs,
optind: "1".to_string(),
});
}));
let result = model::Result {
option: Some(model::OptionOccurrence {
option: 'a',
Expand All @@ -372,7 +372,7 @@ mod tests {

_ = result.report(&mut env, false, Field::dummy("opt_var"));

assert_eq!(env.getopts_state.unwrap().optind, "2");
assert_eq!(env.any.get::<GetoptsState>().unwrap().optind, "2");
}

#[test]
Expand Down
29 changes: 27 additions & 2 deletions yash-builtin/src/getopts/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
//! arguments and `$OPTIND` value in subsequent calls.

use thiserror::Error;
use yash_env::builtin::getopts::GetoptsState;
use yash_env::builtin::getopts::Origin;

/// Type of error returned by [`GetoptsStateRef::verify`]
#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
Expand All @@ -36,6 +34,33 @@ pub enum Error {
DifferentOptind,
}

/// Origin of the arguments parsed by the getopts built-in
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Origin {
/// The arguments are passed directly to the built-in.
DirectArgs,
/// No arguments are passed to the built-in, so the built-in parses the
/// positional parameters.
PositionalParams,
}

/// State shared between getopts built-in invocations
///
/// The getopts built-in is designed to be called multiple times with the same
/// arguments, assuming that the `$OPTIND` variable isn't altered externally.
/// The built-in stores the arguments and the `$OPTIND` value in this data, and
/// verifies if it receives the same arguments and `$OPTIND` value in subsequent
/// calls.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct GetoptsState {
/// Expected arguments to parse
pub args: Vec<String>,
/// Expected origin of the arguments
pub origin: Origin,
/// Expected value of `$OPTIND`
pub optind: String,
}

/// Borrowed version of [`GetoptsState`]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct GetoptsStateRef<'a, I> {
Expand Down
8 changes: 7 additions & 1 deletion yash-env/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- The `Env` struct now implements `yash_syntax::decl_util::Glossary`.
- The `Env` struct now contains the `any` field of type `DataSet`.
- The `DataSet` struct is defined in the newly added `any` module.
It can be used to store arbitrary data.
- The `builtin::Builtin` struct now has the `is_declaration_utility` field.
- The `builtin::Builtin` struct now can be constructed with the associated
function `new`.
Expand All @@ -29,7 +32,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed

- The implementation of `From` for converting `errno::Errno` to and from
`nix::errno::Errno`.
`nix::errno::Errno`
- The `getopts_state` field from the `Env` struct
magicant marked this conversation as resolved.
Show resolved Hide resolved
- The `builtin::getopts` module and its contents
(the `GetoptsState` struct and the `Origin` enum)
- Internal dependencies:
- nix 0.29.0

Expand Down
197 changes: 197 additions & 0 deletions yash-env/src/any.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// This file is part of yash, an extended POSIX shell.
// Copyright (C) 2025 WATANABE Yuki
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Types for storing arbitrary data in the environment
//!
//! This module provides [`DataSet`] for storing arbitrary data in [`Env`].
//! It internally uses [`Any`] to store data of arbitrary types.
//!
//! [`Env`]: crate::Env

use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt::Debug;

/// Entry in the [`DataSet`]
#[derive(Debug)]
struct Entry {
data: Box<dyn Any>,
clone: fn(&dyn Any) -> Box<dyn Any>,
}
// TODO: When dyn upcasting coercion[1] is stabilized, we will be able to define
// `trait Data: Any + Clone {}` and insert `Box<dyn Data>` into `DataSet`
// without the need to store the `clone` function.
// [1]: https://github.com/rust-lang/rust/issues/65991

impl Clone for Entry {
fn clone(&self) -> Self {
Self {
data: (self.clone)(&*self.data),
clone: self.clone,
}
}
}

/// Clones data of the specified type
///
/// This function is used to clone data stored in the [`DataSet`].
#[must_use]
fn clone<T: Clone + 'static>(data: &dyn Any) -> Box<dyn Any> {
Box::new(data.downcast_ref::<T>().unwrap().clone())
}

/// Collection of arbitrary data
///
/// This struct is used to store arbitrary data in the environment.
/// Data stored in this struct are identified by their [`TypeId`], so you cannot
/// store multiple data instances of the same type.
#[derive(Clone, Debug, Default)]
pub struct DataSet {
inner: HashMap<TypeId, Entry>,
}

impl DataSet {
/// Creates a new empty `DataSet`.
#[must_use]
pub fn new() -> Self {
Self::default()
}

/// Inserts a new data into the `DataSet`.
///
/// If data of the same type is already stored in `self`, it is replaced.
pub fn insert<T: Clone + 'static>(&mut self, data: Box<T>) -> Option<Box<T>> {
let clone = clone::<T>;
let entry = Entry { data, clone };
self.inner
.insert(TypeId::of::<T>(), entry)
.map(|old| old.data.downcast().unwrap())
}

/// Obtains a reference to the data of the specified type.
#[must_use]
pub fn get<T: 'static>(&self) -> Option<&T> {
self.inner
.get(&TypeId::of::<T>())
.map(|entry| entry.data.downcast_ref().unwrap())
}

/// Obtains a mutable reference to the data of the specified type.
#[must_use]
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.inner
.get_mut(&TypeId::of::<T>())
.map(|entry| entry.data.downcast_mut().unwrap())
}

/// Obtains a reference to the data of the specified type, or inserts a new
/// data if it does not exist.
///
/// If the data does not exist, the data is created by calling the provided
/// closure and inserted into the `DataSet`, and a reference to the data is
/// returned. If the data already exists, a reference to the existing data
/// is returned and the closure is not called.
pub fn get_or_insert_with<T, F>(&mut self, f: F) -> &mut T
where
T: Clone + 'static,
F: FnOnce() -> Box<T>,
{
self.inner
.entry(TypeId::of::<T>())
.or_insert_with(|| {
let data = f();
let clone = clone::<T>;
Entry { data, clone }
})
.data
.downcast_mut()
.unwrap()
}

/// Removes the data of the specified type from the `DataSet`.
///
/// Returns the data if it exists.
pub fn remove<T: 'static>(&mut self) -> Option<Box<T>> {
self.inner
.remove(&TypeId::of::<T>())
.map(|entry| entry.data.downcast().unwrap())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn insert_and_get() {
let mut data_set = DataSet::new();
let old = data_set.insert(Box::new(42i32));
assert_eq!(old, None);
assert_eq!(data_set.get::<i32>(), Some(&42));
}

#[test]
fn insert_again() {
let mut data_set = DataSet::new();
data_set.insert(Box::new(42i32));
let old = data_set.insert(Box::new(43i32));
assert_eq!(old, Some(Box::new(42)));
assert_eq!(data_set.get::<i32>(), Some(&43));
}

#[test]
fn get_mut() {
let mut data_set = DataSet::new();
data_set.insert(Box::new(42i32));
let data = data_set.get_mut::<i32>().unwrap();
assert_eq!(data, &42);
*data = 43;
assert_eq!(data_set.get::<i32>(), Some(&43));
}

#[test]
fn get_or_insert_with() {
let mut data_set = DataSet::new();
let data = data_set.get_or_insert_with(|| Box::new(0i8));
assert_eq!(data, &0);
*data = 1;
let data = data_set.get_or_insert_with::<i8, _>(|| unreachable!());
assert_eq!(data, &1);
}

#[test]
fn remove_existing() {
let mut data_set = DataSet::new();
data_set.insert(Box::new(42i32));
let data = data_set.remove::<i32>().unwrap();
assert_eq!(*data, 42);
}

#[test]
fn remove_nonexisting() {
let mut data_set = DataSet::new();
let data = data_set.remove::<i32>();
assert_eq!(data, None);
}

#[test]
fn clone() {
let mut data_set = DataSet::new();
data_set.insert::<i32>(Box::new(42));
let clone = data_set.clone();
assert_eq!(clone.get::<i32>(), Some(&42));
}
}
2 changes: 0 additions & 2 deletions yash-env/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;

pub mod getopts;

/// Types of built-in utilities
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Type {
Expand Down
Loading
Loading