├── _config.yml ├── .gitignore ├── demo └── tztail.gif ├── tests ├── inputs │ └── test_read_from_file.txt └── integration_tests.rs ├── Cargo.toml ├── src ├── output_formatter.rs ├── reader.rs ├── args.rs ├── main.rs ├── format.rs └── converter.rs ├── LICENSE-MIT ├── .travis.yml ├── README.md └── Cargo.lock /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | *.log -------------------------------------------------------------------------------- /demo/tztail.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecasualcoder/tztail/HEAD/demo/tztail.gif -------------------------------------------------------------------------------- /tests/inputs/test_read_from_file.txt: -------------------------------------------------------------------------------- 1 | 2018-11-21T17:26:30+0700 postgres parameters not changed 2 | 2018-11-21T18:26:30+0400 postgres parameters not changed -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tztail" 3 | version = "1.2.0" 4 | authors = ["Aswin Karthik "] 5 | description = "tztail (TimeZoneTAIL) allows you to view logs in the timezone you want" 6 | license-file = "LICENSE-MIT" 7 | documentation = "https://github.com/thecasualcoder/tztail/blob/master/README.md" 8 | repository= "https://github.com/thecasualcoder/tztail" 9 | 10 | [dependencies] 11 | atty = "0.2" 12 | chrono = "0.4" 13 | chrono-tz = "0.5" 14 | colored = "1.6" 15 | regex = "1" 16 | 17 | [dependencies.clap] 18 | version = "2" 19 | features = ["suggestions", "color", "wrap_help"] 20 | 21 | [dev-dependencies] 22 | lazy_static = "1.2.0" 23 | escargot = "0.3.1" 24 | assert_cmd = "0.10.2" -------------------------------------------------------------------------------- /src/output_formatter.rs: -------------------------------------------------------------------------------- 1 | use colored::*; 2 | use converter::TimedLog; 3 | 4 | // OutputFormatter can either format the target time as a colored 5 | // string or a plain string based on a flag 6 | pub struct OutputFormatter { 7 | colored: bool, 8 | } 9 | 10 | impl OutputFormatter { 11 | pub fn plain() -> OutputFormatter { 12 | OutputFormatter { colored: false } 13 | } 14 | 15 | pub fn colored() -> OutputFormatter { 16 | OutputFormatter { colored: true } 17 | } 18 | 19 | // format does a string replace of the original_time in log 20 | // TODO: Is string replace slow? Is there a better way? 21 | pub fn format(&self, t: TimedLog) -> String { 22 | if t.converted { 23 | let target_time = if self.colored { 24 | format!("{}", t.target_time.unwrap().red()) 25 | } else { 26 | t.target_time.unwrap() 27 | }; 28 | 29 | return t.log.replace(&t.original_time.unwrap(), &target_time); 30 | } 31 | 32 | String::from(t.log) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 aswinkarthik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | sudo: required 4 | 5 | rust: 6 | - beta 7 | - nightly 8 | - 1.30.0 # minimum supported version 9 | 10 | env: 11 | global: 12 | - PROJECT_NAME=tztail 13 | 14 | matrix: 15 | allow_failures: 16 | - rust: nightly 17 | fast_finish: true 18 | include: 19 | - os: linux 20 | rust: stable 21 | env: TARGET=x86_64-linux 22 | - os: osx 23 | rust: stable 24 | env: TARGET=x86_64-darwin 25 | 26 | script: 27 | - date 28 | - cargo build --all 29 | - cargo test --all 30 | 31 | before_deploy: 32 | - cargo build --release 33 | - mv ./target/release/tztail ./ 34 | - tar cvfz $PROJECT_NAME-$TRAVIS_TAG-$TARGET.tar.gz ./tztail 35 | - PREVIOUS_TAG=$(git tag | tail -2 | head -1) 36 | - echo 'Changelog:' 37 | - git log ${PREVIOUS_TAG}..${TRAVIS_TAG} --oneline 38 | - export GITHUB_RELEASE_NAME="v${TRAVIS_TAG}" 39 | 40 | deploy: 41 | name: ${GITHUB_RELEASE_NAME} 42 | provider: releases 43 | api_key: 44 | secure: Kh6rCM6lWjDa/sOE16oh1+HMq7fagupzG89mtJ4LZCp1BubdraN6xfi3exeVycoxumsiOqFpP3A5EaeU/3xRbO3d/9zBKM0rGr7oPIxWn8O/NX4bPjcGXBlAAtn1knBqL37liAj6mJn1vN5kwOWZLwWoFkVbWhtv/EUGKB8Zbv4oSJ3itBcli1xppArPVnEFAartivl2erRdVCSUDWY5YIgUYQyW3vWz/eTvpmlQIJfMGMycvHktclJcjLVMynPougHWYnhwZ0+mVxLl4DVaiD/km+HdP2nzIrfAo0dTfY3lvvWaV2L8XvCEzCaVqN7SpE49IMvUiA4YdEEmWGco+B5blx4nSV1cQ7F5EuQyh5QWrSAYrOO8tZSOl9+WmA1O4jo6iBOQQwS46claX05LJy8Q+izUDwrPYbyXRn3b9DC5WWATWfHeyI9yz7sLyQJ+uD/9k7ve9IUM234KJrjdcHKV+lW+XYaWV+fH0GN45NKi3t06+RT6mj+nwECTtvZPeV8+nwJ4vDIxWofRSVKOg5CYWEB8wxY3BsYqppbgbOZ6NoR8LPaFjaMmc0Amit53ZJq7d7XB0YgJCb2fjihu4Ed77hl1s4bLz577gTmc0udaN9tzdyBrijbuHer8IqVX39RL/aslnGbLvURXnQtPGaWBO7RqL8B3cjqM6C6w0QE= 45 | file: '$PROJECT_NAME-$TRAVIS_TAG-$TARGET.tar.gz' 46 | skip_cleanup: true 47 | on: 48 | tags: true 49 | repo: thecasualcoder/tztail 50 | condition: $TRAVIS_RUST_VERSION = stable && $TARGET != "" 51 | -------------------------------------------------------------------------------- /src/reader.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{self, BufRead, BufReader, Stdin}; 3 | 4 | // Abstraction to read from source 5 | // first_line is read first. 6 | // This can be used to further optimization if needed like autodetecting the format before starting conversion. 7 | pub struct InputReader<'a> { 8 | reader: Box, 9 | pub first_line: String, 10 | } 11 | 12 | // Input can either be a single file or STDIN 13 | pub enum Input<'a> { 14 | Stdin(&'a Stdin), 15 | File(&'a str), 16 | } 17 | 18 | impl<'a> InputReader<'a> { 19 | // Instantiate InputReader based on the type of Input 20 | pub fn new(input: Input) -> io::Result { 21 | match input { 22 | Input::Stdin(stdin) => { 23 | let mut reader = stdin.lock(); 24 | 25 | let mut first_line = String::new(); 26 | reader.read_line(&mut first_line)?; 27 | 28 | Ok(InputReader { 29 | reader: Box::new(reader), 30 | first_line: first_line, 31 | }) 32 | } 33 | Input::File(filename) => { 34 | let file = File::open(filename)?; 35 | let mut reader = BufReader::new(file); 36 | 37 | let mut first_line = String::new(); 38 | reader.read_line(&mut first_line)?; 39 | 40 | Ok(InputReader { 41 | reader: Box::new(reader), 42 | first_line: first_line, 43 | }) 44 | } 45 | } 46 | } 47 | 48 | // Proxy call for read_line of the underling BufRead 49 | pub fn read_line(&mut self, buf: &mut String) -> io::Result { 50 | self.reader.read_line(buf) 51 | } 52 | 53 | // Getter for first_line 54 | pub fn first_line(&self) -> &str { 55 | &self.first_line 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use atty::Stream; 2 | use clap::ArgMatches; 3 | use output_formatter::OutputFormatter; 4 | 5 | type Result = ::std::result::Result; 6 | 7 | // To represent command line arguments 8 | pub struct Args<'a> { 9 | pub filename: Option<&'a str>, 10 | pub custom_format: Option<&'a str>, 11 | pub timezone: Option<&'a str>, 12 | pub should_follow: bool, 13 | pub color_choice: ColorChoice, 14 | } 15 | 16 | impl<'a> Args<'a> { 17 | // Parses ArgMatches into Args 18 | pub fn parse(matches: &'a ArgMatches) -> Result> { 19 | Ok(Args { 20 | filename: matches.value_of("FILE"), 21 | custom_format: matches.value_of("format"), 22 | timezone: matches.value_of("timezone"), 23 | should_follow: matches.is_present("follow"), 24 | color_choice: ColorChoice::new(matches.value_of("color")), 25 | }) 26 | } 27 | } 28 | 29 | // ColorChoice can be made from the command line 30 | // Auto is to decide automatically. If auto is selected, and stdout is a tty, it is colored else it is not. 31 | // Always to force using color 32 | // Never to force not using color 33 | pub enum ColorChoice { 34 | Auto, 35 | Always, 36 | Never, 37 | } 38 | 39 | impl ColorChoice { 40 | // Instantiate ColorChoice based on cli option chosen 41 | fn new(choice: Option<&str>) -> ColorChoice { 42 | match choice { 43 | Some("always") => ColorChoice::Always, 44 | Some("never") => ColorChoice::Never, 45 | _ => ColorChoice::Auto, 46 | } 47 | } 48 | 49 | // Builder for output formatter from a ColorChoice 50 | pub fn build_formatter(&self) -> OutputFormatter { 51 | match self { 52 | ColorChoice::Auto => { 53 | return if atty::is(Stream::Stdout) { 54 | OutputFormatter::colored() 55 | } else { 56 | OutputFormatter::plain() 57 | } 58 | } 59 | ColorChoice::Never => OutputFormatter::plain(), 60 | ColorChoice::Always => OutputFormatter::colored(), 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | extern crate assert_cmd; 2 | extern crate escargot; 3 | #[macro_use] 4 | extern crate lazy_static; 5 | extern crate chrono; 6 | extern crate chrono_tz; 7 | 8 | use assert_cmd::prelude::*; 9 | use chrono::prelude::*; 10 | use chrono::DateTime; 11 | 12 | use escargot::CargoRun; 13 | use std::process::Command; 14 | 15 | lazy_static! { 16 | static ref CARGO_RUN: CargoRun = escargot::CargoBuild::new() 17 | .bin("tztail") 18 | .current_release() 19 | .run() 20 | .unwrap(); 21 | } 22 | 23 | fn tztail() -> Command { 24 | CARGO_RUN.command() 25 | } 26 | 27 | fn convert_to_localtimezone(input: &str, format: &str) -> String { 28 | let local_timezone = Local::now().timezone(); 29 | return DateTime::parse_from_str(input, format) 30 | .unwrap() 31 | .with_timezone(&local_timezone) 32 | .format(format) 33 | .to_string(); 34 | } 35 | 36 | #[test] 37 | fn test_stdin() { 38 | let remote_time = "19/Nov/2018:15:59:47 +0000"; 39 | let local_time = convert_to_localtimezone(remote_time, "%d/%b/%Y:%H:%M:%S %z"); 40 | tztail() 41 | .with_stdin() 42 | .buffer(format!("[{}] PUT /some/nginxurl", remote_time)) 43 | .assert() 44 | .success() 45 | .stdout(format!("[{}] PUT /some/nginxurl", local_time)) 46 | .stderr(""); 47 | } 48 | 49 | #[test] 50 | fn test_target_timezone() { 51 | tztail() 52 | .arg("--timezone") 53 | .arg("US/Pacific") 54 | .with_stdin() 55 | .buffer("2018-11-21T22:48:14+0100: This is a log") 56 | .assert() 57 | .success() 58 | .stdout("2018-11-21T13:48:14-0800: This is a log") 59 | .stderr(""); 60 | } 61 | 62 | #[test] 63 | fn test_read_from_file() { 64 | let local_time = ( 65 | convert_to_localtimezone("2018-11-21T17:26:30+0700", "%Y-%m-%dT%H:%M:%S%z"), 66 | convert_to_localtimezone("2018-11-21T19:56:30+0530", "%Y-%m-%dT%H:%M:%S%z"), 67 | ); 68 | tztail() 69 | .arg("tests/inputs/test_read_from_file.txt") 70 | .assert() 71 | .success() 72 | .stdout(format!( 73 | "{} postgres parameters not changed\n{} postgres parameters not changed", 74 | local_time.0, local_time.1, 75 | )).stderr(""); 76 | } 77 | 78 | #[test] 79 | fn test_custom_format() { 80 | let remote_time = "2018/11/21 22:48:14 +03:00"; 81 | let local_time = convert_to_localtimezone(remote_time, "%Y/%m/%d %H:%M:%S %:z"); 82 | tztail() 83 | .arg("--format") 84 | .arg("%Y/%m/%d %H:%M:%S %:z") 85 | .with_stdin() 86 | .buffer("2018/11/21 22:48:14 +03:00: This is a custom formatted log") 87 | .assert() 88 | .success() 89 | .stdout(format!("{}: This is a custom formatted log", local_time)) 90 | .stderr(""); 91 | } 92 | 93 | #[test] 94 | fn test_default_utc() { 95 | tztail() 96 | .arg("-t") 97 | .arg("Asia/Kolkata") 98 | .arg("--format") 99 | .arg("%Y/%m/%dT%H:%M:%S") 100 | .with_stdin() 101 | .buffer("2018/11/21T22:48:14: This is a log in UTC") 102 | .assert() 103 | .success() 104 | .stdout("2018/11/22T04:18:14: This is a log in UTC") 105 | .stderr(""); 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tztail 2 | 3 | [![Build Status](https://travis-ci.org/thecasualcoder/tztail.svg?branch=master)](https://travis-ci.org/thecasualcoder/tztail) 4 | [![crates.io](https://img.shields.io/crates/v/tztail.svg)](https://crates.io/crates/tztail) 5 | ![Crates.io](https://img.shields.io/crates/d/tztail) 6 | 7 | tztail (TimeZoneTAIL) allows you to view logs in the timezone you want. Its tail with timezone. 8 | 9 | ## Install 10 | 11 | _Using Homebrew_ 12 | 13 | ```bash 14 | brew tap thecasualcoder/stable 15 | brew install tztail 16 | ``` 17 | 18 | _Using Cargo_ 19 | 20 | ```bash 21 | cargo install tztail 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```bash 27 | $ tztail --help 28 | tztail (TimeZoneTAIL) allows you to view logs in the timezone you want 29 | 30 | USAGE: 31 | tztail [FILE] 32 | 33 | OPTIONS: 34 | -t, --timezone Sets the timezone in which output should be printed. (Default: local timezone) 35 | -f, --follow Follow the file indefinitely as changes are added. (Default: Off) 36 | --format Custom format for parsing dates. (Default: autodetected patterns) 37 | -h, --help Prints help information 38 | -V, --version Prints version information 39 | 40 | ARGS: 41 | File to tail. STDIN by default 42 | ``` 43 | 44 | ## Features 45 | 46 | - Supports few standard formats with which auto detection is done when parsing logs. 47 | - Supports specifying custom format for parsing in case it is a non-standard format. See [here](https://docs.rs/chrono/0.4.6/chrono/format/strftime/index.html#specifiers) for formats. 48 | - Autodetect source timezone if present in logs. Example (`2014-11-28T12:00:09+0100` is CET) 49 | - Output logs to local timezone by default 50 | 51 | ## Demo 52 | 53 | ![demo](/demo/tztail.gif) 54 | 55 | ## Autodetectable formats 56 | 57 | Most used autodetectable formats 58 | 59 | | Name | Example | 60 | | ---------------- | ------------------------------- | 61 | | RFC2822 | Fri, 28 Nov 2014 12:00:09 +0000 | 62 | | RFC3339 | 2014-11-28T12:00:09+0000 | 63 | | Nginx Log format | 04/Nov/2018:12:13:49 +0000 | 64 | 65 | ## Usecase 66 | 67 | This tool can be used to convert timestamps in a log to any desired timezone while tailing logs. Eg. In case your logs are in UTC and you want to view it in a different timezone say. Asia/Kolkata (IST), pipe the logs through `tztail`. 68 | 69 | ```bash 70 | ## Example usage 71 | $ cat somelog # A log in UTC 72 | 2018-11-03 19:47:20.279044 I mvcc: finished scheduled compaction at 104794 (took 748.443µs) 73 | 2018-11-03 19:52:20.282913 I mvcc: store.index: compact 105127 74 | 75 | $ cat somelog | tztail --timezone Asia/Kolkata # Timestamps converted to IST 76 | 2018-11-04 01:17:20.279044 I mvcc: finished scheduled compaction at 104794 (took 748.443µs) 77 | 2018-11-04 01:22:20.282913 I mvcc: store.index: compact 105127 78 | ``` 79 | 80 | It allows to specify a custom format as well. 81 | 82 | ```bash 83 | ## Example usage 84 | $ cat somelog # A log in non-standard format 85 | 2018-11-03 20:07:20 mvcc: store.index: compact 106120 86 | 2018-11-03 20:07:20 mvcc: finished scheduled compaction at 106120 (took 933.25µs) 87 | 88 | $ cat somelog | tztail -t Asia/Kolkata --format "%Y-%m-%d %H:%M:%S" 89 | 2018-11-04 01:37:20 mvcc: store.index: compact 106120 90 | 2018-11-04 01:37:20 mvcc: finished scheduled compaction at 106120 (took 933.25µs) 91 | ``` 92 | 93 | ## Building from source 94 | 95 | Checkout the code and build locally. Needs rust compiler 1.30 or above. 96 | 97 | ```bash 98 | $ git clone https://github.com/thecasualcoder/tztail 99 | $ cd tztail 100 | 101 | # To build binary locally 102 | $ cargo build --release 103 | 104 | # To install binary locally in Cargo bin path 105 | $ cargo install 106 | 107 | # To run tests 108 | $ cargo test 109 | ``` 110 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate clap; 3 | extern crate atty; 4 | extern crate chrono; 5 | extern crate chrono_tz; 6 | extern crate colored; 7 | extern crate regex; 8 | mod args; 9 | mod converter; 10 | mod format; 11 | mod output_formatter; 12 | mod reader; 13 | 14 | use args::Args; 15 | use clap::{App, AppSettings, Arg}; 16 | use converter::Converter; 17 | use reader::*; 18 | use std::error::Error; 19 | use std::io; 20 | use std::io::Write; 21 | use std::process; 22 | 23 | fn run(args: Args) -> Result { 24 | let Args { 25 | filename, 26 | custom_format: fmt, 27 | timezone: tz, 28 | should_follow: follow, 29 | color_choice, 30 | } = args; 31 | 32 | let c = Converter::new(tz, fmt)?; 33 | let stdin = io::stdin(); 34 | let stdout = io::stdout(); 35 | let mut writer = stdout.lock(); 36 | 37 | let reader = match filename { 38 | Some("-") => InputReader::new(Input::Stdin(&stdin)), 39 | Some(name) => InputReader::new(Input::File(&name)), 40 | None => InputReader::new(Input::Stdin(&stdin)), 41 | }; 42 | 43 | let mut reader = match reader { 44 | Ok(r) => r, 45 | Err(err) => return handle_err(err), 46 | }; 47 | 48 | let mut has_next = true; 49 | let mut buf = String::new(); 50 | 51 | let formatter = color_choice.build_formatter(); 52 | 53 | match write!( 54 | writer, 55 | "{}", 56 | formatter.format(c.convert(reader.first_line())) 57 | ) { 58 | Ok(_) => (), 59 | Err(err) => return handle_err(err), 60 | }; 61 | 62 | while follow || has_next { 63 | match reader.read_line(&mut buf) { 64 | Ok(bytes) if bytes > 0 => { 65 | match write!(writer, "{}", formatter.format(c.convert(&buf))) { 66 | Ok(_) => (), 67 | Err(err) => return handle_err(err), 68 | } 69 | 70 | buf.clear(); 71 | has_next = true; 72 | } 73 | Ok(_) => { 74 | has_next = false; 75 | } 76 | Err(err) => return handle_err(err), 77 | } 78 | } 79 | 80 | return Ok(true); 81 | } 82 | 83 | fn handle_err(err: std::io::Error) -> Result { 84 | eprintln!("{}: {}", "Exited while reading lines", err.description()); 85 | return Err(format!("Exited while reading lines: {}", err)); 86 | } 87 | 88 | fn main() { 89 | let app = App::new(crate_name!()) 90 | .setting(AppSettings::ColorAuto) 91 | .setting(AppSettings::ColoredHelp) 92 | .setting(AppSettings::DeriveDisplayOrder) 93 | .setting(AppSettings::UnifiedHelpMessage) 94 | .version(crate_version!()) 95 | .about("tztail (TimeZoneTAIL) allows you to view logs in the timezone you want") 96 | .arg(Arg::with_name("FILE").help("File to tail. STDIN by default")) 97 | .arg( 98 | Arg::with_name("timezone") 99 | .long("timezone") 100 | .short("t") 101 | .value_name("TIMEZONE") 102 | .required(false) 103 | .takes_value(true) 104 | .help("Sets the timezone in which output should be printed. (Default: local timezone)"), 105 | ).arg( 106 | Arg::with_name("follow") 107 | .long("follow") 108 | .short("f") 109 | .value_name("FOLLOW") 110 | .required(false) 111 | .takes_value(false) 112 | .help("Follow the file indefinitely as changes are added. (Default: Off)"), 113 | ).arg( 114 | Arg::with_name("format") 115 | .long("format") 116 | .value_name("FORMAT") 117 | .required(false) 118 | .takes_value(true) 119 | .help("Custom format for parsing dates. (Default: autodetected patterns)") 120 | ).arg( 121 | Arg::with_name("color") 122 | .long("color") 123 | .value_name("COLOR_CHOICE") 124 | .possible_values(&["never", "auto", "always"]) 125 | .required(false) 126 | .help("Controls when to use color") 127 | ); 128 | 129 | let result = Args::parse(&app.get_matches()).and_then(run); 130 | 131 | match result { 132 | Err(error) => { 133 | eprintln!("{}: {}", "Exited non-successfully", error); 134 | process::exit(1); 135 | } 136 | Ok(false) => process::exit(1), 137 | Ok(true) => process::exit(0), 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/format.rs: -------------------------------------------------------------------------------- 1 | use regex::{Match, Regex}; 2 | 3 | // Format holds a format and the regex to capture the format from 4 | // a string. It also hold information on if its timezone aware format 5 | pub struct Format { 6 | fmt: String, 7 | re: Regex, 8 | timezone_aware: bool, 9 | } 10 | 11 | impl Format { 12 | // Proxies request to re.find 13 | pub fn find<'a>(&self, input: &'a str) -> Option> { 14 | return self.re.find(input); 15 | } 16 | 17 | // Getter for timezone aware 18 | pub fn is_timezone_aware(&self) -> bool { 19 | return self.timezone_aware; 20 | } 21 | 22 | // Getter for fmt 23 | pub fn fmt(&self) -> &str { 24 | return &self.fmt; 25 | } 26 | 27 | // To create a new Format type from a format string 28 | pub fn new(fmt: &str) -> Format { 29 | let re = create_re(fmt); 30 | 31 | return Format { 32 | fmt: String::from(fmt), 33 | re: re, 34 | timezone_aware: format_has_timezone(fmt), 35 | }; 36 | } 37 | } 38 | 39 | // Utility function to create and compile a regex for the given format. 40 | fn create_re(format: &str) -> Regex { 41 | let format_str = String::from(format); 42 | let regex_str = FORMAT_TO_REGEX 43 | .iter() 44 | .fold(format_str, |acc, item| acc.replace(item.0, item.1)); 45 | return Regex::new(®ex_str).unwrap(); 46 | } 47 | 48 | // Checks if the given format has any timezone specifiers 49 | fn format_has_timezone(fmt: &str) -> bool { 50 | const TIMEZONE_SPECIFIERS: [&str; 4] = ["%Z", "%z", "%:z", "%#z"]; 51 | for spec in TIMEZONE_SPECIFIERS.iter() { 52 | if fmt.contains(spec) { 53 | return true; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | const FORMAT_TO_REGEX: [(&'static str, &'static str); 51] = [ 60 | // Date Specifiers 61 | ("%Y", r"\d{4}"), 62 | ("%C", r"\d{2}"), 63 | ("%y", r"\d{2}"), 64 | ("%m", r"\d{2}"), 65 | ("%b", r"\w{3}"), 66 | ("%B", r"\w+"), 67 | ("%h", r"\w{3}"), 68 | ("%d", r"\d{2}"), 69 | ("%e", r"\d+"), 70 | ("%e", r"\d+"), 71 | ("%a", r"\w{3}"), 72 | ("%A", r"\w+"), 73 | ("%w", r"\d"), 74 | ("%u", r"\d"), 75 | ("%U", r"\d{2}"), 76 | ("%W", r"\d{2}"), 77 | ("%G", r"\d{4}"), 78 | ("%g", r"\d{2}"), 79 | ("%V", r"\d{2}"), 80 | ("%j", r"\d{3}"), 81 | ("%D", r"\d{2}/\d{2}/\d{2}"), 82 | ("%x", r"\d{2}/\d{2}/\d{2}"), 83 | ("%F", r"\d{4}-\d{2}-\d{2}"), 84 | ("%v", r"\d{2}-\w{3}-\d{4}"), 85 | // Time Specifiers 86 | ("%H", r"\d{2}"), 87 | ("%k", r"\d+"), 88 | ("%I", r"\d{2}"), 89 | ("%l", r"\d{1,2}"), 90 | ("%P", r"[ap]m)"), 91 | ("%p", r"[AP]M)"), 92 | ("%M", r"\d{2}"), 93 | ("%S", r"\d{2}"), 94 | ("%f", r"\d+"), 95 | ("%3f", r"\d{3}"), 96 | ("%6f", r"\d{6}"), 97 | ("%9f", r"\d{9}"), 98 | ("%.f", r"\.\d+"), 99 | ("%.3f", r"\.\d{3}"), 100 | ("%.6f", r"\.\d{6}"), 101 | ("%.9f", r"\.\d{9}"), 102 | ("%R", r"\d{2}:\d{2}"), 103 | ("%T", r"\d{2}:\d{2}:\d{2}"), 104 | ("%X", r"\d{2}:\d{2}:\d{2}"), 105 | ("%r", r"\d{2}:\d{2}:\d{2} [AP]M"), 106 | // Timezone Specifiers 107 | ("%Z", r"[A-Z]+"), 108 | ("%z", r"[\+\-]\d{4}"), 109 | ("%:z", r"\+\d{2}:\d{2}"), 110 | ("%#z", r"\+\d{2,4}"), 111 | //Date & Time Specifiers 112 | ("%c", r"\w{3} \w{3} \d+ \d{2}:\d{2}:\d{2} \d{4}"), 113 | ( 114 | "%+", 115 | r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+\+\d{2}:\d{2}", 116 | ), 117 | ("%s", r"\d+"), 118 | ]; 119 | 120 | #[cfg(test)] 121 | mod format_tests { 122 | use format::Format; 123 | #[test] 124 | fn test_new() { 125 | let fmt = Format::new("%Y-%m-%d %H:%M:%S"); 126 | 127 | assert!(!fmt.is_timezone_aware()); 128 | 129 | match fmt.re.find("2019-08-08 10:20:24") { 130 | Some(found) => { 131 | assert_eq!(found.as_str(), "2019-08-08 10:20:24"); 132 | assert_eq!(found.start(), 0); 133 | assert_eq!(found.end(), "2019-08-08 10:20:24".len()); 134 | } 135 | None => assert!(false), 136 | } 137 | 138 | match fmt.re.find("20190-08-08 10:20:24") { 139 | Some(found) => { 140 | assert_eq!(found.start(), 1); 141 | } 142 | None => assert!(false), 143 | } 144 | 145 | match fmt.re.find("some random string") { 146 | Some(_) => assert!(false), 147 | None => assert!(true), 148 | } 149 | } 150 | 151 | #[test] 152 | fn test_new_with_timezone() { 153 | let fmt = Format::new("%Y-%m-%d %H:%M:%S %z"); 154 | 155 | assert!(fmt.is_timezone_aware()); 156 | let valid_str = "2019-08-08 10:20:24 +0000"; 157 | match fmt.re.find(valid_str) { 158 | Some(found) => { 159 | assert_eq!(found.as_str(), valid_str); 160 | assert_eq!(found.start(), 0); 161 | assert_eq!(found.end(), valid_str.len()); 162 | } 163 | None => assert!(false), 164 | } 165 | 166 | let fmt = Format::new("%Y-%m-%d %H:%M:%S %Z"); 167 | assert!(fmt.timezone_aware); 168 | match fmt.re.find("2019-08-08 10:20:24 IST") { 169 | Some(found) => { 170 | assert_eq!(found.as_str(), "2019-08-08 10:20:24 IST"); 171 | } 172 | None => assert!(false), 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/converter.rs: -------------------------------------------------------------------------------- 1 | use chrono::prelude::*; 2 | use chrono::{DateTime, TimeZone, Utc}; 3 | use chrono_tz::Tz; 4 | use format::Format; 5 | use std::vec::Vec; 6 | 7 | // Converter can be used to convert all the datetimes present in a single line 8 | // 9 | // timezone represents the target timezone in which output should be printed. 10 | // formats are the list of all formats the log is evaluated against 11 | pub struct Converter { 12 | formats: Vec, 13 | timezone: Option, 14 | local: DateTime, 15 | } 16 | 17 | #[derive(PartialEq, Debug)] 18 | pub struct TimedLog<'a> { 19 | pub original_time: Option, 20 | pub target_time: Option, 21 | pub log: &'a str, 22 | pub converted: bool, 23 | } 24 | 25 | // The default auto-detectable formats supported. 26 | // Add standard formats here 27 | // They get converted into Regexes and are validated 28 | const DEFAULT_FORMATS: &[&str] = &[ 29 | "%Y-%m-%dT%H:%M:%S%z", // 2014-11-28T12:00:09+0000 30 | "%Y-%m-%d %H:%M:%S%z", // 2014-11-28 12:00:09+0000 31 | "%Y-%m-%dT%H:%M:%S %z", // 2014-11-28T12:00:09 +0000 32 | "%Y-%m-%d %H:%M:%S %z", // 2014-11-28 12:00:09 +0000 33 | "%d/%b/%Y:%H:%M:%S %z", // 04/Nov/2018:12:13:49 +0000 Nginx 34 | "%d/%b/%Y:%H:%M:%S%.3f %z", // 04/Nov/2018:12:13:49.334 +0000 Nginx 35 | "%d/%b/%Y:%H:%M:%S", // 04/Nov/2018:12:13:49 HAProxy 36 | "%a, %d %b %Y %H:%M:%S %z", // Fri, 28 Nov 2014 12:00:09 +0000 37 | "%Y-%m-%dT%H:%M:%SZ", // 2014-11-28T12:00:09Z 38 | "%Y-%m-%dT%H:%M:%S", // 2014-11-28T12:00:09 39 | "%Y-%m-%d %H:%M:%S", // 2014-11-28 12:00:09 40 | ]; 41 | 42 | // Parses timezone from a string 43 | fn parse_timezone(tz_str: &str) -> Option { 44 | match tz_str.parse() { 45 | Ok(tz) => return Some(tz), 46 | Err(err) => { 47 | eprintln!( 48 | "Using local timezone as given timezone is not valid: {}", 49 | err 50 | ); 51 | return None; 52 | } 53 | }; 54 | } 55 | 56 | impl Converter { 57 | // Public method to create a new Converter 58 | // Takes in two optional paramters 59 | // 60 | // 1. Timezone 61 | // 2. Fmt 62 | // 63 | // If `timezone` is not specified, the system's local timezone is used. 64 | // If `fmt` is not specified, the autodetectable default formats are used. 65 | pub fn new(tz_str: Option<&str>, fmt: Option<&str>) -> Result { 66 | let timezone = match tz_str { 67 | Some(timezone) => parse_timezone(timezone), 68 | None => None, 69 | }; 70 | 71 | let formats = match fmt { 72 | Some(fmt) => vec![Format::new(fmt)], 73 | None => DEFAULT_FORMATS.iter().map(|f| Format::new(f)).collect(), 74 | }; 75 | 76 | Ok(Converter { 77 | formats: formats, 78 | timezone: timezone, 79 | local: Local::now(), 80 | }) 81 | } 82 | 83 | // The method that converts a given string into the target timezone 84 | // It also tries to "detect" the source timezone if available, or it will assume UTC 85 | // TODO: the formats are looped sequentially. Use RegexSet to parallely match all expressions 86 | // TODO: If there is a hit in autodetected formats, prioritize it 87 | // TODO: Allow target timezone configurable 88 | pub fn convert<'a>(&self, input: &'a str) -> TimedLog<'a> { 89 | for format in &self.formats { 90 | match format.find(input) { 91 | Some(found) => { 92 | let source_datetime = found.as_str(); 93 | 94 | if format.is_timezone_aware() { 95 | let dt = match DateTime::parse_from_str(source_datetime, format.fmt()) { 96 | Ok(dt) => dt, 97 | Err(err) => { 98 | eprintln!( 99 | "Error when parsing from string that is datetime aware: {}", 100 | err 101 | ); 102 | return TimedLog { 103 | converted: false, 104 | log: input, 105 | original_time: None, 106 | target_time: None, 107 | }; 108 | } 109 | }; 110 | 111 | let target_datetime = match self.timezone { 112 | Some(tz) => dt.with_timezone(&tz).format(format.fmt()).to_string(), 113 | None => dt 114 | .with_timezone(&self.local.timezone()) 115 | .format(format.fmt()) 116 | .to_string(), 117 | }; 118 | 119 | return TimedLog { 120 | converted: true, 121 | original_time: Some(String::from(source_datetime)), 122 | target_time: Some(target_datetime), 123 | log: input, 124 | }; 125 | } else { 126 | let dt = match Utc.datetime_from_str(source_datetime, format.fmt()) { 127 | Ok(dt) => dt, 128 | Err(err) => { 129 | eprintln!("Error when parsing using UTC: {}", err); 130 | return TimedLog { 131 | converted: false, 132 | log: input, 133 | original_time: None, 134 | target_time: None, 135 | }; 136 | } 137 | }; 138 | 139 | let target_datetime = match self.timezone { 140 | Some(tz) => dt.with_timezone(&tz).format(format.fmt()).to_string(), 141 | None => dt 142 | .with_timezone(&self.local.timezone()) 143 | .format(format.fmt()) 144 | .to_string(), 145 | }; 146 | 147 | return TimedLog { 148 | converted: true, 149 | original_time: Some(String::from(source_datetime)), 150 | target_time: Some(target_datetime), 151 | log: input, 152 | }; 153 | } 154 | } 155 | None => { 156 | continue; 157 | } 158 | } 159 | } 160 | 161 | return TimedLog { 162 | converted: false, 163 | log: input, 164 | original_time: None, 165 | target_time: None, 166 | }; 167 | } 168 | } 169 | 170 | // A function to test various formats 171 | fn _chrono(input: &str) -> String { 172 | let dt = match DateTime::parse_from_str(input, "%Y-%m-%d %H:%M:%S %z") { 173 | Ok(dt) => dt, 174 | Err(err) => { 175 | println!("Error: {}", err); 176 | return String::from(input); 177 | } 178 | }; 179 | let local: DateTime = Local::now(); 180 | let tz = local.timezone(); 181 | return dt 182 | .with_timezone(&tz) 183 | .format("%Y-%m-%d %H:%M:%S %z") 184 | .to_string(); 185 | } 186 | 187 | #[cfg(test)] 188 | mod converter_tests { 189 | use chrono::DateTime; 190 | 191 | #[test] 192 | fn test_new() { 193 | match super::Converter::new(Some("Random/str"), None) { 194 | Ok(_) => assert!(true), 195 | Err(_) => assert!(false), 196 | }; 197 | 198 | match super::Converter::new(Some("Asia/Kolkata"), None) { 199 | Ok(c) => { 200 | assert!(true); 201 | assert_eq!(c.formats.len(), super::DEFAULT_FORMATS.len()); 202 | } 203 | Err(_) => assert!(false), 204 | }; 205 | 206 | match super::Converter::new(Some("Asia/Kolkata"), Some("%Y-%m-%d %H:%M:%S %z")) { 207 | Ok(c) => { 208 | assert!(true); 209 | assert_eq!(c.formats.len(), 1); 210 | } 211 | Err(_) => assert!(false), 212 | }; 213 | } 214 | 215 | #[test] 216 | fn test_convert() { 217 | use super::TimedLog; 218 | 219 | struct TestCase<'a> { 220 | timezone: Option<&'a str>, 221 | format: Option<&'a str>, 222 | inputs: Vec<&'a str>, 223 | outputs: Vec>, 224 | }; 225 | 226 | fn convert_utc_to_localtimezone(input: &str, format: &str) -> String { 227 | use chrono::TimeZone; 228 | 229 | let local_timezone = super::Local::now().timezone(); 230 | return super::Utc 231 | .datetime_from_str(input, format) 232 | .unwrap() 233 | .with_timezone(&local_timezone) 234 | .format(format) 235 | .to_string(); 236 | }; 237 | 238 | fn convert_to_localtimezone(input: &str, format: &str) -> String { 239 | let local_timezone = super::Local::now().timezone(); 240 | return DateTime::parse_from_str(input, format) 241 | .unwrap() 242 | .with_timezone(&local_timezone) 243 | .format(format) 244 | .to_string(); 245 | }; 246 | 247 | let local_timezone_case_1 = 248 | convert_utc_to_localtimezone("2002-10-02 15:00:00", "%Y-%m-%d %H:%M:%S"); 249 | let local_timezone_case_2 = 250 | convert_to_localtimezone("2012-07-24T23:14:29-0700", "%Y-%m-%dT%H:%M:%S%z"); 251 | 252 | let testcases = vec![ 253 | TestCase { 254 | timezone: Some("Asia/Kolkata"), 255 | format: None, 256 | inputs: vec!["A random log without out any time. It should be left untouched"], 257 | outputs: vec![TimedLog { 258 | original_time: None, 259 | target_time: None, 260 | converted: false, 261 | log: "A random log without out any time. It should be left untouched", 262 | }], 263 | }, 264 | TestCase { 265 | timezone: Some("Asia/Kolkata"), 266 | format: Some("%Y-%m-%d %H:%M:%S %z"), 267 | inputs: vec![ 268 | "2018-08-08 10:32:15 +0000", 269 | "2018-03-03 10:32:15 +0700", 270 | "2018-08-08 10:32:15 -0200", 271 | ], 272 | outputs: vec![ 273 | TimedLog { 274 | log: "2018-08-08 10:32:15 +0000", 275 | original_time: Some(String::from("2018-08-08 10:32:15 +0000")), 276 | target_time: Some(String::from("2018-08-08 16:02:15 +0530")), 277 | converted: true, 278 | }, 279 | TimedLog { 280 | log: "2018-03-03 10:32:15 +0700", 281 | original_time: Some(String::from("2018-03-03 10:32:15 +0700")), 282 | target_time: Some(String::from("2018-03-03 09:02:15 +0530")), 283 | converted: true, 284 | }, 285 | TimedLog { 286 | log: "2018-08-08 10:32:15 -0200", 287 | original_time: Some(String::from("2018-08-08 10:32:15 -0200")), 288 | target_time: Some(String::from("2018-08-08 18:02:15 +0530")), 289 | converted: true, 290 | }, 291 | ], 292 | }, 293 | TestCase { 294 | timezone: Some("Asia/Kolkata"), 295 | format: Some("%Y-%m-%d %H:%M:%S"), 296 | inputs: vec!["2018-11-03 22:39:33 Some random log"], 297 | outputs: vec![TimedLog { 298 | log: "2018-11-03 22:39:33 Some random log", 299 | original_time: Some(String::from("2018-11-03 22:39:33")), 300 | target_time: Some(String::from("2018-11-04 04:09:33")), 301 | converted: true, 302 | }], 303 | }, 304 | TestCase { 305 | timezone: Some("Europe/Paris"), 306 | format: None, 307 | inputs: vec![ 308 | "Fri, 28 Nov 2014 12:00:09 +0000", 309 | "Thu, 27 Nov 2014 01:00:09 +0530", 310 | "14/Nov/2018:22:14:27 -0800", 311 | "2014-11-28T12:00:09+0500", 312 | "2014-11-28 12:00:09+0500", 313 | "2014-11-28T12:00:09 +0500", 314 | "2014-11-28 12:00:09 +0500", 315 | "04/Nov/2018:12:13:49 +0500 Nginx", 316 | "04/Nov/2018:12:13:49.334 +0500 Nginx", 317 | "04/Nov/2018:12:13:49 HAProxy", 318 | ], 319 | outputs: vec![ 320 | TimedLog { 321 | log: "Fri, 28 Nov 2014 12:00:09 +0000", 322 | original_time: Some(String::from("Fri, 28 Nov 2014 12:00:09 +0000")), 323 | target_time: Some(String::from("Fri, 28 Nov 2014 13:00:09 +0100")), 324 | converted: true, 325 | }, 326 | TimedLog { 327 | log: "Thu, 27 Nov 2014 01:00:09 +0530", 328 | original_time: Some(String::from("Thu, 27 Nov 2014 01:00:09 +0530")), 329 | target_time: Some(String::from("Wed, 26 Nov 2014 20:30:09 +0100")), 330 | converted: true, 331 | }, 332 | TimedLog { 333 | log: "14/Nov/2018:22:14:27 -0800", 334 | original_time: Some(String::from("14/Nov/2018:22:14:27 -0800")), 335 | target_time: Some(String::from("15/Nov/2018:07:14:27 +0100")), 336 | converted: true, 337 | }, 338 | TimedLog { 339 | log: "2014-11-28T12:00:09+0500", 340 | original_time: Some(String::from("2014-11-28T12:00:09+0500")), 341 | target_time: Some(String::from("2014-11-28T08:00:09+0100")), 342 | converted: true, 343 | }, 344 | TimedLog { 345 | log: "2014-11-28 12:00:09+0500", 346 | original_time: Some(String::from("2014-11-28 12:00:09+0500")), 347 | target_time: Some(String::from("2014-11-28 08:00:09+0100")), 348 | converted: true, 349 | }, 350 | TimedLog { 351 | log: "2014-11-28T12:00:09 +0500", 352 | original_time: Some(String::from("2014-11-28T12:00:09 +0500")), 353 | target_time: Some(String::from("2014-11-28T08:00:09 +0100")), 354 | converted: true, 355 | }, 356 | TimedLog { 357 | log: "2014-11-28 12:00:09 +0500", 358 | original_time: Some(String::from("2014-11-28 12:00:09 +0500")), 359 | target_time: Some(String::from("2014-11-28 08:00:09 +0100")), 360 | converted: true, 361 | }, 362 | TimedLog { 363 | log: "04/Nov/2018:12:13:49 +0500 Nginx", 364 | original_time: Some(String::from("04/Nov/2018:12:13:49 +0500")), 365 | target_time: Some(String::from("04/Nov/2018:08:13:49 +0100")), 366 | converted: true, 367 | }, 368 | TimedLog { 369 | log: "04/Nov/2018:12:13:49.334 +0500 Nginx", 370 | original_time: Some(String::from("04/Nov/2018:12:13:49.334 +0500")), 371 | target_time: Some(String::from("04/Nov/2018:08:13:49.334 +0100")), 372 | converted: true, 373 | }, 374 | TimedLog { 375 | log: "04/Nov/2018:12:13:49 HAProxy", 376 | original_time: Some(String::from("04/Nov/2018:12:13:49")), 377 | target_time: Some(String::from("04/Nov/2018:13:13:49")), 378 | converted: true, 379 | }, 380 | ], 381 | }, 382 | TestCase { 383 | timezone: None, 384 | format: None, 385 | inputs: vec!["2002-10-02 15:00:00", "2012-07-24T23:14:29-0700"], 386 | outputs: vec![ 387 | TimedLog { 388 | log: "2002-10-02 15:00:00", 389 | original_time: Some(String::from("2002-10-02 15:00:00")), 390 | target_time: Some(String::from(local_timezone_case_1)), 391 | converted: true, 392 | }, 393 | TimedLog { 394 | log: "2012-07-24T23:14:29-0700", 395 | original_time: Some(String::from("2012-07-24T23:14:29-0700")), 396 | target_time: Some(String::from(local_timezone_case_2)), 397 | converted: true, 398 | }, 399 | ], 400 | }, 401 | ]; 402 | 403 | for test in testcases { 404 | let converter = match super::Converter::new(test.timezone, test.format) { 405 | Ok(c) => c, 406 | Err(_) => { 407 | assert!(false); 408 | return; 409 | } 410 | }; 411 | 412 | for i in 0..test.inputs.len() { 413 | let input = test.inputs[i]; 414 | let expected_output = &test.outputs[i]; 415 | 416 | let output = converter.convert(input); 417 | 418 | assert_eq!(output, *expected_output); 419 | } 420 | } 421 | } 422 | 423 | // #[test] 424 | // fn test_chrono() { 425 | // let input = "2018-08-08 10:10:10 +0000"; 426 | // let output = super::chrono(input); 427 | // let expected_output = "2018-08-08 14:40:10 +05300"; 428 | 429 | // assert_eq!(output, expected_output) 430 | // } 431 | } 432 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "aho-corasick" 3 | version = "0.6.9" 4 | source = "registry+https://github.com/rust-lang/crates.io-index" 5 | dependencies = [ 6 | "memchr 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 7 | ] 8 | 9 | [[package]] 10 | name = "ansi_term" 11 | version = "0.11.0" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | dependencies = [ 14 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 15 | ] 16 | 17 | [[package]] 18 | name = "assert_cmd" 19 | version = "0.10.2" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | dependencies = [ 22 | "escargot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 23 | "predicates 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 24 | "predicates-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 25 | "predicates-tree 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 26 | ] 27 | 28 | [[package]] 29 | name = "atty" 30 | version = "0.2.11" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | dependencies = [ 33 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 34 | "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 35 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 36 | ] 37 | 38 | [[package]] 39 | name = "bitflags" 40 | version = "1.0.4" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | 43 | [[package]] 44 | name = "cfg-if" 45 | version = "0.1.6" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | 48 | [[package]] 49 | name = "chrono" 50 | version = "0.4.6" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | dependencies = [ 53 | "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", 54 | "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 55 | "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 56 | ] 57 | 58 | [[package]] 59 | name = "chrono-tz" 60 | version = "0.5.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | dependencies = [ 63 | "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 64 | "parse-zoneinfo 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 65 | ] 66 | 67 | [[package]] 68 | name = "clap" 69 | version = "2.32.0" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | dependencies = [ 72 | "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 73 | "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 74 | "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 75 | "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 76 | "term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 77 | "textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", 78 | "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 79 | "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 80 | ] 81 | 82 | [[package]] 83 | name = "colored" 84 | version = "1.6.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | dependencies = [ 87 | "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 88 | ] 89 | 90 | [[package]] 91 | name = "difference" 92 | version = "2.0.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | 95 | [[package]] 96 | name = "escargot" 97 | version = "0.3.1" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | dependencies = [ 100 | "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", 101 | "serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", 102 | ] 103 | 104 | [[package]] 105 | name = "itoa" 106 | version = "0.4.3" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | 109 | [[package]] 110 | name = "kernel32-sys" 111 | version = "0.2.2" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | dependencies = [ 114 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 115 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 116 | ] 117 | 118 | [[package]] 119 | name = "lazy_static" 120 | version = "1.2.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | 123 | [[package]] 124 | name = "libc" 125 | version = "0.2.43" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | 128 | [[package]] 129 | name = "memchr" 130 | version = "2.1.1" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | dependencies = [ 133 | "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 134 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 135 | "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 136 | ] 137 | 138 | [[package]] 139 | name = "num-integer" 140 | version = "0.1.39" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | dependencies = [ 143 | "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 144 | ] 145 | 146 | [[package]] 147 | name = "num-traits" 148 | version = "0.2.6" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | 151 | [[package]] 152 | name = "parse-zoneinfo" 153 | version = "0.2.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | dependencies = [ 156 | "regex 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 157 | ] 158 | 159 | [[package]] 160 | name = "predicates" 161 | version = "1.0.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | dependencies = [ 164 | "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 165 | "predicates-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 166 | ] 167 | 168 | [[package]] 169 | name = "predicates-core" 170 | version = "1.0.0" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | 173 | [[package]] 174 | name = "predicates-tree" 175 | version = "1.0.0" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | dependencies = [ 178 | "predicates-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 179 | "treeline 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 180 | ] 181 | 182 | [[package]] 183 | name = "proc-macro2" 184 | version = "0.4.24" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | dependencies = [ 187 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 188 | ] 189 | 190 | [[package]] 191 | name = "quote" 192 | version = "0.6.10" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | dependencies = [ 195 | "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", 196 | ] 197 | 198 | [[package]] 199 | name = "redox_syscall" 200 | version = "0.1.40" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | 203 | [[package]] 204 | name = "redox_termios" 205 | version = "0.1.1" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | dependencies = [ 208 | "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 209 | ] 210 | 211 | [[package]] 212 | name = "regex" 213 | version = "1.0.5" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | dependencies = [ 216 | "aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)", 217 | "memchr 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 218 | "regex-syntax 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", 219 | "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 220 | "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 221 | ] 222 | 223 | [[package]] 224 | name = "regex-syntax" 225 | version = "0.6.2" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | dependencies = [ 228 | "ucd-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 229 | ] 230 | 231 | [[package]] 232 | name = "ryu" 233 | version = "0.2.7" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | 236 | [[package]] 237 | name = "serde" 238 | version = "1.0.80" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | dependencies = [ 241 | "serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", 242 | ] 243 | 244 | [[package]] 245 | name = "serde_derive" 246 | version = "1.0.80" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | dependencies = [ 249 | "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", 250 | "quote 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", 251 | "syn 0.15.21 (registry+https://github.com/rust-lang/crates.io-index)", 252 | ] 253 | 254 | [[package]] 255 | name = "serde_json" 256 | version = "1.0.33" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | dependencies = [ 259 | "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 260 | "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 261 | "serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", 262 | ] 263 | 264 | [[package]] 265 | name = "strsim" 266 | version = "0.7.0" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | 269 | [[package]] 270 | name = "syn" 271 | version = "0.15.21" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | dependencies = [ 274 | "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", 275 | "quote 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", 276 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 277 | ] 278 | 279 | [[package]] 280 | name = "term_size" 281 | version = "0.3.1" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | dependencies = [ 284 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 285 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 286 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 287 | ] 288 | 289 | [[package]] 290 | name = "termion" 291 | version = "1.5.1" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | dependencies = [ 294 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 295 | "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 296 | "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 297 | ] 298 | 299 | [[package]] 300 | name = "textwrap" 301 | version = "0.10.0" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | dependencies = [ 304 | "term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 305 | "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 306 | ] 307 | 308 | [[package]] 309 | name = "thread_local" 310 | version = "0.3.6" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | dependencies = [ 313 | "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 314 | ] 315 | 316 | [[package]] 317 | name = "time" 318 | version = "0.1.40" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | dependencies = [ 321 | "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", 322 | "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 323 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 324 | ] 325 | 326 | [[package]] 327 | name = "treeline" 328 | version = "0.1.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | 331 | [[package]] 332 | name = "tztail" 333 | version = "1.2.0" 334 | dependencies = [ 335 | "assert_cmd 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", 336 | "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 337 | "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 338 | "chrono-tz 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 339 | "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", 340 | "colored 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", 341 | "escargot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 342 | "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 343 | "regex 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 344 | ] 345 | 346 | [[package]] 347 | name = "ucd-util" 348 | version = "0.1.2" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | 351 | [[package]] 352 | name = "unicode-width" 353 | version = "0.1.5" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | 356 | [[package]] 357 | name = "unicode-xid" 358 | version = "0.1.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | 361 | [[package]] 362 | name = "utf8-ranges" 363 | version = "1.0.2" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | 366 | [[package]] 367 | name = "vec_map" 368 | version = "0.8.1" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | 371 | [[package]] 372 | name = "version_check" 373 | version = "0.1.5" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | 376 | [[package]] 377 | name = "winapi" 378 | version = "0.2.8" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | 381 | [[package]] 382 | name = "winapi" 383 | version = "0.3.6" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | dependencies = [ 386 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 387 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 388 | ] 389 | 390 | [[package]] 391 | name = "winapi-build" 392 | version = "0.1.1" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | 395 | [[package]] 396 | name = "winapi-i686-pc-windows-gnu" 397 | version = "0.4.0" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | 400 | [[package]] 401 | name = "winapi-x86_64-pc-windows-gnu" 402 | version = "0.4.0" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | 405 | [metadata] 406 | "checksum aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9a933f4e58658d7b12defcf96dc5c720f20832deebe3e0a19efd3b6aaeeb9e" 407 | "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 408 | "checksum assert_cmd 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b7ac5c260f75e4e4ba87b7342be6edcecbcb3eb6741a0507fda7ad115845cc65" 409 | "checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" 410 | "checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" 411 | "checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" 412 | "checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" 413 | "checksum chrono-tz 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44420a821d3075c6b4dcdba557104274a240b5b6e323dc17136507e96ca2db59" 414 | "checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e" 415 | "checksum colored 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc0a60679001b62fb628c4da80e574b9645ab4646056d7c9018885efffe45533" 416 | "checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 417 | "checksum escargot 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "19db1f7e74438642a5018cdf263bb1325b2e792f02dd0a3ca6d6c0f0d7b1d5a5" 418 | "checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" 419 | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 420 | "checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1" 421 | "checksum libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)" = "76e3a3ef172f1a0b9a9ff0dd1491ae5e6c948b94479a3021819ba7d860c8645d" 422 | "checksum memchr 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0a3eb002f0535929f1199681417029ebea04aadc0c7a4224b46be99c7f5d6a16" 423 | "checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" 424 | "checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" 425 | "checksum parse-zoneinfo 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "089a398ccdcdd77b8c38909d5a1e4b67da1bc4c9dbfe6d5b536c828eddb779e5" 426 | "checksum predicates 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fa984b7cd021a0bf5315bcce4c4ae61d2a535db2a8d288fc7578638690a7b7c3" 427 | "checksum predicates-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178" 428 | "checksum predicates-tree 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124" 429 | "checksum proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)" = "77619697826f31a02ae974457af0b29b723e5619e113e9397b8b82c6bd253f09" 430 | "checksum quote 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "53fa22a1994bd0f9372d7a816207d8a2677ad0325b073f5c5332760f0fb62b5c" 431 | "checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" 432 | "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 433 | "checksum regex 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2069749032ea3ec200ca51e4a31df41759190a88edca0d2d86ee8bedf7073341" 434 | "checksum regex-syntax 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "747ba3b235651f6e2f67dfa8bcdcd073ddb7c243cb21c442fc12395dfcac212d" 435 | "checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" 436 | "checksum serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)" = "15c141fc7027dd265a47c090bf864cf62b42c4d228bbcf4e51a0c9e2b0d3f7ef" 437 | "checksum serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)" = "225de307c6302bec3898c51ca302fc94a7a1697ef0845fcee6448f33c032249c" 438 | "checksum serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)" = "c37ccd6be3ed1fdf419ee848f7c758eb31b054d7cd3ae3600e3bae0adf569811" 439 | "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" 440 | "checksum syn 0.15.21 (registry+https://github.com/rust-lang/crates.io-index)" = "816b7af21405b011a23554ea2dc3f6576dc86ca557047c34098c1d741f10f823" 441 | "checksum term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9e5b9a66db815dcfd2da92db471106457082577c3c278d4138ab3e3b4e189327" 442 | "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" 443 | "checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6" 444 | "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" 445 | "checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" 446 | "checksum treeline 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" 447 | "checksum ucd-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d0f8bfa9ff0cadcd210129ad9d2c5f145c13e9ced3d3e5d948a6213487d52444" 448 | "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" 449 | "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 450 | "checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737" 451 | "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 452 | "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 453 | "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 454 | "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" 455 | "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 456 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 457 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 458 | --------------------------------------------------------------------------------