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. <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::base::{RUMResult, RUMVec};
46    use crate::cpu::CPU_PAGE_SIZE;
47    use crate::strings::rumtk_format;
48    use std::io::{stdin, stdout, BufWriter, Read, Write};
49    #[cfg(any(target_os = "unix", target_os = "linux", target_os = "macos"))]
50    use std::os::fd::{AsRawFd, FromRawFd};
51
52    const STD_IN_STEP_SIZE: usize = u16::MAX as usize;
53
54    pub type BufferSlice = Vec<u8>;
55    pub type BufferChunk = [u8; STD_IN_STEP_SIZE];
56
57    ///
58    /// Consumes the incoming buffer in chunks of [CPU_PAGE_SIZE](CPU_PAGE_SIZE) bytes size
59    /// until no more bytes are present.
60    ///
61    /// To avoid calling a blocking read, we check if the read yielded an amount of bytes fewer than
62    /// the requested chunk size.
63    ///
64    /// ## Example
65    ///
66    /// ```
67    /// use rumtk_core::cli::cli_utils::{read_stdin};
68    /// use rumtk_core::cpu::CPU_PAGE_SIZE;
69    ///
70    /// let stdin_data = read_stdin(CPU_PAGE_SIZE).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(minimum_size: usize) -> RUMResult<RUMVec<u8>> {
76        let mut stdin_buffer = RUMVec::with_capacity(minimum_size);
77
78        loop {
79            let s = read_some_stdin(&mut stdin_buffer)?;
80
81            // If we attempt the next read, it is likely to be a 0 byte read. Why does this matter?
82            // Well, if the other end of the pipe is still open, the read call will stall in Rust's
83            // std and it is because it expects EOF but that never comes.
84            // If you look at https://man7.org/linux/man-pages/man2/read.2.html, read should return
85            // 0 and simply let us naturally break, but a read < than requested buffer appears to be
86            // an equally valid way to handle terminal and piped data.
87            // We have to check against 0 because the more optimal the buffer size we ask for (e.g.
88            // larger reads for minimum syscall time) the less likely the kernel is ready with the
89            // requested data.
90
91            // Example reads at u16::Max
92            // Read 65535 bytes
93            // Read 61441 bytes
94            // Read 65535 bytes
95            // Read 61441 bytes
96            // Read 8192 bytes
97            // Read 65535 bytes
98            // Read 61441 bytes
99            // Read 65535 bytes
100            // Read 61441 bytes
101            // Read 8192 bytes
102            // Read 65535 bytes
103            // Read 61441 bytes
104            // Read 65535 bytes
105            // Read 61441 bytes
106            // Read 8192 bytes
107            // Read 65535 bytes
108            // Read 61441 bytes
109            // Read 65535 bytes
110            // Read 61441 bytes
111            // Read 8192 bytes
112            // Read 65535 bytes
113            // Read 61441 bytes
114            // Read 65535 bytes
115            // Read 61441 bytes
116            // Read 8192 bytes
117            // Read 65535 bytes
118            // Read 61441 bytes
119            // Read 65535 bytes
120            // Read 61441 bytes
121            // Read 8192 bytes
122            // Read 65535 bytes
123            // Read 61441 bytes
124            // Read 65535 bytes
125            // Read 61441 bytes
126            // Read 8192 bytes
127            // Read 65535 bytes
128            // Read 61441 bytes
129            // Read 65535 bytes
130            // Read 61441 bytes
131            // Read 8192 bytes
132            // Read 65535 bytes
133            // Read 61441 bytes
134            // Read 65535 bytes
135            // Read 41974 bytes
136            // Read 0 bytes
137            // Total: 2331637 => 2.2MB
138            if s == 0 {
139                break;
140            }
141        }
142
143        Ok(stdin_buffer)
144    }
145
146    ///
147    /// Consumes the incoming buffer in chunks of [CPU_SIMD_64_SIZE](crate::cpu::CPU_SIMD_64_SIZE) bytes size.
148    ///
149    /// ## Example
150    ///
151    /// ```
152    /// use std::io::stdin;
153    /// use std::io::prelude::*;
154    /// use std::process::{Command, Stdio};
155    /// use rumtk_core::cli::cli_utils::{read_some_stdin};
156    /// use rumtk_core::cpu::CPU_PAGE_SIZE;
157    /// use rumtk_core::base::RUMVec;
158    ///
159    /// let mut stdin_buffer = RUMVec::with_capacity(CPU_PAGE_SIZE);
160    /// let mut s = read_some_stdin(&mut stdin_buffer).unwrap();
161    /// let mut totas_s = s;
162    /// while s > 0 {
163    ///    s = read_some_stdin(&mut stdin_buffer).unwrap();
164    ///    totas_s += s;
165    /// }
166    ///
167    /// assert_eq!(totas_s, 0, "Returned data with {} size even though we expected 0 bytes!", totas_s)
168    /// ```
169    ///
170    #[inline]
171    pub fn read_some_stdin(buf: &mut BufferSlice) -> RUMResult<usize> {
172        let mut chunk: BufferChunk = [0; STD_IN_STEP_SIZE];
173
174        match stdin().read(&mut chunk[..]) {
175            Ok(s) => {
176                buf.extend_from_slice(&chunk[..s]);
177                Ok(s)
178            }
179            Err(e) => Err(rumtk_format!("Error reading stdin chunk because {}!", e)),
180        }
181    }
182
183    ///
184    /// writes [`stringview`] to `stdout`.
185    ///
186    pub fn write_string_stdout(data: &str) -> RUMResult<()> {
187        write_stdout(&data.as_bytes())
188    }
189
190    ///
191    /// Writes [RUMBuffer] to `stdout`.
192    ///
193    #[cfg(target_os = "windows")]
194    pub fn write_stdout(data: &[u8]) -> RUMResult<()> {
195        match stdout().write_all(data) {
196            Ok(_) => {}
197            Err(e) => return Err(rumtk_format!("Error writing to stdout because => {}", e)),
198        };
199        flush_stdout()?;
200        Ok(())
201    }
202
203    #[cfg(any(target_os = "unix", target_os = "linux", target_os = "macos"))]
204    pub fn write_stdout(data: &[u8]) -> RUMResult<()> {
205        // Create an unbuffered File handle from file descriptor 1 (stdout)
206        let stdout_fd = stdout().as_raw_fd();
207
208        {
209            let mut file = BufWriter::new(unsafe { std::fs::File::from_raw_fd(stdout_fd) });
210
211            // Write bytes directly
212            match file.write_all(data) {
213                Ok(_) => {
214                    file.flush().unwrap_or_default();
215                    std::mem::forget(file);
216                },
217                Err(e) => return Err(rumtk_format!("Error writing to stdout because {}", e)),
218            };
219        }
220
221        flush_stdout()?;
222        Ok(())
223    }
224
225    fn flush_stdout() -> RUMResult<()> {
226        match stdout().flush() {
227            Ok(_) => Ok(()),
228            Err(e) => Err(rumtk_format!("Error flushing stdout because => {}", e)),
229        }
230    }
231
232    pub fn print_license_notice(program: &str, year: &str, author_list: &Vec<&str>) {
233        let authors = author_list.join(", ");
234        let notice = rumtk_format!(
235            r"  {program}  Copyright (C) {year}  {authors}
236                This program comes with ABSOLUTELY NO WARRANTY.
237                This is free software, and you are welcome to redistribute it
238                under certain conditions."
239        );
240        eprintln!("{}", notice);
241    }
242}
243
244pub mod macros {
245    ///
246    /// Reads STDIN and unescapes the incoming message.
247    /// Return this unescaped message.
248    ///
249    /// # Example
250    ///
251    /// ## Without specifying the minimum read size.
252    /// ```
253    /// use rumtk_core::base::{RUMResult, RUMVec};
254    /// use rumtk_core::buffers::*;
255    /// use rumtk_core::rumtk_read_stdin;
256    ///
257    /// fn test_read_stdin() -> RUMResult<RUMVec<u8>> {
258    ///     rumtk_read_stdin!()
259    /// }
260    ///
261    /// match test_read_stdin() {
262    ///     Ok(s) => (),
263    ///     Err(e) => panic!("Error reading stdin because => {}", e)
264    /// }
265    /// ```
266    ///
267    /// ## With specifying the minimum read size.
268    /// ```
269    /// use rumtk_core::base::{RUMResult, RUMVec};
270    /// use rumtk_core::buffers::*;
271    /// use rumtk_core::rumtk_read_stdin;
272    ///
273    /// fn test_read_stdin() -> RUMResult<RUMVec<u8>> {
274    ///     rumtk_read_stdin!(1024)
275    /// }
276    ///
277    /// match test_read_stdin() {
278    ///     Ok(s) => (),
279    ///     Err(e) => panic!("Error reading stdin because => {}", e)
280    /// }
281    /// ```
282    ///
283    #[macro_export]
284    macro_rules! rumtk_read_stdin {
285        (  ) => {{
286            use $crate::cpu::CPU_PAGE_SIZE;
287            use $crate::cli::cli_utils::{read_stdin};
288            read_stdin(CPU_PAGE_SIZE * CPU_PAGE_SIZE)
289        }};
290        ( $min_expected_size:expr ) => {{
291            use $crate::cli::cli_utils::{read_stdin};
292            read_stdin($min_expected_size)
293        }};
294    }
295
296    ///
297    /// Writes [RUMString](crate::strings::RUMString) or [RUMBuffer](crate::types::RUMBuffer) to `stdout`.
298    ///
299    /// If the `binary` parameter is passed, we push the `message` parameter directly to `stdout`. the
300    /// `message` parameter has to be of type [RUMBuffer](crate::types::RUMBuffer).
301    ///
302    /// ## Example
303    ///
304    /// ### Default / Pushing a String
305    /// ```
306    /// use rumtk_core::rumtk_write_stdout;
307    ///
308    /// rumtk_write_stdout!("I ❤ my wife!");
309    /// ```
310    ///
311    /// ## Pushing Binary Buffer
312    /// ```
313    /// use rumtk_core::rumtk_write_stdout;
314    /// use rumtk_core::buffers::{new_random_rumbuffer, DEFAULT_BUFFER_CHUNK_SIZE};
315    ///
316    /// let buffer = new_random_rumbuffer::<DEFAULT_BUFFER_CHUNK_SIZE>();
317    /// rumtk_write_stdout!(buffer, true);
318    /// ```
319    ///
320    #[macro_export]
321    macro_rules! rumtk_write_stdout {
322        ( $message:expr ) => {{
323            use $crate::cli::cli_utils::write_string_stdout;
324            write_string_stdout(&$message)
325        }};
326        ( $message:expr, $binary:expr ) => {{
327            use $crate::cli::cli_utils::write_stdout;
328            write_stdout(&$message)
329        }};
330    }
331
332    ///
333    /// Prints the mandatory GPL License Notice to terminal!
334    ///
335    /// # Example
336    /// ## Default
337    /// ```
338    /// use rumtk_core::rumtk_print_license_notice;
339    ///
340    /// rumtk_print_license_notice!();
341    /// ```
342    /// ## Program Only
343    /// ```
344    /// use rumtk_core::rumtk_print_license_notice;
345    ///
346    /// rumtk_print_license_notice!("RUMTK");
347    /// ```
348    /// ## Program + Year
349    /// ```
350    /// use rumtk_core::rumtk_print_license_notice;
351    ///
352    /// rumtk_print_license_notice!("RUMTK", "2025");
353    /// ```
354    /// ## Program + Year + Authors
355    /// ```
356    /// use rumtk_core::rumtk_print_license_notice;
357    ///
358    /// rumtk_print_license_notice!("RUMTK", "2025", &vec!["Luis M. Santos, M.D."]);
359    /// ```
360    ///
361    #[macro_export]
362    macro_rules! rumtk_print_license_notice {
363        ( ) => {{
364            use $crate::cli::cli_utils::print_license_notice;
365
366            print_license_notice("RUMTK", "2025", &vec!["Luis M. Santos, M.D."]);
367        }};
368        ( $program:expr ) => {{
369            use $crate::cli::cli_utils::print_license_notice;
370            print_license_notice(&$program, "2025", &vec!["2025", "Luis M. Santos, M.D."]);
371        }};
372        ( $program:expr, $year:expr ) => {{
373            use $crate::cli::cli_utils::print_license_notice;
374            print_license_notice(&$program, &$year, &vec!["Luis M. Santos, M.D."]);
375        }};
376        ( $program:expr, $year:expr, $authors:expr ) => {{
377            use $crate::cli::cli_utils::print_license_notice;
378            print_license_notice(&$program, &$year, &$authors);
379        }};
380    }
381}