Эмулируем shebang на Windows

в 19:14, , рубрики: Песочница, метки: , ,

На unix системах все сделано очень удобно. Одним из таких удобств является shebang.
Вкратце — shebang позволяет указать нам в какое приложение будет передан тот или иной файл при попытке его выполнить.
Но на операционных системах от Microsoft такого функционала нет, поэтому мы попробуем сделать аналог.

Windows предоставляет нам следующие вещи:

  • переменная PATHEXT: перечисленные через точку с запятой расширения файлов которые могут быть запущены
  • утилита ftype: позволяет создать соответствие между типом файла и программы, которая будет запущена при попытке выполнить файл
  • утилита assoc: позволяет создать соответствие между расширением файла и типом файла

Утилита ftype работает следующим образом:

ftype shebang=C:WindowsSystem32shebang.exe "%1" %*

При этом вызове в реестре будет создана запись для файлов типа shebang и при попытке запуска которого будет вызван исполняемый файл shebang.exe, где %1 — путь к файлу; %* — аргументы с которыми файл был запущен.
При вызове assoc .=shebang будет задано соответствие между файлами с расширением .(то есть — файлы без расширения) и типом файла shebang. Далее вызовем setx /m PATHEXT "%PATHEXT%;." и готово — теперь при попытке запустить файл без расширения в консоли или с проводника будет запущено приложение shebang.exe.
Код shebang.exe спрятан под спойлер.

Код shebang.exe

#include <iostream>
#include <fstream>
#include <list>
#include <map>
#include <sstream>
#include <boost/regex.hpp>
#include <boost/format.hpp>
#include <windows.h>

void exit_with_error(std::wstring errorMessage, int exitCode) {
	std::wcerr << errorMessage << std::endl;
	std::exit(exitCode);
}

std::wstring get_shebang_executable(std::wstring file) {
	std::wstring result;
	std::wifstream _file(file);
	std::getline(_file, result);
	if (result.size() < 3) {
		exit_with_error(
			boost::str(boost::wformat(L"File %s does not contain shebang string.") %
			file),
			-1);
	}
	boost::wregex shebang_regexp(L"#! *(.*)");
	boost::wsmatch match_result;
	if (boost::regex_search(result, match_result, shebang_regexp)) {
		result = match_result[1];
	}
	else {
		exit_with_error(
			boost::str(boost::wformat(L"File %s does not contain shebang string.") %
			file),
			-1);
	}
	return result;
}

void run_process_and_wait(std::list<std::wstring> &process_cmd,
	std::map<std::wstring, std::wstring> &env) {
	std::wstringstream ss;
	for (auto it = process_cmd.begin(); it != process_cmd.end(); it++) {
		if (it != process_cmd.begin()) {
			ss << L" ";
		}
		ss << *it;
	}
	std::wstring command_line = ss.str();
	wchar_t *envBlock = NULL;
	if (!env.empty()) {
		envBlock = create_env_block(env);
	}
	PROCESS_INFORMATION piProcInfo;
	STARTUPINFO siStartInfo;
	HANDLE in = GetStdHandle(STD_INPUT_HANDLE);
	HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
	HANDLE err = GetStdHandle(STD_ERROR_HANDLE);
	ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION));
	ZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
	siStartInfo.cb = sizeof(STARTUPINFO);
	siStartInfo.hStdError = err;
	siStartInfo.hStdOutput = out;
	siStartInfo.hStdInput = in;
	siStartInfo.dwFlags |= STARTF_USESTDHANDLES;
	BOOL bSuccess = CreateProcess(NULL, const_cast<LPWSTR>(command_line.c_str()),
		NULL, NULL, TRUE, CREATE_UNICODE_ENVIRONMENT,
		envBlock, NULL, &siStartInfo, &piProcInfo);
	if (!bSuccess)
		exit_with_error(
		boost::str(boost::wformat(L"Failed to execute commandline:%s") %
		command_line),
		-1);
	WaitForSingleObject(piProcInfo.hProcess, INFINITE);
}

std::list<std::wstring> parse_arguments(int argc, wchar_t **argv) {
	std::list<std::wstring> arguments;
	arguments.push_back(get_shebang_executable(std::wstring(argv[1])));
	arguments.push_back(std::wstring(argv[1]));
	for (int i = 2; i < argc; i++) {
		arguments.push_back(std::wstring(argv[i]));
	}
	return arguments;
}

void run_process_and_wait(std::list<std::wstring> &process_cmd) {
	std::map<std::wstring, std::wstring> empty;
	run_process_and_wait(process_cmd, empty);
}

int wmain(int argc, wchar_t **argv) {
	if (argc < 2) {
		exit_with_error(L"Usage: shebang.exe shebang_file arg1...", -1);
	}
	run_process_and_wait(parse_arguments(argc, argv));
	return 0;
}

Вкратце — shebang.exe принимает в качестве аргумента файл, считывает с него первую строку, достает с нее путь к исполняемому файлу и запускает тот файл с аргументами переданными в shebang.exe, при этом перенаправляя потоки ввода-вывода дочернего процесса в свои собственные и ожидая завершения дочернего процесса. Еще можно добавить ассоциации с другими расширениями файлов. Для примера — .py. Потом при надобности прописывать в скриптах разные версии питона и запускать их без лишних телодвижений.

Вот таким образом мы немного костыльно смогли частично реализовать поддержку shebang на Wndows.

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js