diff --git a/Makefile b/Makefile index 19bc50449..09190f2eb 100644 --- a/Makefile +++ b/Makefile @@ -149,6 +149,7 @@ TEST_PROGS := \ mktemp \ mv \ nl \ + od \ paste \ printf \ ptx \ diff --git a/src/od/od.rs b/src/od/od.rs index 969ee55ea..7b52011e6 100644 --- a/src/od/od.rs +++ b/src/od/od.rs @@ -14,11 +14,19 @@ extern crate getopts; use std::fs::File; use std::io::Read; use std::mem; -use std::path::Path; - +use std::io::BufReader; +use std::io::Write; +use std::io; + #[derive(Debug)] enum Radix { Decimal, Hexadecimal, Octal, Binary } - + +#[derive(Debug)] +enum InputSource<'a> { + FileName(&'a str ), + Stdin +} + pub fn uumain(args: Vec) -> i32 { let mut opts = getopts::Options::new(); @@ -50,51 +58,138 @@ pub fn uumain(args: Vec) -> i32 { Ok(r) => r, Err(f) => { panic!("Invalid -A/--address-radix\n{}", f) } }; - - let fname = match args.last() { - Some(n) => n, - None => { panic!("Need fname for now") ; } - }; - - odfunc(&input_offset_base, &fname); - - 0 + + // Gather up file names - args which don't start with '-' + let fnames = args[1..] + .iter() + .filter(|w| !w.starts_with('-') || w == &"--" ) // "--" starts with '-', but it denotes stdin, not a flag + .map(|x| match x.as_str() { "--" => InputSource::Stdin, x => InputSource::FileName(x)}) + .collect::>(); + + // With no filenames, od uses stdin as input. + if fnames.len() == 0 { + odfunc(&input_offset_base, &[InputSource::Stdin]) + } + else { + odfunc(&input_offset_base, &fnames) + } } -fn odfunc(input_offset_base: &Radix, fname: &str) { - let mut f = match File::open(Path::new(fname)) { - Ok(f) => f, - Err(e) => panic!("file error: {}", e) - }; - - let mut addr = 0; - let bytes = &mut [b'\x00'; 16]; - loop { - match f.read(bytes) { - Ok(0) => { - print_with_radix(input_offset_base, addr); - print!("\n"); - break; - } - Ok(n) => { - print_with_radix(input_offset_base, addr); - for b in 0 .. n / mem::size_of::() { - let bs = &bytes[(2 * b) .. (2 * b + 2)]; - let p: u16 = (bs[1] as u16) << 8 | bs[0] as u16; - print!(" {:06o}", p); +const LINEBYTES:usize = 16; +const WORDBYTES:usize = 2; + +fn odfunc(input_offset_base: &Radix, fnames: &[InputSource]) -> i32 { + + let mut status = 0; + let mut ni = fnames.iter(); + { + // Open and return the next file to process as a BufReader + // Returns None when no more files. + let mut next_file = || -> Option> { + // loop retries with subsequent files if err - normally 'loops' once + loop { + match ni.next() { + None => return None, + Some(input) => match *input { + InputSource::Stdin => return Some(Box::new(BufReader::new(std::io::stdin()))), + InputSource::FileName(fname) => match File::open(fname) { + Ok(f) => return Some(Box::new(BufReader::new(f))), + Err(e) => { + // If any file can't be opened, + // print an error at the time that the file is needed, + // then move on the the next file. + // This matches the behavior of the original `od` + let _ = writeln!(&mut std::io::stderr(), "od: '{}': {}", fname, e); + if status == 0 {status = 1} + } + } + } } - if n % mem::size_of::() == 1 { - print!(" {:06o}", bytes[n - 1]); - } - print!("\n"); - addr += n; - }, - Err(_) => { - print_with_radix(input_offset_base, addr); - break; } }; + + let mut curr_file: Box = match next_file() { + Some(f) => f, + None => { + return 1; + } + }; + + let mut exhausted = false; // There is no more input, gone to the end of the last file. + + // Fill buf with bytes read from the list of files + // Returns Ok() + // Handles io errors itself, thus always returns OK + // Fills the provided buffer completely, unless it has run out of input. + // If any call returns short (< buf.len()), all subsequent calls will return Ok<0> + let mut f_read = |buf: &mut [u8]| -> io::Result { + if exhausted { + Ok(0) + } else { + let mut xfrd = 0; + // while buffer we are filling is not full.. May go thru several files. + 'fillloop: while xfrd < buf.len() { + loop { // stdin may return on 'return' (enter), even though the buffer isn't full. + xfrd += match curr_file.read(&mut buf[xfrd..]) { + Ok(0) => break, + Ok(n) => n, + Err(e) => panic!("file error: {}", e), + }; + if xfrd == buf.len() { + // transferred all that was asked for. + break 'fillloop; + } + } + curr_file = match next_file() { + Some(f) => f, + None => { + exhausted = true; + break; + } + }; + } + Ok(xfrd) + } + }; + + let mut addr = 0; + let bytes = &mut [b'\x00'; LINEBYTES]; + loop { // print each line + print_with_radix(input_offset_base, addr); // print offset + match f_read(bytes) { + Ok(0) => { + print!("\n"); + break; + } + Ok(n) => { + print!(" "); // 4 spaces after offset - we print 2 more before each word + + for b in 0 .. n / mem::size_of::() { + let bs = &bytes[(2 * b) .. (2 * b + 2)]; + let p: u16 = (bs[1] as u16) << 8 | bs[0] as u16; + print!(" {:06o}", p); + } + if n % mem::size_of::() == 1 { + print!(" {:06o}", bytes[n - 1]); + } + + // Add extra spaces to pad out the short, presumably last, line. + if nwidth$}", "", width=(words_short)*(6+2)); + } + + print!("\n"); + addr += n; + }, + Err(_) => { + break; + } + }; + }; }; + status } fn parse_radix(radix_str: Option) -> Result { diff --git a/tests/od.rs b/tests/od.rs new file mode 100644 index 000000000..d177d344c --- /dev/null +++ b/tests/od.rs @@ -0,0 +1,128 @@ +#[macro_use] +mod common; + +use common::util::*; +use std::path::Path; +use std::env; +use std::io::Write; +use std::fs::File; +use std::fs::remove_file; + +static UTIL_NAME: &'static str = "od"; + +// octal dump of 'abcdefghijklmnopqrstuvwxyz\n' +static ALPHA_OUT: &'static str = "0000000 061141 062143 063145 064147 065151 066153 067155 070157\n0000020 071161 072163 073165 074167 075171 000012 \n0000033\n"; + +// XXX We could do a better job of ensuring that we have a fresh temp dir to ourself, +// not a general one ful of other proc's leftovers. + +// Test that od can read one file and dump with default format +#[test] +fn test_file() { + let (_, mut ucmd) = testing(UTIL_NAME); + use std::env; + let temp = env::temp_dir(); + let tmpdir = Path::new(&temp); + let file = tmpdir.join("test"); + + { + let mut f = File::create(&file).unwrap(); + match f.write_all(b"abcdefghijklmnopqrstuvwxyz\n") { + Err(_) => panic!("Test setup failed - could not write file"), + _ => {} + } + } + + let result = ucmd.arg(file.as_os_str()).run(); + + assert_empty_stderr!(result); + assert!(result.success); + assert_eq!(result.stdout, ALPHA_OUT); + + let _ = remove_file(file); +} + +// Test that od can read 2 files and concatenate the contents +#[test] +fn test_2files() { + let (_, mut ucmd) = testing(UTIL_NAME); + let temp = env::temp_dir(); + let tmpdir = Path::new(&temp); + let file1 = tmpdir.join("test1"); + let file2 = tmpdir.join("test2"); + + for &(n,a) in [(1,"a"), (2,"b")].iter() { + println!("number: {} letter:{}", n, a); + } + + for &(path,data)in &[(&file1, "abcdefghijklmnop"),(&file2, "qrstuvwxyz\n")] { + let mut f = File::create(&path).unwrap(); + match f.write_all(data.as_bytes()) { + Err(_) => panic!("Test setup failed - could not write file"), + _ => {} + } + } + + let result = ucmd.arg(file1.as_os_str()).arg(file2.as_os_str()).run(); + + assert_empty_stderr!(result); + assert!(result.success); + assert_eq!(result.stdout, ALPHA_OUT); + + let _ = remove_file(file1); + let _ = remove_file(file2); +} + +// Test that od gives non-0 exit val for filename that dosen't exist. +#[test] +fn test_no_file() { + let (_, mut ucmd) = testing(UTIL_NAME); + let temp = env::temp_dir(); + let tmpdir = Path::new(&temp); + let file = tmpdir.join("}surely'none'would'thus'a'file'name"); + + let result = ucmd.arg(file.as_os_str()).run(); + + assert!(!result.success); +} + +// Test that od reads from stdin instead of a file +#[test] +fn test_from_stdin() { + let (_, mut ucmd) = testing(UTIL_NAME); + + let input = "abcdefghijklmnopqrstuvwxyz\n"; + let result = ucmd.run_piped_stdin(input.as_bytes()); + + assert_empty_stderr!(result); + assert!(result.success); + assert_eq!(result.stdout, ALPHA_OUT); + +} + +// Test that od reads from stdin and also from files +#[test] +fn test_from_mixed() { + let (_, mut ucmd) = testing(UTIL_NAME); + + let temp = env::temp_dir(); + let tmpdir = Path::new(&temp); + let file1 = tmpdir.join("test-1"); + let file3 = tmpdir.join("test-3"); + + let (data1, data2, data3) = ("abcdefg","hijklmnop","qrstuvwxyz\n"); + for &(path,data)in &[(&file1, data1),(&file3, data3)] { + let mut f = File::create(&path).unwrap(); + match f.write_all(data.as_bytes()) { + Err(_) => panic!("Test setup failed - could not write file"), + _ => {} + } + } + + let result = ucmd.arg(file1.as_os_str()).arg("--").arg(file3.as_os_str()).run_piped_stdin(data2.as_bytes()); + + assert_empty_stderr!(result); + assert!(result.success); + assert_eq!(result.stdout, ALPHA_OUT); + +}