aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock289
-rw-r--r--Cargo.toml10
-rw-r--r--README.md78
-rw-r--r--src/main.rs340
-rw-r--r--test_data/a.txt9
-rw-r--r--test_data/b.txt5
-rw-r--r--test_data/c.txt5
-rw-r--r--test_data/sentences.txt14
9 files changed, 751 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..9e3897a
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,289 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "anstream"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "4.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "getrandom"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "libc"
+version = "0.2.153"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "spout"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "rand",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
+
+[[package]]
+name = "syn"
+version = "2.0.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..21804a9
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "spout"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+rand = "0.8.5"
+clap = { version = "4.5.2", features = ["derive"] }
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..861a4f0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,78 @@
+# spout
+
+A terminal application for generating nonsense.
+
+## Installation
+
+1. Clone this repo
+2. `cargo build`
+
+## Usage
+
+### Command line options
+
+```
+Usage: spout [OPTIONS] <DIRECTORY>
+
+Arguments:
+ <DIRECTORY> Path to data directory
+
+Options:
+ -n, --sentences <N_SENTENCES> Number of sentences to generate [default: 1]
+ --unique Don't re-use entries in data files (program will fail upon running out)
+ -h, --help Print help
+```
+
+### Template syntax
+
+In order to generate a sentence, the program will fill in gaps in a sentence template.
+Every randomized part of a sentence is surrounded on both sides by `%%`.
+Between these markers is some kind of definition of how the gap should be filled:
+
+```
+Direct: %%foo%% - choose a word from the 'foo' category
+Variable: %%?foo%% - choose a word from the category assigned to the 'foo' variable
+Multiple: %%foo|bar%% - choose a word from either the 'foo' or 'bar' category
+```
+
+Categories are sets of words/phrases that are randomly chosen from during sentence generation.
+
+Variables are randomly assigned a category from all available categories, and will be interpreted as that category throughout a sentence.
+This is useful for when you want to include multiple words from the same category, but don't care which one.
+
+Separating category names and variables with a `|` will choose between the given options. This can be used when you want to pick a word from "foo" or "bar", but not "baz".
+
+### Data files
+
+This program requires a directory of text files in a particular format in order to work.
+The directory can be anywhere on your system that you have read access.
+This directory must contain a text file named `sentences.txt`.
+It must also contain a text file for every template category that is used in the sentences file.
+The program does not enforce the latter restriction until it attempts to generate sentences, crashing if a category file is missing.
+
+The sentences file must contain exactly one sentence template per line.
+Each category file also separates words by line.
+Indeed, "words" can actually be phrases with spaces and punctuation (but not line breaks).
+In both types of file, empty lines are ignored.
+
+### Examples
+
+These examples suppose the following files and sentence templates.
+
+Example directory:
+```
+bar.txt
+baz.txt
+foo.txt
+sentences.txt
+```
+
+Example templates:
+```
+I think %%foo%% is pretty neat.
+I think %%bar%% is kind of lame.
+%%foo%% is certainly better than %%bar%%.
+I would take %%foo|baz%% over %%bar%% any day.
+%%?x%% and %%?x%% are certainly things that exist.
+```
+
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..2fe5a26
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,340 @@
+use std::collections::HashMap;
+use std::ffi::{OsStr, OsString};
+use std::fmt;
+use std::fs;
+use std::io;
+use std::path::PathBuf;
+use std::process;
+
+use rand::{thread_rng, seq::{SliceRandom, IteratorRandom}};
+use clap::Parser;
+
+#[derive(Debug)]
+enum Error {
+ NoSentences,
+ MultipleSentences,
+ BadTemplate(String),
+ IOFail(io::Error),
+ OutOfSentences,
+ OutOfWords(OsString),
+ UnknownCategory(OsString),
+ NoCategories,
+ DuplicateCategory(OsString),
+ BadPath(PathBuf),
+ EmptyOption,
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Error::NoSentences =>
+ write!(f, "no sentences.txt file found"),
+ Error::MultipleSentences =>
+ write!(f, "multiple sentences.txt files found, i'll be real i don't know how this is possible"),
+ Error::BadTemplate(t) =>
+ write!(f, "unable to parse template sentence: {}", t),
+ Error::IOFail(e) =>
+ write!(f, "IO error: {:?}", e),
+ Error::OutOfSentences =>
+ write!(f, "ran out of sentences"),
+ Error::OutOfWords(c) =>
+ write!(f, "ran out of words in category: {:?}", c),
+ Error::UnknownCategory(c) =>
+ write!(f, "tried to pick word in unknown category: {:?}", c),
+ Error::NoCategories =>
+ write!(f, "no categories provided"),
+ Error::DuplicateCategory(c) =>
+ write!(f, "tried to create a category twice: {:?}", c),
+ Error::BadPath(p) =>
+ write!(f, "failed to parse path: {:?}", p),
+ Error::EmptyOption =>
+ write!(f, "multi-option variable had an empty option"),
+ }
+ }
+}
+
+impl From<io::Error> for Error {
+ fn from(error: io::Error) -> Self {
+ Error::IOFail(error)
+ }
+}
+
+type ProgResult<T> = Result<T, Error>;
+
+#[derive(Parser, Debug)]
+#[command(name = "spout")]
+#[command(about = "Generates nonsense", long_about = None)]
+struct Args {
+ /// Path to data directory
+ directory: String,
+
+ /// Number of sentences to generate
+ #[arg(short = 'n', long = "sentences", default_value_t = 1)]
+ n_sentences: u32,
+
+ /// Don't re-use entries in data files (program will fail upon running out)
+ #[arg(long)]
+ unique: bool,
+}
+
+#[derive(Debug, Clone)]
+enum DropIn {
+ Basic(String),
+ Var(String),
+ OneOf(Vec<DropIn>),
+}
+
+impl DropIn {
+ pub fn parse(text: &str) -> ProgResult<Self> {
+ if text.is_empty() {
+ return Err(Error::EmptyOption)
+ }
+
+ if text.contains('|') {
+ let mut option_strs = text.split('|').collect::<Vec<&str>>();
+ option_strs.dedup();
+ let options = option_strs.into_iter()
+ .map(DropIn::parse)
+ .collect::<Result<Vec<_>, _>>()?;
+ return Ok(DropIn::OneOf(options))
+ }
+
+ if let Some(var_name) = text.strip_prefix('?') {
+ return Ok(DropIn::Var(var_name.to_string()));
+ }
+
+ Ok(DropIn::Basic(text.to_string()))
+ }
+}
+
+#[derive(Debug)]
+struct CategorySet {
+ var_table: HashMap<String, OsString>,
+ categories: HashMap<OsString, Vec<String>>,
+ unique: bool,
+}
+
+impl CategorySet {
+ pub fn new(unique: bool) -> Self {
+ CategorySet {
+ var_table: HashMap::new(),
+ categories: HashMap::new(),
+ unique,
+ }
+ }
+
+ fn resolve_variable(&mut self, var: &str) -> ProgResult<OsString> {
+ if !self.var_table.contains_key(var) {
+ let new_cat = self.random_category()?;
+ self.var_table.insert(var.to_string(), new_cat);
+ }
+ Ok(self.var_table.get(var).unwrap().clone())
+ }
+
+ pub fn add_category(&mut self, name: OsString, mut words: Vec<String>) -> ProgResult<()> {
+ words.retain(|s| !s.is_empty());
+ words.shuffle(&mut thread_rng());
+ match self.categories.insert(name.clone(), words) {
+ None => Ok(()),
+ Some(_) => Err(Error::DuplicateCategory(name)),
+ }
+ }
+
+ // not affected by --unique
+ pub fn random_category(&self) -> ProgResult<OsString> {
+ self.categories.keys()
+ .choose(&mut thread_rng())
+ .ok_or(Error::NoCategories)
+ .map(|c| c.clone())
+ }
+
+ pub fn random_from_drop_in(&mut self, drop_in: &DropIn) -> ProgResult<String> {
+ let simple_di = if let DropIn::OneOf(ds) = drop_in {
+ ds.choose(&mut thread_rng())
+ .expect("got a OneOf that is empty, somehow")
+ } else {
+ &drop_in
+ };
+
+ let category = match simple_di {
+ DropIn::Basic(s) => {
+ OsString::from(s)
+ },
+ DropIn::Var(s) => {
+ self.resolve_variable(&s)?
+ },
+ DropIn::OneOf(_) => {
+ panic!("Nested OneOf constructs are not supported")
+ },
+ };
+
+ let words = self.categories.get_mut(&category)
+ .ok_or(Error::UnknownCategory(category.clone()))?;
+
+ if self.unique {
+ words.pop()
+ .ok_or(Error::OutOfWords(category))
+ } else {
+ words.choose(&mut thread_rng())
+ .ok_or(Error::OutOfWords(category))
+ .map(|c| c.clone())
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+struct Sentence {
+ text_fragments: Vec<String>,
+ drop_ins: Vec<DropIn>,
+}
+
+impl Sentence {
+ pub fn parse(original: String) -> ProgResult<Self> {
+ let mut text = original.as_str();
+ let mut text_fragments = vec![];
+ let mut drop_ins = vec![];
+
+ while let Some((front, back)) = text.split_once("%%") {
+ let (drop_in_body, rest) = back.split_once("%%")
+ .ok_or(Error::BadTemplate(original.clone()))?;
+ let drop_in = DropIn::parse(drop_in_body)?;
+
+ text_fragments.push(front.to_string());
+ drop_ins.push(drop_in);
+ text = rest;
+ }
+ text_fragments.push(text.to_string());
+
+ Ok(Sentence {
+ text_fragments,
+ drop_ins,
+ })
+ }
+
+ pub fn generate(&self, categories: &mut CategorySet) -> ProgResult<String> {
+ let mut text_iter = self.text_fragments.iter();
+ let mut drop_ins_iter = self.drop_ins.iter();
+ let mut buffer = String::new();
+
+ while let Some(drop_in) = drop_ins_iter.next() {
+ let text = text_iter.next()
+ .expect("invariant: number of text = number of drop-in + 1");
+ buffer.push_str(text);
+
+ let drop_in_text = categories.random_from_drop_in(&drop_in)?;
+ buffer.push_str(drop_in_text.as_str());
+ }
+
+ let end_text = text_iter.next()
+ .expect("invariant: number of text = number of drop-in + 1");
+ buffer.push_str(end_text);
+
+ Ok(buffer)
+ }
+}
+
+#[derive(Debug)]
+struct SentenceSet {
+ sentences: Vec<Sentence>,
+ unique: bool,
+}
+
+impl SentenceSet {
+ pub fn new(mut sentence_strings: Vec<String>, unique: bool) -> ProgResult<Self> {
+ sentence_strings.retain(|s| !s.is_empty());
+ sentence_strings.shuffle(&mut thread_rng());
+
+ let sentences = sentence_strings.into_iter()
+ .map(Sentence::parse)
+ .collect::<Result<Vec<_>, _>>()?;
+
+ Ok(SentenceSet {
+ sentences,
+ unique,
+ })
+ }
+
+ pub fn random_sentence(&mut self) -> ProgResult<Sentence> {
+ if self.unique {
+ self.sentences.pop()
+ .ok_or(Error::OutOfSentences)
+ } else {
+ self.sentences.choose(&mut thread_rng())
+ .ok_or(Error::OutOfSentences)
+ .map(|s| s.clone())
+ }
+ }
+}
+
+fn read_lines(path: &PathBuf) -> ProgResult<Vec<String>> {
+ let mut lines = vec![];
+
+ let text = fs::read_to_string(path)?;
+ for line in text.lines() {
+ lines.push(line.to_string());
+ }
+
+ Ok(lines)
+}
+
+fn read_files(dir_path: &str, unique: bool) -> ProgResult<(SentenceSet, CategorySet)> {
+ let mut opt_sentences: Option<SentenceSet> = None;
+ let mut categories = CategorySet::new(unique);
+
+ let dir = fs::read_dir(dir_path)?;
+ for entry in dir {
+ let path_buf = entry?.path();
+ if path_buf.is_file() {
+ let extension = path_buf.extension()
+ .ok_or(Error::BadPath(path_buf.clone()))?;
+ if extension == OsStr::new("txt") {
+ let lines = read_lines(&path_buf)?;
+ let filename = path_buf.file_name()
+ .ok_or(Error::BadPath(path_buf.clone()))?;
+ if filename == OsStr::new("sentences.txt") {
+ if opt_sentences.is_none() {
+ opt_sentences = Some(SentenceSet::new(lines, unique)?);
+ } else {
+ return Err(Error::MultipleSentences);
+ }
+ } else {
+ let cat_name = path_buf.file_stem()
+ .expect("filename stem unwrapping osstr")
+ .to_os_string();
+ categories.add_category(cat_name, lines)?;
+ }
+ }
+ }
+ }
+
+ match opt_sentences {
+ None => Err(Error::NoSentences),
+ Some(s) => Ok((s, categories)),
+ }
+}
+
+fn crash(e: Error) -> ! {
+ eprintln!("[ERROR] {}", e);
+ process::exit(1);
+}
+
+fn sub_main(args: Args) -> ProgResult<()> {
+ let (mut sentences, mut categories) = read_files(&args.directory, args.unique)?;
+
+ for _i in 0..args.n_sentences {
+ let sentence = sentences.random_sentence()?;
+ let generated = sentence.generate(&mut categories)?;
+ println!("{}", generated);
+ }
+
+ Ok(())
+}
+
+fn main() {
+ let args = Args::parse();
+
+ if let Err(e) = sub_main(args) {
+ crash(e)
+ }
+}
+
diff --git a/test_data/a.txt b/test_data/a.txt
new file mode 100644
index 0000000..a9c9381
--- /dev/null
+++ b/test_data/a.txt
@@ -0,0 +1,9 @@
+a_one
+a_two
+a_three
+a_four with spaces
+a_five
+
+
+
+a_six lots of line breaks above this one
diff --git a/test_data/b.txt b/test_data/b.txt
new file mode 100644
index 0000000..b6bb927
--- /dev/null
+++ b/test_data/b.txt
@@ -0,0 +1,5 @@
+b_one
+b_two
+b_three
+b_four with spaces
+b_five
diff --git a/test_data/c.txt b/test_data/c.txt
new file mode 100644
index 0000000..76cc922
--- /dev/null
+++ b/test_data/c.txt
@@ -0,0 +1,5 @@
+c_one
+c_two
+c_three
+c_four with spaces
+c_five
diff --git a/test_data/sentences.txt b/test_data/sentences.txt
new file mode 100644
index 0000000..1538972
--- /dev/null
+++ b/test_data/sentences.txt
@@ -0,0 +1,14 @@
+This sentence tests type a: %%a%%.
+This sentence tests type b: %%b%%.
+This sentence tests type c: %%c%%.
+This sentence uses types a, b, and c in reverse order: %%c%% %%b%% %%a%%.
+This sentence uses a variable: %%?x%%
+This sentence uses one variable once, then another twice: %%?x%% %%?y%% %%?y%%
+This sentence uses four different variables: %%?w%% %%?x%% %%?y%% %%?z%%
+This sentence uses four identical variables: %%?x%% %%?x%% %%?x%% %%?x%%
+This sentence uses type c, then a variable: %%c%% %%?x%%
+This sentence uses type c, then a variable named 'c': %%c%% %%?c%%
+This sentence uses 10 identical variables: %%?x%% %%?x%% %%?x%% %%?x%% %%?x%% %%?x%% %%?x%% %%?x%% %%?x%% %%?x%%
+This sentence uses either type a or b: %%a|b%%
+This sentence uses a variable, then either type a or the same variable: %%?x%% %%a|?x%%
+malformed sentence %%a||d%%