Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion contrib/clarity-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

use std::io::Write;
use std::io::{Read as _, Write};
use std::path::PathBuf;
use std::{fs, io};

Expand Down Expand Up @@ -114,6 +114,46 @@ fn friendly_expect_opt<A>(input: Option<A>, msg: &str) -> A {
})
}

/// Read text content from a file path or stdin if path is "-"
pub fn read_file_or_stdin(path: &str) -> String {
if path == "-" {
let mut buffer = String::new();
io::stdin()
.read_to_string(&mut buffer)
.expect("Error reading from stdin");
buffer
} else {
fs::read_to_string(path).unwrap_or_else(|e| panic!("Error reading file {path}: {e}"))
}
}

/// Read binary content from a file path or stdin if path is "-"
pub fn read_file_or_stdin_bytes(path: &str) -> Vec<u8> {
if path == "-" {
let mut buffer = vec![];
io::stdin()
.read_to_end(&mut buffer)
.expect("Error reading from stdin");
buffer
} else {
fs::read(path).unwrap_or_else(|e| panic!("Error reading file {path}: {e}"))
}
}

/// Read content from an optional file path, defaulting to stdin if None or "-"
pub fn read_optional_file_or_stdin(path: Option<&PathBuf>) -> String {
match path {
Some(p) => read_file_or_stdin(p.to_str().expect("Invalid UTF-8 in path")),
None => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: None could defer to - handling instead of duplicating the logic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% agree!

let mut buffer = String::new();
io::stdin()
.read_to_string(&mut buffer)
.expect("Error reading from stdin");
buffer
}
}
}

/// Represents an initial allocation entry from JSON
#[derive(Deserialize)]
pub struct InitialAllocation {
Expand Down
33 changes: 3 additions & 30 deletions contrib/clarity-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,47 +14,20 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

use std::io::Read;
use std::path::PathBuf;
use std::{fs, io, process};
use std::process;

use clap::{Parser, Subcommand};
use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier};
use clarity::vm::{ClarityVersion, SymbolicExpression};
use clarity_cli::{
DEFAULT_CLI_EPOCH, execute_check, execute_eval, execute_eval_at_block,
execute_eval_at_chaintip, execute_eval_raw, execute_execute, execute_generate_address,
execute_initialize, execute_launch, execute_repl, vm_execute_in_epoch,
execute_initialize, execute_launch, execute_repl, read_file_or_stdin,
read_optional_file_or_stdin, vm_execute_in_epoch,
};
use stacks_common::types::StacksEpochId;

/// Read content from a file path or stdin if path is "-"
fn read_file_or_stdin(path: &str) -> String {
if path == "-" {
let mut buffer = String::new();
io::stdin()
.read_to_string(&mut buffer)
.expect("Error reading from stdin");
buffer
} else {
fs::read_to_string(path).unwrap_or_else(|e| panic!("Error reading file {path}: {e}"))
}
}

/// Read content from an optional file path, defaulting to stdin if None or "-"
fn read_optional_file_or_stdin(path: Option<&PathBuf>) -> String {
match path {
Some(p) => read_file_or_stdin(p.to_str().expect("Invalid UTF-8 in path")),
None => {
let mut buffer = String::new();
io::stdin()
.read_to_string(&mut buffer)
.expect("Error reading from stdin");
buffer
}
}
}

