Full Code of JeffrayZhang/terse-cli for AI

main 4104691e4906 cached
13 files
14.5 KB
3.9k tokens
25 symbols
1 requests
Download .txt
Repository: JeffrayZhang/terse-cli
Branch: main
Commit: 4104691e4906
Files: 13
Total size: 14.5 KB

Directory structure:
gitextract_0gob4d2c/

├── .gitignore
├── Cargo.toml
├── LICENSE.md
├── example/
│   ├── Cargo.toml
│   └── main.rs
├── readme.md
├── src/
│   └── main.rs
├── terse_cli/
│   ├── Cargo.toml
│   └── lib.rs
├── terse_cli_lib/
│   ├── Cargo.toml
│   └── lib.rs
└── tests/
    ├── main.rs
    └── test_cli_subcommand.rs

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
/target


================================================
FILE: Cargo.toml
================================================
[package]
name = "terse-cli-workspace"
version = "0.1.0"
edition = "2021"

[dependencies]
terse_cli_lib = { path = "./terse_cli_lib" }
quote = "1.0.37"
pretty_assertions = "1.4.1"

[workspace]
members = ["terse_cli_lib", "terse_cli", "example"]


================================================
FILE: LICENSE.md
================================================
MIT License

Copyright (c) 2024 Jeffray Zhang

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: example/Cargo.toml
================================================
[package]
name = "example"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4.5.21", features = ["derive", "unstable-doc"] }
terse_cli = { path = "../terse_cli" }

[[bin]]
name = "cli"
path = "main.rs"


================================================
FILE: example/main.rs
================================================
use clap::Parser;
use terse_cli::{command, subcommands};

/// Example: cargo run -- command-one --a 3
#[command]
fn command_one(a: i32, b: Option<i32>) -> i32 {
    a + b.unwrap_or(0)
}

/// Example: cargo run -- my-subcommands command-two --name Bob
#[command]
fn command_two(name: String) -> String {
    format!("hello {}", name)
}

/// Example: cargo run -- my-subcommands command-three --a 7 --b 3
#[command]
fn command_three(a: i32, b: i32) -> String {
    format!("the difference is {}", a - b)
}

/// Example: cargo run -- my-subcommands command-four
#[command]
fn command_four() -> String {
    "command four".to_string()
}

subcommands!(
    /// These are the subcommands
    my_subcommands, [command_two, command_three, command_four]);

subcommands!(
    /// Example docs for the "root"
    cli, [command_one, my_subcommands]);

fn main() {
    cli::run(cli::Args::parse());
}

// you can also use `--help` as you would expect
// Example: cargo run -- my-subcommands --help


================================================
FILE: readme.md
================================================

# Terse CLI

