Skip to content

Commit

Permalink
feat: add tree cmd and --format args to CLI (#118)
Browse files Browse the repository at this point in the history
The commit adds a new `tree` command to the OmniBOR CLI which recurses
through the subtree rooted as a particularly directory, and prints the
Artifact IDs of everything in the directory.

It also adds a --format arg to both existing commands to optionally
print things in JSON Lines format.

Signed-off-by: Andrew Lilley Brinker <[email protected]>
  • Loading branch information
alilleybrinker authored Feb 21, 2024
1 parent 3602382 commit 07ff016
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 8 deletions.
23 changes: 21 additions & 2 deletions omnibor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,37 @@ repository = "https://github.com/omnibor/omnibor-rs"
version = "0.3.0"

[dependencies]

# Library dependencies

gitoid = "0.5.0"
tokio = { version = "1.36.0", features = ["io-util"] }
url = "2.5.0"
clap = { version = "4.5.1", features = ["derive"], optional = true }

# Binary-only dependencies

anyhow = { version = "1.0.80", optional = true }
async-recursion = { version = "1.0.5", optional = true }
async-walkdir = { version = "1.0.0", optional = true }
clap = { version = "4.5.1", features = ["derive"], optional = true }
futures-lite = { version = "2.2.0", optional = true }
serde_json = { version = "1.0.114", optional = true }

[dev-dependencies]
tokio = { version = "1.36.0", features = ["io-util", "fs"] }
tokio-test = "0.4.3"

[features]
build-binary = ["anyhow", "clap"]
build-binary = [
"dep:anyhow",
"dep:async-recursion",
"dep:async-walkdir",
"dep:clap",
"dep:futures-lite",
"dep:serde_json",
"tokio/fs",
"tokio/rt-multi-thread"
]

[[bin]]
name = "omnibor"
Expand Down
174 changes: 168 additions & 6 deletions omnibor/src/bin/omnibor.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
use anyhow::anyhow;
use anyhow::Context as _;
use anyhow::Error;
use anyhow::Result;
use async_recursion::async_recursion;
use async_walkdir::WalkDir;
use clap::Args;
use clap::Parser;
use clap::Subcommand;
use futures_lite::stream::StreamExt as _;
use omnibor::Sha256;
use serde_json::json;
use std::default::Default;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use std::fs::File;
use std::path::Path;
use std::path::PathBuf;
use std::process::ExitCode;
use std::str::FromStr;
use tokio::fs::File as AsyncFile;
use tokio::runtime::Runtime;

fn main() -> ExitCode {
let args = Cli::parse();

let result = match args.command {
Command::Id(args) => run_id(args),
Command::Id(ref args) => run_id(args),
Command::Tree(ref args) => run_tree(args),
};

if let Err(err) = result {
eprintln!("{}", err);
if let Err(e) = result {
if let Some(format) = &args.format() {
print_error(e, *format);
} else {
print_plain_error(e);
}

return ExitCode::FAILURE;
}

ExitCode::SUCCESS
}


/*===========================================================================
* CLI Arguments
*-------------------------------------------------------------------------*/
Expand All @@ -35,18 +54,75 @@ struct Cli {
command: Command,
}

impl Cli {
fn format(&self) -> Option<Format> {
match &self.command {
Command::Id(args) => Some(args.format),
Command::Tree(args) => Some(args.format),
}
}
}

#[derive(Debug, Subcommand)]
enum Command {
/// Print the Artifact ID of the path given.
Id(IdArgs),
/// Print the Artifact IDs of a directory tree.
Tree(TreeArgs),
}

#[derive(Debug, Args)]
struct IdArgs {
/// The path to identify.
path: PathBuf,

/// The format of output
#[arg(short = 'f', long = "format", default_value_t)]
format: Format,
}

#[derive(Debug, Args)]
struct TreeArgs {
/// The root of the tree to identify.
path: PathBuf,

/// The format of output (can be "plain" or "json")
#[arg(short = 'f', long = "format", default_value_t)]
format: Format,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Format {
Plain,
Json,
}

impl Default for Format {
fn default() -> Self {
Format::Plain
}
}

impl Display for Format {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Format::Plain => write!(f, "plain"),
Format::Json => write!(f, "json"),
}
}
}

impl FromStr for Format {
type Err = Error;

fn from_str(s: &str) -> Result<Format> {
match s {
"plain" => Ok(Format::Plain),
"json" => Ok(Format::Json),
_ => Err(anyhow!("unknown format '{}'", s)),
}
}
}

/*===========================================================================
* Command Implementations
Expand All @@ -58,10 +134,96 @@ type ArtifactId = omnibor::ArtifactId<Sha256>;
/// Run the `id` subcommand.
///
/// This command just produces the `gitoid` URL for the given file.
fn run_id(args: IdArgs) -> Result<()> {
fn run_id(args: &IdArgs) -> Result<()> {
let path = &args.path;
let file = File::open(path).with_context(|| format!("failed to open '{}'", path.display()))?;
let id = ArtifactId::id_reader(&file).context("failed to produce Artifact ID")?;
println!("{}", id.url());

match args.format {
Format::Plain => {
println!("{}", id.url());
}
Format::Json => {
let output = json!({ "id": id.url().to_string() });
println!("{}", output);
}
}

Ok(())
}

/// Run the `tree` subcommand.
///
/// This command produces the `gitoid` URL for all files in a directory tree.
fn run_tree(args: &TreeArgs) -> Result<()> {
#[async_recursion]
async fn process_dir(path: &Path, format: Format) -> Result<()> {
let mut entries = WalkDir::new(path);

loop {
match entries.next().await {
Some(Ok(entry)) => {
let path = &entry.path();

let file_type = entry
.file_type()
.await
.with_context(|| format!("unable to identify file type for '{}'", path.display()))?;

if file_type.is_dir() {
process_dir(path, format).await?;
continue;
}

let mut file = AsyncFile::open(path)
.await
.with_context(|| format!("failed to open file '{}'", path.display()))?;

let id = ArtifactId::id_async_reader(&mut file)
.await
.with_context(|| {
format!("failed to produce Artifact ID for '{}'", path.display())
})?;

match format {
Format::Plain => println!("{} => {}", path.display(), id.url()),
Format::Json => {
let output = json!({
"path": path.display().to_string(),
"id": id.url().to_string()
});

println!("{}", output);
}
}
}
Some(Err(e)) => print_error(Error::from(e), format),
None => break,
}
}

Ok(())
}

let runtime = Runtime::new().context("failed to initialize the async runtime")?;
runtime.block_on(process_dir(&args.path, args.format))
}

/// Print an error, respecting formatting.
fn print_error(error: Error, format: Format) {
match format {
Format::Plain => print_plain_error(error),
Format::Json => {
let output = json!({
"error": error.to_string(),
});

eprintln!("{}", output);
}
}
}

/// Print an error in plain formatting.
fn print_plain_error(error: Error) {
eprintln!("error: {}", error);
}

0 comments on commit 07ff016

Please sign in to comment.