From 7021a5157d41cc87e58ad42ace7a5ad644d418fa Mon Sep 17 00:00:00 2001 From: ZhouXY-PKU Date: Wed, 7 Oct 2026 15:12:36 +0800 Subject: [PATCH 1/3] feat: -p/--param to enable commandline parameter input and -in to set input filename --- source/source_io/CMakeLists.txt | 1 + source/source_io/input_help.cpp | 5 + source/source_io/parse_args.cpp | 34 +++++ source/source_io/parse_command_line.cpp | 81 ++++++++++++ source/source_io/parse_command_line.h | 39 ++++++ source/source_io/test/CMakeLists.txt | 6 + .../test/parse_command_line_test.cpp | 116 ++++++++++++++++++ source/source_main/driver.cpp | 32 ++--- source/source_main/driver.h | 20 ++- source/source_main/main.cpp | 22 +++- 10 files changed, 336 insertions(+), 20 deletions(-) create mode 100644 source/source_io/parse_command_line.cpp create mode 100644 source/source_io/parse_command_line.h create mode 100644 source/source_io/test/parse_command_line_test.cpp diff --git a/source/source_io/CMakeLists.txt b/source/source_io/CMakeLists.txt index 9c5411a589f..68ec6800e0d 100644 --- a/source/source_io/CMakeLists.txt +++ b/source/source_io/CMakeLists.txt @@ -40,6 +40,7 @@ list(APPEND objects module_json/para_json.cpp parse_args.cpp input_help.cpp + parse_command_line.cpp module_chgpot/get_pchg_pw.cpp module_wf/get_wf_pw.cpp ) diff --git a/source/source_io/input_help.cpp b/source/source_io/input_help.cpp index 9681f717841..bd618e8074a 100644 --- a/source/source_io/input_help.cpp +++ b/source/source_io/input_help.cpp @@ -452,6 +452,11 @@ void ParameterHelp::show_general_help(std::ostream& os) { os << " -i, -I, --info Display detailed build information\n"; os << " -h, --help [param] Display help for parameter (or this message)\n"; os << " -s, --search Search for parameters matching query\n"; + os << " -p, --parameter (Not taking effects yet)\n"; + os << " Set an INPUT variable from the command line.\n"; + // os << " usable in INPUT as ${name} or $name.\n"; + // os << " Repeatable; overrides INPUT 'variable'.\n"; + os << " -in, --input Path to INPUT (default: INPUT)\n"; os << " --check-input Check input file syntax and exit\n"; os << " --generate-parameters-yaml\n"; os << " Dump all parameter metadata as YAML\n"; diff --git a/source/source_io/parse_args.cpp b/source/source_io/parse_args.cpp index 046bc527028..2d454c424eb 100644 --- a/source/source_io/parse_args.cpp +++ b/source/source_io/parse_args.cpp @@ -272,6 +272,40 @@ void parse_args(int argc, char** argv) { ModuleIO::ReadInput::check_mode = true; } + // ---- NEW: pass-through branches for run-control options. + // These flags are owned by ModuleIO::parse_command_line, which is + // called right after this function in main(). Here we only validate + // the token count and skip the tokens, so they survive the + // unknown-argument fallback below. No state is stored here + // (governance rule 1: no new globals). + else if (arg == "-p" || arg == "--param") + { + // Needs two following tokens: + const bool has_pair = (i + 2 < argc); + if (!has_pair) + { + const std::string msg = "Error: Option " + arg + + " requires "; + std::cerr << msg << std::endl; + std::exit(1); + } + // Skip " "; full validation (empty name, + // digit-leading name) is done in parse_command_line. + i += 2; + } + else if (arg == "-in" || arg == "--input") + { + // Needs one following token: + const bool has_file = (i + 1 < argc); + if (!has_file) + { + const std::string msg = "Error: Option " + arg + + " requires "; + std::cerr << msg << std::endl; + std::exit(1); + } + ++i; + } else { // Error message goes to stderr diff --git a/source/source_io/parse_command_line.cpp b/source/source_io/parse_command_line.cpp new file mode 100644 index 00000000000..6091f5eaf23 --- /dev/null +++ b/source/source_io/parse_command_line.cpp @@ -0,0 +1,81 @@ +#include "source_io/parse_command_line.h" + +#include +#include +#include + +namespace ModuleIO +{ + +CommandLineArgs parse_command_line(int argc, char* argv[]) +{ + CommandLineArgs args; + for (int i = 1; i < argc; ++i) + { + const std::string arg = argv[i]; + + if (arg == "-p" || arg == "--parameter") + { + if (i + 2 >= argc) + { + const std::string msg = "Option " + arg + + " requires "; + throw std::runtime_error(msg); + } + ++i; + const std::string name = argv[i]; + if (name.empty()) + { + const std::string msg = "Invalid variable name: (empty)"; + throw std::runtime_error(msg); + } + const unsigned char first_char + = static_cast(name[0]); + const bool starts_with_digit = (std::isdigit(first_char) != 0); + if (starts_with_digit) + { + const std::string msg = "Invalid variable name: " + name; + throw std::runtime_error(msg); + } + ++i; + const std::string value = argv[i]; + args.vars[name] = value; // later duplicates override earlier ones + } + else if (arg == "-in" || arg == "--input") + { + if (i + 1 >= argc) + { + const std::string msg = "Option " + arg + " requires "; + throw std::runtime_error(msg); + } + ++i; + args.input_file = argv[i]; + } + else + { + // Unreachable in normal flow: parse_args rejects unknown + // flags before this function runs. Kept as a defensive check + // so parse_command_line stays self-contained for tests. + const std::string msg = "Unknown option: " + arg; + throw std::runtime_error(msg); + } + } + return args; +} + +void print_help(const std::string& bin) +{ + // Run-control options only; the general help is owned by + // ParameterHelp::show_general_help (via parse_args). + std::printf( + "Run-control options of %s:\n" + " -p, --parameter \n" + " Set an INPUT variable from the command line,\n" + " usable in INPUT as ${name} or $name.\n" + " Repeatable; overrides INPUT 'variable'.\n" + " -in, --input Path to INPUT (default: INPUT)\n", + bin.c_str()); +} + +} // namespace ModuleIO + diff --git a/source/source_io/parse_command_line.h b/source/source_io/parse_command_line.h new file mode 100644 index 00000000000..39e9b2684b8 --- /dev/null +++ b/source/source_io/parse_command_line.h @@ -0,0 +1,39 @@ +#ifndef ModuleIO_PARSE_COMMAND_LINE_H +#define ModuleIO_PARSE_COMMAND_LINE_H + +#include +#include + +namespace ModuleIO +{ + +/// Run-control options parsed after ModuleIO::parse_args. +/// +/// Flag ownership: +/// - Informational flags (-v/-i/-h/-s/--generate-parameters-yaml) and +/// --check-input are owned by ModuleIO::parse_args and never reach +/// this function. +/// - -p/--param and -in/--input are validated (token count only) +/// and skipped by parse_args, then fully parsed here. +struct CommandLineArgs +{ + // -p / --param : INPUT variable injection (repeatable) + // These override INPUT 'variable' definitions. + std::map vars; + + // -in / --input : explicit INPUT path, default "INPUT" + std::string input_file = "INPUT"; +}; + +/// Parses -p/--param and -in/--input options. +/// Throws std::runtime_error on malformed arguments (parse_args has +/// already filtered unknown flags before this function is called). +CommandLineArgs parse_command_line(int argc, char* argv[]); + +/// Prints usage for the run-control options handled here. +void print_help(const std::string& bin_name); + +} // namespace ModuleIO + +#endif // ModuleIO_PARSE_COMMAND_LINE_H + diff --git a/source/source_io/test/CMakeLists.txt b/source/source_io/test/CMakeLists.txt index 3069f6b8510..56c15637fa4 100644 --- a/source/source_io/test/CMakeLists.txt +++ b/source/source_io/test/CMakeLists.txt @@ -113,6 +113,12 @@ AddTest( SOURCES input_help_test.cpp ../input_help.cpp ) +AddTest( + TARGET MODULE_IO_parse_command_line_test + LIBS parameter base device io_input + SOURCES parse_command_line_test.cpp ../parse_command_line.cpp +) + AddTest( TARGET MODULE_IO_bessel_basis_test LIBS parameter base device diff --git a/source/source_io/test/parse_command_line_test.cpp b/source/source_io/test/parse_command_line_test.cpp new file mode 100644 index 00000000000..d210611e0c5 --- /dev/null +++ b/source/source_io/test/parse_command_line_test.cpp @@ -0,0 +1,116 @@ +#include "source_io/parse_command_line.h" + +#include +#include + +namespace +{ +/// Builds an argv array from strings and returns the token count +/// (excluding the terminating nullptr) via the out-parameter. +/// Lifetime note: `storage` and `strings` must outlive the parse call. +int make_argv(std::vector& strings, + std::vector& storage) +{ + strings.insert(strings.begin(), "abacus"); + storage.clear(); + storage.reserve(strings.size()); + for (size_t i = 0; i < strings.size(); ++i) + { + storage.push_back(&strings[i][0]); + } + storage.push_back(nullptr); + return static_cast(strings.size()); +} +} // namespace + +TEST(ParseCommandLineTest, ShortFormSingleVar) +{ + char* argv[] = {(char*)"abacus", + (char*)"-p", (char*)"a", (char*)"1", nullptr}; + const ModuleIO::CommandLineArgs args + = ModuleIO::parse_command_line(4, argv); + ASSERT_EQ(args.vars.size(), 1u); + EXPECT_EQ(args.vars.at("a"), "1"); +} + +TEST(ParseCommandLineTest, LongFormSingleVar) +{ + char* argv[] = {(char*)"abacus", + (char*)"--parameter", (char*)"ecutwfc", (char*)"120", + nullptr}; + const ModuleIO::CommandLineArgs args + = ModuleIO::parse_command_line(4, argv); + EXPECT_EQ(args.vars.at("ecutwfc"), "120"); +} + +TEST(ParseCommandLineTest, MultipleVarsLastWins) +{ + std::vector tokens + = {"-p", "a", "1", "-p", "a", "2", "-p", "b", "x"}; + std::vector storage; + const int argc = make_argv(tokens, storage); + const ModuleIO::CommandLineArgs args + = ModuleIO::parse_command_line(argc, storage.data()); + EXPECT_EQ(args.vars.at("a"), "2"); + EXPECT_EQ(args.vars.at("b"), "x"); +} + +TEST(ParseCommandLineTest, VarPlusCustomInput) +{ + char* argv[] = {(char*)"abacus", + (char*)"-p", (char*)"suffix", (char*)"TestRun", + (char*)"-in", (char*)"myINPUT", + nullptr}; + const ModuleIO::CommandLineArgs args + = ModuleIO::parse_command_line(6, argv); + EXPECT_EQ(args.vars.at("suffix"), "TestRun"); + EXPECT_EQ(args.input_file, "myINPUT"); +} + +TEST(ParseCommandLineTest, DefaultInputPath) +{ + char* argv[] = {(char*)"abacus", nullptr}; + const ModuleIO::CommandLineArgs args + = ModuleIO::parse_command_line(1, argv); + EXPECT_EQ(args.input_file, "INPUT"); + EXPECT_TRUE(args.vars.empty()); +} + +TEST(ParseCommandLineTest, MissingValueThrows) +{ + char* argv[] = {(char*)"abacus", (char*)"-p", (char*)"a", nullptr}; + EXPECT_THROW(ModuleIO::parse_command_line(3, argv), std::runtime_error); +} + +TEST(ParseCommandLineTest, EmptyNameThrows) +{ + char* argv[] = {(char*)"abacus", + (char*)"-p", (char*)"", (char*)"x", nullptr}; + EXPECT_THROW(ModuleIO::parse_command_line(4, argv), std::runtime_error); +} + +TEST(ParseCommandLineTest, DigitLeadingNameThrows) +{ + char* argv[] = {(char*)"abacus", + (char*)"-p", (char*)"1abc", (char*)"x", nullptr}; + EXPECT_THROW(ModuleIO::parse_command_line(4, argv), std::runtime_error); +} + +TEST(ParseCommandLineTest, UnknownOptionThrows) +{ + char* argv[] = {(char*)"abacus", (char*)"--bogus", nullptr}; + EXPECT_THROW(ModuleIO::parse_command_line(2, argv), std::runtime_error); +} + +// The exact-argc contract: with argc == 9 the last pair is truncated +// and must throw (documents that argc counts tokens, not nullptr). +TEST(ParseCommandLineTest, TruncatedArgcThrows) +{ + char* argv[] = {(char*)"abacus", + (char*)"-p", (char*)"a", (char*)"1", + (char*)"-p", (char*)"a", (char*)"2", + (char*)"-p", (char*)"b", (char*)"x", + nullptr}; + EXPECT_THROW(ModuleIO::parse_command_line(9, argv), std::runtime_error); +} + diff --git a/source/source_main/driver.cpp b/source/source_main/driver.cpp index 155f18a0f6d..5671322639c 100644 --- a/source/source_main/driver.cpp +++ b/source/source_main/driver.cpp @@ -1,9 +1,9 @@ #include "source_main/driver.h" - #include "source_base/global_file.h" #include "source_base/memory_recorder.h" #include "source_base/timer.h" #include "source_esolver/esolver.h" +#include "source_io/parse_command_line.h" #include "source_io/module_output/cal_test.h" #include "source_io/module_parameter/input_conv.h" #include "source_io/module_json/para_json.h" @@ -25,7 +25,7 @@ Driver::~Driver() { } -void Driver::init() +void Driver::init(const ModuleIO::CommandLineArgs& cli) { // 1) Let's start by printing a title. ModuleBase::TITLE("Driver", "ABACUS_begins"); @@ -35,7 +35,7 @@ void Driver::init() ModuleBase::timer::start(); // 3) Welcome to the atomic world! Let's do some fancy stuff here. - this->atomic_world(); + this->atomic_world(cli); // 4) All timers recorders are printed. ModuleBase::timer::finish(GlobalV::ofs_running); @@ -43,16 +43,15 @@ void Driver::init() // 5) All memory recorders are printed. ModuleBase::Memory::print_all(GlobalV::ofs_running); - // 6) Print the final time, hopefully it will not cost too long. + // 6) Print the final time, hopefully it will not cost too long. time_t time_finish = std::time(nullptr); ModuleIO::print_time(time_start, time_finish); // 7) Clean up: close all of the running logs - ModuleBase::Global_File::close_all_log(GlobalV::MY_RANK, PARAM.inp.out_alllog,PARAM.inp.calculation); - + ModuleBase::Global_File::close_all_log(GlobalV::MY_RANK, PARAM.inp.out_alllog, PARAM.inp.calculation); } -void Driver::print_start_info() +void Driver::print_start_info(const std::string& input_card) { ModuleBase::TITLE("Driver", "print_start_info"); #ifdef VERSION @@ -108,10 +107,10 @@ void Driver::print_start_info() GlobalV::ofs_running << "\n READING GENERAL INFORMATION" << std::endl; ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "global_out_dir", PARAM.globalv.global_out_dir); - ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "global_in_card", PARAM.globalv.global_in_card); + ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "global_in_card", input_card); } -void Driver::reading() +void Driver::reading(const ModuleIO::CommandLineArgs& cli) { ModuleBase::TITLE("Driver", "reading"); ModuleBase::timer::start("Driver", "reading"); @@ -120,8 +119,11 @@ void Driver::reading() GlobalV::NPROC = PARAM.globalv.nproc; // (1) read the input file + // The command line variable pool (-p/--parameter) is forwarded + // explicitly; no global state is introduced (governance rule 1). ModuleIO::ReadInput input(PARAM.globalv.myrank); - input.read_parameters(PARAM, PARAM.globalv.global_in_card); + // input.read_parameters(PARAM, cli.input_file, cli.vars); + input.read_parameters(PARAM, cli.input_file); ModuleBase::set_quit_out_dir(PARAM.globalv.global_out_dir); ModuleBase::set_quit_calculation(PARAM.inp.calculation); @@ -141,11 +143,11 @@ void Driver::reading() // (2) create the output directory, running_*.log and print info input.create_directory(PARAM); - this->print_start_info(); + this->print_start_info(cli.input_file); // (3) write the input file std::stringstream ss1; - ss1 << PARAM.globalv.global_out_dir << PARAM.globalv.global_in_card << ".info"; + ss1 << PARAM.globalv.global_out_dir << cli.input_file << ".info"; input.write_parameters(PARAM, ss1.str()); // (*temp*) copy the variables from INPUT to each class @@ -186,13 +188,13 @@ void Driver::reading() ModuleBase::timer::end("Driver", "reading"); } -void Driver::atomic_world() +void Driver::atomic_world(const ModuleIO::CommandLineArgs& cli) { ModuleBase::TITLE("Driver", "atomic_world"); ModuleBase::timer::start("Driver", "atomic_world"); - // reading information - this->reading(); + // reading information + this->reading(cli); // where the actual stuff is done this->driver_run(); diff --git a/source/source_main/driver.h b/source/source_main/driver.h index e0204d079ca..c6d8ff59d1c 100644 --- a/source/source_main/driver.h +++ b/source/source_main/driver.h @@ -1,6 +1,12 @@ #ifndef DRIVER_H #define DRIVER_H +#include + +namespace ModuleIO +{ +struct CommandLineArgs; +} class Driver { @@ -11,28 +17,34 @@ class Driver /** * @brief An interface function. * This function calls "this->reading()", "this->atomic_world()" in order. + * + * The parsed command line arguments are passed explicitly so that the + * INPUT variable pool injected via -p/--parameter reaches ReadInput + * without going through any global state (governance rule 1). */ - void init(); + void init(const ModuleIO::CommandLineArgs& cli); private: /** * @brief Print the start information. * */ - void print_start_info(); + void print_start_info(const std::string& input_card); /** * @brief Reading the parameters and split the MPI world. * This function read the parameter in "INPUT", "STRU" etc, * and split the MPI world into different groups. + * + * The command line variable pool is forwarded to ReadInput here. */ - void reading(); + void reading(const ModuleIO::CommandLineArgs& cli); /** * @brief An interface function. * This function calls "this->driver_run()" to do calculation, * and log the time and memory consumed during calculation. */ - void atomic_world(); + void atomic_world(const ModuleIO::CommandLineArgs& cli); // the actual calculations void driver_run(); diff --git a/source/source_main/main.cpp b/source/source_main/main.cpp index e22d10455a2..28c452cc09a 100644 --- a/source/source_main/main.cpp +++ b/source/source_main/main.cpp @@ -7,6 +7,7 @@ #include "fftw3.h" #include "source_base/parallel_global.h" #include "source_io/parse_args.h" +#include "source_io/parse_command_line.h" #include "source_io/module_parameter/parameter.h" #include "source_main/version.h" #ifdef _OPENMP @@ -86,11 +87,30 @@ int main(int argc, char** argv) #endif PARAM.set_pal_param(my_rank, nproc, nthread_per_proc); + // Step 1: informational flags + --check-input. + // May exit(0)/exit(1) for -v/-i/-h/-s/--generate-parameters-yaml; + // -p/-in tokens are validated and passed through. + ModuleIO::parse_args(argc, argv); + + // Step 2: run-control options. All ranks execute the identical + // parse, so the result is rank-consistent without any Bcast. + ModuleIO::CommandLineArgs cli; + try + { + cli = ModuleIO::parse_command_line(argc, argv); + } + catch (const std::exception& e) + { + const std::string err = e.what(); + std::fprintf(stderr, "Command line error: %s\n", err.c_str()); + ModuleIO::print_help(argv[0]); + return 1; + } /* main program for doing electronic structure calculations. */ Driver DD; - DD.init(); + DD.init(cli); /* After running mpi version of abacus, release the mpi resources. From 4eb38768804bc136e3e13770be8f75946661e47d Mon Sep 17 00:00:00 2001 From: ZhouXY-PKU Date: Wed, 7 Oct 2026 20:23:00 +0800 Subject: [PATCH 2/3] Change --param to --parameter. --- source/source_io/parse_args.cpp | 2 +- source/source_io/parse_command_line.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/source/source_io/parse_args.cpp b/source/source_io/parse_args.cpp index 2d454c424eb..dad5fe68c58 100644 --- a/source/source_io/parse_args.cpp +++ b/source/source_io/parse_args.cpp @@ -278,7 +278,7 @@ void parse_args(int argc, char** argv) // the token count and skip the tokens, so they survive the // unknown-argument fallback below. No state is stored here // (governance rule 1: no new globals). - else if (arg == "-p" || arg == "--param") + else if (arg == "-p" || arg == "--parameter") { // Needs two following tokens: const bool has_pair = (i + 2 < argc); diff --git a/source/source_io/parse_command_line.h b/source/source_io/parse_command_line.h index 39e9b2684b8..6cc549ffdf2 100644 --- a/source/source_io/parse_command_line.h +++ b/source/source_io/parse_command_line.h @@ -13,11 +13,11 @@ namespace ModuleIO /// - Informational flags (-v/-i/-h/-s/--generate-parameters-yaml) and /// --check-input are owned by ModuleIO::parse_args and never reach /// this function. -/// - -p/--param and -in/--input are validated (token count only) +/// - -p/--parameter and -in/--input are validated (token count only) /// and skipped by parse_args, then fully parsed here. struct CommandLineArgs { - // -p / --param : INPUT variable injection (repeatable) + // -p / --parameter : INPUT variable injection (repeatable) // These override INPUT 'variable' definitions. std::map vars; @@ -25,7 +25,7 @@ struct CommandLineArgs std::string input_file = "INPUT"; }; -/// Parses -p/--param and -in/--input options. +/// Parses -p/--parameter and -in/--input options. /// Throws std::runtime_error on malformed arguments (parse_args has /// already filtered unknown flags before this function is called). CommandLineArgs parse_command_line(int argc, char* argv[]); From 1be31037051ae0ad9c1b8860309685c762cf0bfe Mon Sep 17 00:00:00 2001 From: Levi Zhou <31941107+ZhouXY-PKU@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:09:50 +0800 Subject: [PATCH 3/3] Update Makefile.Objects --- source/Makefile.Objects | 1 + 1 file changed, 1 insertion(+) diff --git a/source/Makefile.Objects b/source/Makefile.Objects index 98d85e38d9c..cc79dd1086c 100644 --- a/source/Makefile.Objects +++ b/source/Makefile.Objects @@ -717,6 +717,7 @@ OBJS_IO=module_parameter/input_conv.o\ init_info.o\ output_info.o\ parse_args.o\ + parse_command_line.o\ module_parameter/availability.o\ module_parameter/availability_validator.o\ module_parameter/read_input.o\