#![allow(dead_code)]
use std::{
borrow::Cow,
+ collections::BTreeMap,
str::FromStr,
sync::{Arc, OnceLock},
};
+use anyhow::anyhow;
+use clap::{ArgAction, ArgMatches, Args, FromArgMatches, value_parser};
use enum_map::EnumMap;
use enumset::{EnumSet, EnumSetType};
+use itertools::Itertools;
use pivot::PivotTable;
use serde::Serialize;
}
}
-pub struct Match {
+#[derive(Clone, Debug)]
+pub struct Selection {
/// - `None`: Include all objects.
- /// - `Some(false)`: Include only visible objects.
- /// - `Some(true)`: Include only hidden objects.
- hidden: Option<bool>,
+ /// - `Some(true)`: Include only visible objects.
+ /// - `Some(false)`: Include only hidden objects.
+ pub visible: Option<bool>,
/// - `None`: Include all objects.
/// - `Some(false)`: Include only objects with no error on loading.
/// - `Some(true)`: Include only objects with an error on loading.
- error: Option<bool>,
+ pub error: Option<bool>,
/// Classes to include.
- classes: EnumSet<Class>,
+ pub classes: EnumSet<Class>,
/// Command names to match.
- commands: StringMatch,
+ pub commands: StringMatch,
/// Subtypes to match.
- subtypes: StringMatch,
+ pub subtypes: StringMatch,
/// Labels to match.
- labels: StringMatch,
+ pub labels: StringMatch,
- /// Include objects under commands with indexes listed in COMMANDS. Indexes
- /// are 1-based. Everything is included if N_COMMANDS is 0.
- nth_commands: Vec<usize>,
+ /// Include objects under commands with the given 1-based indexes. Without
+ /// any indexes, include all objects.
+ pub nth_commands: Vec<usize>,
+
+ /// Include the objects with the given 1-based indexes within each of the
+ /// commands that are included. Indexes are 1-based. Negative indexes
+ /// count backward from the last object in a command.
+ pub instances: Vec<isize>,
+
+ /// Include only XML and binary member names that match. Without any member
+ /// names, include all objects.
+ pub members: Vec<String>,
+}
+
+impl Selection {
+ pub fn parse_nth_commands(s: &str) -> Result<Vec<usize>, anyhow::Error> {
+ s.split(',')
+ .map(|s| match s.parse::<usize>() {
+ Ok(0) => Err(anyhow!("--nth-commmands values must be positive")),
+ Ok(n) => Ok(n),
+ Err(error) => Err(error.into()),
+ })
+ .collect()
+ }
- /// Include XML and binary member names that match (except that everything
- /// is included by default if empty).
- members: Vec<String>,
+ pub fn parse_instances(s: &str) -> Result<Vec<isize>, anyhow::Error> {
+ s.split(',')
+ .map(|s| match s.parse::<isize>() {
+ Ok(0) => Err(anyhow!("--instances values must be nonzero")),
+ Ok(n) => Ok(n),
+ Err(error) => Err(error.into()),
+ })
+ .collect()
+ }
- /// Include the objects with indexes listed in INSTANCES within each of the
- /// commands that are included. Indexes are 1-based. Index -1 means the
- /// last object within a command.
- instances: Vec<isize>,
+ pub fn parse_classes(s: &str) -> Result<EnumSet<Class>, anyhow::Error> {
+ if s.is_empty() {
+ return Ok(EnumSet::all());
+ }
+ let (s, invert) = match s.strip_prefix('^') {
+ Some(rest) => (rest, true),
+ None => (s, false),
+ };
+ let mut classes = EnumSet::empty();
+ for name in s.split(',') {
+ if name == "all" {
+ classes = EnumSet::all();
+ } else {
+ classes.insert(
+ name.trim()
+ .parse()
+ .map_err(|_| anyhow!("unknown output class `{name}`"))?,
+ );
+ }
+ }
+ if invert {
+ classes = !classes;
+ }
+ Ok(classes)
+ }
}
+impl Default for Selection {
+ fn default() -> Self {
+ Self {
+ visible: Some(true),
+ error: None,
+ classes: EnumSet::all(),
+ commands: Default::default(),
+ subtypes: Default::default(),
+ labels: Default::default(),
+ nth_commands: Default::default(),
+ members: Default::default(),
+ instances: Default::default(),
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
pub enum StringMatch {
Include(Vec<String>),
Exclude(Vec<String>),
}
}
+/// Can't fail.
+#[derive(Debug, thiserror::Error)]
+pub enum Infallible {}
+
+impl FromStr for StringMatch {
+ type Err = Infallible;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if let Some(rest) = s.strip_prefix("^") {
+ Ok(Self::Exclude(rest.split(",").map_into().collect()))
+ } else {
+ Ok(Self::Include(s.split(",").map_into().collect()))
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct Selections(pub Vec<Selection>);
+
+impl FromArgMatches for Selections {
+ fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
+ let mut this = Self::default();
+ this.update_from_arg_matches(matches)?;
+ Ok(this)
+ }
+
+ fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> {
+ #[derive(Debug)]
+ enum Value {
+ Or,
+ Classes(EnumSet<Class>),
+ Commands(StringMatch),
+ Subtypes(StringMatch),
+ Labels(StringMatch),
+ NthCommands(Vec<usize>),
+ Instances(Vec<isize>),
+ ShowHidden(bool),
+ Errors(bool),
+ }
+
+ fn extract<F, T: Clone + Send + Sync + 'static>(
+ matches: &ArgMatches,
+ id: &clap::Id,
+ output: &mut BTreeMap<usize, Value>,
+ f: F,
+ ) where
+ F: Fn(T) -> Value,
+ {
+ for (value, index) in matches
+ .try_get_many::<T>(id.as_str())
+ .unwrap()
+ .unwrap()
+ .zip(matches.indices_of(id.as_str()).unwrap())
+ {
+ output.insert(index, f(value.clone()));
+ }
+ }
+
+ println!("{:#?}", matches.ids());
+ let mut values = BTreeMap::new();
+ for id in matches.ids() {
+ if matches.try_get_many::<clap::Id>(id.as_str()).is_ok() {
+ // ignore groups
+ continue;
+ }
+ let value_source = matches
+ .value_source(id.as_str())
+ .expect("id came from matches");
+ if value_source != clap::parser::ValueSource::CommandLine {
+ // Any other source just gets tacked on at the end (like default values)
+ continue;
+ }
+ match id.as_str() {
+ "or" => extract(matches, id, &mut values, |_: bool| Value::Or),
+ "select" => extract(matches, id, &mut values, Value::Classes),
+ "commands" => extract(matches, id, &mut values, Value::Commands),
+ "subtypes" => extract(matches, id, &mut values, Value::Subtypes),
+ "labels" => extract(matches, id, &mut values, Value::Labels),
+ "nth-commands" => extract(matches, id, &mut values, Value::NthCommands),
+ "instances" => extract(matches, id, &mut values, Value::Instances),
+ "show-hidden" => extract(matches, id, &mut values, Value::ShowHidden),
+ "errors" => extract(matches, id, &mut values, Value::Errors),
+ _ => unreachable!(),
+ }
+ }
+
+ if values.is_empty() {
+ return Ok(());
+ }
+
+ let mut selection = Selection::default();
+ for value in values.into_values() {
+ match value {
+ Value::Or => self.0.push(std::mem::take(&mut selection)),
+ Value::Classes(classes) => selection.classes = classes,
+ Value::Commands(commands) => selection.commands = commands,
+ Value::Subtypes(subtypes) => selection.subtypes = subtypes,
+ Value::Labels(labels) => selection.labels = labels,
+ Value::NthCommands(nth_commands) => selection.nth_commands = nth_commands,
+ Value::Instances(instances) => selection.instances = instances,
+ Value::ShowHidden(show) => selection.visible = if show { None } else { Some(true) },
+ Value::Errors(only) => selection.error = if only { Some(true) } else { None },
+ }
+ }
+ self.0.push(selection);
+ Ok(())
+ }
+}
+
+impl Args for Selections {
+ fn augment_args(cmd: clap::Command) -> clap::Command {
+ SelectionArgs::augment_args(cmd.next_help_heading("Input selection options"))
+ }
+
+ fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
+ Self::augment_args(cmd)
+ }
+}
+
+/// Show information about SPSS viewer files (SPV files).
+#[derive(Args, Clone, Debug)]
+struct SelectionArgs {
+ /// Classes of objects to include or, with leading `^`, to exclude. The
+ /// supported classes are: charts, headings, logs, models, tables, texts,
+ /// trees, warnings, outlineheaders, pagetitle, notes, unknown, other.
+ #[arg(long, required = false, value_parser = Selection::parse_classes, action = ArgAction::Append)]
+ select: EnumSet<Class>,
+
+ /// Identifiers of commands to include or, with leading `^`, to exclude.
+ #[arg(long, required = false, value_parser = StringMatch::from_str, action = ArgAction::Append)]
+ commands: StringMatch,
+
+ /// Table subtypes to include or, with leading `^`, to exclude.
+ #[arg(long, required = false, value_parser = StringMatch::from_str, action = ArgAction::Append)]
+ subtypes: StringMatch,
+
+ /// Labels (table titles) to include or, with leading `^`, to exclude.
+ #[arg(long, required = false, value_parser = StringMatch::from_str, action = ArgAction::Append)]
+ labels: StringMatch,
+
+ /// Include only the Nth (1-based) instance of the selected commands.
+ #[arg(long, required = false, value_parser = Selection::parse_nth_commands, action = ArgAction::Append)]
+ nth_commands: Vec<usize>,
+
+ /// Include hidden objects in the output (by default, they are excluded)
+ #[arg(long, required = false, action = ArgAction::Append)]
+ show_hidden: bool,
+
+ /// Include only objects that cause an error when read (by default, objects
+ /// with and without errors are included).
+ #[arg(long, required = false, action = ArgAction::Append)]
+ errors: bool,
+
+ /// Include only XML and binary member names that match. Without any member
+ /// names, include all objects.
+ pub members: Vec<String>,
+
+ /// Separate two groups of selection options.
+ #[arg(long, action = ArgAction::Append, long = "or", num_args = 0, value_parser = value_parser!(bool), default_missing_value = "true", default_value = "false")]
+ _or: bool,
+}
+
#[cfg(test)]
mod tests {
- use crate::output::StringMatch;
+ use enumset::EnumSet;
+
+ use crate::output::{Class, Selection, StringMatch};
+
+ #[test]
+ fn parse_classes() {
+ assert_eq!(Selection::parse_classes("").unwrap(), EnumSet::all());
+ assert_eq!(
+ Selection::parse_classes("tables").unwrap(),
+ EnumSet::only(Class::Tables)
+ );
+ assert_eq!(
+ Selection::parse_classes("tables,pagetitle").unwrap(),
+ EnumSet::only(Class::Tables) | EnumSet::only(Class::PageTitle)
+ );
+ assert_eq!(
+ Selection::parse_classes("^tables,pagetitle").unwrap(),
+ !(EnumSet::only(Class::Tables) | EnumSet::only(Class::PageTitle))
+ );
+ }
+
+ #[test]
+ fn parse_nth_commands() {
+ assert_eq!(Selection::parse_nth_commands("1").unwrap(), vec![1]);
+ assert_eq!(
+ Selection::parse_nth_commands("1,2,3").unwrap(),
+ vec![1, 2, 3]
+ );
+ assert!(Selection::parse_nth_commands("0").is_err());
+ }
#[test]
fn string_matches() {
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <http://www.gnu.org/licenses/>.
-use anyhow::{Result, anyhow};
+use anyhow::Result;
use clap::{Args, ValueEnum};
-use enumset::EnumSet;
-use pspp::output::Class;
+use pspp::output::Selections;
use std::{fmt::Display, path::PathBuf};
/// Show information about SPSS viewer files (SPV files).
#[arg(required = true)]
input: PathBuf,
- /// Classes of objects to include or, with leading `^`, to exclude. The
- /// supported classes are: charts, headings, logs, models, tables, texts,
- /// trees, warnings, outlineheaders, pagetitle, notes, unknown, other.
- #[arg(long, required = false, value_parser = parse_select, help_heading = "Input selection options")]
- select: EnumSet<Class>,
-
- /// Identifiers of commands to include or, with leading `^`, to exclude.
- #[arg(long, required = false)]
- commands: String,
-
- /// Table subtypes to include or, with leading `^`, to exclude.
- #[arg(long, required = false)]
- subtypes: String,
-
- /// Labels (table titles) to include or, with leading `^`, to exclude.
- #[arg(long, required = false)]
- labels: String,
-}
-
-fn parse_select(s: &str) -> Result<EnumSet<Class>, anyhow::Error> {
- if s.is_empty() {
- return Ok(EnumSet::all());
- }
- let (s, invert) = match s.strip_prefix('^') {
- Some(rest) => (rest, true),
- None => (s, false),
- };
- let mut classes = EnumSet::empty();
- for name in s.split(',') {
- if name == "all" {
- classes = EnumSet::all();
- } else {
- classes.insert(
- name.trim()
- .parse()
- .map_err(|_| anyhow!("unknown output class `{name}`"))?,
- );
- }
- }
- if invert {
- classes = !classes;
- }
- Ok(classes)
+ #[command(flatten)]
+ selection: Selections,
}
/// What to show in a system file.
impl ShowSpv {
pub fn run(self) -> Result<()> {
+ println!("{:#?}", &self);
todo!()
}
}
-
-#[cfg(test)]
-mod tests {
- use enumset::EnumSet;
-
- use crate::show_spv::parse_select;
-
- #[test]
- fn test_parse_select() {
- assert_eq!(parse_select("").unwrap(), EnumSet::all());
- assert_eq!(
- parse_select("tables").unwrap(),
- EnumSet::only(pspp::output::Class::Tables)
- );
- assert_eq!(
- parse_select("tables,pagetitle").unwrap(),
- EnumSet::only(pspp::output::Class::Tables)
- | EnumSet::only(pspp::output::Class::PageTitle)
- );
- assert_eq!(
- parse_select("^tables,pagetitle").unwrap(),
- !(EnumSet::only(pspp::output::Class::Tables)
- | EnumSet::only(pspp::output::Class::PageTitle))
- );
- }
-}