]> pintos-os.org Git - pspp/commitdiff
Allow `pspp convert` to accept multiple input files.
authorBen Pfaff <blp@cs.stanford.edu>
Wed, 29 Jul 2026 20:15:14 +0000 (13:15 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Wed, 29 Jul 2026 20:15:14 +0000 (13:15 -0700)
rust/doc/src/invoking/pc.md
rust/doc/src/invoking/por.md
rust/doc/src/invoking/pspp-convert.md
rust/doc/src/invoking/pspp-show.md
rust/pspp/src/cli/convert.rs
rust/pspp/src/cli/pc.rs
rust/pspp/src/cli/por.rs
rust/pspp/src/cli/show.rs

index 39ea8d5bf71b50fd0b8f27f8d5621df15e90e794..baf2c4ac62bbf556f1fbd2327e2f1aacc2980af9 100644 (file)
@@ -54,7 +54,7 @@ The following `<SUBCOMMAND>`s are available:
 
 `pspp pc` accepts the following options:
 
-* `-o <OUTPUT_OPTIONS>`  
+* `-O <OUTPUT_OPTIONS>`  
   Adds `<OUTPUT_OPTIONS>` to the output engine configuration.  See
   [Output Drivers](output.md) for information on how to configure
   output.
index 1b9ff408e6ad30bca11866f80d5dd62b8f78b481..9ffb6493cf7e372d1d206127c3539f01a5ccb328 100644 (file)
@@ -92,7 +92,7 @@ The following `<SUBCOMMAND>`s are available:
 
 `pspp por` accepts the following options:
 
-* `-o <OUTPUT_OPTIONS>`  
+* `-O <OUTPUT_OPTIONS>`  
   Adds `<OUTPUT_OPTIONS>` to the output engine configuration.  See
   [Output Drivers](output.md) for information on how to configure
   output.
index 25f9db4c16cf720fb1a722d3674fb5f4769bf4dd..825a57cbb6dbae6a5b2e582bde0a4c3f4502ef51 100644 (file)
@@ -4,49 +4,55 @@ The `pspp convert` command reads a file and writes it to another,
 usually in a different format.  The basic syntax is:
 
 ```
-pspp convert <INPUT> [OUTPUT]
+pspp convert <INPUT>... [-o <OUTPUT>]
 ```
 
-which reads `<INPUT>` and writes a copy of it to `[OUTPUT]`, or to the
-terminal if `[OUTPUT]` is omitted.
+which reads each `<INPUT>` and writes a copy to `<OUTPUT>` if the `-o`
+option is specified, or to the terminal otherwise.
 
 `pspp convert` can convert following kinds of files:
 
 * Data files:
 
-  When `<INPUT>` is an SPSS system file or portable file or an SPSS/PC+
-  system file, then `[OUTPUT]` can be a data file format.  The
-  currently supported data file formats are:
+  When `<INPUT>` is a single SPSS system file or portable file or
+  SPSS/PC+ system file, the currently supported output file formats
+  are:
 
   - [Comma-separated value (CSV) files]
   - [SPSS system files]
   - [SPSS portable files]
 
-  If `[OUTPUT]` is omitted, the default is to write the data to stdout
-  in CSV format.
+  If `-o <OUTPUT>` is omitted, the default is to write the data to
+  stdout in CSV format.
 
 * Viewer files:
 
-  When `<INPUT>` is an SPSS viewer (SPV) file, then `[OUTPUT]` may be
-  any [PSPP output format], including:
+  When `<INPUT>` is one or more SPSS viewer (SPV) files, then the
+  output may be any [PSPP output format], including:
 
   - [plain text]
   - [PDF]
   - [HTML]
 
-  If `[OUTPUT]` is omitted, the default is to write the viewer file to
-  stdout in plain text format.
+  If `-o <OUTPUT>` is omitted, the default is to write the viewer file
+  to stdout in plain text format.
 
   For viewer file input, [selection
   options](#viewer-file-selection-options) can select a subset of the
   viewer file for output.
 
+  If more than one viewer file is specified as input, they are
+  concatenated in the order specified to produce the output.
+
 * Chart XML files:
 
   The SPSS graph editor can export charts as individual XML files.
   PSPP can read and convert these to any of the support output
   formats.
 
+  Any number of viewer files or chart XML files may be specified as
+  input.
+
 [Converting SPSS Viewer Files]: pspp-convert-spv.md
 [Comma-separated value (CSV) files]: output.md#comma-separated-value-output-csv
 [SPSS system files]: output.md#system-file-output-sav
@@ -60,6 +66,11 @@ terminal if `[OUTPUT]` is omitted.
 
 `pspp convert` accepts the following options:
 
+* `-o <OUTPUT>`  
+  Sets the output file name.  The extension on `<OUTPUT>` determines
+  the default output format.  Without this option, PSPP writes output
+  to standard output.
+
 * `-e <ENCODING>`  
   `--encoding=<ENCODING>`  
   Sets the character encoding used to read text strings in the input
@@ -100,7 +111,7 @@ terminal if `[OUTPUT]` is omitted.
   > ⚠️ The password (and other command-line options) may be visible to
   > other users on multiuser systems.
 
-* `-o <OUTPUT_OPTIONS>`  
+* `-O <OUTPUT_OPTIONS>`  
   Adds `<OUTPUT_OPTIONS>` to the output engine configuration.  See
   [Output Drivers](output.md) for information on how to configure
   output.
index db8c3fa5eff5aa201ea444c1ccf5be255eec315a..588837c46de938358fca71274509c9ddadd385a4 100644 (file)
@@ -81,7 +81,7 @@ The following `<MODE>`s are available:
   that sets a limit on the number of cases to read.  Without this
   option, PSPP will not read any cases.
 
-* `-o <OUTPUT_OPTIONS>`  
+* `-O <OUTPUT_OPTIONS>`  
   Adds `<OUTPUT_OPTIONS>` to the output engine configuration.  See
   [Output Drivers](output.md) for information on how to configure
   output.
index 175eec211af4456f220b9aac7382124a3d0ab5c2..f70f2474cf4f877650a0458374dcc29790b6d710 100644 (file)
@@ -46,7 +46,7 @@ pub struct Convert {
     ///
     /// * SPSS exported chart files (usually `.xml`).
     #[arg(verbatim_doc_comment)]
-    input: PathBuf,
+    input: Vec<PathBuf>,
 
     /// Output file name (if omitted, output is written to stdout).
     ///
@@ -61,7 +61,7 @@ pub struct Convert {
     /// * plain text
     /// * PDF
     /// * HTML
-    #[arg(verbatim_doc_comment)]
+    #[arg(verbatim_doc_comment, long, short = 'o')]
     output: Option<PathBuf>,
 
     /// The encoding to use for reading the input file.
@@ -86,7 +86,7 @@ pub struct Convert {
     to_unicode: bool,
 
     /// Output driver configuration options.
-    #[arg(short = 'o')]
+    #[arg(short = 'O')]
     output_options: Vec<String>,
 
     /// Selection options for SPV input files.
@@ -103,6 +103,10 @@ impl Convert {
         dictionary: Dictionary,
         cases: Box<dyn Iterator<Item = Result<Case<Vec<Datum<ByteString>>>, AnyError>>>,
     ) -> Result<()> {
+        if self.input.len() > 1 {
+            bail!("'convert' only supports a single data file input");
+        }
+
         // Take only the first `self.max_cases` cases.
         let cases = cases.take(self.max_cases.unwrap_or(usize::MAX));
 
@@ -119,73 +123,92 @@ impl Convert {
     }
 
     pub fn run(self) -> Result<()> {
-        match FileType::from_file(&self.input)? {
-            Some(FileType::System { .. }) => {
-                fn warn(warning: anyhow::Error) {
-                    eprintln!("warning: {warning}");
-                }
+        let mut data = None;
+        let mut page_setup = None;
+        let mut items = Vec::new();
 
-                let mut system_file = ReadOptions::new(warn)
-                    .with_encoding(self.encoding)
-                    .with_password(self.password.clone())
-                    .open_file(&self.input)?;
-                if self.to_unicode {
-                    system_file = system_file.into_unicode();
-                }
-                let (dictionary, _, cases) = system_file.into_parts();
-                let cases = cases.map(|result| result.map_err(AnyError::from));
-                let cases = Box::new(cases)
-                    as Box<dyn Iterator<Item = Result<Case<Vec<Datum<ByteString>>>, AnyError>>>;
-                self.write_data(dictionary, cases)
+        for input in &self.input {
+            if data.is_some() {
+                bail!("convert: Only one data file may be specified.");
             }
-            Some(FileType::Portable) => {
-                fn warn_portable(warning: pspp::por::Warning) {
-                    eprintln!("warning: {warning}");
-                }
 
-                let portable_file = PortableFile::open_file(&self.input, warn_portable)?;
-                let (dictionary, _, cases) = portable_file.into_parts();
-                let cases = cases.map(|result| result.map_err(AnyError::from));
-                let cases = Box::new(cases)
-                    as Box<dyn Iterator<Item = Result<Case<Vec<Datum<ByteString>>>, AnyError>>>;
-                self.write_data(dictionary, cases)
-            }
-            Some(FileType::Pc) => {
-                fn warn_pc(warning: pspp::pc::Warning) {
-                    eprintln!("warning: {warning}");
+            match FileType::from_file(input)? {
+                Some(FileType::System { .. }) => {
+                    fn warn(warning: anyhow::Error) {
+                        eprintln!("warning: {warning}");
+                    }
+
+                    let mut system_file = ReadOptions::new(warn)
+                        .with_encoding(self.encoding)
+                        .with_password(self.password.clone())
+                        .open_file(input)?;
+                    if self.to_unicode {
+                        system_file = system_file.into_unicode();
+                    }
+                    let (dictionary, _, cases) = system_file.into_parts();
+                    let cases = cases.map(|result| result.map_err(AnyError::from));
+                    let cases = Box::new(cases)
+                        as Box<dyn Iterator<Item = Result<Case<Vec<Datum<ByteString>>>, AnyError>>>;
+                    data = Some((dictionary, cases));
                 }
-
-                let pc_file = PcFile::open_file(&self.input, warn_pc)?;
-                let (dictionary, _, cases) = pc_file.into_parts();
-                let cases = cases.map(|result| result.map_err(AnyError::from));
-                let cases = Box::new(cases)
-                    as Box<dyn Iterator<Item = Result<Case<Vec<Datum<ByteString>>>, AnyError>>>;
-                self.write_data(dictionary, cases)
-            }
-            Some(FileType::Viewer { .. }) => {
-                let mut archive = SpvArchive::open_file(&self.input, self.password.as_deref())?;
-                let (items, page_setup) = archive
-                    .read_outline(|e| eprintln!("{e}"))?
-                    .filter(&self.criteria)
-                    .read(&mut archive, &mut |e| eprintln!("{e}"))?
-                    .into_contents();
-                let mut output = self.open_driver("text")?;
-                if let Some(page_setup) = &page_setup {
-                    output.setup(page_setup);
+                Some(FileType::Portable) => {
+                    fn warn_portable(warning: pspp::por::Warning) {
+                        eprintln!("warning: {warning}");
+                    }
+
+                    let portable_file = PortableFile::open_file(input, warn_portable)?;
+                    let (dictionary, _, cases) = portable_file.into_parts();
+                    let cases = cases.map(|result| result.map_err(AnyError::from));
+                    let cases = Box::new(cases)
+                        as Box<dyn Iterator<Item = Result<Case<Vec<Datum<ByteString>>>, AnyError>>>;
+                    data = Some((dictionary, cases));
+                }
+                Some(FileType::Pc) => {
+                    fn warn_pc(warning: pspp::pc::Warning) {
+                        eprintln!("warning: {warning}");
+                    }
+
+                    let pc_file = PcFile::open_file(input, warn_pc)?;
+                    let (dictionary, _, cases) = pc_file.into_parts();
+                    let cases = cases.map(|result| result.map_err(AnyError::from));
+                    let cases = Box::new(cases)
+                        as Box<dyn Iterator<Item = Result<Case<Vec<Datum<ByteString>>>, AnyError>>>;
+                    data = Some((dictionary, cases));
                 }
-                for item in items {
-                    output.write(&Arc::new(item));
+                Some(FileType::Viewer { .. }) => {
+                    let mut archive = SpvArchive::open_file(input, self.password.as_deref())?;
+                    let (mut archive_items, archive_page_setup) = archive
+                        .read_outline(|e| eprintln!("{e}"))?
+                        .filter(&self.criteria)
+                        .read(&mut archive, &mut |e| eprintln!("{e}"))?
+                        .into_contents();
+                    page_setup = page_setup.or(archive_page_setup);
+                    items.append(&mut archive_items);
                 }
-                Ok(())
+                Some(FileType::Graph) => {
+                    let graph = Item::read_graph(input)?;
+                    items.push(graph);
+                }
+                Some(other) => bail!("{}: cannot convert {other} file", input.display()),
+                _ => bail!("{}: unknown input file format", input.display()),
+            }
+        }
+
+        if data.is_some() && !items.is_empty() {
+            bail!("convert: Data and viewer files may not both be specified.");
+        }
+
+        if let Some((dictionary, cases)) = data {
+            self.write_data(dictionary, cases)
+        } else {
+            let mut output = self.open_driver("text")?;
+            if let Some(page_setup) = &page_setup {
+                output.setup(page_setup);
             }
-            Some(FileType::Graph) => {
-                let graph = Item::read_graph(&self.input)?;
-                let mut output = self.open_driver("text")?;
-                output.write(&Arc::new(graph));
-                Ok(())
+            for item in items {
+                output.write(&Arc::new(item));
             }
-            Some(other) => bail!("{}: cannot convert {other} file", self.input.display()),
-            _ => bail!("{}: unknown input file format", self.input.display()),
+            Ok(())
         }
     }
 }
index 84775a1b15f73c6fa734728a81e68e00475422f7..420d8cf17f40f72719ed04b8940de2380ea0076e 100644 (file)
@@ -58,7 +58,7 @@ pub struct Dictionary {
     max_cases: usize,
 
     /// Output driver configuration options.
-    #[arg(short = 'o')]
+    #[arg(short = 'O')]
     output_options: Vec<String>,
 }
 
@@ -73,7 +73,7 @@ pub struct Metadata {
     output: Option<PathBuf>,
 
     /// Output driver configuration options.
-    #[arg(short = 'o')]
+    #[arg(short = 'O')]
     output_options: Vec<String>,
 }
 
index 81fccffdf6ff36c8f6f9654fe01765d95333d6c3..0cadb4b69f8233d5a59b4fa67ca34e55edd884c5 100644 (file)
@@ -162,7 +162,7 @@ pub struct Metadata {
     output: Option<PathBuf>,
 
     /// Output driver configuration options.
-    #[arg(short = 'o', help_heading = "Output options")]
+    #[arg(short = 'O', help_heading = "Output options")]
     output_options: Vec<String>,
 }
 
@@ -202,7 +202,7 @@ pub struct Histogram {
     output: Option<PathBuf>,
 
     /// Output driver configuration options.
-    #[arg(short = 'o', help_heading = "Output options")]
+    #[arg(short = 'O', help_heading = "Output options")]
     output_options: Vec<String>,
 }
 
index f3b2791ae2ade7e68b333ef79bd7b756b3060449..392cf029b21ddc0e1dd97cd5b1a986a739746d45 100644 (file)
@@ -106,7 +106,7 @@ pub struct Dictionary {
     max_cases: u64,
 
     /// Output driver configuration options.
-    #[arg(short = 'o', help_heading = "Output options")]
+    #[arg(short = 'O', help_heading = "Output options")]
     output_options: Vec<String>,
 }
 
@@ -189,7 +189,7 @@ pub struct Encodings {
     max_cases: u64,
 
     /// Output driver configuration options.
-    #[arg(short = 'o', help_heading = "Output options")]
+    #[arg(short = 'O', help_heading = "Output options")]
     output_options: Vec<String>,
 }
 
@@ -243,7 +243,7 @@ pub struct Raw {
     max_cases: u64,
 
     /// Output driver configuration options.
-    #[arg(short = 'o', help_heading = "Output options")]
+    #[arg(short = 'O', help_heading = "Output options")]
     output_options: Vec<String>,
 }
 
@@ -288,7 +288,7 @@ pub struct Decoded {
     encoding: Option<&'static Encoding>,
 
     /// Output driver configuration options.
-    #[arg(short = 'o', help_heading = "Output options")]
+    #[arg(short = 'O', help_heading = "Output options")]
     output_options: Vec<String>,
 }