idx-cli/src/main.rs
0xrsydn 04eaa75882 fix: handle missing scrip column in April 2026 KSEI PDF
The April 2026 IDX ownership PDF omits the zero-valued scrip column for
some holders, causing the parser to leave holdings_scripless/scrip as
empty strings. This broke normalize_ksei_row which called parse_id_number
on empty input.

Two-layer fix:
- Parser: pop_numeric_tail handles 2-column (missing scrip) rows by
  defaulting scrip to "0"
- Normalizer: parse_id_number_or_zero backstop treats empty component
  share fields as 0 while still requiring total_shares

Also includes AGENTS.md refresh, formatting cleanup (rustfmt), and
version bump to 0.2.2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:15:19 +07:00

126 lines
3.3 KiB
Rust

pub mod analysis;
mod api;
mod cache;
mod cli;
mod config;
mod curl_impersonate;
mod error;
mod output;
#[cfg(feature = "ownership")]
pub mod ownership;
pub mod runtime {
use std::fmt::Display;
use std::sync::atomic::{AtomicBool, Ordering};
static QUIET: AtomicBool = AtomicBool::new(false);
pub fn set_quiet(quiet: bool) {
QUIET.store(quiet, Ordering::Relaxed);
}
pub fn is_quiet() -> bool {
QUIET.load(Ordering::Relaxed)
}
pub fn info(message: impl Display) {
if !is_quiet() {
eprintln!("info: {message}");
}
}
pub fn warn(message: impl Display) {
if !is_quiet() {
eprintln!("warning: {message}");
}
}
}
use clap::CommandFactory;
use clap::Parser;
use clap_complete::{generate, shells};
use crate::api::default_provider;
use crate::cli::{Cli, Commands};
use crate::config::IdxConfig;
use crate::error::IdxError;
use crate::output::{OutputFormat, emit_error};
fn main() {
if let Err(err) = run() {
std::process::exit(err.exit_code());
}
}
fn run() -> Result<(), IdxError> {
let cli = Cli::parse();
runtime::set_quiet(cli.quiet);
let config = match IdxConfig::load_with_cli(&cli) {
Ok(config) => config,
Err(err) => {
emit_error(&err, &bootstrap_output_format(&cli));
return Err(err);
}
};
match &cli.command {
Commands::Version => {
println!("{}", env!("CARGO_PKG_VERSION"));
}
Commands::Completions { shell } => {
let mut cmd = Cli::command();
let name = cmd.get_name().to_owned();
match shell {
cli::Shell::Bash => generate(shells::Bash, &mut cmd, name, &mut std::io::stdout()),
cli::Shell::Zsh => generate(shells::Zsh, &mut cmd, name, &mut std::io::stdout()),
cli::Shell::Fish => generate(shells::Fish, &mut cmd, name, &mut std::io::stdout()),
}
}
Commands::Stocks(stocks) => {
let provider = default_provider(config.provider, cli.verbose > 0);
if let Err(err) = cli::stocks::handle(
stocks,
&config,
&provider,
cli.offline,
cli.no_cache,
cli.verbose > 0,
) {
emit_error(&err, &config.output);
return Err(err);
}
}
Commands::Config(cfg) => {
if let Err(err) = cli::config::handle(cfg) {
emit_error(&err, &config.output);
return Err(err);
}
}
Commands::Cache(cache) => {
if let Err(err) = cli::cache::handle(cache) {
emit_error(&err, &config.output);
return Err(err);
}
}
#[cfg(feature = "ownership")]
Commands::Ownership(cmd) => {
if let Err(err) = cli::ownership::handle(&cmd.command, &config) {
emit_error(&err, &config.output);
return Err(err);
}
}
}
Ok(())
}
fn bootstrap_output_format(cli: &Cli) -> OutputFormat {
if let Some(output) = cli.output {
return output;
}
match std::env::var("IDX_OUTPUT") {
Ok(value) if value.eq_ignore_ascii_case("json") => OutputFormat::Json,
_ => OutputFormat::Table,
}
}