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