1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::{
7 fs,
8 io::{self, Write},
9 process::ExitCode,
10};
11
12const DEFAULT_DICT: &str = "/usr/share/dict/words";
13const ALT_DICT: &str = "/usr/share/dict/web2";
14
15#[derive(Parser)]
16#[command(
17 name = "look",
18 version,
19 about = "Display lines beginning with a given string"
20)]
21pub struct Args {
22 #[arg(short = 'a', long = "alternative")]
24 alternative: bool,
25
26 #[arg(short = 'd', long = "alphanum")]
29 alphanum: bool,
30
31 #[arg(short = 'f', long = "ignore-case")]
33 ignore_case: bool,
34
35 #[arg(short = 't', long = "terminate")]
37 terminate: Option<char>,
38
39 pub string: String,
41
42 pub file: Option<String>,
44}
45
46pub fn run(args: Args) -> ExitCode {
47 let using_dict = args.file.is_none();
48 let alphanum = args.alphanum || using_dict;
50 let ignore_case = args.ignore_case || using_dict;
51
52 let path = if let Some(ref f) = args.file {
53 f.clone()
54 } else if args.alternative {
55 ALT_DICT.to_string()
56 } else if let Ok(w) = std::env::var("WORDLIST") {
57 w
58 } else {
59 DEFAULT_DICT.to_string()
60 };
61
62 let content = match fs::read_to_string(&path) {
63 Ok(c) => c,
64 Err(e) => {
65 eprintln!("look: {path}: {e}");
66 return ExitCode::from(2);
67 }
68 };
69
70 let search =
71 prepare_key(&args.string, args.terminate, alphanum, ignore_case);
72
73 let lines: Vec<&str> = content.lines().collect();
74
75 let start = lines.partition_point(|line| {
77 let key = line_key(line, search.len(), alphanum, ignore_case);
78 key < search
79 });
80
81 let stdout = io::stdout();
82 let mut out = io::BufWriter::new(stdout.lock());
83 let mut found = false;
84
85 for line in &lines[start..] {
86 let key = line_key(line, search.len(), alphanum, ignore_case);
87 if key != search {
88 break;
89 }
90 found = true;
91 if let Err(e) = writeln!(out, "{line}") {
92 eprintln!("look: {e}");
93 return ExitCode::from(2);
94 }
95 }
96
97 if found {
98 ExitCode::SUCCESS
99 } else {
100 ExitCode::from(1)
101 }
102}
103
104fn prepare_key(
106 s: &str,
107 terminate: Option<char>,
108 alphanum: bool,
109 ignore_case: bool,
110) -> String {
111 let s = match terminate {
112 Some(t) => match s.find(t) {
113 Some(pos) => &s[..pos + t.len_utf8()],
114 None => s,
115 },
116 None => s,
117 };
118 normalize(s, alphanum, ignore_case)
119}
120
121fn line_key(
124 line: &str,
125 key_len: usize,
126 alphanum: bool,
127 ignore_case: bool,
128) -> String {
129 if alphanum {
130 let mut result = String::with_capacity(key_len);
131 for ch in line.chars() {
132 if result.len() >= key_len {
133 break;
134 }
135 if ch.is_alphanumeric() || ch == ' ' || ch == '\t' {
136 if ignore_case {
137 for c in ch.to_lowercase() {
138 result.push(c);
139 }
140 } else {
141 result.push(ch);
142 }
143 }
144 }
145 result
146 } else {
147 let prefix: String = line.chars().take(key_len).collect();
148 if ignore_case {
149 prefix.to_lowercase()
150 } else {
151 prefix
152 }
153 }
154}
155
156fn normalize(s: &str, alphanum: bool, ignore_case: bool) -> String {
157 let filtered: String = if alphanum {
158 s.chars()
159 .filter(|c| c.is_alphanumeric() || *c == ' ' || *c == '\t')
160 .collect()
161 } else {
162 s.to_string()
163 };
164 if ignore_case {
165 filtered.to_lowercase()
166 } else {
167 filtered
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use std::io::Write;
175 use tempfile::NamedTempFile;
176
177 fn make_file(content: &str) -> NamedTempFile {
178 let mut f = NamedTempFile::new().unwrap();
179 f.write_all(content.as_bytes()).unwrap();
180 f.flush().unwrap();
181 f
182 }
183
184 fn run_look(
185 string: &str,
186 content: &str,
187 alphanum: bool,
188 ignore_case: bool,
189 terminate: Option<char>,
190 ) -> (ExitCode, String) {
191 let file = make_file(content);
192 let args = Args {
193 alternative: false,
194 alphanum,
195 ignore_case,
196 terminate,
197 string: string.to_string(),
198 file: Some(file.path().to_string_lossy().to_string()),
199 };
200
201 let search =
203 prepare_key(&args.string, args.terminate, alphanum, ignore_case);
204 let content = fs::read_to_string(file.path()).unwrap();
205 let lines: Vec<&str> = content.lines().collect();
206 let start = lines.partition_point(|line| {
207 let key = line_key(line, search.len(), alphanum, ignore_case);
208 key < search
209 });
210
211 let mut output = Vec::new();
212 let mut found = false;
213 for line in &lines[start..] {
214 let key = line_key(line, search.len(), alphanum, ignore_case);
215 if key != search {
216 break;
217 }
218 found = true;
219 writeln!(output, "{line}").unwrap();
220 }
221
222 let code = if found {
223 ExitCode::SUCCESS
224 } else {
225 ExitCode::from(1)
226 };
227 (code, String::from_utf8(output).unwrap())
228 }
229
230 #[test]
231 fn basic_prefix_match() {
232 let (_, out) =
233 run_look("b", "apple\nbanana\nberry\ncherry", false, false, None);
234 assert_eq!(out, "banana\nberry\n");
235 }
236
237 #[test]
238 fn no_match_returns_1() {
239 let (code, out) = run_look("zzz", "apple\nbanana", false, false, None);
240 assert_eq!(out, "");
241 assert_eq!(code, ExitCode::from(1));
242 }
243
244 #[test]
245 fn case_insensitive() {
246 let (_, out) = run_look("APPLE", "apple\nbanana", false, true, None);
247 assert_eq!(out, "apple\n");
248 }
249
250 #[test]
251 fn alphanum_mode() {
252 let (_, out) =
253 run_look("ban", "ban-ana\nbanana\nberry", true, false, None);
254 assert_eq!(out, "ban-ana\nbanana\n");
255 }
256
257 #[test]
258 fn terminate_char() {
259 let (_, out) = run_look(
260 "apple pie",
261 "apple\napple pie\nbanana",
262 false,
263 false,
264 Some(' '),
265 );
266 assert_eq!(out, "apple pie\n");
269 }
270}