1 #[macro_use]
2 extern crate clap;
3 
4 mod lib;
5 use lib::Petnames;
6 
7 use std::collections::HashSet;
8 use std::fmt;
9 use std::fs;
10 use std::io::{self, Write};
11 use std::path;
12 use std::process;
13 use std::str::FromStr;
14 
15 use clap::Arg;
16 use rand::seq::IteratorRandom;
17 
main()18 fn main() {
19     let matches = app().get_matches();
20     match run(matches) {
21         Err(Error::Disconnected) => {
22             process::exit(0);
23         }
24         Err(e) => {
25             eprintln!("Error: {}", e);
26             process::exit(1);
27         }
28         Ok(()) => {
29             process::exit(0);
30         }
31     }
32 }
33 
34 enum Error {
35     Io(io::Error),
36     FileIo(path::PathBuf, io::Error),
37     Cardinality(String),
38     Alliteration(String),
39     Disconnected,
40 }
41 
42 impl fmt::Display for Error {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result43     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44         match *self {
45             Error::Io(ref e) => write!(f, "{}", e),
46             Error::FileIo(ref path, ref e) => write!(f, "{}: {}", e, path.display()),
47             Error::Cardinality(ref message) => write!(f, "cardinality is zero: {}", message),
48             Error::Alliteration(ref message) => write!(f, "cannot alliterate: {}", message),
49             Error::Disconnected => write!(f, "caller disconnected / stopped reading"),
50         }
51     }
52 }
53 
54 impl From<io::Error> for Error {
from(error: io::Error) -> Self55     fn from(error: io::Error) -> Self {
56         Error::Io(error)
57     }
58 }
59 
app<'a, 'b>() -> clap::App<'a, 'b>60 fn app<'a, 'b>() -> clap::App<'a, 'b> {
61     clap::App::new("rust-petname")
62         .version(crate_version!())
63         .author(crate_authors!())
64         .about("Generate human readable random names.")
65         .after_help(concat!(
66             "Based on Dustin Kirkland's petname project ",
67             "<https://github.com/dustinkirkland/petname>."
68         ))
69         .arg(
70             Arg::with_name("words")
71                 .short("w")
72                 .long("words")
73                 .value_name("WORDS")
74                 .default_value("2")
75                 .help("Number of words in name")
76                 .takes_value(true)
77                 .validator(can_be_parsed::<u8>),
78         )
79         .arg(
80             Arg::with_name("separator")
81                 .short("s")
82                 .long("separator")
83                 .value_name("SEP")
84                 .default_value("-")
85                 .help("Separator between words")
86                 .takes_value(true),
87         )
88         .arg(
89             Arg::with_name("complexity")
90                 .short("c")
91                 .long("complexity")
92                 .value_name("COM")
93                 .possible_values(&["0", "1", "2"])
94                 .hide_possible_values(true)
95                 .default_value("0")
96                 .help("Use small words (0), medium words (1), or large words (2)")
97                 .takes_value(true),
98         )
99         .arg(
100             Arg::with_name("directory")
101                 .short("d")
102                 .long("dir")
103                 .value_name("DIR")
104                 .help("Directory containing adjectives.txt, adverbs.txt, names.txt")
105                 .conflicts_with("complexity")
106                 .takes_value(true),
107         )
108         .arg(
109             Arg::with_name("count")
110                 .long("count")
111                 .value_name("COUNT")
112                 .default_value("1")
113                 .help("Generate multiple names; pass 0 to produce infinite names!")
114                 .takes_value(true)
115                 .validator(can_be_parsed::<usize>),
116         )
117         .arg(
118             Arg::with_name("letters")
119                 .short("l")
120                 .long("letters")
121                 .value_name("LETTERS")
122                 .default_value("0")
123                 .help("Maxiumum number of letters in each word; 0 for unlimited")
124                 .takes_value(true)
125                 .validator(can_be_parsed::<usize>),
126         )
127         .arg(
128             Arg::with_name("alliterate")
129                 .short("a")
130                 .long("alliterate")
131                 .help("Generate names where each word begins with the same letter")
132                 .takes_value(false),
133         )
134         .arg(
135             // For compatibility with upstream.
136             Arg::with_name("ubuntu")
137                 .short("u")
138                 .long("ubuntu")
139                 .help("Alias; see --alliterate")
140                 .takes_value(false),
141         )
142 }
143 
run(matches: clap::ArgMatches) -> Result<(), Error>144 fn run(matches: clap::ArgMatches) -> Result<(), Error> {
145     // Unwrapping is safe because these options have defaults.
146     let opt_separator = matches.value_of("separator").unwrap();
147     let opt_words = matches.value_of("words").unwrap();
148     let opt_complexity = matches.value_of("complexity").unwrap();
149     let opt_count = matches.value_of("count").unwrap();
150     let opt_letters = matches.value_of("letters").unwrap();
151     let opt_alliterate = matches.is_present("alliterate") || matches.is_present("ubuntu");
152 
153     // Optional arguments without defaults.
154     let opt_directory = matches.value_of("directory");
155 
156     // Parse numbers. Validated so unwrapping is okay.
157     let opt_words: u8 = opt_words.parse().unwrap();
158     let opt_count: usize = opt_count.parse().unwrap();
159     let opt_letters: usize = opt_letters.parse().unwrap();
160 
161     // Load custom word lists, if specified.
162     let words = match opt_directory {
163         Some(dirname) => Words::load(dirname)?,
164         None => Words::Builtin,
165     };
166 
167     // Select the appropriate word list.
168     let mut petnames = match words {
169         Words::Custom(ref adjectives, ref adverbs, ref names) => {
170             Petnames::init(&adjectives, &adverbs, &names)
171         }
172         Words::Builtin => match opt_complexity {
173             "0" => Petnames::small(),
174             "1" => Petnames::medium(),
175             "2" => Petnames::large(),
176             _ => Petnames::small(),
177         },
178     };
179 
180     // If requested, limit the number of letters.
181     if opt_letters != 0 {
182         petnames.retain(|s| s.len() <= opt_letters);
183     }
184 
185     // Check cardinality.
186     if petnames.cardinality(opt_words) == 0 {
187         return Err(Error::Cardinality(
188             "no petnames to choose from; try relaxing constraints".to_string(),
189         ));
190     }
191 
192     // We're going to need a source of randomness.
193     let mut rng = rand::thread_rng();
194 
195     // If requested, choose a random letter then discard all words that do not
196     // begin with that letter.
197     if opt_alliterate {
198         // We choose the first letter from the intersection of the first letters
199         // of each word list in `petnames`.
200         let firsts =
201             common_first_letters(&petnames.adjectives, &[&petnames.adverbs, &petnames.names]);
202         // Choose the first letter at random; fails if there are no letters.
203         match firsts.iter().choose(&mut rng) {
204             Some(c) => petnames.retain(|s| s.starts_with(*c)),
205             None => {
206                 return Err(Error::Alliteration(
207                     "word lists have no initial letters in common".to_string(),
208                 ))
209             }
210         };
211     }
212 
213     // Get an iterator for the names we want to print out.
214     let names = petnames.iter(&mut rng, opt_words, opt_separator);
215 
216     // Manage stdout.
217     let stdout = io::stdout();
218     let mut writer = io::BufWriter::new(stdout.lock());
219 
220     // Stream if count is 0.
221     if opt_count == 0 {
222         for name in names {
223             writeln!(writer, "{}", name).map_err(suppress_disconnect)?;
224         }
225     } else {
226         for name in names.take(opt_count) {
227             writeln!(writer, "{}", name)?;
228         }
229     }
230 
231     Ok(())
232 }
233 
can_be_parsed<INTO>(value: String) -> Result<(), String> where INTO: FromStr, <INTO as FromStr>::Err: std::fmt::Display,234 fn can_be_parsed<INTO>(value: String) -> Result<(), String>
235 where
236     INTO: FromStr,
237     <INTO as FromStr>::Err: std::fmt::Display,
238 {
239     match value.parse::<INTO>() {
240         Err(e) => Err(format!("{}", e)),
241         Ok(_) => Ok(()),
242     }
243 }
244 
common_first_letters(init: &[&str], more: &[&[&str]]) -> HashSet<char>245 fn common_first_letters(init: &[&str], more: &[&[&str]]) -> HashSet<char> {
246     let mut firsts = first_letters(init);
247     let firsts_other: Vec<HashSet<char>> = more.iter().map(|list| first_letters(list)).collect();
248     firsts.retain(|c| firsts_other.iter().all(|fs| fs.contains(c)));
249     firsts
250 }
251 
first_letters(names: &[&str]) -> HashSet<char>252 fn first_letters(names: &[&str]) -> HashSet<char> {
253     names.iter().filter_map(|s| s.chars().next()).collect()
254 }
255 
256 enum Words {
257     Custom(String, String, String),
258     Builtin,
259 }
260 
261 impl Words {
262     // Load word lists from the given directory. This function expects to find three
263     // files in that directory: `adjectives.txt`, `adverbs.txt`, and `names.txt`.
264     // Each should be valid UTF-8, and contain words separated by whitespace.
load<T: AsRef<path::Path>>(dirname: T) -> Result<Self, Error>265     fn load<T: AsRef<path::Path>>(dirname: T) -> Result<Self, Error> {
266         let dirname = dirname.as_ref();
267         Ok(Self::Custom(
268             read_file_to_string(dirname.join("adjectives.txt"))?,
269             read_file_to_string(dirname.join("adverbs.txt"))?,
270             read_file_to_string(dirname.join("names.txt"))?,
271         ))
272     }
273 }
274 
read_file_to_string<P: AsRef<path::Path>>(path: P) -> Result<String, Error>275 fn read_file_to_string<P: AsRef<path::Path>>(path: P) -> Result<String, Error> {
276     fs::read_to_string(&path).map_err(|error| Error::FileIo(path.as_ref().to_path_buf(), error))
277 }
278 
suppress_disconnect(err: io::Error) -> Error279 fn suppress_disconnect(err: io::Error) -> Error {
280     match err.kind() {
281         io::ErrorKind::BrokenPipe => Error::Disconnected,
282         _ => err.into(),
283     }
284 }
285