Skip to main content

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