65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (3投票s)

2022 年 5 月 26 日

Apache

4分钟阅读

viewsIcon

6288

downloadIcon

111

查看第三个主要版本的 mscript,用于用简单的 mscript 替换您糟糕的批处理文件。

引言

通过 mscript 的第三个主要版本,我收到了反馈和指导,这些反馈和指导为我的代码更改和 DLL 开发提供了信息。我欢迎您对好、坏和丑陋的见解,我承诺在继续塑造 mscript 以摆脱批处理文件限制以及 Powershell 和 Python 的负担时,会考虑您的输入。

在本文中,我将介绍新的数据库和日志记录 DLL,以及新的参数处理函数 parseArgs()

在深入介绍 API 和实现细节之前,让我们先来看几个示例脚本。

  1. file_search.ms - 将文本文件的行加载到 4db 中,然后对行进行全文本关键字搜索
  2. 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 已经存在很长时间了。源代码可在 GitHubsrc 目录中以 .h / .c 对的形式提供。代码以 Python 为中心且可移植。我能够大幅修改我的副本,使其仅支持 Win32,而没有任何其他通用支持。除了删除大量代码外,我还将全局临界区更改为 std::mutex / std::unique_lock,并且(不得不)尽可能使用 strcpy_ssprintf_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 编程的 dbdbreaderstrnum 类产生更好的东西,那么它就是值得的。如果您想直接进行 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 日:初始版本
© . All rights reserved.