Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
08f8898
apply rustfmt
Paradiesstaub Mar 7, 2019
cb096bf
fix clippy warnings
Paradiesstaub Mar 7, 2019
18fcfe6
declare Rust 2018 edition in cargo.toml
Paradiesstaub Mar 7, 2019
1b445eb
update dependencies
Paradiesstaub Mar 8, 2019
43d78ec
shorten some if statements
Paradiesstaub Mar 7, 2019
3af31fd
prevent reference to opt by cloning ffor early
Paradiesstaub Mar 7, 2019
5e10d28
use for_each
Paradiesstaub Mar 7, 2019
9cc0561
use for_each II
Paradiesstaub Mar 7, 2019
1c264de
use is_empty() on String
Paradiesstaub Mar 7, 2019
8a28324
Counter::new_from_opt
Paradiesstaub Mar 7, 2019
7c48a4d
impl Default for Summary
Paradiesstaub Mar 7, 2019
acac946
fn print_results
Paradiesstaub Mar 7, 2019
20916aa
fn check_error_code
Paradiesstaub Mar 7, 2019
ee97032
trait StringFromTempfileStart
Paradiesstaub Mar 7, 2019
8889daf
compute counter values early
Paradiesstaub Mar 7, 2019
9c2c3b3
exit_app
Paradiesstaub Mar 8, 2019
0ca98e9
fn setup & struct State
Paradiesstaub Mar 8, 2019
53e3c33
fn summary_exit_status
Paradiesstaub Mar 8, 2019
90da6e9
move print_results into State impl
Paradiesstaub Mar 8, 2019
dce56e4
fn run_shell_command
Paradiesstaub Mar 8, 2019
a2d2a81
split loop body into own function
Paradiesstaub Mar 8, 2019
4d413f4
move setup() and structopt to setup.rs
Paradiesstaub Mar 8, 2019
15e231e
move State to state.rs
Paradiesstaub Mar 8, 2019
36b6ad0
split loop code from state.rs into loop_step.rs
Paradiesstaub Mar 9, 2019
03d0bc5
trait Env
Paradiesstaub Mar 9, 2019
ae98b94
trait ShellCommand
Paradiesstaub Mar 9, 2019
50a1564
keep process::exit in main
Paradiesstaub Mar 9, 2019
62eb1cf
LoopModel with step() method
Paradiesstaub Mar 15, 2019
8e1ddda
move State object instead of using &mut State in loop_step
Paradiesstaub Mar 15, 2019
ebfb61d
use again custom iterator in main loop
Paradiesstaub Mar 15, 2019
5b90466
clean-up
Paradiesstaub Mar 15, 2019
056f9be
move LoopIterator into own file
Paradiesstaub Mar 15, 2019
46eb285
use impl Trait
Paradiesstaub Mar 15, 2019
8cc290e
move io-funtions into io.rs
Paradiesstaub Mar 15, 2019
4580ac8
keep StructOpt local to setup.rs
Paradiesstaub Mar 15, 2019
736e19d
use process::exit too when no command is supplied
Paradiesstaub Mar 15, 2019
4e22847
enum ExitCode
Paradiesstaub Mar 15, 2019
0722efe
move main loop to run.rs
Paradiesstaub Mar 16, 2019
7e67f38
rename struct Setup to App
Paradiesstaub Mar 16, 2019
7c26fc0
merge ErrorCode into ExitCode
Paradiesstaub Mar 16, 2019
6d0eadf
use mem::swap instead of clone() to get items out of Opt in setup.rs
Paradiesstaub Mar 16, 2019
02da008
struct AppError and first test
Paradiesstaub Mar 16, 2019
78fb1f4
rename run.rs to app.rs and move the App struct into it
Paradiesstaub Mar 18, 2019
5815868
simplified loop_step
Paradiesstaub Mar 18, 2019
5d7bc67
struct PreExitTasks
Paradiesstaub Mar 18, 2019
ce91641
move IO out of App::run()
Paradiesstaub Mar 18, 2019
1680280
move items into LoopLiterator
Paradiesstaub Mar 18, 2019
b49dba9
impl Default for LoopModel and some clean-up
Paradiesstaub Mar 18, 2019
31530ea
App::run() example test
Paradiesstaub Mar 18, 2019
1d3af00
clean-up
Paradiesstaub Mar 20, 2019
d7f15e4
remove tempfile dependence
Paradiesstaub Mar 18, 2019
753a088
remove global buffer
Paradiesstaub Mar 24, 2019
e0e9fee
derive Default
Paradiesstaub Mar 26, 2019
64f1b1a
rustfmt.toml & updated dependencies
Paradiesstaub Mar 31, 2019
1e91cc4
Printer with generic Write trait
Paradiesstaub Mar 31, 2019
b2b56c3
use crate smart-default
Paradiesstaub Apr 1, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 78 additions & 161 deletions Cargo.lock