/// Parse epoch string to StacksEpochId
fn parse_epoch(epoch_str: Option<&String>) -> StacksEpochId {
if let Some(s) = epoch_str {
Expand Down
13 changes: 9 additions & 4 deletions contrib/stacks-inspect/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::{fs, process};

use clarity::types::chainstate::SortitionId;
use clarity::util::hash::{Sha512Trunc256Sum, to_hex};
use clarity_cli::read_file_or_stdin;
use regex::Regex;
use rusqlite::{Connection, OpenFlags};
use stacks_common::types::chainstate::{BlockHeaderHash, StacksBlockId};
Expand Down Expand Up @@ -568,18 +569,22 @@ pub fn command_contract_hash(argv: &[String], _conf: Option<&Config>) {
let print_help_and_exit = || -> ! {
let n = &argv[0];
eprintln!("Usage:");
eprintln!(" {n} <path-to-contract>");
eprintln!(" {n} <CONTRACT_PATH | - (stdin)>");
process::exit(1);
};

// Process CLI args
let contract_path = argv.get(1).unwrap_or_else(|| print_help_and_exit());
let contract_source = fs::read_to_string(contract_path)
.unwrap_or_else(|e| panic!("Failed to read contract file {contract_path:?}: {e}"));
let contract_source = read_file_or_stdin(contract_path);

let hash = Sha512Trunc256Sum::from_data(contract_source.as_bytes());
let hex_string = to_hex(hash.as_bytes());
println!("Contract hash for {contract_path}:\n{hex_string}");
let source_name = if contract_path == "-" {
"stdin"
} else {
contract_path
};
println!("Contract hash for {source_name}:\n{hex_string}");
}

/// Fetch and process a `StagingBlock` from database and call `replay_block()` to validate
Expand Down
65 changes: 38 additions & 27 deletions contrib/stacks-inspect/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ extern crate stacks_common;
use clarity::consts::CHAIN_ID_MAINNET;
use clarity::types::StacksEpochId;
use clarity::types::chainstate::StacksPrivateKey;
use clarity_cli::DEFAULT_CLI_EPOCH;
use clarity_cli::{DEFAULT_CLI_EPOCH, read_file_or_stdin, read_file_or_stdin_bytes};
use stacks_inspect::{
command_contract_hash, command_replay_mock_mining, command_try_mine, command_validate_block,
command_validate_block_nakamoto, drain_common_opts,
Expand Down Expand Up @@ -394,12 +394,21 @@ fn main() {

if argv[1] == "decode-tx" {
if argv.len() < 3 {
eprintln!("Usage: {} decode-tx TRANSACTION", argv[0]);
eprintln!(
"Usage: {} decode-tx <TX_HEX | TX_FILE | - (stdin)>",
argv[0]
);
process::exit(1);
}

let tx_str = &argv[2];
let tx_bytes = hex_bytes(tx_str)
let tx_arg = &argv[2];
let tx_str = if tx_arg == "-" || std::path::Path::new(tx_arg).exists() {
read_file_or_stdin(tx_arg).trim().to_string()
} else {
// Treat as hex string directly
tx_arg.clone()
};
let tx_bytes = hex_bytes(&tx_str)
.map_err(|_e| {
eprintln!("Failed to decode transaction: must be a hex string");
process::exit(1);
Expand Down Expand Up @@ -430,13 +439,12 @@ fn main() {

if argv[1] == "decode-block" {
if argv.len() < 3 {
eprintln!("Usage: {} decode-block BLOCK_PATH", argv[0]);
eprintln!("Usage: {} decode-block <BLOCK_PATH | - (stdin)>", argv[0]);
process::exit(1);
}

let block_path = &argv[2];
let block_data =
fs::read(block_path).unwrap_or_else(|_| panic!("Failed to open {block_path}"));
let block_data = read_file_or_stdin_bytes(block_path);

let block = StacksBlock::consensus_deserialize(&mut io::Cursor::new(&block_data))
.map_err(|_e| {
Expand All @@ -451,12 +459,21 @@ fn main() {

if argv[1] == "decode-nakamoto-block" {
if argv.len() < 3 {
eprintln!("Usage: {} decode-nakamoto-block BLOCK_HEX", argv[0]);
eprintln!(
"Usage: {} decode-nakamoto-block <BLOCK_HEX | HEX_FILE | - (stdin)>",
argv[0]
);
process::exit(1);
}

let block_hex = &argv[2];
let block_data = hex_bytes(block_hex).unwrap_or_else(|_| panic!("Failed to decode hex"));
let block_arg = &argv[2];
let block_hex = if block_arg == "-" || std::path::Path::new(block_arg).exists() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach of making the "value or filename" decision based on whether a file of said name exists feels off to me, I don't think I've ever seen that. Do you know of any precedent for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's a good point, you're right that this pattern is uncommon. My reasoning was:

  1. Backward compatibility: Commands like decode-tx previously only accepted hex strings directly. By checking file existence, existing scripts passing hex strings continue to work unchanged, while also enabling file/stdin input.
  2. Practical safety: For hex inputs (transaction hashes, block data), it's extremely unlikely a valid hex string would collide with an existing file path. These are typically 64+ character strings like 0a1b2c3d....
  3. Convenience: Users don't need to remember different flags (--file vs --hex) for a simple decode operation.

But I totally understand the concern and "extremely unlikely" is not something that I totally love either. Do you think I should add --file and --hex flags?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably wouldn't need both -- if the default (backwards-compatible) behavior is a direct value, then you just need --file (or something like it) to change the semantics. So this:

stacks-inspect foo c0ff33

would be the value 0xc0ff33, and this:

stacks-inspect foo --file c0ff33

would refer to a file named c0ff33.

This does feel cleaner to me, but you have more intuition on the practical uses of this.

Copy link
Contributor Author

@simone-stacks simone-stacks Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's definitely cleaner! Since I'm working on the refactoring of stacks-inspect using clap, and adding --file would require a dedicated extract_flag function or something like that, that would then be erased in favor of clap's parsing, I'd address both your comments in the clap refactoring PR

read_file_or_stdin(block_arg).trim().to_string()
} else {
// Treat as hex string directly
block_arg.clone()
};
let block_data = hex_bytes(&block_hex).unwrap_or_else(|_| panic!("Failed to decode hex"));
let block = NakamotoBlock::consensus_deserialize(&mut io::Cursor::new(&block_data))
.map_err(|_e| {
eprintln!("Failed to decode block");
Expand All @@ -470,11 +487,10 @@ fn main() {

if argv[1] == "decode-net-message" {
let data: String = argv[2].clone();
let buf = if data == "-" {
let mut buffer = vec![];
io::stdin().read_to_end(&mut buffer).unwrap();
buffer
let buf = if data == "-" || std::path::Path::new(&data).exists() {
read_file_or_stdin_bytes(&data)
} else {
// Parse as JSON array of bytes
let data: serde_json::Value = serde_json::from_str(data.as_str()).unwrap();
let data_array = data.as_array().unwrap();
let mut buf = vec![];
Expand Down Expand Up @@ -804,15 +820,14 @@ check if the associated microblocks can be downloaded
if argv[1] == "decode-microblocks" {
if argv.len() < 3 {
eprintln!(
"Usage: {} decode-microblocks MICROBLOCK_STREAM_PATH",
"Usage: {} decode-microblocks <MICROBLOCK_STREAM_PATH | - (stdin)>",
argv[0]
);
process::exit(1);
}

let mblock_path = &argv[2];
let mblock_data =
fs::read(mblock_path).unwrap_or_else(|_| panic!("Failed to open {mblock_path}"));
let mblock_data = read_file_or_stdin_bytes(mblock_path);

let mut cursor = io::Cursor::new(&mblock_data);
let mut debug_cursor = LogReader::from_reader(&mut cursor);
Expand Down Expand Up @@ -1028,7 +1043,7 @@ check if the associated microblocks can be downloaded
if argv[1] == "post-stackerdb" {
if argv.len() < 4 {
eprintln!(
"Usage: {} post-stackerdb slot_id slot_version privkey data",
"Usage: {} post-stackerdb slot_id slot_version privkey <DATA | DATA_FILE | - (stdin)>",
&argv[0]
);
process::exit(1);
Expand All @@ -1038,11 +1053,10 @@ check if the associated microblocks can be downloaded
let privkey: String = argv[4].clone();
let data: String = argv[5].clone();

let buf = if data == "-" {
let mut buffer = vec![];
io::stdin().read_to_end(&mut buffer).unwrap();
buffer
let buf = if data == "-" || std::path::Path::new(&data).exists() {
read_file_or_stdin_bytes(&data)
} else {
// Use the argument directly as data
data.as_bytes().to_vec()
};

Expand Down Expand Up @@ -1221,7 +1235,7 @@ check if the associated microblocks can be downloaded
if argv[1] == "shadow-chainstate-patch" {
if argv.len() < 5 {
eprintln!(
"Usage: {} shadow-chainstate-patch CHAINSTATE_DIR NETWORK SHADOW_BLOCKS_PATH.JSON",
"Usage: {} shadow-chainstate-patch CHAINSTATE_DIR NETWORK <SHADOW_BLOCKS.JSON | - (stdin)>",
&argv[0]
);
process::exit(1);
Expand All @@ -1232,10 +1246,7 @@ check if the associated microblocks can be downloaded
let shadow_blocks_json_path = argv[4].as_str();

let shadow_blocks_hex = {
let mut blocks_json_file =
File::open(shadow_blocks_json_path).expect("Unable to open file");
let mut buffer = vec![];
blocks_json_file.read_to_end(&mut buffer).unwrap();
let buffer = read_file_or_stdin_bytes(shadow_blocks_json_path);
let shadow_blocks_hex: Vec<String> = serde_json::from_slice(&buffer).unwrap();
shadow_blocks_hex
};
Expand Down
Loading