rumtk_core/cli.rs
1/*
2 * rumtk attempts to implement HL7 and medical protocols for interoperability in medicine.
3 * This toolkit aims to be reliable, simple, performant, and standards compliant.
4 * Copyright (C) 2025 Luis M. Santos, M.D. <lsantos@medicalmasses.com>
5 * Copyright (C) 2025 MedicalMasses L.L.C. <contact@medicalmasses.com>
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21///
22/// Tools for handling reading and writing from the standard I/O/E.
23///
24/// Per this [stackoverflow discussion](https://unix.stackexchange.com/questions/37508/in-what-order-do-piped-commands-run).
25/// Note:
26///```text
27/// Piped commands run concurrently. When you run ps | grep …, it's the luck of the draw (or a matter of details of the workings of the shell combined with scheduler fine-tuning deep in the bowels of the kernel) as to whether ps or grep starts first, and in any case they continue to execute concurrently.
28///
29/// This is very commonly used to allow the second program to process data as it comes out from the first program, before the first program has completed its operation. For example
30///
31/// grep pattern very-large-file | tr a-z A-Z
32/// begins to display the matching lines in uppercase even before grep has finished traversing the large file.
33///
34/// grep pattern very-large-file | head -n 1
35/// displays the first matching line, and may stop processing well before grep has finished reading its input file.
36///
37/// If you read somewhere that piped programs run in sequence, flee this document. Piped programs run concurrently and always have.
38/// ```
39///
40/// I bring the note above because that was my original understanding, but I have had to spend a
41/// crazy amount of time trying to get data flowing from one process to another without the initial
42/// process first exiting.
43///
44pub mod cli_utils {
45 use crate::core::{RUMResult, RUMVec};
46 use crate::strings::{rumtk_format, RUMStringConversions};
47 use crate::types::RUMBuffer;
48 use std::io::{stdin, stdout, Read, Write};
49
50 pub const BUFFER_SIZE: usize = 1024 * 4;
51 pub const BUFFER_CHUNK_SIZE: usize = 512;
52
53 pub type BufferSlice = Vec<u8>;
54 pub type BufferChunk = [u8; BUFFER_CHUNK_SIZE];
55
56 ///
57 /// Consumes the incoming buffer in chunks of [BUFFER_CHUNK_SIZE](BUFFER_CHUNK_SIZE) bytes size
58 /// until no more bytes are present.
59 ///
60 /// To avoid calling a blocking read, we check if the read yielded an amount of bytes fewer than
61 /// the requested chunk size.
62 ///
63 /// ## Example
64 ///
65 /// ```
66 /// use rumtk_core::cli::cli_utils::{read_stdin};
67 ///
68 /// let stdin_data = read_stdin().unwrap();
69 ///
70 /// assert_eq!(stdin_data.len(), 0, "Returned data with {} size even though we expected 0 bytes!", stdin_data.len())
71 /// ```
72 ///
73 pub fn read_stdin() -> RUMResult<RUMBuffer> {
74 let mut stdin_buffer = RUMVec::with_capacity(BUFFER_SIZE);
75 let mut s = read_some_stdin(&mut stdin_buffer)?;
76
77 while s > 0 {
78 s = read_some_stdin(&mut stdin_buffer)?;
79
80 // If we attempt the next read, it is likely to be a 0 byte read. Why does this matter?
81 // Well, if the other end of the pipe is still open, the read call will stall in Rust's
82 // std and I am not sure why.
83 // If you look at https://man7.org/linux/man-pages/man2/read.2.html, read should return
84 // 0 and simply let us naturally break, but a read < than requested buffer appears to be
85 // an equally canonical way to handle terminal and piped data.
86 if s < BUFFER_CHUNK_SIZE {
87 break;
88 }
89 }
90
91 Ok(RUMBuffer::from(stdin_buffer))
92 }
93
94 ///
95 /// Consumes the incoming buffer in chunks of [BUFFER_CHUNK_SIZE](BUFFER_CHUNK_SIZE) bytes size.
96 ///
97 /// ## Example
98 ///
99 /// ```
100 /// use std::io::stdin;
101 /// use std::io::prelude::*;
102 /// use std::process::{Command, Stdio};
103 /// use rumtk_core::cli::cli_utils::{read_some_stdin, BUFFER_SIZE, BUFFER_CHUNK_SIZE};
104 /// use rumtk_core::core::RUMVec;
105 ///
106 /// let mut stdin_buffer = RUMVec::with_capacity(BUFFER_SIZE);
107 /// let mut s = read_some_stdin(&mut stdin_buffer).unwrap();
108 /// let mut totas_s = s;
109 /// while s > 0 {
110 /// s = read_some_stdin(&mut stdin_buffer).unwrap();
111 /// totas_s += s;
112 /// }
113 ///
114 /// assert_eq!(totas_s, 0, "Returned data with {} size even though we expected 0 bytes!", totas_s)
115 /// ```
116 ///
117 pub fn read_some_stdin(buf: &mut BufferSlice) -> RUMResult<usize> {
118 let mut chunk: BufferChunk = [0; BUFFER_CHUNK_SIZE];
119 match stdin().read(&mut chunk) {
120 Ok(s) => {
121 let slice = &chunk[0..s];
122
123 if s > 0 {
124 buf.extend_from_slice(slice);
125 }
126
127 Ok(s)
128 }
129 Err(e) => Err(rumtk_format!("Error reading stdin chunk because {}!", e)),
130 }
131 }
132
133 ///
134 /// writes [`stringview`] to `stdout`.
135 ///
136 pub fn write_string_stdout(data: &str) -> RUMResult<()> {
137 write_stdout(&data.to_buffer())
138 }
139
140 ///
141 /// Writes [RUMBuffer] to `stdout`.
142 ///
143 pub fn write_stdout(data: &RUMBuffer) -> RUMResult<()> {
144 match stdout().write_all(data) {
145 Ok(_) => {}
146 Err(e) => return Err(rumtk_format!("Error writing to stdout because => {}", e)),
147 };
148 flush_stdout()?;
149 Ok(())
150 }
151
152 fn flush_stdout() -> RUMResult<()> {
153 match stdout().flush() {
154 Ok(_) => Ok(()),
155 Err(e) => Err(rumtk_format!("Error flushing stdout because => {}", e)),
156 }
157 }
158
159 pub fn print_license_notice(program: &str, year: &str, author_list: &Vec<&str>) {
160 let authors = author_list.join(", ");
161 let notice = rumtk_format!(
162 r" {program} Copyright (C) {year} {authors}
163 This program comes with ABSOLUTELY NO WARRANTY.
164 This is free software, and you are welcome to redistribute it
165 under certain conditions."
166 );
167 eprintln!("{}", notice);
168 }
169}
170
171pub mod macros {
172 ///
173 /// Reads STDIN and unescapes the incoming message.
174 /// Return this unescaped message.
175 ///
176 /// # Example
177 /// ```
178 /// use rumtk_core::core::RUMResult;
179 /// use rumtk_core::types::RUMBuffer;
180 /// use rumtk_core::rumtk_read_stdin;
181 ///
182 /// fn test_read_stdin() -> RUMResult<RUMBuffer> {
183 /// rumtk_read_stdin!()
184 /// }
185 ///
186 /// match test_read_stdin() {
187 /// Ok(s) => (),
188 /// Err(e) => panic!("Error reading stdin because => {}", e)
189 /// }
190 /// ```
191 ///
192 #[macro_export]
193 macro_rules! rumtk_read_stdin {
194 ( ) => {{
195 use $crate::cli::cli_utils::read_stdin;
196 read_stdin()
197 }};
198 }
199
200 ///
201 /// Writes [RUMString](crate::strings::RUMString) or [RUMBuffer](crate::types::RUMBuffer) to `stdout`.
202 ///
203 /// If the `binary` parameter is passed, we push the `message` parameter directly to `stdout`. the
204 /// `message` parameter has to be of type [RUMBuffer](crate::types::RUMBuffer).
205 ///
206 /// ## Example
207 ///
208 /// ### Default / Pushing a String
209 /// ```
210 /// use rumtk_core::rumtk_write_stdout;
211 ///
212 /// rumtk_write_stdout!("I ❤ my wife!");
213 /// ```
214 ///
215 /// ## Pushing Binary Buffer
216 /// ```
217 /// use rumtk_core::rumtk_write_stdout;
218 /// use rumtk_core::buffers::{new_random_rumbuffer, DEFAULT_BUFFER_CHUNK_SIZE};
219 ///
220 /// let buffer = new_random_rumbuffer::<DEFAULT_BUFFER_CHUNK_SIZE>();
221 /// rumtk_write_stdout!(buffer, true);
222 /// ```
223 ///
224 #[macro_export]
225 macro_rules! rumtk_write_stdout {
226 ( $message:expr ) => {{
227 use $crate::cli::cli_utils::write_string_stdout;
228 write_string_stdout(&$message)
229 }};
230 ( $message:expr, $binary:expr ) => {{
231 use $crate::cli::cli_utils::write_stdout;
232 write_stdout(&$message)
233 }};
234 }
235
236 ///
237 /// Prints the mandatory GPL License Notice to terminal!
238 ///
239 /// # Example
240 /// ## Default
241 /// ```
242 /// use rumtk_core::rumtk_print_license_notice;
243 ///
244 /// rumtk_print_license_notice!();
245 /// ```
246 /// ## Program Only
247 /// ```
248 /// use rumtk_core::rumtk_print_license_notice;
249 ///
250 /// rumtk_print_license_notice!("RUMTK");
251 /// ```
252 /// ## Program + Year
253 /// ```
254 /// use rumtk_core::rumtk_print_license_notice;
255 ///
256 /// rumtk_print_license_notice!("RUMTK", "2025");
257 /// ```
258 /// ## Program + Year + Authors
259 /// ```
260 /// use rumtk_core::rumtk_print_license_notice;
261 ///
262 /// rumtk_print_license_notice!("RUMTK", "2025", &vec!["Luis M. Santos, M.D."]);
263 /// ```
264 ///
265 #[macro_export]
266 macro_rules! rumtk_print_license_notice {
267 ( ) => {{
268 use $crate::cli::cli_utils::print_license_notice;
269
270 print_license_notice("RUMTK", "2025", &vec!["Luis M. Santos, M.D."]);
271 }};
272 ( $program:expr ) => {{
273 use $crate::cli::cli_utils::print_license_notice;
274 print_license_notice(&$program, "2025", &vec!["2025", "Luis M. Santos, M.D."]);
275 }};
276 ( $program:expr, $year:expr ) => {{
277 use $crate::cli::cli_utils::print_license_notice;
278 print_license_notice(&$program, &$year, &vec!["Luis M. Santos, M.D."]);
279 }};
280 ( $program:expr, $year:expr, $authors:expr ) => {{
281 use $crate::cli::cli_utils::print_license_notice;
282 print_license_notice(&$program, &$year, &$authors);
283 }};
284 }
285}