Large diffs are not rendered by default.

33 changes: 17 additions & 16 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
[package]
name = "loop-rs"
version = "0.6.1"
authors = ["Rich Jones <[email protected]>"]
description = "UNIX's missing loop command"
name = "loop-rs"
version = "0.6.1"
authors = ["Rich Jones <[email protected]>"]
description = "UNIX's missing loop command"
edition = "2018"

documentation = "https://github.com/Miserlou/Loop"
homepage = "https://github.com/Miserlou/Loop"
repository = "https://github.com/Miserlou/Loop"
homepage = "https://github.com/Miserlou/Loop"
repository = "https://github.com/Miserlou/Loop"

readme = "README.md"
license = "MIT"
readme = "README.md"
license = "MIT"

[dependencies]
structopt = "0.2"
humantime = "1.1.1"
atty = "0.2"
regex = "1.0.0"
subprocess = "0.1.12"
tempfile = "3.0.3"
structopt = "0.2.15"
humantime = "1.2.0"
atty = "0.2.11"
regex = "1.1.5"
subprocess = "0.1.18"
smart-default = "0.5.1"

[[bin]]
name = "loop"
path = "src/main.rs"
name = "loop"
path = "src/main.rs"
18 changes: 18 additions & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
max_width = 80
newline_style = "Unix"
use_field_init_shorthand = true
use_try_shorthand = true

# wrap_comments = true
# comment_width = 100
# fn_single_line = true
# format_strings = true
# merge_imports = true
# match_block_trailing_comma = true
# force_multiline_blocks = true
# normalize_comments = true
# struct_field_align_threshold = 20
# spaces_around_ranges = true
# format_doc_comments = true
# condense_wildcard_suffixes = true
# overflow_delimited_expr = true
144 changes: 144 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use crate::io::{ExitCode, Printer};
use crate::loop_iterator::LoopIterator;
use crate::loop_step::LoopModel;
use crate::state::State;

use std::io::Write;
use std::time::{Duration, Instant};

pub struct App {
pub every: Option<Duration>,
pub loop_model: LoopModel,
pub iterator: LoopIterator,
}

