mscript 3.0:数据库编程、日志记录和多项语言改进






4.80/5 (3投票s)
查看第三个主要版本的 mscript,用于用简单的 mscript 替换您糟糕的批处理文件。
引言
通过 mscript 的第三个主要版本,我收到了反馈和指导,这些反馈和指导为我的代码更改和 DLL 开发提供了信息。我欢迎您对好、坏和丑陋的见解,我承诺在继续塑造 mscript 以摆脱批处理文件限制以及 Powershell 和 Python 的负担时,会考虑您的输入。
在本文中,我将介绍新的数据库和日志记录 DLL,以及新的参数处理函数 parseArgs()
。
在深入介绍 API 和实现细节之前,让我们先来看几个示例脚本。
- file_search.ms - 将文本文件的行加载到 4db 中,然后对行进行全文本关键字搜索
- file_sizes.ms - 加载目录的文件大小信息,然后提供文件计数和占用的空间搜索
1. file_search.ms - 文件行的全文本关键字搜索
/*
This sample program reads lines from an input file
and writes them to a 4db NoSQL database.
Once the database is loaded, the user enters search terms and gets matching lines.
This sample demonstrates command line processing, file I/O, and 4db database programming.
Note the extensive use of \ line continuations: mscripts may be line-based,
but they don't have to be ugly!
*/
+ "mscript-db.dll"
/ Define the command line flags we expect as arguments specifications
/ The arguments specifications are represented as a list of indexes
$ arg_specs = \
list \
( \
index \
( \
"flag", "-in", \
"long-flag", "--input_file_path", \
"description", "Path of the input file to process", \
"takes", true, \
"required", true \
), \
index \
( \
"flag", "-db", \
"long-flag", "--database_file_path", \
"description", "Path for the database file", \
"takes", true, \
"required", true \
), \
index \
( \
"flag", "-enc", \
"long-flag", "--text_encoding", \
"description", "The encoding of the input text file", \
"takes", true, \
"required", true, \
"default", "utf8" \
) \
)
$ args = parseArgs(arguments, arg_specs)
$ input_file_path = args.get("-in")
$ database_file_path = args.get("-db")
$ text_encoding = args.get("-enc")
>>
>> Command line arguments:
> "Input file: " + input_file_path
> "Database file: " + database_file_path
> "Text encoding: " + text_encoding
! err
> "Error processing command line parameters: " + err
exit(1)
}
>>
>> Reading lines from input file...
$ file_lines = readFileLines(input_file_path, text_encoding)
! err
> "Error reading input file: " + err
exit(1)
}
> "File Line Count: " + file_lines.length()
>> Getting 4db database up and running...
exec('del "' + database_file_path + '"') // fresh DB file every time
msdb_4db_init("db", database_file_path)
! err
> "Error initializing database: " + err
exit(1)
}
>> Importing lines into database...
++ f : 0 -> file_lines.length() -1
/ NOTE: Using line as the primary key did not handle identical lines,
/ hence the line number
$ line = file_lines.get(f)
$ line_number = f + 1
msdb_4db_define("db", "lines", line_number + ": " +
line, index("len", length(line)))
}
! err
> "Error importing lines into database: " + err
exit(1)
}
>> Database loaded, ready to query!
O // loop forever...until Ctrl-C at least
>>
>> Enter your search criteria:
$ criteria = trimmed(input())
? criteria.length() == 0
^
}
>>
> "Criteria: " + criteria
>>
/ Get the results, using the "value" column, which is the primary key
/ which is the text of the line, to SELECT out, and to MATCH the criteria
/ We sort by id for top-to-bottom file output
/ The criteria is passed into msdb_4db_query as a param->value index
$ results = \
msdb_4db_query \
( \
"db", \
"SELECT value, id FROM lines WHERE value MATCHES @criteria ORDER BY id", \
index("@criteria", criteria) \
)
/ Process the results
$ result_count = results.length()
> "Results: " + (result_count - 1) // ignore the column headers "result"
++ idx : 1 -> result_count - 1
$ line = results.get(idx)
> line.get(0) // Ignore the id column which was just needed for sorting
}
! err
> "Error getting search results: " + err
}
}
2. file_sizes.ms - 用于文件大小统计信息的目录索引
这些示例演示了目录处理、日志记录和 SQL 编程。
/*
This sample determines the size of files in a given directory
and then the user can get file size stats
The progress in generating this data is written to a log file
*/
+ "mscript-log.dll"
+ "mscript-db.dll"
$ arg_specs = \
list \
( \
index \
( \
"flag", "-dir", \
"long-flag", "--directory", \
"description", "What directory should be processed to get file sizes?", \
"takes", true, \
"default", "." \
), \
index \
( \
"flag", "-db", \
"long-flag", "--database-file-path", \
"description", "Where should the file sizes database be put?", \
"takes", true, \
"default", "file-sizes.db" \
), \
index \
( \
"flag", "-log", \
"long-flag", "--log-file-path", \
"description", "Where should log output be written?", \
"takes", true, \
"default", "file-sizes.log" \
), \
index \
( \
"flag", "-ll", \
"long-flag", "--log-level", \
"description", "What level to log at? (DEBUG, INFO, ERROR, or NONE)", \
"takes", true, \
"default", "INFO" \
), \
index \
( \
"flag", "-rebuild", \
"long-flag", "--rebuild-index", \
"description", "Should the program start over and rebuild the index?", \
"default", false \
) \
)
$ args = parseArgs(arguments, arg_specs)
$ starting_dir_path = args.get("-dir")
$ db_file_path = args.get("-db")
$ log_file_path = args.get("-log")
$ log_level = args.get("-ll")
$ rebuild = args.get("-rebuild")
>>
>> Configuration:
> "Dir Path: " + starting_dir_path
> "DB File Path: " + db_file_path
> "Log File Path: " + log_file_path
> "Log Level: " + log_level
> "Rebuild: " + rebuild
>>
! err
> "Processing command line arguments failed: " + err
exit(1)
}
/ Start logging to a fresh file every time
$ exec_options = index("ignore_errors", true)
exec(fmt('del "{0}"', log_file_path), exec_options)
mslog_start(log_file_path, log_level)
~ logError(msg)
mslog_error(msg)
> "ERROR: " + msg
}
~ logMsg(msg)
mslog_info(msg)
> msg
}
~ logDebug(msg)
mslog_debug(msg)
}
$ did_db_file_exist = false
{
$ exec_result = exec(fmt('dir "{0}"', db_file_path), exec_options)
did_db_file_exist =
firstLocation(trimmed(exec_result.get("output")), "File Not Found") < 0
}
? !did_db_file_exist
rebuild = true
}
? rebuild
exec(fmt('del "{0}"', db_file_path), exec_options)
}
? rebuild
logMsg("Processing files and folders...")
msdb_sql_init("db", db_file_path)
msdb_sql_exec \
( \
"db", \
"CREATE TABLE file_sizes (FilePath STRING NOT NULL,
SizeBytes NUMBER NOT NULL)" \
)
processDir(starting_dir_path)
msdb_sql_close("db")
}
msdb_sql_init("db", db_file_path)
! err
logError("Setting up index failed: " + err)
exit(1)
}
O // UI loop
>>
>> Enter the path pattern to compute stats. Like *.mp3
>>
$ pattern = trimmed(input())
$ sql_pattern = "%" + pattern.replaced("*", "%")
$ sql_query = \
"SELECT COUNT(*), SUM(SizeBytes) FROM file_sizes WHERE FilePath LIKE @like"
$ results = msdb_sql_exec("db", sql_query, index("@like", sql_pattern))
? results.length() <= 1
>> No results
^
}
$ result = results.get(1)
>>
$ file_count = result.get(0)
? file_count = null
file_count = 0
}
logMsg("Count: " + file_count)
/ Size (GB):
$ size_bytes = result.get(1)
? size_bytes = null
size_bytes = 0
}
$ size_str = ""
? size_bytes > 1024 * 1024 * 1024
size_str = "Size (GB): " + round(size_bytes / 1024 / 1024 / 1024, 2)
}
? size_bytes > 1024 * 1024
size_str = "Size (MB): " + round(size_bytes / 1024 / 1024, 2)
}
<>
size_str = "Size (KB): " + round(size_bytes / 1024, 2)
}
logMsg(size_str)
! err
> "Querying interface failed: " + err
^
}
}
/ Unreachable...
msdb_sql_close("db")
mslog_stop()
>> All done.
/*
Implementation of search index population
Recursive function that runs DIR at each level and parses out files to add to index
and directories to recurse on
*/
~ processDir(dirPath)
logDebug("DIR Path: " + dirPath)
> dirPath
$ dir_output_lines = null
{
$ dir_result = exec(fmt('dir "{0}"', dirPath))
$ dir_output = dir_result.get("output")
dir_output_lines = splitLines(dir_output)
}
$ found_dirs = list()
$ found_file_sizes = index()
$ line_pattern = \
"[0-9\/]+" + \
"\s*" + \
"[0-9\:]+\s*(AM|PM)" + \
"\s*" + \
"((\<DIR\>\s*)|([0-9\,]+))" + \
"\s*" + \
"(\S.*)"
@ line : dir_output_lines
/ Skip header lines
? line.firstLocation(" ") == 0
^
}
/ Match up the DIR parts
$ matches = line.getMatches(line_pattern, true)
// > "DEBUG: matches: " + matches
? matches.length() < 2 // get at least the file size and path stem
^
}
$ path_stem = trimmed(matches.get(matches.length() - 1))
? path_stem = "." OR path_stem = ".."
^
}
$ full_path = path_stem
? dirPath <> "."
full_path = dirPath + "\" + full_path
}
$ len_str = replaced(trimmed(matches.get(matches.length() - 2)), ",", "")
? len_str.length() == 0
found_dirs.add(full_path)
}
<>
found_file_sizes.add(full_path, number(len_str))
}
}
! err
> fmt('Error processing directory "{0}": {1}', dirPath, err
exit(1)
}
/ load the file size data we found into the DB
$ insert_statement = \
"INSERT INTO file_sizes (FilePath, SizeBytes) VALUES (@filePath, @sizeBytes)"
$ insert_params = index()
@ file_path : found_file_sizes
insert_params.set("@filePath", file_path)
insert_params.set("@sizeBytes", found_file_sizes.get(file_path))
msdb_sql_exec("db", insert_statement, insert_params)
}
/ recurse on the dirs we found
@ dir_path : found_dirs
processDir(dir_path)
}
}
新的 DLL API
让我们深入了解一下驱动这些 mscript
的代码。
mscript-log
我发现使用良好的日志记录是调试脚本的最佳方式。正确使用日志记录已显著加快了我的脚本编写速度。没有日志记录,调试可能会很困难且混乱。
cx_Logging 已经存在很长时间了。源代码可在 GitHub 的 src 目录中以 .h / .c 对的形式提供。代码以 Python 为中心且可移植。我能够大幅修改我的副本,使其仅支持 Win32,而没有任何其他通用支持。除了删除大量代码外,我还将全局临界区更改为 std::mutex
/ std::unique_lock
,并且(不得不)尽可能使用 strcpy_s
和 sprintf_s
,并对 char*
用法进行抽查以验证安全性。这对于胆小的人来说并不容易,但它已经完成了,现在我们可以将其用作简单的 mscript
API。
mslog_start(filename, log_level, optional_settings_index)
mslog_stop()
mslog_setlevel(log_level)
mslog_getlevel()
mslog_error(message)
mslog_info(message)
mslog_debug(message)
这是日志记录 DLL 的源代码。
#include "pch.h"
#include "cx_Logging.h"
using namespace mscript;
wchar_t* __cdecl mscript_GetExports()
{
std::vector<std::wstring> exports
{
L"mslog_getlevel",
L"mslog_setlevel",
L"mslog_start",
L"mslog_stop",
L"mslog_error",
L"mslog_info",
L"mslog_debug",
};
return module_utils::getExports(exports);
}
void mscript_FreeString(wchar_t* str)
{
delete[] str;
}
static unsigned long getLogLevel(const std::wstring& level)
{
if (level == L"INFO")
return LOG_LEVEL_INFO;
else if (level == L"DEBUG")
return LOG_LEVEL_DEBUG;
else if (level == L"ERROR")
return LOG_LEVEL_ERROR;
else if (level == L"NONE")
return LOG_LEVEL_NONE;
else
raiseWError(L"Invalid log level: " + level);
}
static std::wstring getLogLevel(unsigned long level)
{
switch (level)
{
case LOG_LEVEL_INFO: return L"INFO";
case LOG_LEVEL_DEBUG: return L"DEBUG";
case LOG_LEVEL_ERROR: return L"ERROR";
case LOG_LEVEL_NONE: return L"NONE";
default: raiseError("Invalid log level: " + num2str(level));
}
}
wchar_t* mscript_ExecuteFunction(const wchar_t* functionName,
const wchar_t* parametersJson)
{
try
{
std::wstring funcName = functionName;
auto params = module_utils::getParams(parametersJson);
if (funcName == L"mslog_getlevel")
{
if (params.size() != 0)
raiseError("Takes no parameters");
return module_utils::jsonStr(getLogLevel(GetLoggingLevel()));
}
if (funcName == L"mslog_setlevel")
{
if (params.size() != 1 || params[0].type() != object::STRING)
raiseError("Pass in the log level: DEBUG, INFO, ERROR, or NONE");
unsigned result = SetLoggingLevel
(getLogLevel(toUpper(params[0].stringVal())));
return module_utils::jsonStr(bool(result == 0));
}
if (funcName == L"mslog_start")
{
if (params.size() < 2 || params.size() > 3)
raiseError("Takes three parameters: filename,
log level, and optional settings index");;
if (params[0].type() != object::STRING)
raiseError("First parameter must be filename string");
if (params[1].type() != object::STRING)
raiseError("Second parameter must be log level string:
DEBUG, INFO, ERROR, or NONE");
if (params.size() == 3 && params[2].type() != object::INDEX)
raiseError("Third parameter must be a settings index");
std::wstring filename = params[0].stringVal();
unsigned log_level = getLogLevel(toUpper(params[1].stringVal()));
object::index index =
params.size() == 3
? params[2].indexVal()
: object::index();
object max_files;
if
(
index.tryGet(toWideStr("maxFiles"), max_files)
&&
(
max_files.type() != object::NUMBER
||
max_files.numberVal() < 0
||
unsigned(max_files.numberVal()) != max_files.numberVal()
)
)
{
raiseError("Invalid maxFiles parameter");
}
if (max_files.type() == object::NOTHING)
max_files = double(1);
object max_file_size_bytes;
if
(
index.tryGet(toWideStr("maxFileSizeBytes"), max_file_size_bytes)
&&
(
max_file_size_bytes.type() != object::NUMBER
||
max_file_size_bytes.numberVal() < 0
||
unsigned(max_file_size_bytes.numberVal())
!= max_file_size_bytes.numberVal()
)
)
{
raiseError("Invalid maxFileSizeBytes parameter");
}
if (max_file_size_bytes.type() == object::NOTHING)
max_file_size_bytes = double(DEFAULT_MAX_FILE_SIZE);
object prefix;
if
(
index.tryGet(toWideStr("prefix"), prefix)
&&
prefix.type() != object::STRING
)
{
raiseError("Invalid prefix parameter");
}
if (prefix.type() == object::NOTHING)
prefix = toWideStr(DEFAULT_PREFIX);
unsigned result =
StartLogging
(
toNarrowStr(filename).c_str(),
log_level,
unsigned(max_files.numberVal()),
unsigned(max_file_size_bytes.numberVal()),
toNarrowStr(prefix.stringVal()).c_str()
);
return module_utils::jsonStr(bool(result == 0));
}
if (funcName == L"mslog_stop")
{
StopLogging();
return module_utils::jsonStr(true);
}
{
auto under_parts = split(funcName, L"_");
if (under_parts.size() != 2)
raiseWError(L"Invalid function name: " + funcName);
unsigned int log_level = getLogLevel(toUpper(under_parts[1]));
if (params.size() != 1 || params[0].type() != object::STRING)
raiseWError(funcName + L":
pass the log message as one string parameter");
unsigned result =
LogMessage(log_level, toNarrowStr(params[0].stringVal()).c_str());
return module_utils::jsonStr(bool(result == 0));
}
// Unreachable
//raiseWError(L"Unknown mscript-log function: " + funcName);
}
catch (const user_exception& exp)
{
return module_utils::errorStr(functionName, exp);
}
catch (const std::exception& exp)
{
return module_utils::errorStr(functionName, exp);
}
catch (...)
{
return nullptr;
}
}
mscript-db
我一直在写关于 4db 以及促成它的各种事情的文章。如果所有这些都没有比用于直接 SQLite 编程的 db
、dbreader
和 strnum
类产生更好的东西,那么它就是值得的。如果您想直接进行 SQL 操作,我不会强迫您使用 4db。因此,一个 DLL 中有两个 API。
SQLite
msdb_sql_init(db_file_path)
msdb_sql_close()
msdb_sql_exec(sql_query)
msdb_sql_rows_affected()
msdb_sql_last_inserted_id()
4db
msdb_4db_init(db_file_path)
msdb_4db_close()
msdb_4db_define(table_name, primary_key_value, metadata_index)
msdb_4db_query(sql_query, parameters_index)
msdb_4db_delete(table_name, primary_key_value)
msdb_4db_drop(table_name)
请注意,我在这里简化了接口。看起来似乎只有一个数据库全局打开。我不想那么受限制,所以我内置了一个机制,允许同时打开多个数据库。
#include "pch.h"
// One thing at a time
std::mutex g_mutex;
// Global database access object storage
// Applications start with a name and a path to the DB file on disk,
// then refer to that database by name after that
std::unordered_map<std::wstring, std::shared_ptr<fourdb::ctxt>> g_contexts;
std::unordered_map<std::wstring, std::shared_ptr<fourdb::db>> g_db_conns;
//
// CONVERSION ROUTINES
//
static fourdb::strnum convert(const mscript::object& obj)
{
switch (obj.type())
{
case mscript::object::NOTHING:
return double(0.0);
case mscript::object::STRING:
return obj.stringVal();
case mscript::object::NUMBER:
return obj.numberVal();
case mscript::object::BOOL:
return obj.boolVal() ? 1.0 : 0.0;
default:
raiseError("Invalid object type for conversion to 4db: " +
mscript::num2str(int(obj.type())));
}
}
static mscript::object convert(const fourdb::strnum& obj)
{
if (obj.isStr())
return obj.str();
else
return obj.num();
}
static fourdb::paramap convert(const mscript::object::index& idx)
{
fourdb::paramap ret_val;
for (const auto& it : idx.vec())
ret_val.insert({ it.first.toString(), convert(it.second) });
return ret_val;
}
// Given a 4db context name, get the context object
static std::shared_ptr<fourdb::ctxt> get4db(const std::wstring& name)
{
const auto& it = g_contexts.find(name);
if (it == g_contexts.end())
raiseWError(L"4db not found: " + name + L" - call msdb_4db_init first");
return it->second;
}
// Given a SQL DB context name, get the DB object
static std::shared_ptr<fourdb::db> getSqldb(const std::wstring& name)
{
const auto& it = g_db_conns.find(name);
if (it == g_db_conns.end())
raiseWError(L"SQL DB not found: " + name + L" - call msdb_sql_init first");
return it->second;
}
// Given a DB reader, return a list of lists,
// column header names in the first list, row data in the following lists
static mscript::object::list processDbReader(fourdb::dbreader& reader)
{
mscript::object::list ret_val;
ret_val.push_back(mscript::object::list()); // column names
unsigned col_count = reader.getColCount();
ret_val[0].listVal().reserve(col_count);
for (unsigned c = 0; c < col_count; ++c)
ret_val[0].listVal().push_back(reader.getColName(c));
while (reader.read())
{
ret_val.emplace_back(mscript::object::list());
mscript::object::list& row_list = ret_val.back().listVal();
row_list.reserve(col_count);
for (unsigned c = 0; c < col_count; ++c)
{
bool is_null = false;
fourdb::strnum val = reader.getStrNum(c, is_null);
row_list.push_back(is_null ? mscript::object() : convert(val));
}
}
return ret_val;
}
wchar_t* __cdecl mscript_GetExports()
{
std::vector<std::wstring> exports
{
L"msdb_sql_init",
L"msdb_sql_close",
L"msdb_sql_exec",
L"msdb_sql_rows_affected",
L"msdb_sql_last_inserted_id",
L"msdb_4db_init",
L"msdb_4db_close",
L"msdb_4db_define",
L"msdb_4db_undefine",
L"msdb_4db_query",
L"msdb_4db_delete",
L"msdb_4db_drop",
L"msdb_4db_reset",
L"msdb_4db_get_schema",
};
return mscript::module_utils::getExports(exports);
}
void mscript_FreeString(wchar_t* str)
{
delete[] str;
}
wchar_t* mscript_ExecuteFunction(const wchar_t* functionName,
const wchar_t* parametersJson)
{
try
{
std::wstring funcName = functionName;
auto params = mscript::module_utils::getParams(parametersJson);
std::unique_lock ctx_lock(g_mutex);
if (funcName == L"msdb_sql_init")
{
if
(
params.size() != 2
||
params[0].type() != mscript::object::STRING
||
params[1].type() != mscript::object::STRING
)
{
raiseError("Takes two parameters: a name for the database,
and the path to the DB file");
}
std::wstring db_name = params[0].stringVal();
std::wstring db_file_path = params[1].stringVal();
if (g_db_conns.find(db_name) != g_db_conns.end())
raiseWError(L"Database already initialized: " + db_name);
auto new_db = std::make_shared<fourdb::db>
(mscript::toNarrowStr(db_file_path));
g_db_conns.insert({ db_name, new_db });
return mscript::module_utils::jsonStr(mscript::object());
}
else if (funcName == L"msdb_sql_close")
{
if
(
params.size() != 1
||
params[0].type() != mscript::object::STRING
)
{
raiseError("Takes the name of the database");
}
auto db_it = g_db_conns.find(params[0].stringVal());
if (db_it != g_db_conns.end())
g_db_conns.erase(db_it);
return mscript::module_utils::jsonStr(mscript::object());
}
else if (funcName == L"msdb_sql_exec")
{
if
(
(params.size() != 2 && params.size() != 3)
||
params[0].type() != mscript::object::STRING
||
params[1].type() != mscript::object::STRING
||
(params.size() == 3 && params[2].type() != mscript::object::INDEX)
)
{
raiseError("Takes the name of the database, the SQL query,
and an optional index of query parameters");
}
auto sql_db = getSqldb(params[0].stringVal());
std::wstring sql_query = params[1].stringVal();
auto params_idx =
params.size() >= 3
? params[2].indexVal()
: mscript::object::index();
fourdb::paramap query_params = convert(params_idx);
auto reader = sql_db->execReader(sql_query, query_params);
auto results = processDbReader(*reader);
return mscript::module_utils::jsonStr(results);
}
else if (funcName == L"msdb_sql_rows_affected")
{
if
(
params.size() != 1
||
params[0].type() != mscript::object::STRING
)
{
raiseError("Takes the database name");
}
auto sql_db = getSqldb(params[0].stringVal());
int64_t rows_affected =
sql_db->execScalarInt64(L"SELECT changes()").value();
return mscript::module_utils::jsonStr(double(rows_affected));
}
else if (funcName == L"msdb_sql_last_inserted_id")
{
if
(
params.size() != 1
||
params[0].type() != mscript::object::STRING
)
{
raiseError("Takes the database name");
}
auto sql_db = getSqldb(params[0].stringVal());
int64_t last_inserted_id =
sql_db->execScalarInt64(L"SELECT last_insert_rowid()").value();
return mscript::module_utils::jsonStr(double(last_inserted_id));
}
else if (funcName == L"msdb_4db_init")
{
if
(
params.size() != 2
||
params[0].type() != mscript::object::STRING
||
params[1].type() != mscript::object::STRING
)
{
raiseError("Takes two parameters: a name for the context,
and the path to the DB file");
}
std::wstring db_name = params[0].stringVal();
std::wstring db_file_path = params[1].stringVal();
if (g_contexts.find(db_name) != g_contexts.end())
raiseWError(L"Context already initialized: " + db_name);
auto ctxt_ptr = std::make_shared<fourdb::ctxt>
(mscript::toNarrowStr(db_file_path));
g_contexts.insert({ db_name, ctxt_ptr });
return mscript::module_utils::jsonStr(mscript::object());
}
else if (funcName == L"msdb_4db_close")
{
if
(
params.size() != 1
||
params[0].type() != mscript::object::STRING
)
{
raiseError("Takes the name of the context");
}
auto ctxt_it = g_contexts.find(params[0].stringVal());
if (ctxt_it != g_contexts.end())
g_contexts.erase(ctxt_it);
return mscript::module_utils::jsonStr(mscript::object());
}
else if (funcName == L"msdb_4db_define")
{
if
(
params.size() != 4
||
params[0].type() != mscript::object::STRING
||
params[1].type() != mscript::object::STRING
//|| object
//params[2].type() != mscript::object::STRING
||
params[3].type() != mscript::object::INDEX
)
{
raiseError("Takes four parameters: the name of the context,
the table name, the key value, and an index of name-value pairs");
}
auto ctxt = get4db(params[0].stringVal());
const std::wstring& table_name = params[1].stringVal();
const fourdb::strnum key_value = convert(params[2]);
const fourdb::paramap metadata = convert(params[3].indexVal());
ctxt->define(table_name, key_value, metadata);
return mscript::module_utils::jsonStr(mscript::object());
}
else if (funcName == L"msdb_4db_undefine")
{
if
(
params.size() != 4
||
params[0].type() != mscript::object::STRING
||
params[1].type() != mscript::object::STRING
//|| object
//params[2].type() != mscript::object::STRING
||
params[3].type() != mscript::object::STRING
)
{
raiseError("Takes four parameters: the name of the context,
the table name, the key value, and the name of the metadata to remove");
}
auto ctxt = get4db(params[0].stringVal());
const std::wstring& table_name = params[1].stringVal();
const fourdb::strnum key_value = convert(params[2]);
const std::wstring& metadata_name = params[3].stringVal();
ctxt->undefine(table_name, key_value, metadata_name);
return mscript::module_utils::jsonStr(mscript::object());
}
else if (funcName == L"msdb_4db_query")
{
if
(
params.size() != 3
||
params[0].type() != mscript::object::STRING
||
params[1].type() != mscript::object::STRING
||
params[2].type() != mscript::object::INDEX
)
{
raiseError("Takes three parameters: the name of the context,
the SQL query, and an index of name-value parameters");
}
auto ctxt = get4db(params[0].stringVal());
fourdb::select sql_select = fourdb::sql::parse(params[1].stringVal());
const fourdb::paramap query_params = convert(params[2].indexVal());
for (const auto& param_it : query_params)
sql_select.addParam(param_it.first, param_it.second);
auto reader = ctxt->execQuery(sql_select);
auto results = processDbReader(*reader);
return mscript::module_utils::jsonStr(results);
}
else if (funcName == L"msdb_4db_delete")
{
if
(
params.size() != 3
||
params[0].type() != mscript::object::STRING
||
params[1].type() != mscript::object::STRING
//|| object
//params[2].type() != mscript::object::STRING
)
{
raiseError("Takes three parameters: the name of the context,
the table name, and the key value");
}
auto ctxt = get4db(params[0].stringVal());
const std::wstring& table_name = params[1].stringVal();
const fourdb::strnum key_value = convert(params[2]);
ctxt->deleteRow(table_name, key_value);
return mscript::module_utils::jsonStr(mscript::object());
}
else if (funcName == L"msdb_4db_drop")
{
if
(
params.size() != 2
||
params[0].type() != mscript::object::STRING
||
params[1].type() != mscript::object::STRING
)
{
raiseError("Takes two parameters: the name of the context,
and the table name");
}
auto ctxt = get4db(params[0].stringVal());
const std::wstring& table_name = params[1].stringVal();
ctxt->drop(table_name);
return mscript::module_utils::jsonStr(mscript::object());
}
else if (funcName == L"msdb_4db_reset")
{
if
(
params.size() != 1
||
params[0].type() != mscript::object::STRING
)
{
raiseError("Takes the name of the context to reset");
}
auto ctxt = get4db(params[0].stringVal());
ctxt->reset();
return mscript::module_utils::jsonStr(mscript::object());
}
else if (funcName == L"msdb_4db_get_schema")
{
if
(
params.size() != 1
||
params[0].type() != mscript::object::STRING
)
{
raiseError("Takes the name of the context to get the schema of");
}
auto ctxt = get4db(params[0].stringVal());
const auto schema = ctxt->getSchema();
mscript::object::index table_schema;
for (const auto& tables_it : schema.vec())
{
const std::wstring& table_name = tables_it.first;
mscript::object::list table_columns;
table_columns.reserve(tables_it.second->size());
for (const std::wstring& col : *tables_it.second)
table_columns.push_back(col);
table_schema.set(table_name, table_columns);
}
return mscript::module_utils::jsonStr(table_schema);
}
else
raiseWError(L"Unknown function");
}
catch (const mscript::user_exception& exp)
{
return mscript::module_utils::errorStr(functionName, exp);
}
catch (const std::exception& exp)
{
return mscript::module_utils::errorStr(functionName, exp);
}
catch (...)
{
return nullptr;
}
}
参数解析
一个重要的功能请求是强大的命令行参数解析。您需要能够指定应用程序处理的标志,例如 -i
,以及标志的长版本,例如 --input
,以及标志的描述,例如“指定输入文件
”,以及该标志是否需要值(例如,“--input
”标志将需要),该值是否必须是数字,默认值,以及该标志是否是必需的。哇,这真是一句话!功能强大。
这是头文件。
#pragma once
#include "object.h"
namespace mscript
{
object
parseArgs
(
const object::list& arguments,
const object::list& argumentSpecs
);
}
它接受 mscript
可以传递的全局“arguments
”变量的参数字符串列表,该列表由 mscript
EXE 设置,但此代码和 mscript
包装器函数不假定这一点。它还接受一个“参数规范”列表。每个参数规范都是一个索引,用于定义程序可以处理的参数、其标志、其长标志等。此函数返回一个索引,该索引将标志映射到该标志的值,对于不带值的标志返回 true,对于缺失的标志返回 false。特殊的伪标志 "" 返回与标志无关的值列表,例如您的主要参数。您的调用代码将探测返回的索引以控制其行为。
这是参数解析器的实现。
#include "pch.h"
#include "parse_args.h"
#include "exe_version.h"
#include "utils.h"
using namespace mscript;
struct arg_spec
{
// required flag data
std::wstring flag; // -?
std::wstring long_flag; // --help
std::wstring description; // Get some help!
// optional flags
bool takes = false;
bool numeric = false;
bool required = false;
object default_value;
};
static std::wstring getStrFlagValue(const object::index& argumentSpec,
const std::string& flagName)
{
object flag_value;
if (!argumentSpec.tryGet(toWideStr(flagName), flag_value) ||
flag_value.type() != object::STRING)
raiseError("Argument spec lacks " + flagName + " string setting");
std::wstring flag_str = trim(flag_value.stringVal());
if (flag_str.empty())
raiseError("Argument spec lacks " + flagName + " non-empty string setting");
return flag_str;
}
static bool getBoolFlagValue(const object::index& argumentSpec,
const std::string& flagName)
{
object flag_value;
if (!argumentSpec.tryGet(toWideStr(flagName), flag_value))
return false;
if (flag_value.type() != object::BOOL)
raiseError("Argument spec " + flagName + " setting is not bool");
return flag_value.boolVal();
}
object
mscript::parseArgs
(
const object::list& arguments,
const object::list& argumentSpecs
)
{
// Set up shop
object ret_val = object::index();
object::index& ret_val_index = ret_val.indexVal();
// Start off with our list of un-flagged arguments
// NOTE: The object::list inside raw_arg_list is passed by reference
// so changes made to it along the way are reflected in the returned list
object raw_arg_list = object::list();
ret_val_index.set(toWideStr(""), raw_arg_list);
// Pre-process the argument specs
std::vector<arg_spec> local_specs;
for (const auto& cur_input_spec : argumentSpecs)
{
if (cur_input_spec.type() != object::INDEX)
raiseError("Invalid argument spec type: " +
object::getTypeName(cur_input_spec.type()));
const object::index& cur_input_index = cur_input_spec.indexVal();
arg_spec cur_spec;
cur_spec.flag = getStrFlagValue(cur_input_index, "flag");
cur_spec.long_flag = getStrFlagValue(cur_input_index, "long-flag");
cur_spec.description = getStrFlagValue(cur_input_index, "description");
cur_spec.takes = getBoolFlagValue(cur_input_index, "takes");
cur_spec.numeric = getBoolFlagValue(cur_input_index, "numeric");
cur_spec.required = getBoolFlagValue(cur_input_index, "required");
object default_value;
if (cur_input_index.tryGet(toWideStr("default"), default_value))
cur_spec.default_value = default_value;
else if (!cur_spec.takes) // normal flag
cur_spec.default_value = false;
local_specs.push_back(cur_spec);
}
// Add help flag processing if not already defined
bool already_had_help = false;
for (const auto& cur_spec : local_specs)
{
if (cur_spec.flag == L"-?" || cur_spec.long_flag == L"--help")
{
already_had_help = true;
break;
}
}
if (!already_had_help)
{
arg_spec new_spec;
new_spec.flag = L"-?";
new_spec.long_flag = L"--help";
new_spec.description = L"Get usage of this script";
new_spec.default_value = false;
local_specs.insert(local_specs.begin(), new_spec);
}
// Add default values to seed the return value index
for (const auto& arg_spec : local_specs)
ret_val_index.set(arg_spec.flag, arg_spec.default_value);
// Validate the arguments
bool help_exit_suppressed = false;
for (size_t a = 0; a < arguments.size(); ++a)
{
if (arguments[a].type() != object::STRING)
{
raiseWError(L"Invalid command-line argument,
not a string: #" + num2wstr(double(a)) +
L" - " + arguments[a].toString());
}
if (arguments[a].stringVal() == L"--suppress-help-quit")
help_exit_suppressed = true;
}
// Loop over the arguments
bool help_was_output = false;
for (size_t a = 0; a < arguments.size(); ++a)
{
const std::wstring& cur_arg = arguments[a].stringVal();
if (cur_arg == L"--suppress-help-quit")
continue;
bool has_next_arg = false;
object next_arg;
if (a < arguments.size() - 1)
{
next_arg = arguments[a + 1];
if (!startsWith(next_arg.toString(), L"-"))
has_next_arg = true;
}
if (cur_arg.empty() || cur_arg[0] != '-')
{
raw_arg_list.listVal().push_back(cur_arg);
continue;
}
if (!already_had_help && (cur_arg == L"-?" || cur_arg == L"--help"))
{
std::wstring mscript_exe_path = getExeFilePath();
std::wcout
<< mscript_exe_path
<< L" - v" << toWideStr(getBinaryVersion(mscript_exe_path))
<< L"\n";
size_t max_flags_len = 0;
for (const auto& arg_spec : local_specs)
{
size_t cur_len = arg_spec.flag.size() + arg_spec.long_flag.size();
if (cur_len > max_flags_len)
max_flags_len = cur_len;
}
static std::wstring flag_separator = L", ";
static size_t flag_separator_len = flag_separator.length();
size_t max_flags_output_len = max_flags_len + flag_separator_len;
for (const auto& arg_spec : local_specs)
{
std::wcout
<< std::left
<< std::setfill(L' ')
<< std::setw(max_flags_output_len)
<< (arg_spec.flag + flag_separator + arg_spec.long_flag)
<< L": "
<< arg_spec.description;
if (arg_spec.takes)
{
std::wcout << " - type=" << (arg_spec.numeric ? "num" : "str");
if (arg_spec.default_value.type() != object::NOTHING)
std::wcout << " - default=" << arg_spec.default_value.toString();
if (arg_spec.required)
std::wcout << " - [REQUIRED]";
}
std::wcout << L"\n";
}
std::wcout << std::flush;
if (!help_exit_suppressed)
exit(0);
ret_val_index.set(toWideStr("-?"), true);
help_was_output = true;
continue;
}
// Loop over the argument specs to find this argument as a flag or long flag
bool found_flag = false;
for (const auto& cur_spec : local_specs)
{
if (!(cur_spec.flag == cur_arg || cur_spec.long_flag == cur_arg))
continue;
else
found_flag = true;
const std::wstring& cur_flag = cur_spec.flag;
if (cur_spec.takes)
{
if (!has_next_arg)
{
if (cur_spec.default_value != object())
next_arg = cur_spec.default_value;
else
raiseWError(L"No value for flag that takes
next argument: " + cur_arg);
}
// Convert the next argument into its final value
if (cur_spec.numeric && next_arg.type() != object::NUMBER)
{
try
{
ret_val_index.set(cur_flag, std::stod(next_arg.toString()));
}
catch (...)
{
raiseWError(L"Converting argument to number failed: " +
cur_spec.flag + L" - " + next_arg.toString());
}
}
else
ret_val_index.set(cur_flag, next_arg);
// Skip the next argument we just processed
a = a + 1;
}
else // non-taking flag
{
ret_val_index.set(cur_flag, true);
}
}
if (!found_flag)
raiseWError(L"Unknown command line flag: " + cur_arg);
}
// Enforce that all required flags were specified...unless -? / --help
if (!help_was_output)
{
for (const auto& cur_spec : local_specs)
{
if (cur_spec.required && ret_val_index.get
(cur_spec.flag).type() == object::NOTHING)
raiseWError(L"Required argument not provided: " + cur_spec.flag);
}
}
// All done
return ret_val;
}
结论
我邀请您访问 mscript.io,了解版本历史记录,包括 3.0 版本中的重大更改、新旧函数的完整文档,以及上面提到的其他语言更改,例如预处理器、语法检查器,以及 *
和 &
语句前缀不再是必需的基本更改。
我真心认为您会喜欢编写 mscript
。试试看,并告诉我您的想法。
历史
- 2022 年 5 月 26 日:初始版本