A wrapper around [clap](https://github.com/clap-rs/clap) that lets you build CLI applications with very little boilerplate code.

Modeled after [tiangolo's typer python library](https://github.com/fastapi/typer), you simply define your commands and subcommands as functions and annotate them with the `#[command]` attribute.

## Current Status: Alpha

This is a work in progress. The core functionality is implemented, but if you want any customization on how your CLI is used (e.g. positional arguments, custom help messages, etc.) those things are not yet supported.

Known issues:
- [ ] every command must have a return type that implements `Display`
- [ ] positional arguments are not yet supported
- [ ] argument docs are not yet supported

## Installation

Install both clap and terse_cli:

```sh
$ cargo add clap --features derive
$ cargo add terse_cli
```

## Example

Below snippet is from [./example/main.rs](./example/main.rs).

```rs
use clap::Parser;
use terse_cli::{command, subcommands};

/// Example: cargo run -- command-one --a 3
#[command]
fn command_one(a: i32, b: Option<i32>) -> i32 {
    a + b.unwrap_or(0)
}

/// Example: cargo run -- my-subcommands command-two --name Bob
#[command]
fn command_two(name: String) -> String {
    format!("hello {}", name)
}

/// Example: cargo run -- my-subcommands command-three --a 7 --b 3
#[command]
fn command_three(a: i32, b: i32) -> String {
    format!("the difference is {}", a - b)
}

/// Example: cargo run -- my-subcommands command-four
#[command]
fn command_four() -> String {
    "command four".to_string()
}

subcommands!(
    /// These are the subcommands
    my_subcommands, [command_two, command_three, command_four]);

subcommands!(
    /// Example docs for the "root"
    cli, [command_one, my_subcommands]);

fn main() {
    cli::run(cli::Args::parse());
}

// you can also use `--help` as you would expect
// Example: cargo run -- my-subcommands --help

```


================================================
FILE: src/main.rs
================================================
fn main() {
    println!("workspace root, this doesn't do anything!");
}


================================================
FILE: terse_cli/Cargo.toml
================================================
[package]
name = "terse_cli"
version = "0.1.4"
authors = ["David McNamee <david@mcnamee.io>", "Jeffray Zhang <zhangjeffray@gmail.com>"]
edition = "2021"
description = "A library for building no-boilerplate CLI apps"
license = "MIT"
repository = "https://github.com/JeffrayZhang/terse-cli"
homepage = "https://github.com/JeffrayZhang/terse-cli"
readme = "../readme.md"

[dependencies]
proc-macro2 = "1.0.92"
terse_cli_lib = { path = "../terse_cli_lib", version = "0.1.2" }

[lib]
proc-macro = true
path = "lib.rs"


================================================
FILE: terse_cli/lib.rs
================================================
extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn command(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let _attr = proc_macro2::TokenStream::from(_attr);
    let item = proc_macro2::TokenStream::from(item);

    terse_cli_lib::command(_attr, item).unwrap().into()
}

#[proc_macro]
pub fn subcommands(item: TokenStream) -> TokenStream {
    terse_cli_lib::subcommands(proc_macro2::TokenStream::from(item))
        .unwrap()
        .into()
}


================================================
FILE: terse_cli_lib/Cargo.toml
================================================
[package]
name = "terse_cli_lib"
version = "0.1.2"
authors = ["David McNamee <david@mcnamee.io>", "Jeffray Zhang <zhangjeffray@gmail.com>"]
edition = "2021"
description = "A library for building no-boilerplate CLI apps"
license = "MIT"

[dependencies]
convert_case = "0.6.0"
proc-macro2 = "1.0.92"
quote = "1.0.37"
syn = { version = "2.0.89", features = ["full"] }

[lib]
path = "lib.rs"


================================================
FILE: terse_cli_lib/lib.rs
================================================
use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::{bracketed, parse2, Attribute, Ident, ItemFn, Token};

fn extract_docs(a: &[Attribute]) -> impl Iterator<Item = &Attribute> {
    a.iter().filter(|a| a.path().is_ident("doc"))
}

#[derive(Debug)]
pub enum CommandMacroError {
    InvalidCliArgumentError(InvalidCliArgumentError),
    InvalidSubcommandFunctionError(InvalidSubcommandFunctionError),
}

/// don't use this directly, use apps/macros instead
pub fn command(
    _attr: TokenStream,
    item: TokenStream,
) -> Result<TokenStream, CommandMacroError> {
    let input = parse2::<ItemFn>(item).map_err(|err| CommandMacroError::InvalidSubcommandFunctionError(InvalidSubcommandFunctionError {
        message: "Failed to parse function",
            err,
        }),
    )?;
    let fn_name = &input.sig.ident;
    let args = &input.sig.inputs;
    let (fields, arg_names): (Vec<_>, Vec<_>) = args
        .iter()
        .map(|arg| match arg {
            syn::FnArg::Typed(syn::PatType { pat, ty, .. }) => {
                Ok((quote! { #[arg(long)] #pat: #ty }, quote! { #pat }))
            }
            _ => Err(CommandMacroError::InvalidCliArgumentError(InvalidCliArgumentError(format!(
                "Invalid cli argument: {}",
                arg.to_token_stream()
            )))),
        })
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .unzip();

    let docs = extract_docs(&input.attrs);

    let expanded = quote! {
        mod #fn_name {
            use super::#fn_name;
            use clap::{command, Parser};

            #[derive(Parser)]
            #[command(version, about, long_about = None)]
            #(#docs)*
            pub struct Args {
                #(#fields),*
            }
            pub fn run(Args { #(#arg_names),* }: Args) {
                let result = #fn_name(#(#arg_names),*);
                println!("{}", result);
            }
        }
        #input
    };
    Ok(expanded)
}

#[derive(Debug)]
pub enum SubcommandsMacroError {
    InvalidIdentifierError(InvalidIdentifierError),
    InvalidIdentifierListError(InvalidIdentifierListError),
}

/// don't use this directly, use apps/macros instead
pub fn subcommands(
    item: TokenStream,
) -> Result<TokenStream, SubcommandsMacroError> {
    let MergeSubcommandsInput {
        sub_doc,
        cli_ident,
        subcommands,
    } = parse2::<MergeSubcommandsInput>(item).map_err(|err| SubcommandsMacroError::InvalidIdentifierListError(InvalidIdentifierListError {
        message: "subcommands only accepts lists of identifiers",
        err,
    }))?;

    let match_arms = subcommands.iter().map(|sc| {
        let ident = &sc.ident;
        let cmd_name = get_command_name(ident);
        quote! {
            Subcommands::#cmd_name(args) => #ident::run(args)
        }
    });
    let command_enum_fields = subcommands.iter().map(|sc| {
        let docs = extract_docs(&sc.attrs);
        let ident = &sc.ident;
        let cmd_name = get_command_name(ident);
        quote! { 
            #(#docs)*
            #cmd_name(#ident::Args)
        }
    });
    let idents_tokens = subcommands.iter().map(|sc| sc.ident.to_token_stream());
    let sub_doc_mod = sub_doc.clone();

    let expanded = quote! {
        #(#sub_doc_mod)*
        mod #cli_ident {
            use super::{#(#idents_tokens),*};
            use clap::{command, Parser, Subcommand};

            #[derive(Subcommand)]
            #(#sub_doc)*
            pub enum Subcommands {
                #(#command_enum_fields),*
            }
            #[derive(Parser)]
            #[command(version, about, long_about = None)]
            pub struct Args {
                #[command(subcommand)]
                command: Subcommands,
            }
            pub fn run(Args { command }: Args) {
                match command {
                    #(#match_arms),*
                };
            }
        }
    };

    Ok(expanded)
}

#[allow(dead_code)]
struct Subcommand {
    attrs: Vec<Attribute>,
    ident: Ident,
}

struct MergeSubcommandsInput {
    sub_doc: Vec<Attribute>,
    cli_ident: Ident,
    subcommands: Vec<Subcommand>,
}

impl Parse for Subcommand {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let attrs = input.call(Attribute::parse_outer)?;
        let ident: Ident = input.parse()?;
        Ok(Subcommand { attrs, ident })
    }
}

impl Parse for MergeSubcommandsInput {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        // parse any doc comments attached to the cli_ident
        let sub_doc = Attribute::parse_outer(input)
            .unwrap_or_default()
            .into_iter()
            .filter(|attr| attr.path().is_ident("doc"))
            .collect::<Vec<_>>();

        let cli_ident: Ident = input.parse()?;
        input.parse::<Token![,]>()?;
        let content;
        bracketed!(content in input);
        let subcommands: syn::punctuated::Punctuated<Subcommand, Token![,]> =
            content.parse_terminated(Subcommand::parse, Token![,])?;
        Ok(MergeSubcommandsInput {
            sub_doc,
            cli_ident,
            subcommands: subcommands.into_iter().collect(),
        })
    }
}

fn get_command_name(func_name: &Ident) -> Ident {
    Ident::new(
        &func_name.to_string().to_case(Case::UpperCamel),
        func_name.span(),
    )
}

#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct InvalidIdentifierError(&'static str);

#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct InvalidIdentifierListError {
    message: &'static str,
    err: syn::Error,
}

#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct InvalidSubcommandFunctionError {
    message: &'static str,
    err: syn::Error,
}

#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct InvalidCliArgumentError(String);


================================================
FILE: tests/main.rs
================================================
mod test_cli_subcommand;


================================================
FILE: tests/test_cli_subcommand.rs
================================================
use terse_cli_lib::{command, subcommands};
use pretty_assertions::assert_eq;
use quote::quote;

#[test]
pub fn test_command_macro() {
    let in_stream = quote! {
        fn my_func(a: i32, b: i32) -> i32 {
            a + b
        }
    };
    let attr_stream = quote! { #[subcommand] };
    let out_stream = command(attr_stream, in_stream).unwrap();
    let expected = quote! {
        mod my_func {
            use super::my_func;
            use clap::{command, Parser};
            #[derive(Parser)]
            # [command (version , about , long_about = None)]
            pub struct Args {
                #[arg(long)]
                a: i32,
                #[arg(long)]
                b: i32
            }
            pub fn run(Args { a, b }: Args) {
                let result = my_func(a, b);
                println!("{}", result);
            }
        }
        fn my_func(a: i32, b: i32) -> i32 {
            a + b
        }
    };
    assert_eq!(out_stream.to_string(), expected.to_string());
}



#[test]
pub fn test_command_macro_no_args() {
    let in_stream = quote! {
        /// doc comment test
        fn my_func() -> i32 {
            42
        }
    };
    let attr_stream = quote! { #[subcommand] };
    let out_stream = command(attr_stream, in_stream).unwrap();
    let expected = quote! {
        mod my_func {
            use super::my_func;
            use clap::{command, Parser};
        
            #[derive(Parser)]
            #[command(version, about, long_about = None)]
            #[doc = r" doc comment test"]
            pub struct Args {}
        
            pub fn run(Args {}: Args) {
                let result = my_func();
                println!("{}", result);
            }
        }

        #[doc = r" doc comment test"]
        fn my_func() -> i32 {
            42
        }        
    };
    assert_eq!(out_stream.to_string(), expected.to_string());
}

#[test]
pub fn test_subcommands_macro() {
    let in_stream = quote! {
        cli, [command_one, command_two]
    };
    let out_stream = subcommands(in_stream).unwrap();

    let expected = quote! {
        mod cli {
            use super::{command_one, command_two};
            use clap::{command, Parser, Subcommand};

            #[derive(Subcommand)]
            pub enum Subcommands {
                CommandOne(command_one::Args),
                CommandTwo(command_two::Args)
            }

            #[derive(Parser)]
            #[command(version, about, long_about = None)]
            pub struct Args {
                #[command(subcommand)]
                command: Subcommands,
            }

            pub fn run(Args { command }: Args) {
                match command {
                    Subcommands::CommandOne(args) => command_one::run(args),
                    Subcommands::CommandTwo(args) => command_two::run(args)
                };
            }
        }
    };
    assert_eq!(out_stream.to_string(), expected.to_string());
}
Download .txt
gitextract_0gob4d2c/

├── .gitignore
├── Cargo.toml
├── LICENSE.md
├── example/
│   ├── Cargo.toml
│   └── main.rs
├── readme.md
├── src/
│   └── main.rs
├── terse_cli/
│   ├── Cargo.toml
│   └── lib.rs
├── terse_cli_lib/
│   ├── Cargo.toml
│   └── lib.rs
└── tests/
    ├── main.rs
    └── test_cli_subcommand.rs
Download .txt
SYMBOL INDEX (25 symbols across 5 files)

FILE: example/main.rs
  function command_one (line 6) | fn command_one(a: i32, b: Option<i32>) -> i32 {
  function command_two (line 12) | fn command_two(name: String) -> String {
  function command_three (line 18) | fn command_three(a: i32, b: i32) -> String {
  function command_four (line 24) | fn command_four() -> String {
  function main (line 36) | fn main() {

FILE: src/main.rs
  function main (line 1) | fn main() {

FILE: terse_cli/lib.rs
  function command (line 5) | pub fn command(_attr: TokenStream, item: TokenStream) -> TokenStream {
  function subcommands (line 13) | pub fn subcommands(item: TokenStream) -> TokenStream {

FILE: terse_cli_lib/lib.rs
  function extract_docs (line 7) | fn extract_docs(a: &[Attribute]) -> impl Iterator<Item = &Attribute> {
  type CommandMacroError (line 12) | pub enum CommandMacroError {
  function command (line 18) | pub fn command(
  type SubcommandsMacroError (line 68) | pub enum SubcommandsMacroError {
  function subcommands (line 74) | pub fn subcommands(
  type Subcommand (line 134) | struct Subcommand {
  type MergeSubcommandsInput (line 139) | struct MergeSubcommandsInput {
  method parse (line 146) | fn parse(input: ParseStream) -> syn::Result<Self> {
  method parse (line 154) | fn parse(input: ParseStream) -> syn::Result<Self> {
  function get_command_name (line 176) | fn get_command_name(func_name: &Ident) -> Ident {
  type InvalidIdentifierError (line 185) | pub struct InvalidIdentifierError(&'static str);
  type InvalidIdentifierListError (line 189) | pub struct InvalidIdentifierListError {
  type InvalidSubcommandFunctionError (line 196) | pub struct InvalidSubcommandFunctionError {
  type InvalidCliArgumentError (line 203) | pub struct InvalidCliArgumentError(String);

FILE: tests/test_cli_subcommand.rs
  function test_command_macro (line 6) | pub fn test_command_macro() {
  function test_command_macro_no_args (line 41) | pub fn test_command_macro_no_args() {
  function test_subcommands_macro (line 75) | pub fn test_subcommands_macro() {
Condensed preview — 13 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (16K chars).
[
  {
    "path": ".gitignore",
    "chars": 8,
    "preview": "/target\n"
  },
  {
    "path": "Cargo.toml",
    "chars": 245,
    "preview": "[package]\nname = \"terse-cli-workspace\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nterse_cli_lib = { path = \"./te"
  },
  {
    "path": "LICENSE.md",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) 2024 Jeffray Zhang\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "example/Cargo.toml",
    "chars": 224,
    "preview": "[package]\nname = \"example\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nclap = { version = \"4.5.21\", features = [\""
  },
  {
    "path": "example/main.rs",
    "chars": 985,
    "preview": "use clap::Parser;\nuse terse_cli::{command, subcommands};\n\n/// Example: cargo run -- command-one --a 3\n#[command]\nfn comm"
  },
  {
    "path": "readme.md",
    "chars": 1950,
    "preview": "\n# Terse CLI\n\nA wrapper around [clap](https://github.com/clap-rs/clap) that lets you build CLI applications with very li"
  },
  {
    "path": "src/main.rs",
    "chars": 73,
    "preview": "fn main() {\n    println!(\"workspace root, this doesn't do anything!\");\n}\n"
  },
  {
    "path": "terse_cli/Cargo.toml",
    "chars": 513,
    "preview": "[package]\nname = \"terse_cli\"\nversion = \"0.1.4\"\nauthors = [\"David McNamee <david@mcnamee.io>\", \"Jeffray Zhang <zhangjeffr"
  },
  {
    "path": "terse_cli/lib.rs",
    "chars": 492,
    "preview": "extern crate proc_macro;\nuse proc_macro::TokenStream;\n\n#[proc_macro_attribute]\npub fn command(_attr: TokenStream, item: "
  },
  {
    "path": "terse_cli_lib/Cargo.toml",
    "chars": 388,
    "preview": "[package]\nname = \"terse_cli_lib\"\nversion = \"0.1.2\"\nauthors = [\"David McNamee <david@mcnamee.io>\", \"Jeffray Zhang <zhangj"
  },
  {
    "path": "terse_cli_lib/lib.rs",
    "chars": 5900,
    "preview": "use convert_case::{Case, Casing};\nuse proc_macro2::TokenStream;\nuse quote::{quote, ToTokens};\nuse syn::parse::{Parse, Pa"
  },
  {
    "path": "tests/main.rs",
    "chars": 25,
    "preview": "mod test_cli_subcommand;\n"
  },
  {
    "path": "tests/test_cli_subcommand.rs",
    "chars": 2973,
    "preview": "use terse_cli_lib::{command, subcommands};\nuse pretty_assertions::assert_eq;\nuse quote::quote;\n\n#[test]\npub fn test_comm"
  }
]

About this extraction

This page contains the full source code of the JeffrayZhang/terse-cli GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 13 files (14.5 KB), approximately 3.9k tokens, and a symbol index with 25 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!