impl App {
#[must_use]
pub fn run<W: Write>(
self,
setup_environment: &impl Fn(Option<String>, f64, f64),
command: &impl Fn() -> (String, ExitCode),
printer: &mut Printer<W>,
) -> ExitCode {
let mut state = State::default();

for it in self.iterator {
let step_start_time = Instant::now();

let is_last = it.is_last;

let setup_envs =
|| setup_environment(it.item, it.actual_count, it.count);

let (break_loop, new_state) =
self.loop_model.step(state, setup_envs, command, printer);
state = new_state;

if break_loop {
break;
}

if is_last {
printer.terminatory_print(|| state.to_string());
} else {
// Delay until next iteration time
if let Some(every) = self.every {
let since = Instant::now().duration_since(step_start_time);

if let Some(time) = every.checked_sub(since) {
std::thread::sleep(time);
}
}
}
}

state.exit_code
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::fmt::{self, Debug, Formatter};

#[test]
#[allow(non_snake_case)]
fn run__num() {
use std::cell::RefCell;

// test that loop step method is called twice, flag: --num 2
let expected_loop_count = 2;

let app = App {
every: None,
iterator: {
let num = Some(expected_loop_count as f64);
let items = vec!["a", "b", "c"]
.into_iter()
.map(str::to_owned)
.collect();
let offset = 0_f64;
let count_by = 1_f64;
LoopIterator::new(offset, count_by, num, items)
},
loop_model: LoopModel::default(),
};

let mut printer = Printer::default();
let counter = RefCell::new(0);

let exit_code = app.run(
&|_item, _index, _count| {},
&|| {
let mut count = counter.borrow_mut();
let output = match *count {
0 => "123".to_owned(),
_ => "abc".to_owned(),
};
*count += 1;

(output, ExitCode::Okay)
},
&mut printer,
);

let inner_data = printer.into_inner();

assert_eq!(ExitCode::Okay, exit_code);
assert_eq!(expected_loop_count, *counter.borrow());
assert_eq!(vec!["123", "abc"], inner_data);
}

impl Printer<Vec<u8>> {
#[allow(dead_code)]
pub fn into_inner(self) -> Vec<String> {
String::from_utf8(self.w)
.unwrap_or_default()
.lines()
.map(str::to_owned)
.collect()
}
}

impl<T> Debug for Printer<T>
where
T: Write + Debug,
{
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}

impl Default for Printer<Vec<u8>> {
fn default() -> Printer<Vec<u8>> {
Printer {
only_last: false,
until_contains: None,
until_match: None,
summary: false,
last_output: String::default(),
w: vec![],
}
}
}
}
161 changes: 161 additions & 0 deletions src/io.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use crate::state::State;

use std::io::Write;

use regex::Regex;
use smart_default::SmartDefault;
use subprocess::{Exec, ExitStatus, Redirection};

pub struct Printer<T: Write> {
pub only_last: bool,
pub until_contains: Option<String>,
pub until_match: Option<Regex>,
pub summary: bool,
pub last_output: String,
pub w: T,
}

impl<W> Printer<W>
where
W: Write,
{
pub fn terminatory_print(&mut self, create_summary: impl Fn() -> String) {
if self.only_last {
writeln!(self.w, "{}", self.last_output).unwrap();
}

if self.summary {
self.w.write_all(create_summary().as_bytes()).unwrap();
}
}

pub fn print(&mut self, text: &str, state: &mut State) {
let mut last_line = String::default();

text.lines().for_each(|line| {
// --only-last
// If we only want output from the last execution,
// defer printing until later
if !self.only_last {
writeln!(self.w, "{}", line).unwrap(); // THIS IS THE MAIN PRINT FUNCTION
}

// --until-contains
// We defer loop breaking until the entire result is printed.
if let Some(ref string) = self.until_contains {
state.has_matched = line.contains(string);
}

// --until-match
if let Some(ref regex) = self.until_match {
state.has_matched = regex.captures(&line).is_some();
}

if self.only_last {
last_line = line.to_owned();
}
});

// maybe keep the last line for later
if self.only_last {
self.last_output = last_line;
}
}
}

pub struct ShellCommand {
pub cmd_with_args: String,
}

impl ShellCommand {
#[must_use]
pub fn run(&self) -> (String, ExitCode) {
Exec::shell(&self.cmd_with_args)
.stdout(Redirection::Pipe)
.stderr(Redirection::Merge)
.capture()
.map(|it| (it.stdout_str(), it.exit_status.into()))
.unwrap()
}
}

pub struct SetupEnv {
pub count_precision: usize,
}

impl SetupEnv {
pub fn run(&self, item: Option<String>, index: f64, count: f64) {
use std::env::set_var;

// THESE ARE FLIPPED AND I CAN'T UNFLIP THEM.
set_var("ACTUALCOUNT", index.to_string());
set_var("COUNT", format!("{:.*}", self.count_precision, count));

// Set current item as environment variable
if let Some(item) = item {
set_var("ITEM", item);
}
}
}

#[derive(Debug, PartialEq, Clone, Copy, SmartDefault)]
pub enum ExitCode {
#[default]
Okay,

Error,
/// e.g. when no argument is passed to loop-rs (2)
MinorError,
/// same exit-code as used by the `timeout` shell command (124)
Timeout,
/// the process has completed, but the exit-code is unknown (99)
Unkonwn,
Other(u32),
}

impl ExitCode {
pub fn success(self) -> bool {
ExitCode::Okay == self
}
}

impl From<u32> for ExitCode {
fn from(n: u32) -> ExitCode {
match n {
0 => ExitCode::Okay,
1 => ExitCode::Error,
2 => ExitCode::MinorError,
99 => ExitCode::Unkonwn,
124 => ExitCode::Timeout,
code => ExitCode::Other(code),
}
}
}

impl From<ExitStatus> for ExitCode {
fn from(exit_status: ExitStatus) -> ExitCode {
match exit_status {
ExitStatus::Exited(code) => ExitCode::from(code),
_ => ExitCode::Unkonwn,
}
}
}

impl Into<u32> for ExitCode {
fn into(self) -> u32 {
match self {
ExitCode::Okay => 0,
ExitCode::Error => 1,
ExitCode::MinorError => 2,
ExitCode::Unkonwn => 99,
ExitCode::Timeout => 124,
ExitCode::Other(code) => code,
}
}
}

impl Into<i32> for ExitCode {
fn into(self) -> i32 {
Into::<u32>::into(self) as i32
}
}
Loading