การจัดการอาร์กิวเมนต์บรรทัดคำสั่งที่ใช้งานง่าย มีประสิทธิภาพ และแสดงออกชัดเจนสำหรับ C++11/14/17 ที่อยู่ใน ไฟล์ส่วนหัวเดียว
ตัวเลือก, ตัวเลือก+ค่า, ค่าตำแหน่ง, คำสั่งตำแหน่ง, ทางเลือกที่ซ้อนกัน, แผนผังการตัดสินใจ, แฟล็กที่เข้าร่วมได้, ตัวกรองค่าที่กำหนดเอง, ...
การสร้างเอกสาร (บรรทัดการใช้งาน, หน้าคู่มือ); การจัดการข้อผิดพลาด
ตัวอย่างมากมาย การทดสอบชุดใหญ่
พิจารณาอินเทอร์เฟซบรรทัดคำสั่งนี้:
เรื่องย่อ แปลง <ไฟล์อินพุต> [-r] [-o <รูปแบบเอาต์พุต>] [-utf16] ตัวเลือก -r, --recursive แปลงไฟล์แบบเรียกซ้ำ -utf16 ใช้การเข้ารหัส UTF-16
นี่คือโค้ดที่กำหนด input file
ค่าตำแหน่งและสามตัวเลือก -r
, -o
และ -utf16
หากการแยกวิเคราะห์ล้มเหลว ตัวอย่างข้อมูลเหมือนหน้า man ที่เป็นค่าเริ่มต้นข้างต้นจะถูกพิมพ์ไปที่ stdout
#include <iostream>#include "clipp.h" โดยใช้เนมสเปซ clipp; ใช้ std::cout; ใช้ std::string;int main(int argc, char* argv[]) { bool rec = false, utf16 = false; string infile = "", fmt = "csv";auto cli = (value("input file", infile),option("-r", "--recursive").set(rec).doc("แปลงไฟล์ ซ้ำ"),ตัวเลือก("-o") & value("รูปแบบเอาต์พุต", fmt),ตัวเลือก("-utf16").set(utf16).doc("ใช้การเข้ารหัส UTF-16") );if(!parse(argc, argv, cli)) cout << make_man_page(cli, argv[0]);// ...}
เรื่องย่อ ตัวค้นหาทำให้ <wordfile> -dict <dictionary> [--ความคืบหน้า] [-v] ตัวค้นหาค้นหา <infile>... -dict <dictionary> [-o <outfile>] [-split|-nosplit] [-v] ตัวช่วยค้นหา [-v] ตัวเลือก --ความคืบหน้า -p แสดงความคืบหน้า -o, --output <outfile> เขียนลงไฟล์แทน stdout -split, -nosplit (ไม่) แยกเอาต์พุต -v, --version แสดงเวอร์ชัน
CLI นี้มีคำสั่งทางเลือกสามคำสั่ง ( make
, find
, help
) บางคำสั่งของค่าตำแหน่ง ( <wordfile>
, <infile>
) ซึ่งคำสั่งหนึ่งสามารถทำซ้ำได้ แฟล็กที่จำเป็นพร้อมค่าอาร์กิวเมนต์ ( -dict <dictionary>
) และ ตัวเลือกที่มีค่าอาร์กิวเมนต์ ( -o <outfile>
) หนึ่งตัวเลือกที่มีสองทางเลือก ( -split
, -nosplit
) และสองตัวเลือกทั่วไป ( -v
, --progress
)
นี่คือโค้ดที่กำหนดอินเทอร์เฟซ สร้างตัวอย่าง man page ด้านบน และ จัดการผลการแยกวิเคราะห์:
ใช้เนมสเปซ clipp; ใช้ std::cout; ใช้ std::string;//ตัวแปรเก็บผลการแยกวิเคราะห์; เริ่มต้นด้วยค่าเริ่มต้นโหมดคลาสเซนัม {make, find, help}; โหมดที่เลือก = โหมด :: ช่วย; มาตรฐาน::เวกเตอร์<สตริง> อินพุต; string dict, out;bool split = false, progr = false;auto Dictionary = required("-dict") & value("dictionary", dict);auto makeMode = (command("make").set(selected,mode) ::ทำ), ค่า ("wordfile", อินพุต), พจนานุกรม, option("--ความคืบหน้า", "-p").set(progr) % "แสดงความคืบหน้า" );auto findMode = (command("find").set(selected,mode::find), ค่า ("infile", อินพุต), พจนานุกรม, (option("-o", "--output") & value("outfile", out)) % "เขียนลงไฟล์แทน stdout", ( option("-split") ).set(split,true) | option("-nosplit").set(split,false) ) % "(อย่า) แยกเอาต์พุต" );auto cli = ( (makeMode | findMode | command("help").set(selected,mode::help) ),option("-v", "--version").call([]{cout << "เวอร์ชัน 1.0nn" ;}).doc("show version") );if(parse(argc, argv, cli)) {switch(selected) {case mode::make: /* ... */ break;case mode::find : : /* ... */ break;case mode::help: cout << make_man_page(cli, "finder"); หยุดพัก; - } อื่น { cout << use_lines(cli, "finder") << 'n'; -
ด้านล่างนี้คือตัวอย่างเล็กๆ น้อยๆ ที่ควรให้แนวคิดเกี่ยวกับวิธีการทำงานของ clipp พิจารณาการตั้งค่าพื้นฐานนี้ด้วยตัวแปรบางตัวที่เราต้องการตั้งค่าโดยใช้อาร์กิวเมนต์บรรทัดคำสั่ง:
int main(int argc, char* argv[]) { ใช้เนมสเปซ clipp;// กำหนดตัวแปรบางตัว bool a = false, b = false;int n = 0, k = 0;double x = 0.0, y = 0.0; std::vector<int> ids;auto cli = ( /* CODE DEFINING COMMAND LINE INTERFACE GOES HERE */ );parse(argc, argv, cli); //ไม่รวม argv[0]std::cout << use_lines(cli, "exe") << 'n'; -
อินเทอร์เฟซ ( usage_lines ) | รหัส (เนื้อหาของวงเล็บ cli ) |
---|---|
exe [-a] | option("-a", "--all").set(a) |
exe [--all] | option("--all", "-a", "--ALL").set(a) |
exe [-a] [-b] | option("-a").set(a), option("-b").set(b) |
exe -a | required("-a").set(a) |
exe [-a] -b | option("-a").set(a), required("-b").set(b) |
exe [-n <times>] | option("-n", "--iter") & value("times", n) |
exe [-n [<times>]] | option("-n", "--iter") & opt_value("times", n) |
exe -n <times> | required("-n", "--iter") & value("times", n) |
exe -n [<times>] | required("-n", "--iter") & opt_value("times", n) |
exe [-c <x> <y>] | option("-c") & value("x", x) & value("y", y) |
exe -c <x> <y> | required("-c") & value("x", x) & value("y", y) |
exe -c <x> [<y>] | required("-c") & value("x", x) & opt_value("y", y) |
exe [-l <lines>...] | option("-l") & values("lines", ids) |
exe [-l [<lines>...]] | option("-l") & opt_values("lines", ids) |
exe [-l <lines>]... | repeatable( option("-l") & value("lines", ids) ) |
exe -l <lines>... | required("-l") & values("lines", ids) |
exe -l [<lines>...] | required("-l") & opt_values("lines", ids) |
exe (-l <lines>)... | repeatable( required("-l") & value("lines", ids) ) |
exe fetch [-a] | command("fetch").set(k,1), option("-a").set(a) |
exe init | fetch [-a] | command("init").set(k,0) | (command("fetch").set(k,1), option("-a").set(a)) |
exe [-a|-b] | option("-a").set(a) | option("-b").set(b) |
exe [-ma|b] | option("-m") & (required("a").set(a) | required("b").set(b)) |
ดูส่วนตัวอย่างสำหรับคำอธิบายโดยละเอียดของแต่ละหัวข้อ
ตัวระบุเนมสเปซจะถูกละเว้นจากตัวอย่างทั้งหมดเพื่อให้อ่านง่ายขึ้น เอนทิตีทั้งหมดถูกกำหนดไว้ใน namespace clipp
int main(int argc, char* argv[]) { ใช้ namespace clipp;auto cli = ( /* CODE DEFINING COMMAND LINE INTERFACE GOES HERE */ );parse(argc, argv, cli); //แยก argv[0]//ถ้าคุณต้องการรวม argv[0]//parse(argv, argv+argc, cli);}
Building Block มีสองประเภทสำหรับอินเทอร์เฟซบรรทัดคำสั่ง: พารามิเตอร์และกลุ่ม ฟังก์ชั่นโรงงานที่มีชื่ออย่างสะดวกจะสร้างพารามิเตอร์หรือกลุ่มพร้อมการตั้งค่าที่ต้องการ
บูล a = เท็จ, f = เท็จ; สตริง; vector<string> vs;auto cli = ( // ตรงกับตำแหน่งที่ต้องการ คำสั่งทำซ้ำได้("push"), // ตรงทุกประการ ใช่ ใช่ norequired("-f", "--file").set(f), // ใช่ทุกประการ no norequired("-a", "--all", "-A").set(a), // ตรง ไม่ ไม่ ไม่ value("file", s), // หาเรื่องใด ๆ ใช่ ใช่ novalues("file", vs), // หาเรื่องใด ๆ ใช่ ใช่ yesopt_value("file", s), // หาเรื่องใด ๆ ไม่ ใช่ noopt_values("file" , vs), // หาเรื่องใด ๆ ไม่ ใช่ ใช่ // พารามิเตอร์ "catch all" - มีประโยชน์สำหรับการจัดการข้อผิดพลาดany_other (vs), // หาเรื่องใด ๆ ไม่ ไม่ ใช่ // จับข้อโต้แย้งที่ตอบสนองภาคแสดงและ ไม่ตรงกับพารามิเตอร์อื่น ๆany(เพรดิเคต, vs) // เพรดิเคต no no yes);
หน้าที่ข้างต้นคือโรงงานอำนวยความสะดวก:
บูล f = จริง; string s;auto v1 =values("file", s);// เทียบเท่ากับ:auto v2 = parameter{match::nonempty}.label("file").blocking(true).repeatable(true).set (s);auto r1 = required("-f", "--file").set(f);// เทียบเท่ากับ:auto r2 = parameter{"-f", "--file"}.required(true).set(f);
พารามิเตอร์ที่จำเป็นจะต้องตรงกับอาร์กิวเมนต์บรรทัดคำสั่งอย่างน้อยหนึ่งรายการ
พารามิเตอร์ที่ทำซ้ำได้สามารถจับคู่อาร์กิวเมนต์จำนวนเท่าใดก็ได้
พารามิเตอร์ที่ไม่ใช่ตำแหน่ง (=ไม่บล็อก) สามารถจับคู่อาร์กิวเมนต์ในลำดับใดก็ได้
พารามิเตอร์ตำแหน่ง (การปิดกั้น) กำหนด "จุดหยุด" เช่น จนกว่าจะตรงกับพารามิเตอร์ทั้งหมดที่ตามมาจะไม่ได้รับอนุญาตให้จับคู่ เมื่อจับคู่แล้ว พารามิเตอร์ทั้งหมดที่อยู่ก่อนหน้า (ภายในกลุ่มปัจจุบัน) จะไม่สามารถเข้าถึงได้
หากคุณต้องการให้พารามิเตอร์จับคู่ตามลำดับ คุณสามารถรวมเข้าด้วยกันโดยใช้ operator &
หรือฟังก์ชันการจัดกลุ่ม in_sequence
:
อินท์ n = 1; สตริง; vector<int> ls;auto cli = (//ตัวเลือกที่มีค่าตัวเลือกที่ต้องการ ("-n", "--ซ้ำ") & ค่า ("ครั้ง", n) // ตั้งค่าสถานะที่จำเป็นพร้อมค่าตัวเลือกที่จำเป็น ("--ไฟล์ ") & opt_value("name", s), //ตัวเลือกที่มีค่าสองค่าพอดีตัวเลือก("-p", "--pos") & value("x") & value("y"),//เหมือนกับ ก่อนโวลต์ vin_sequence( option("-p", "--pos") , value("x") , value("y") ), //ตัวเลือกที่มีค่าอย่างน้อยหนึ่งค่า (และเป็นทางเลือกเพิ่มเติม)option("-l" ) & ค่า ("เส้น", ls) -
พารามิเตอร์ค่าใช้ฟังก์ชันตัวกรองเพื่อทดสอบว่าได้รับอนุญาตให้จับคู่สตริงอาร์กิวเมนต์หรือไม่ ตัวกรองเริ่มต้น match::nonempty
ที่ใช้โดย value
, values
, opt_value
และ opt_values
จะจับคู่สตริงอาร์กิวเมนต์ที่ไม่ว่างเปล่า คุณสามารถจัดหาฟังก์ชันตัวกรอง/ออบเจ็กต์ฟังก์ชันอื่นๆ เป็นอาร์กิวเมนต์แรกของ value
, values
ฯลฯ หรือใช้หนึ่งในฟังก์ชันแฟกทอรีชวเลขในตัวซึ่งครอบคลุมกรณีที่พบบ่อยที่สุด:
ชื่อสตริง; สองเท่า r = 0.0; int n = 0;auto cli = (value("user", name), // จับคู่ stringword ที่ไม่ว่างเปล่า ("user", name), // จับคู่ stringnumber ตัวอักษรและตัวเลขที่ไม่ว่างเปล่า ("ratio", r) , // จับคู่การแสดงสตริงของจำนวนเต็ม ("times", n) // จับคู่การแสดงสตริงของจำนวนเต็ม);
คล้ายกับ value
, opt_value
ฯลฯ นอกจากนี้ยังมีฟังก์ชันสำหรับ words
, opt_word
เป็นต้น
auto is_char = [](const string& arg) { return arg.size() == 1 && std::isalpha(arg[0]); };ถ่าน c = ' '; // ตรงกับค่าตำแหน่งที่ทำซ้ำได้ที่ต้องการ (is_char, "c", c); // ตัวละครตัวหนึ่ง ใช่ ใช่ ไม่ใช่
จัดกลุ่มพารามิเตอร์ที่เข้ากันได้ร่วมกันด้วยวงเล็บและเครื่องหมายจุลภาค:
อัตโนมัติ cli = ( option("-a"), option("-b"), option("-c") );
จัดกลุ่มพารามิเตอร์ที่ไม่เกิดร่วมกันเป็นทางเลือกโดยใช้ operator |
หรือ one_of
:
auto cli1 = ( value("input_file") | command("list") | command("flush") );auto cli2 = one_of( value("input_file") , command("list") , command("flush" ) );
พารามิเตอร์กลุ่มเพื่อให้ต้องจับคู่ตามลำดับโดยใช้ operator &
หรือ in_sequence
:
สองเท่า x = 0, y = 0, z = 0;auto cli1 = ( option("-pos") & value("X",x) & value("Y",y) & value("Z",z ) );auto cli2 = in_sequence( option("-pos") , value("X",x) , value("Y",y) , value("Z",z) );
โปรดทราบว่ากลุ่มที่อยู่รอบๆ จะไม่ได้รับผลกระทบจากสิ่งนี้ เพื่อให้สามารถจับคู่ -a
และ -b
ในลำดับใดก็ได้ ในขณะที่ -b
และค่า X
จะต้องตรงกันตามลำดับ:
บูล a = เท็จ b = เท็จ; int x = 0;auto cli = ( option("-a").set(a), option("-b").set(b) & value("X",x) );
กลุ่มสามารถซ้อนกันและรวมกันเพื่อสร้างอินเทอร์เฟซที่ซับซ้อนโดยพลการ (ดูที่นี่และที่นี่):
auto cli = ( command("push") | ( command("pull"), option("-f", "--force") ) );
กลุ่มสามารถทำซ้ำได้เช่นกัน:
auto cli1 = ทำซ้ำได้ ( command ("พลิก") | command ("flop") );
บังคับใช้คำนำหน้าทั่วไปในกลุ่มธง:
int x = 0;auto cli1 = with_prefix("-", option("a"), option("b") & value("x",x), ... ); // => -a -b ^unaffected^auto cli2 = with_prefix_short_long("-", "--", option("a", "all"), option("b"), ... ); // => -a --ทั้งหมด -b
บังคับคำต่อท้ายทั่วไปบนกลุ่มธง:
int x = 0;auto cli1 = with_suffix("=", option("a") & value("x",x), ... ); // => a= ^unaffected^auto cli2 = with_suffix_short_long(///, ":=", option("a", "all"), option("b"), ... ); // => ก: ทั้งหมด:= ข:
ทำให้กลุ่มธงสามารถเข้าร่วมได้:
auto cli1 = เข้าร่วมได้ ( option("-a"), option("-b")); // จะจับคู่ "-a", "-b", "-ab", "-ba"// ใช้ได้กับคำนำหน้าทั่วไปตามต้องการ: auto cli2 = joinable( option("--xA0"), option("- -xB1")); // จะจับคู่ "--xA0B1" หรือ "--xB1A0" ด้วย
วิธีที่ง่ายที่สุดในการเชื่อมต่ออินเทอร์เฟซบรรทัดคำสั่งกับโค้ดที่เหลือของคุณคือการผูกค่าอ็อบเจ็กต์หรือการเรียกฟังก์ชัน (อ็อบเจ็กต์) กับพารามิเตอร์ (ดูเพิ่มเติมที่นี่):
บูล b = เท็จ; อินท์ ไอ = 5; อินท์ ม. = 0; สตริง x; ifstream fs;auto cli = ( option("-b").set(b), // "-b" ตรวจพบ -> ตั้งค่า b เป็น trueoption("-m").set(m,2), // " -m" ตรวจพบ -> ตั้งค่า m เป็น 2option("-x") & value("X", x), // ตั้งค่า x จาก arg string option("-i") & opt_value("i", i) , // ตั้งค่า i จากสตริง arg option("-v").call( []{ cout << "v"; } ), // call function (object) / lambdaoption("-v")( []{ cout << "v"; } ), // เช่นเดียวกับ lineoption ก่อนหน้า ("-f") & value("file").call([&](string f){ fs.open(f); }) -
ในรหัสการผลิตเราอาจใช้คลาสการตั้งค่า:
การตั้งค่าโครงสร้าง { บูล x = เท็จ; - การตั้งค่า cmdline_settings (int argc, ถ่าน * argv []) { การตั้งค่า s;auto cli = ( option("-x").set(sx), /* ... */ );parse(argc, argv, cli);return s; -
โปรดทราบว่าเป้าหมายต้องเป็น:
ประเภทพื้นฐาน ( int, long int, float, double, ...
)
ประเภทที่สามารถแปลงได้จาก const char*
เอนทิตีที่เรียกได้: ฟังก์ชัน, วัตถุฟังก์ชัน / แลมบ์ดาที่มีรายการพารามิเตอร์ว่างหรือมีพารามิเตอร์เดียวที่สามารถแปลงได้จาก const char*
Docstrings สำหรับกลุ่มและพารามิเตอร์สามารถตั้งค่าด้วยฟังก์ชันสมาชิก doc
หรือด้วย operator %
:
อัตโนมัติ cli = ( ( option("x").set(x).doc("sets X"),option("y").set(y) % "sets Y" - "กลุ่มเอกสาร 1:" % ( option("-g").set(g).doc("activates G"), option("-h").set(h) % "เปิดใช้งาน H" - ( option("-i").set(i) % "เปิดใช้งาน I", option("-j").set(j) % "เปิดใช้งาน J" ).doc("กลุ่มเอกสาร 2:") -
เส้นการใช้งาน:
cout << use_lines(cli, "progname") << 'n';//พร้อมตัวเลือกการจัดรูปแบบ auto fmt = doc_formatting{} .first_column(3) .last_column(79); cout << use_lines(cli, "progname", fmt) << 'n';
เอกสารรายละเอียด:
cout << เอกสารประกอบ (cli) << 'n';//พร้อมตัวเลือกการจัดรูปแบบ auto fmt = doc_formatting{} .first_column(7) .doc_คอลัมน์(15) .last_column(99); ศาล << เอกสาร (cli, fmt) << 'n';
หน้าคน:
auto cli = ( /*อินเทอร์เฟซบรรทัดคำสั่งกำหนดโค้ดอยู่ที่นี่*/ ); cout << make_man_page(cli, "progname") << 'n';//พร้อมตัวเลือกการจัดรูปแบบauto fmt = doc_formatting{} .first_column(7) .doc_คอลัมน์(15) .last_column(99); ศาล << make_man_page (cli, "progname", fmt) << 'n';
แต่ละพารามิเตอร์สามารถมีฟังก์ชันตัวจัดการเหตุการณ์แนบอยู่ได้ สิ่งเหล่านี้ถูกเรียกใช้หนึ่งครั้งสำหรับแต่ละอาร์กิวเมนต์ที่แมปกับพารามิเตอร์ (หรือหนึ่งครั้งต่อเหตุการณ์ที่หายไป):
string file = "default.txt";auto param = required("-nof").set(file,"") | จำเป็น("-f") & ค่า("ไฟล์", ไฟล์) // ในวันที่ 2, 3, 4,... ตรงกัน (ในกรณีนี้จะเกิดข้อผิดพลาด) .if_repeated( [] { /* ... */ } ) // หากค่าที่ต้องการขาดหายไป .if_missing( [] { /* ... */ } ) // หากไม่สามารถเข้าถึงได้ เช่น ไม่มีแฟล็ก "-f" หน้าชื่อไฟล์ .if_blocked( [] { /* ... */ } ) // หากการจับคู่ขัดแย้งกับทางเลือกอื่น "-nof" .if_conflicted( [] { /* ... */ } );
ฟังก์ชั่นตัวจัดการยังสามารถรับ int ซึ่งถูกกำหนดให้เป็นดัชนีอาร์กิวเมนต์ที่เหตุการณ์เกิดขึ้นก่อน:
string file = "default.txt";auto param = required("-nof").set(file,"") | จำเป็น("-f") & ค่า("ไฟล์", ไฟล์) .if_repeated ( [] (int argIdx) { /* ... */ } ) .if_missing ( [] (int argIdx) { /* ... */ } ) .if_blocked ( [] (int argIdx) { /* ... */ } ) .if_conflicted( [] (int argIdx) { /* ... */ } );
หากเราให้ -f -b
หรือ -b -f -a
เป็นอาร์กิวเมนต์บรรทัดคำสั่งสำหรับ CLI ต่อไปนี้ ข้อผิดพลาดจะถูกรายงาน เนื่องจากค่าหลัง -f
ไม่ใช่ทางเลือก:
auto cli = ( option("-a"), option("-f") & value("ชื่อไฟล์"), option("-b") );
ลักษณะการทำงานนี้ใช้ได้กับกรณีการใช้งานส่วนใหญ่ แต่ถ้าเราต้องการให้โปรแกรมของเราใช้สตริงใดๆ เป็นชื่อไฟล์ เนื่องจากชื่อไฟล์ของเราอาจขัดแย้งกับชื่อแฟล็กล่ะ? เราสามารถทำให้พารามิเตอร์ค่าโลภด้วย operator !
- ด้วยวิธีนี้ สตริงถัดไปหลังจาก -f
จะถูกจับคู่กับลำดับความสำคัญสูงสุดเสมอทันทีที่ได้รับ -f
:
auto cli = ( option("-a"), option("-f") & !value("ชื่อไฟล์"), option("-b") );// ^~~~~~
ระวังให้มาก กับพารามิเตอร์โลภ!
auto cli = ( /* อินเทอร์เฟซของคุณที่นี่ */ );auto res = parse(argc, argv, cli);if(res.any_error()) { /* ... */ }//aggregated problemsif(res.unmapped_args_count ()) { /* ... */ }if(res.any_bad_repeat()) { /* ... */ }if(res.any_blocked()) { /* ... */ }if(res.any_conflict()) { /* ... */ }