├── .gitignore ├── LICENSE.md ├── README.md ├── applets ├── _false.rs ├── _true.rs ├── cal.rs ├── cat.rs ├── clear.rs ├── dirname.rs ├── echo.rs ├── head.rs ├── mkdir.rs ├── mod.rs ├── pwd.rs ├── rmdir.rs ├── seq.rs ├── sleep.rs ├── tee.rs ├── wc.rs ├── which.rs └── yes.rs ├── common.rs ├── roundup.sh ├── run-test.sh ├── rustybox.rs └── tests ├── cal.sh ├── dirname.sh └── seq.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Lau Ching Jun 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rustybox 2 | Rustybox is something like busybox, but written in Rust language. 3 | 4 | # How to build 5 | To compile, run 6 | ``` 7 | rustc rustybox.rs 8 | ``` 9 | and an executable named `rustybox` will be generated in the current directory. 10 | 11 | To run a tool, just execute 12 | ``` 13 | ./rustybox cal 14 | ``` 15 | 16 | Or, you can create a symbolic link to rustybox 17 | ``` 18 | ln -s rustybox cal 19 | ./cal 20 | ``` 21 | 22 | # License 23 | Rustybox is released under MIT license 24 | -------------------------------------------------------------------------------- /applets/_false.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | pub fn main(_: &[~str]) { 3 | std::os::set_exit_status(1); 4 | } 5 | 6 | -------------------------------------------------------------------------------- /applets/_true.rs: -------------------------------------------------------------------------------- 1 | pub fn main(_: &[~str]) { 2 | } 3 | -------------------------------------------------------------------------------- /applets/cal.rs: -------------------------------------------------------------------------------- 1 | use getopts::{getopts,optflag,optopt}; 2 | use std; 3 | use std::io::{println,print}; 4 | use common; 5 | use time; 6 | 7 | static WEEK_HEADER : &'static str = "Su Mo Tu We Th Fr Sa"; 8 | static MONTH_NAMES : [&'static str, ..13] = ["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; 9 | static CUMULATIVE_DAYS : [int, ..14] = [0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]; 10 | 11 | pub fn main(args: &[~str]) { 12 | let opts = ~[ 13 | optopt("m", "", "Display the specified month.", "month"), 14 | optflag("y", "", "Display a calendar for the specified year."), 15 | ]; 16 | let usage = "cal [-y] [-m month] [[month] year]"; 17 | let matches = match getopts(args.tail(), opts) { 18 | Err(f) => { 19 | common::err_write_line(f.to_err_msg()); 20 | common::print_usage(usage, opts); 21 | std::os::set_exit_status(1); 22 | return; 23 | } 24 | Ok(m) => { m } 25 | }; 26 | 27 | let (mut month_specified, mut month, year) = match matches.free.as_slice() { 28 | [ref m, ref y] => { 29 | let month = match convert_month(*m) { 30 | Some(m) => m, 31 | None => { 32 | std::os::set_exit_status(1); 33 | return; 34 | } 35 | }; 36 | let year = match convert_year(*y) { 37 | Some(y) => y, 38 | None => { 39 | std::os::set_exit_status(1); 40 | return; 41 | } 42 | }; 43 | (true, month, year) 44 | } 45 | [ref y] => { 46 | let year = match convert_year(*y) { 47 | Some(y) => y, 48 | None => { 49 | std::os::set_exit_status(1); 50 | return; 51 | } 52 | }; 53 | (false, 1, year) 54 | } 55 | [] => { 56 | //current month, current year 57 | let tm = time::now(); 58 | (true, (tm.tm_mon + 1) as int, (tm.tm_year + 1900) as int) 59 | } 60 | _ => { 61 | //show error 62 | common::print_usage(usage, opts); 63 | std::os::set_exit_status(1); 64 | return; 65 | } 66 | }; 67 | 68 | if !month_specified && matches.opt_present("m") { 69 | month_specified = true; 70 | month = match convert_month(matches.opt_str("m").unwrap()) { 71 | Some(m) => m, 72 | None => { 73 | std::os::set_exit_status(1); 74 | return; 75 | } 76 | }; 77 | } 78 | 79 | if month_specified { 80 | let month_grid = build_month(month, year, true); 81 | for i in month_grid.iter() { 82 | println(*i); 83 | } 84 | } else { 85 | println(build_year_title(year)); 86 | println(""); 87 | for i in range(0, 4) { 88 | let mut months = ~[]; 89 | for j in range(1, 4) { 90 | let m = i*3 + j; 91 | months.push(build_month(m, year, false)); 92 | } 93 | // print three months 94 | let row_len = WEEK_HEADER.len(); 95 | for i in range(0, months[0].len()) { 96 | print(months[0][i]); 97 | print(" ".repeat(row_len - months[0][i].len() + 2)); 98 | print(months[1][i]); 99 | print(" ".repeat(row_len - months[1][i].len() + 2)); 100 | println(months[2][i]); 101 | } 102 | } 103 | } 104 | } 105 | 106 | fn convert_month(m: &str) -> Option { 107 | match from_str::(m) { 108 | Some(m) if m >= 1 && m <= 12 => Some(m), 109 | _ => { 110 | common::err_write_line(format!("cal: {:s} is neither a month number (1..12) nor a name", m)); 111 | None 112 | } 113 | } 114 | } 115 | 116 | fn convert_year(y: &str) -> Option { 117 | match from_str::(y) { 118 | Some(y) if y >= 1 && y <= 9999 => Some(y), 119 | _ => { 120 | common::err_write_line(format!("cal: year {:s} not in range 1..9999", y)); 121 | None 122 | } 123 | } 124 | } 125 | 126 | fn number_of_days_in_month(month: int, year: int) -> int { 127 | match month { 128 | 1|3|5|7|8|10|12 => 31, 129 | 2 if year%400 == 0 || (year%4 == 0 && year%100 != 0) => 29, 130 | 2 => 28, 131 | _ => 30 132 | } 133 | } 134 | 135 | fn day_of_week(d: int, m: int, y: int) -> int { 136 | // number of days since year 1 137 | // gregorian calendar expected 138 | let days = (y-1) * 365 // all days for previous years 139 | + (y-1)/4 // plus leap days 140 | - (y-1)/100 // it is not a leap year if year is divisible by 100 141 | + (y-1)/400 // but it is a leap year if year is divisible by 400 142 | + CUMULATIVE_DAYS[m] // cumulative days for a given month 143 | + if (y%400 == 0 || (y%4 == 0 && y%100 != 0)) && m > 2 {1} else {0} // adjustment for leap year 144 | + d; 145 | 146 | return days%7; 147 | } 148 | 149 | fn build_title(month: int, year: int) -> ~str { 150 | let title = if year > 0 { 151 | format!("{:s} {:d}", MONTH_NAMES[month], year) 152 | } else { 153 | format!("{:s}", MONTH_NAMES[month]) 154 | }; 155 | let prepend_len = (WEEK_HEADER.len() - title.len())/2; 156 | return " ".repeat(prepend_len) + title; 157 | } 158 | 159 | fn build_year_title(year: int) -> ~str { 160 | let title = format!("{:d}", year); 161 | let prepend_len = (WEEK_HEADER.len()*3 + 4 - title.len())/2; 162 | return " ".repeat(prepend_len) + title; 163 | } 164 | 165 | fn build_month(month: int, year: int, year_in_title: bool) -> ~[~str] { 166 | let first_day = day_of_week(1, month, year); 167 | let day_num = number_of_days_in_month(month, year); 168 | 169 | let mut out = ~[]; 170 | 171 | out.push(build_title(month, if year_in_title { year } else { 0 })); 172 | out.push(WEEK_HEADER.to_owned()); 173 | 174 | let mut s = ~""; 175 | let mut count = 0; 176 | for i in range(-first_day + 1, day_num+1) { 177 | if i <= 0 { 178 | s.push_str(" "); 179 | } else { 180 | s.push_str(format!("{:>2d}", i)); 181 | } 182 | count += 1; 183 | if count%7 == 0 { 184 | out.push(s); 185 | s = ~""; 186 | } else if i != day_num { 187 | s.push_char(' '); 188 | } 189 | } 190 | out.push(s); 191 | while out.len() < 8 { 192 | out.push(~""); 193 | } 194 | return out; 195 | } 196 | -------------------------------------------------------------------------------- /applets/cat.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::io::fs::File; 3 | use std::path::Path; 4 | use common; 5 | 6 | pub fn main(args: &[~str]) { 7 | match args { 8 | [_] => { 9 | match std::io::util::copy(&mut std::io::stdin(), &mut std::io::stdout()) { 10 | Err(e) => { 11 | if e.kind != std::io::EndOfFile { 12 | common::err_write_line(format!("cat: stdin: {:s}", e.desc)); 13 | std::os::set_exit_status(1); 14 | } 15 | } 16 | _ => {} 17 | }; 18 | } 19 | [_, ..filenames] => { 20 | for fname in filenames.iter() { 21 | let mut f = match File::open(&Path::new(fname.as_slice())) { 22 | Ok(f) => f, 23 | Err(e) => { 24 | common::err_write_line(format!("cat: {:s}: {:s}", *fname, e.desc)); 25 | std::os::set_exit_status(1); 26 | continue; 27 | } 28 | }; 29 | match std::io::util::copy(&mut f, &mut std::io::stdout()) { 30 | Err(e) => { 31 | if e.kind != std::io::EndOfFile { 32 | common::err_write_line(format!("cat: {:s}: {:s}", *fname, e.desc)); 33 | std::os::set_exit_status(1); 34 | } 35 | } 36 | _ => {} 37 | } 38 | } 39 | } 40 | _ => unreachable!() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /applets/clear.rs: -------------------------------------------------------------------------------- 1 | use std::io::print; 2 | 3 | pub fn main(_: &[~str]) { 4 | print("\x1b[H\x1b[2J"); 5 | } 6 | -------------------------------------------------------------------------------- /applets/dirname.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::path::Path; 3 | use common; 4 | 5 | pub fn main(args: &[~str]) { 6 | match args { 7 | [_] => { 8 | common::err_write_line("usage: dirname path"); 9 | std::os::set_exit_status(1); 10 | return; 11 | } 12 | [_, ref f, ..] => { 13 | let p = Path::new(f.as_slice()); 14 | let _ = std::io::stdout().write(p.dirname()); //TODO should handle return value? 15 | let _ = std::io::stdout().write_char('\n'); //TODO should handle return value? 16 | } 17 | _ => unreachable!() 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /applets/echo.rs: -------------------------------------------------------------------------------- 1 | use std::io::{print,println}; 2 | 3 | pub fn main(args: &[~str]) { 4 | match args { 5 | [_, ref firstarg, ..args] if *firstarg == ~"-n" => print(args.connect(" ")), 6 | [_, ..args] => println(args.connect(" ")), 7 | _ => println(""), 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /applets/head.rs: -------------------------------------------------------------------------------- 1 | use getopts::{getopts,optopt}; 2 | use std::io::fs::File; 3 | use std::path::Path; 4 | use std; 5 | use std::io; 6 | use std::io::println; 7 | use common; 8 | 9 | fn head_file(filename: &str, linecount: int, bytecount: int) -> io::IoResult<()> { 10 | let mut f = try!(File::open(&Path::new(filename))); 11 | head_reader(&mut f, linecount, bytecount) 12 | } 13 | 14 | fn head_reader (f: &mut R, linecount: int, bytecount: int) -> io::IoResult<()> { 15 | let mut stdout = std::io::stdout(); 16 | let mut buf = [0u8, ..1024]; 17 | if bytecount > 0 { 18 | let len = match f.read(buf) { 19 | Ok(len) => len, 20 | Err(ref e) if e.kind == std::io::EndOfFile => 0, 21 | Err(e) => { return Err(e); } 22 | }; 23 | try!(stdout.write(buf.slice(0, len))); 24 | } else { 25 | let mut buf = [0u8, ..1024]; 26 | let mut count = linecount; 27 | while count > 0 { 28 | let len = try!(f.read(buf)); 29 | for i in range(0, len) { 30 | if buf[i] == '\n' as u8 { 31 | count -= 1; 32 | if count == 0 { 33 | // print until current point 34 | try!(stdout.write(buf.slice(0, i+1))); 35 | break; 36 | } 37 | } 38 | } 39 | if count > 0 { 40 | try!(stdout.write(buf)); 41 | } 42 | } 43 | } 44 | return Ok(()); 45 | } 46 | 47 | pub fn main(args: &[~str]) { 48 | let opts = ~[ 49 | optopt("n", "", "number of lines", "lines"), 50 | optopt("c", "", "number of bytes", "bytes"), 51 | ]; 52 | let matches = match getopts(args.tail(), opts) { 53 | Err(f) => { 54 | common::err_write_line(f.to_err_msg()); 55 | common::print_usage("head [-n lines | -c bytes] [file ...]", opts); 56 | std::os::set_exit_status(1); 57 | return; 58 | } 59 | Ok(m) => { m } 60 | }; 61 | 62 | let c = match matches.opt_str("c") { 63 | Some(s) => match from_str::(s) { 64 | Some(n) if n > 0 => n, 65 | _ => { 66 | common::err_write_line(format!("illegal byte count -- {}", s)); 67 | std::os::set_exit_status(1); 68 | return; 69 | } 70 | }, 71 | None => -1 72 | }; 73 | let n = match matches.opt_str("n") { 74 | Some(s) => match from_str::(s) { 75 | Some(n) if n > 0 => n, 76 | _ => { 77 | common::err_write_line(format!("illegal line count -- {}", s)); 78 | std::os::set_exit_status(1); 79 | return; 80 | } 81 | }, 82 | None => if c > 0 { -1 } else { 10 } 83 | }; 84 | if n * c >= 0 { 85 | common::err_write_line("can't combine line and byte counts"); 86 | std::os::set_exit_status(1); 87 | return; 88 | } 89 | match matches.free.as_slice() { 90 | [] => { 91 | let filename = "stdin"; 92 | match head_reader(& mut std::io::stdin(), n, c) { 93 | Err(e) => { 94 | if e.kind != std::io::EndOfFile { 95 | common::err_write_line(format!("head: {:s}: {:s}", filename, e.desc)); 96 | std::os::set_exit_status(1); 97 | } 98 | } 99 | _ => {} 100 | } 101 | } 102 | [..filenames] => { 103 | /* show file header */ 104 | let mut first = true; 105 | let multiple = filenames.len() > 1; 106 | for filename in filenames.iter() { 107 | if !first { 108 | println(""); 109 | } 110 | first = false; 111 | if multiple { 112 | println!("==> {} <==", *filename); 113 | } 114 | match head_file(*filename, n, c) { 115 | Err(e) => { 116 | if e.kind != std::io::EndOfFile { 117 | common::err_write_line(format!("head: {:s}: {:s}", *filename, e.desc)); 118 | std::os::set_exit_status(1); 119 | } 120 | } 121 | _ => {} 122 | } 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /applets/mkdir.rs: -------------------------------------------------------------------------------- 1 | use getopts::{getopts,optflag}; 2 | use std; 3 | use std::path::Path; 4 | use common; 5 | 6 | pub fn main(args: &[~str]) { 7 | let opts = ~[ 8 | optflag("p", "", "Create intermediate directories as required."), 9 | ]; 10 | let usage = "mkdir [-p] directory ..."; 11 | let matches = match getopts(args.tail(), opts) { 12 | Err(f) => { 13 | common::err_write_line(f.to_err_msg()); 14 | common::print_usage(usage, opts); 15 | std::os::set_exit_status(1); 16 | return; 17 | } 18 | Ok(m) => { m } 19 | }; 20 | if matches.free.len() == 0 { 21 | common::print_usage(usage, opts); 22 | std::os::set_exit_status(1); 23 | return; 24 | } 25 | 26 | let recursive = matches.opt_present("p"); 27 | 28 | for d in matches.free.iter() { 29 | match 30 | if recursive { 31 | std::io::fs::mkdir_recursive(&Path::new(d.as_slice()), std::io::UserDir) 32 | } else { 33 | std::io::fs::mkdir(&Path::new(d.as_slice()), std::io::UserDir) 34 | } 35 | { 36 | Err(e) => { 37 | common::err_write_line(format!("mkdir: {:s}: {:s}", *d, e.desc)); 38 | std::os::set_exit_status(1); 39 | } 40 | _ => {} 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /applets/mod.rs: -------------------------------------------------------------------------------- 1 | mod cal; 2 | mod cat; 3 | mod clear; 4 | mod dirname; 5 | mod echo; 6 | mod _false; 7 | mod head; 8 | mod mkdir; 9 | mod pwd; 10 | mod rmdir; 11 | mod seq; 12 | mod sleep; 13 | mod tee; 14 | mod _true; 15 | mod wc; 16 | mod which; 17 | mod yes; 18 | 19 | pub fn find_applet(name: &str) -> Option { 20 | match name { 21 | "cal" => Some(cal::main), 22 | "cat" => Some(cat::main), 23 | "clear" => Some(clear::main), 24 | "dirname" => Some(dirname::main), 25 | "echo" => Some(echo::main), 26 | "false" => Some(_false::main), 27 | "head" => Some(head::main), 28 | "mkdir" => Some(mkdir::main), 29 | "pwd" => Some(pwd::main), 30 | "rmdir" => Some(rmdir::main), 31 | "seq" => Some(seq::main), 32 | "sleep" => Some(sleep::main), 33 | "tee" => Some(tee::main), 34 | "true" => Some(_true::main), 35 | "wc" => Some(wc::main), 36 | "which" => Some(which::main), 37 | "yes" => Some(yes::main), 38 | _ => None 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /applets/pwd.rs: -------------------------------------------------------------------------------- 1 | use getopts::{getopts,optflag}; 2 | use std; 3 | use std::io::println; 4 | use common; 5 | use std::path::Path; 6 | 7 | pub fn main(args: &[~str]) { 8 | let opts = ~[ 9 | optflag("L", "", "Display the logical current working directory."), 10 | optflag("P", "", "Display the physical current working directory (all symbolic links resolved)."), 11 | ]; 12 | let matches = match getopts(args.tail(), opts) { 13 | Err(f) => { 14 | common::err_write_line(f.to_err_msg()); 15 | common::print_usage("pwd: usage: pwd [-LP]", opts); 16 | std::os::set_exit_status(1); 17 | return; 18 | } 19 | Ok(m) => { m } 20 | }; 21 | 22 | let mut stdout = std::io::stdout(); 23 | 24 | if !matches.opt_present("P") { 25 | let cwd = std::os::getenv("PWD"); 26 | // If we're trying to find the logical current directory and that fails, behave as if -P was specified. 27 | if cwd.is_some() && cwd.as_ref().unwrap()[0] == '/' as u8 { 28 | let cwd = cwd.unwrap(); 29 | match (Path::new(cwd.as_slice()).stat(), Path::new(".").stat()) { 30 | (Ok(cwd_stat), Ok(dot_stat)) => { 31 | if cwd_stat.unstable.device == dot_stat.unstable.device && cwd_stat.unstable.inode == dot_stat.unstable.inode { 32 | println(cwd); 33 | return; 34 | } 35 | } 36 | _ => {} 37 | } 38 | } 39 | } 40 | 41 | // else print physical path 42 | let cwd = std::os::getcwd(); 43 | let _ = stdout.write(cwd.as_vec()); //TODO should handle return value? 44 | let _ = stdout.write_char('\n'); //TODO should handle return value? 45 | } 46 | -------------------------------------------------------------------------------- /applets/rmdir.rs: -------------------------------------------------------------------------------- 1 | use getopts::{getopts,optflag}; 2 | use std; 3 | use std::path::Path; 4 | use common; 5 | 6 | pub fn main(args: &[~str]) { 7 | let opts = ~[ 8 | optflag("p", "", "Recursively remove all empty directories"), 9 | ]; 10 | let usage = "rmdir [-p] directory ..."; 11 | let matches = match getopts(args.tail(), opts) { 12 | Err(f) => { 13 | common::err_write_line(f.to_err_msg()); 14 | common::print_usage(usage, opts); 15 | std::os::set_exit_status(1); 16 | return; 17 | } 18 | Ok(m) => { m } 19 | }; 20 | if matches.free.len() == 0 { 21 | common::print_usage(usage, opts); 22 | std::os::set_exit_status(1); 23 | return; 24 | } 25 | 26 | let recursive = matches.opt_present("p"); 27 | 28 | for d in matches.free.iter() { 29 | if recursive { 30 | let mut path = Path::new(d.as_slice()); 31 | loop { 32 | match std::io::fs::rmdir(&path) { 33 | Err(e) => { 34 | common::err_write_line(format!("rmdir: {:s}: {:s}", path.as_str().unwrap(), e.desc)); 35 | std::os::set_exit_status(1); 36 | break; 37 | } 38 | _ => {} 39 | } 40 | path.pop(); 41 | if !path.clone().pop() { 42 | break; 43 | } 44 | } 45 | } else { 46 | match std::io::fs::rmdir(&Path::new(d.as_slice())) { 47 | Err(e) => { 48 | common::err_write_line(format!("rmdir: {:s}: {:s}", *d, e.desc)); 49 | std::os::set_exit_status(1); 50 | } 51 | _ => {} 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /applets/seq.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use common; 3 | 4 | fn err_not_float(arg: &str) { 5 | common::err_write_line(format!("seq: invalid floating point argument: {:s}", arg)); 6 | std::os::set_exit_status(1); 7 | } 8 | 9 | pub fn main(args: &[~str]) { 10 | let (first, step, last) = match args { 11 | [_, ref last] => { 12 | match from_str::(*last) { 13 | Some(last_f) => (1.0, 1.0, last_f), 14 | _ => { 15 | err_not_float(*last); 16 | return; 17 | } 18 | } 19 | } 20 | [_, ref first, ref last] => { 21 | match (from_str::(*first), from_str::(*last)) { 22 | (Some(first_f), Some(last_f)) => if last_f >= first_f { 23 | (first_f, 1.0, last_f) 24 | } else { 25 | (first_f, -1.0, last_f) 26 | }, 27 | (f, _) => { 28 | err_not_float(*(if f.is_none() {first} else {last})); 29 | return; 30 | } 31 | } 32 | } 33 | [_, ref first, ref step, ref last] => { 34 | match (from_str::(*first), from_str::(*step), from_str::(*last)) { 35 | (Some(first_f), Some(step_f), Some(last_f)) => (first_f, step_f, last_f), 36 | (f, s, _) => { 37 | err_not_float(*first); 38 | err_not_float(*(if f.is_none() {first} else if s.is_none() {step} else {last})); 39 | return; 40 | } 41 | } 42 | } 43 | _ => { 44 | common::err_write_line("usage: seq [first [incr]] last"); 45 | std::os::set_exit_status(1); 46 | return; 47 | } 48 | }; 49 | 50 | if step == 0.0 { 51 | common::err_write_line("seq: zero increment"); 52 | std::os::set_exit_status(1); 53 | return; 54 | } 55 | if first > last && step > 0.0 { 56 | common::err_write_line("seq: needs negative decrement"); 57 | std::os::set_exit_status(1); 58 | return; 59 | } 60 | if first < last && step < 0.0 { 61 | common::err_write_line("seq: needs positive increment"); 62 | std::os::set_exit_status(1); 63 | return; 64 | } 65 | 66 | let mut cur = first; 67 | while cur <= last { 68 | println!("{}", cur); 69 | cur += step; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /applets/sleep.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use common; 3 | 4 | pub fn main(args: &[~str]) { 5 | match args { 6 | [_, ref s, ..] => { 7 | match from_str::(*s) { 8 | Some(secs) if secs > 0.0 => { 9 | std::io::timer::sleep((secs * 1000.0) as u64); 10 | } 11 | _ => {} 12 | } 13 | } 14 | _ => { 15 | common::err_write_line("usage: sleep seconds"); 16 | std::os::set_exit_status(1); 17 | return; 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /applets/tee.rs: -------------------------------------------------------------------------------- 1 | use getopts::{getopts,optflag}; 2 | use std::io::fs::File; 3 | use std::path::Path; 4 | use std; 5 | use common; 6 | 7 | pub fn main(args: &[~str]) { 8 | let opts = ~[ 9 | optflag("a", "", "Append the output to the files rather than overwriting them."), 10 | ]; 11 | let matches = match getopts(args.tail(), opts) { 12 | Err(f) => { 13 | common::err_write_line(f.to_err_msg()); 14 | common::print_usage("usage: tee [-a] [file ...]", opts); 15 | std::os::set_exit_status(1); 16 | return; 17 | } 18 | Ok(m) => { m } 19 | }; 20 | let is_append = matches.opt_present("a"); 21 | let mut files = ~[~std::io::stdout() as ~Writer]; 22 | 23 | // open files 24 | for filename in matches.free.iter() { 25 | let mode = if is_append { std::io::Append } else { std::io::Truncate }; 26 | match File::open_mode(&Path::new(filename.as_slice()), mode, std::io::Write) { 27 | Ok(f) => {files.push(~f as ~Writer);} 28 | Err(e) => { 29 | common::err_write_line(format!("tee: {:s}: {:s}", *filename, e.desc)); 30 | std::os::set_exit_status(1); 31 | } 32 | }; 33 | } 34 | 35 | // wait from stdin and write to files 36 | let mut buf = [0u8, ..1024]; 37 | let mut stdin = std::io::stdin(); 38 | loop { 39 | let len = match stdin.read(buf) { 40 | Ok(len) => len, 41 | _ => { return; } 42 | }; 43 | for f in files.mut_iter() { 44 | match f.write(buf.slice(0, len)) { 45 | Err(ref e) if e.kind == std::io::EndOfFile => { break; } 46 | Err(e) => { 47 | common::err_write_line(format!("tee: {:s}", e.desc)); 48 | std::os::set_exit_status(1); 49 | } 50 | _ => {} 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /applets/wc.rs: -------------------------------------------------------------------------------- 1 | use getopts::{getopts,optflag}; 2 | use std; 3 | use std::io::fs::File; 4 | use std::path::Path; 5 | use common; 6 | 7 | pub fn main(args: &[~str]) { 8 | let opts = ~[ 9 | optflag("c", "", "The number of bytes in each input file is written to the standard output."), 10 | optflag("w", "", "The number of words in each input file is written to the standard output."), 11 | optflag("l", "", "The number of lines in each input file is written to the standard output."), 12 | ]; 13 | let matches = match getopts(args.tail(), opts) { 14 | Err(f) => { 15 | common::err_write_line(f.to_err_msg()); 16 | common::print_usage("wc [-clw] [file ...]", opts); 17 | std::os::set_exit_status(1); 18 | return; 19 | } 20 | Ok(m) => { m } 21 | }; 22 | 23 | let (print_char, print_word, print_line) = match (matches.opt_present("c"), matches.opt_present("w"), matches.opt_present("l")) { 24 | (false, false, false) => (true, true, true), 25 | (c,w,l) => (c,w,l) 26 | }; 27 | 28 | match matches.free.as_slice() { 29 | [] => { 30 | let result = wc_reader("", & mut std::io::stdin()); 31 | print_result("", result, print_char, print_word, print_line); 32 | } 33 | [..filenames] => { 34 | let mut total: (u64, u64, u64) = (0,0,0); 35 | 36 | for filename in filenames.iter() { 37 | let mut f = match File::open(&Path::new(filename.as_slice())) { 38 | Err(e) => { 39 | common::err_write_line(format!("wc: {:s}: {:s}", *filename, e.desc)); 40 | return; 41 | } 42 | Ok(f) => f 43 | }; 44 | let result = wc_reader(*filename, &mut f); 45 | print_result(*filename, result, print_char, print_word, print_line); 46 | total = match (total, result) { 47 | ((a1,b1,c1), (a2,b2,c2)) => (a1+a2, b1+b2, c1+c2) 48 | } 49 | } 50 | 51 | if filenames.len() > 1 { 52 | print_result("total", total, print_char, print_word, print_line); 53 | } 54 | } 55 | } 56 | } 57 | 58 | fn wc_reader (filename: &str, reader: &mut R) -> (u64, u64, u64) { 59 | let mut buf = [0u8, ..4096]; 60 | 61 | let mut linecount = 0; 62 | let mut charcount = 0; 63 | let mut wordcount = 0; 64 | let mut inword = false; 65 | 66 | let len = match reader.read(buf) { 67 | Err(e) => { 68 | if e.kind != std::io::EndOfFile { 69 | common::err_write_line(format!("wc: {:s}: {:s}", filename, e.desc)); 70 | } 71 | return (charcount, wordcount, linecount); 72 | } 73 | Ok(l) => l, 74 | }; 75 | 76 | for i in range(0, len) { 77 | let c = buf[i]; 78 | charcount += 1; 79 | 80 | if c == '\n' as u8 { 81 | linecount += 1; 82 | } 83 | 84 | if !inword { 85 | if c >= 33 /* printable non-space character in ascii */ { 86 | inword = true; 87 | wordcount += 1; 88 | } 89 | } else { 90 | if c <= 32 { 91 | inword = false; 92 | } 93 | } 94 | } 95 | 96 | return (charcount, wordcount, linecount); 97 | } 98 | 99 | fn print_result(filename: &str, result: (u64, u64, u64), printc: bool, printw: bool, printl: bool) { 100 | match result { 101 | (c, w, l) => { 102 | if printl { 103 | print!(" {:7u}", l); 104 | } 105 | if printw { 106 | print!(" {:7u}", w); 107 | } 108 | if printc { 109 | print!(" {:7u}", c); 110 | } 111 | println!(" {:s}", filename); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /applets/which.rs: -------------------------------------------------------------------------------- 1 | use getopts::{getopts,optflag}; 2 | use std; 3 | use std::path::Path; 4 | use common; 5 | 6 | pub fn main(args: &[~str]) { 7 | let opts = ~[ 8 | optflag("a", "", "List all instances of executables found (instead of just the first one of each)."), 9 | optflag("s", "", "No output."), 10 | ]; 11 | let usage = "which [-as] program ..."; 12 | let matches = match getopts(args.tail(), opts) { 13 | Err(f) => { 14 | common::err_write_line(f.to_err_msg()); 15 | common::print_usage(usage, opts); 16 | std::os::set_exit_status(1); 17 | return; 18 | } 19 | Ok(m) => { m } 20 | }; 21 | 22 | let silent = matches.opt_present("s"); 23 | let showall = matches.opt_present("a"); 24 | 25 | let pathenv = match std::os::getenv("PATH") { 26 | Some(s) => s, 27 | None => { 28 | std::os::set_exit_status(1); 29 | return; 30 | } 31 | }; 32 | let paths: ~[Path] = pathenv.split(':').map(|x| { Path::new(x) }).collect(); 33 | 34 | let mut stdout = std::io::stdout(); 35 | 36 | match matches.free.as_slice() { 37 | [] => { 38 | common::print_usage(usage, opts); 39 | std::os::set_exit_status(1); 40 | return; 41 | } 42 | [..cmds] => { 43 | for cmd in cmds.iter() { 44 | let mut found = false; 45 | for path in paths.iter() { 46 | let mut p = path.clone(); 47 | p.push(cmd.as_slice()); 48 | if check_path(&p) { 49 | found = true; 50 | if !silent { 51 | let _ = stdout.write(p.as_vec()); //TODO should handle return value? 52 | let _ = stdout.write_char('\n'); //TODO should handle return value? 53 | } 54 | if !showall { 55 | break; 56 | } 57 | } 58 | } 59 | if !found { 60 | std::os::set_exit_status(1); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | fn check_path(p: &Path) -> bool { 68 | if ! p.exists() { return false; } 69 | match p.stat() { 70 | Ok(filestat) => { 71 | if filestat.kind == std::io::TypeFile && filestat.perm & 0111 != 0 { 72 | return true; 73 | } 74 | } 75 | _ => {} 76 | } 77 | return false; 78 | } 79 | -------------------------------------------------------------------------------- /applets/yes.rs: -------------------------------------------------------------------------------- 1 | use std::io::println; 2 | 3 | pub fn main(args: &[~str]) { 4 | let default = ~"y"; 5 | let s = match args { 6 | [_, ref s, ..] => s, 7 | [_] => &default, 8 | _ => unreachable!() 9 | }; 10 | loop { 11 | println(*s); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common.rs: -------------------------------------------------------------------------------- 1 | use getopts::OptGroup; 2 | use std::io::stdio; 3 | 4 | pub fn print_usage(usage: &str, opts: &[OptGroup]) { 5 | err_write_line(format!("Usage: {}", usage)); 6 | for o in opts.iter() { 7 | match *o { 8 | OptGroup{short_name: ref s, long_name: ref l, desc: ref d, ..} => { 9 | if *l == ~"" { 10 | err_write_line(format!("\t-{}: {}", *s, *d)) 11 | } else if *s == ~"" { 12 | err_write_line(format!("\t--{}: {}", *l, *d)) 13 | } else { 14 | err_write_line(format!("\t-{}, --{}: {}", *s, *l, *d)) 15 | } 16 | } 17 | }; 18 | } 19 | } 20 | 21 | pub fn err_write_line(s: &str) { 22 | let _ = stdio::stderr().write_line(s); 23 | } 24 | -------------------------------------------------------------------------------- /roundup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # [r5]: roundup.5.html 3 | # [r1t]: roundup-1-test.sh.html 4 | # [r5t]: roundup-5-test.sh.html 5 | # 6 | # _(c) 2010 Blake Mizerany - MIT License_ 7 | # 8 | # Spray **roundup** on your shells to eliminate weeds and bugs. If your shells 9 | # survive **roundup**'s deathly toxic properties, they are considered 10 | # roundup-ready. 11 | # 12 | # **roundup** reads shell scripts to form test plans. Each 13 | # test plan is sourced into a sandbox where each test is executed. 14 | # 15 | # See [roundup-1-test.sh.html][r1t] or [roundup-5-test.sh.html][r5t] for example 16 | # test plans. 17 | # 18 | # __Install__ 19 | # 20 | # git clone http://github.com/bmizerany/roundup.git 21 | # cd roundup 22 | # make 23 | # sudo make install 24 | # # Alternatively, copy `roundup` wherever you like. 25 | # 26 | # __NOTE__: Because test plans are sourced into roundup, roundup prefixes its 27 | # variable and function names with `roundup_` to avoid name collisions. See 28 | # "Sandbox Test Runs" below for more insight. 29 | 30 | # Usage and Prerequisites 31 | # ----------------------- 32 | 33 | # Exit if any following command exits with a non-zero status. 34 | set -e 35 | 36 | # The current version is set during `make version`. Do not modify this line in 37 | # anyway unless you know what you're doing. 38 | ROUNDUP_VERSION="0.0.5" 39 | export ROUNDUP_VERSION 40 | 41 | # Usage is defined in a specific comment syntax. It is `grep`ed out of this file 42 | # when needed (i.e. The Tomayko Method). See 43 | # [shocco](http://rtomayko.heroku.com/shocco) for more detail. 44 | #/ usage: roundup [--help|-h] [--version|-v] [plan ...] 45 | 46 | roundup_usage() { 47 | grep '^#/' <"$0" | cut -c4- 48 | } 49 | 50 | while test "$#" -gt 0 51 | do 52 | case "$1" in 53 | --help|-h) 54 | roundup_usage 55 | exit 0 56 | ;; 57 | --version|-v) 58 | echo "roundup version $ROUNDUP_VERSION" 59 | exit 0 60 | ;; 61 | --color) 62 | color=always 63 | shift 64 | ;; 65 | -) 66 | echo >&2 "roundup: unknown switch $1" 67 | exit 1 68 | ;; 69 | *) 70 | break 71 | ;; 72 | esac 73 | done 74 | 75 | # Consider all scripts with names matching `*-test.sh` the plans to run unless 76 | # otherwise specified as arguments. 77 | if [ "$#" -gt "0" ] 78 | then 79 | roundup_plans="$@" 80 | else 81 | roundup_plans="$(ls *-test.sh)" 82 | fi 83 | 84 | : ${color:="auto"} 85 | 86 | # Create a temporary storage place for test output to be retrieved for display 87 | # after failing tests. 88 | roundup_tmp="$PWD/.roundup.$$" 89 | mkdir -p "$roundup_tmp" 90 | 91 | trap "rm -rf \"$roundup_tmp\"" EXIT INT 92 | 93 | # __Tracing failures__ 94 | roundup_trace() { 95 | # Delete the first two lines that represent roundups execution of the 96 | # test function. They are useless to the user. 97 | sed '1d' | 98 | # Delete the last line which is the "set +x" of the error trap 99 | sed '$d' | 100 | # Replace the rc=$? of the error trap with an verbose string appended 101 | # to the failing command trace line. 102 | sed '$s/.*rc=/exit code /' | 103 | # Trim the two left most `+` signs. They represent the depth at which 104 | # roundup executed the function. They also, are useless and confusing. 105 | sed 's/^++//' | 106 | # Indent the output by 4 spaces to align under the test name in the 107 | # summary. 108 | sed 's/^/ /' | 109 | # Highlight the last line in front of the exit code to bring notice to 110 | # where the error occurred. 111 | # 112 | # The sed magic puts every line into the hold buffer first, then 113 | # substitutes in the previous hold buffer content, prints that and starts 114 | # with the next cycle. At the end the last line (in the hold buffer) 115 | # is printed without substitution. 116 | sed -n "x;1!{ \$s/\(.*\)/$mag\1$clr/; };1!p;\$x;\$p" 117 | } 118 | 119 | # __Other helpers__ 120 | 121 | # Track the test stats while outputting a real-time report. This takes input on 122 | # **stdin**. Each input line must come in the format of: 123 | # 124 | # # The plan description to be displayed 125 | # d 126 | # 127 | # # A passing test 128 | # p 129 | # 130 | # # A failed test 131 | # f 132 | roundup_summarize() { 133 | set -e 134 | 135 | # __Colors for output__ 136 | 137 | # Use colors if we are writing to a tty device. 138 | if (test -t 1) || (test $color = always) 139 | then 140 | red=$(printf "\033[31m") 141 | grn=$(printf "\033[32m") 142 | mag=$(printf "\033[35m") 143 | clr=$(printf "\033[m") 144 | cols=$(tput cols) 145 | fi 146 | 147 | # Make these available to `roundup_trace`. 148 | export red grn mag clr 149 | 150 | ntests=0 151 | passed=0 152 | failed=0 153 | 154 | : ${cols:=10} 155 | 156 | while read status name 157 | do 158 | case $status in 159 | p) 160 | ntests=$(expr $ntests + 1) 161 | passed=$(expr $passed + 1) 162 | printf " %-48s " "$name:" 163 | printf "$grn[PASS]$clr\n" 164 | ;; 165 | f) 166 | ntests=$(expr $ntests + 1) 167 | failed=$(expr $failed + 1) 168 | printf " %-48s " "$name:" 169 | printf "$red[FAIL]$clr\n" 170 | roundup_trace < "$roundup_tmp/$name" 171 | ;; 172 | d) 173 | printf "%s\n" "$name" 174 | ;; 175 | esac 176 | done 177 | # __Test Summary__ 178 | # 179 | # Display the summary now that all tests are finished. 180 | yes = | head -n 57 | tr -d '\n' 181 | printf "\n" 182 | printf "Tests: %3d | " $ntests 183 | printf "Passed: %3d | " $passed 184 | printf "Failed: %3d" $failed 185 | printf "\n" 186 | 187 | # Exit with an error if any tests failed 188 | test $failed -eq 0 || exit 2 189 | } 190 | 191 | # Sandbox Test Runs 192 | # ----------------- 193 | 194 | # The above checks guarantee we have at least one test. We can now move through 195 | # each specified test plan, determine its test plan, and administer each test 196 | # listed in a isolated sandbox. 197 | for roundup_p in $roundup_plans 198 | do 199 | # Create a sandbox, source the test plan, run the tests, then leave 200 | # without a trace. 201 | ( 202 | # Consider the description to be the `basename` of the plan minus the 203 | # tailing -test.sh. 204 | roundup_desc=$(basename "$roundup_p" -test.sh) 205 | 206 | # Define functions for 207 | # [roundup(5)][r5] 208 | 209 | # A custom description is recommended, but optional. Use `describe` to 210 | # set the description to something more meaningful. 211 | # TODO: reimplement this. 212 | describe() { 213 | roundup_desc="$*" 214 | } 215 | 216 | # Provide default `before` and `after` functions that run only `:`, a 217 | # no-op. They may or may not be redefined by the test plan. 218 | before() { :; } 219 | after() { :; } 220 | 221 | # Seek test methods and aggregate their names, forming a test plan. 222 | # This is done before populating the sandbox with tests to avoid odd 223 | # conflicts. 224 | 225 | # TODO: I want to do this with sed only. Please send a patch if you 226 | # know a cleaner way. 227 | roundup_plan=$( 228 | grep "^it_.*()" $roundup_p | 229 | sed "s/\(it_[a-zA-Z0-9_]*\).*$/\1/g" 230 | ) 231 | 232 | # We have the test plan and are in our sandbox with [roundup(5)][r5] 233 | # defined. Now we source the plan to bring its tests into scope. 234 | . ./$roundup_p 235 | 236 | # Output the description signal 237 | printf "d %s" "$roundup_desc" | tr "\n" " " 238 | printf "\n" 239 | 240 | for roundup_test_name in $roundup_plan 241 | do 242 | # Any number of things are possible in `before`, `after`, and the 243 | # test. Drop into an subshell to contain operations that may throw 244 | # off roundup; such as `cd`. 245 | ( 246 | # Output `before` trace to temporary file. If `before` runs cleanly, 247 | # the trace will be overwritten by the actual test case below. 248 | { 249 | # redirect tracing output of `before` into file. 250 | { 251 | set -x 252 | # If `before` wasn't redefined, then this is `:`. 253 | before 254 | } &>"$roundup_tmp/$roundup_test_name" 255 | # disable tracing again. Its trace output goes to /dev/null. 256 | set +x 257 | } &>/dev/null 258 | 259 | # exit subshell with return code of last failing command. This 260 | # is needed to see the return code 253 on failed assumptions. 261 | # But, only do this if the error handling is activated. 262 | set -E 263 | trap 'rc=$?; set +x; set -o | grep "errexit.*on" >/dev/null && exit $rc' ERR 264 | 265 | # If `before` wasn't redefined, then this is `:`. 266 | before 267 | 268 | # Momentarily turn off auto-fail to give us access to the tests 269 | # exit status in `$?` for capturing. 270 | set +e 271 | ( 272 | # Set `-xe` before the test in the subshell. We want the 273 | # test to fail fast to allow for more accurate output of 274 | # where things went wrong but not in _our_ process because a 275 | # failed test should not immediately fail roundup. Each 276 | # tests trace output is saved in temporary storage. 277 | set -xe 278 | $roundup_test_name 279 | ) >"$roundup_tmp/$roundup_test_name" 2>&1 280 | 281 | # We need to capture the exit status before returning the `set 282 | # -e` mode. Returning with `set -e` before we capture the exit 283 | # status will result in `$?` being set with `set`'s status 284 | # instead. 285 | roundup_result=$? 286 | 287 | # It's safe to return to normal operation. 288 | set -e 289 | 290 | # If `after` wasn't redefined, then this runs `:`. 291 | after 292 | 293 | # This is the final step of a test. Print its pass/fail signal 294 | # and name. 295 | if [ "$roundup_result" -ne 0 ] 296 | then printf "f" 297 | else printf "p" 298 | fi 299 | 300 | printf " $roundup_test_name\n" 301 | ) 302 | done 303 | ) 304 | done | 305 | 306 | # All signals are piped to this for summary. 307 | roundup_summarize 308 | -------------------------------------------------------------------------------- /run-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname $0)" 3 | if [ -z "$1" ] 4 | then 5 | bash ./roundup.sh tests/*.sh 6 | else 7 | bash ./roundup.sh "$@" 8 | fi 9 | -------------------------------------------------------------------------------- /rustybox.rs: -------------------------------------------------------------------------------- 1 | extern crate getopts; 2 | extern crate time; 3 | mod applets; 4 | mod common; 5 | 6 | fn rustybox_main(args: &[~str]) { 7 | let file_path = std::path::Path::new(args[0].as_slice()); 8 | let name = match file_path.filename_str() { 9 | Some(n) => n, 10 | None => { 11 | common::err_write_line("unknown error"); 12 | std::os::set_exit_status(1); 13 | return; 14 | } 15 | }; 16 | if name == "rustybox" { 17 | return rustybox_main(args.tail()); 18 | } 19 | match applets::find_applet(name) { 20 | Some(f) => f(args), 21 | None => { 22 | common::err_write_line(format!("Applet {} not found!", name)); 23 | std::os::set_exit_status(1); 24 | return; 25 | } 26 | } 27 | } 28 | 29 | fn main() { 30 | rustybox_main(std::os::args()); 31 | } 32 | -------------------------------------------------------------------------------- /tests/cal.sh: -------------------------------------------------------------------------------- 1 | describe "seq" 2 | 3 | _test() { 4 | cmd=$1 5 | shift 6 | test "$(./rustybox $cmd "$@")" == "$($cmd "$@")" 7 | } 8 | 9 | it_cal_normal() { 10 | _test cal 11 | } 12 | 13 | it_cal_february_leap_year() { 14 | _test cal 2 1956 15 | } 16 | 17 | it_cal_february_common_year() { 18 | _test cal 2 1957 19 | } 20 | 21 | it_cal_february_common_year_divisible_by_four() { 22 | _test cal 2 1900 23 | } 24 | 25 | it_cal_year() { 26 | # alignment of the year is different in rustybox and bsd cal 27 | test "$(./rustybox cal 1956 | sed 1d)" == "$(cal 1956 | sed 1d)" 28 | } 29 | 30 | it_cal_fail1() { 31 | ! ./rustybox cal 1 2 3 32 | } 33 | -------------------------------------------------------------------------------- /tests/dirname.sh: -------------------------------------------------------------------------------- 1 | describe "dirname" 2 | 3 | _test() { 4 | cmd=$1 5 | shift 6 | test "$(./rustybox $cmd "$@")" == "$($cmd "$@")" 7 | } 8 | 9 | it_dirname_normal1() { 10 | _test dirname $PWD 11 | } 12 | 13 | it_dirname_normal2() { 14 | _test dirname / 15 | } 16 | 17 | it_dirname_normal3() { 18 | _test dirname . 19 | } 20 | 21 | it_dirname_normal4() { 22 | _test dirname asd 23 | } 24 | 25 | it_dirname_normal5() { 26 | _test dirname asd/asd 27 | } 28 | 29 | it_dirname_normal5() { 30 | _test dirname "" 31 | } 32 | 33 | it_dirname_fail1() { 34 | ! ./rustybox dirname 35 | } 36 | -------------------------------------------------------------------------------- /tests/seq.sh: -------------------------------------------------------------------------------- 1 | describe "seq" 2 | 3 | _test() { 4 | cmd=$1 5 | shift 6 | test "$(./rustybox $cmd "$@")" == "$($cmd "$@")" 7 | } 8 | 9 | it_seq_normal1() { 10 | _test seq 5 11 | } 12 | 13 | it_seq_normal2() { 14 | _test seq 6 31 15 | } 16 | 17 | it_seq_normal3() { 18 | _test seq 1 3 8 19 | } 20 | 21 | it_seq_normal4() { 22 | _test seq -5 1.5 8 23 | } 24 | 25 | it_seq_fail1() { 26 | ! ./rustybox seq 27 | } 28 | 29 | it_seq_fail2() { 30 | ! ./rustybox seq 1 2 3 4 31 | } 32 | 33 | it_seq_fail3() { 34 | ! ./rustybox seq -1 -1 1 35 | } 36 | 37 | it_seq_fail4() { 38 | ! ./rustybox seq -1 0 1 39 | } 40 | --------------------------------------------------------------------------------