akv-cli 0.7.1

The Azure Key Vault CLI (unofficial) can read secrets from Key Vault, securely pass secrets to other commands or inject them into configuration files, encrypt and decrypt secrets, and managed keys and secrets in Key Vault.
Documentation
// Copyright 2025 Heath Stewart.
// Licensed under the MIT License. See LICENSE.txt in the project root for license information.

use akv_cli::{ErrorKind, ResultExt};
use azure_identity::DeveloperToolsCredential;
use azure_storage_blob::BlobServiceClient;
use clap::{Parser, Subcommand};
use futures::TryStreamExt;
use std::{env, path::PathBuf};
use tokio::{
    fs,
    io::{self, AsyncWriteExt},
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    dotazure::load()?;
    let args = Args::parse();

    let endpoint = env::var("AZURE_STORAGE_SERVICE_ENDPOINT")
        .with_context(ErrorKind::Other, "$AZURE_STORAGE_SERVICE_ENDPOINT required")?;

    // Get a container client.
    let credential = DeveloperToolsCredential::new(None)?;
    let client = BlobServiceClient::new(&endpoint, Some(credential), None)?
        .blob_container_client("examples");

    if args.list_args().is_some() {
        // List blobs within the "examples" container.
        let mut pager = client.list_blobs(None)?;
        while let Some(blob) = pager.try_next().await? {
            let blob_name = blob.name.and_then(|n| n.content);
            let blob_name = blob_name.as_deref().unwrap_or("(unknown)");
            let content_type = blob.properties.and_then(|p| p.content_type);
            let content_type = content_type.as_deref().unwrap_or("(unknown)");
            println!("{blob_name} ({content_type})");
        }
    } else if let Some(Commands::Read(args)) = args.command {
        // Read a blob within the "examples" container.
        let content = client
            .blob_client(&args.name)
            .download(None)
            .await?
            .into_body()
            .collect()
            .await?;
        match args.output {
            Some(path) => fs::write(path, content).await?,
            None => io::stdout().write_all(content.as_ref()).await?,
        };
    }

    Ok(())
}

#[derive(Debug, Parser)]
#[command(args_conflicts_with_subcommands = true)]
struct Args {
    #[command(subcommand)]
    command: Option<Commands>,

    /// List blobs.
    #[command(flatten)]
    list: ListArgs,
}

impl Args {
    fn list_args(&self) -> Option<&ListArgs> {
        match self.command {
            Some(Commands::List(ref args)) => Some(args),
            Some(_) => None,
            None => Some(&self.list),
        }
    }
}

#[derive(Debug, Subcommand)]
enum Commands {
    /// List blobs (default).
    List(ListArgs),

    /// Read a blob.
    Read(ReadArgs),
}

#[derive(Debug, clap::Args)]
struct ListArgs;

#[derive(Debug, clap::Args)]
struct ReadArgs {
    /// Name of the blob to read.
    #[arg(short = 'n', long)]
    name: String,

    /// Optional file output.
    #[arg(short = 'o', long)]
    output: Option<PathBuf>,
}