Parser poleceń

0

Cześć,

mój problem jest dość prosty, jednak jego implementacja wydaje mi się bardzo złożona lub brakuje mi wiedzy o jakimś istotnym elemencie. Mianowicie chciałem zrobić parser, który zamieniałby polecenie wpisane w konsoli, np:

test Ala 3

na uruchomienie fukcji z parametrami

test("Ala", 3);

Wcześniej chciałem rejestrować komendy w mniej więcej taki sposób:

register_cmd(parser, "test", 2, test);

Czy takie coś jest możliwe i ewentualnie w którym kierunku poszukiwać rozwiązania?

Dzięki za pomoc!
Pozdrawiam

0

Jak najbardziej jest to możliwe; w absolutnie najprostszej wersji:
http://ideone.com/25zAZM ;-) (oczywiście to podejście nie ma żadnego sensu na dłuższą metę)

Do poczytania: mapy, wskaźniki na funkcje, tokenizacja.

0

W bardziej ograniczony sposób to _Generic mógłby rozwiązać kolegi problem. (C11)

Albo jeszcze coś w tym stylu `void job( void(*fun)(), int typ_funkcji, *void data );
zależnie od typu castujemy funkcję na prawidłową i date na prawidłową np. stukture.
tam w nawiasach nie ma voida bo to pointer do funkcji która przyjmuje jakies tam argumenty może ich nie być mogą być.

Przeszło mi też przez myśl va_list + ptr do funkcji.

Same dzikie haki. ;-)

0

Dzięki za odpowiedzi, ale właśnie szukam takich dzikich haków, a nie trywialnych rozwiązań :D Marzy mi się, żeby możliwe było zdefiniowanie nowej komendy z pliku tekstowego, która będzie uruchamiała istniejącą funkcję i pobierała odpowiednie argumenty. Też myślałem, żeby kombinować coś z va_list, ale jak potem argumenty przekazać odpowiednio do docelowej funkcji (np: int test(int, char*, float))?

0

Może zrób obiekt kompilatora, który w locie sparsuje polecenie na bajtkod, który wyegzekujesz.

W pythonie jest taki mechanizm jak getattr, który po podaniu obiektu i "nazwyfunkcji", przeszukuje przestrzeń nazw obiektu podanego i zwraca tą metodę, którą potem egzekujesz np. z jednym argumentem **args, z kolei tylko dana funkcja wie ile parametrów potrzebuje, a jak zabraknie to wywalasz wyjątek z komunikatem, będzie to w miarę polimorficzne.

0

Tak, bajtkod i od razu jitowanie na x86 w locie zróbmy. Autorowi chodzi (póki co) wyłącznie o przetwarzanie prostych poleceń - czas na parsery i kompilatory przyjdzie potem :P

4

Spróbuj coś takiego:

#include <iostream>
#include <functional>
#include <vector>
#include <string>
#include <sstream>
#include <algorithm>
#include <unordered_map>
#include <iterator>
using namespace std;

using arguments = vector<string>;
using command = function<void(arguments const &)>;
using commands = unordered_map<string, command>;

vector<string> str_to_tokens(string const &src) {
	istringstream stream(src);
	return {
		istream_iterator<string>{stream},
		istream_iterator<string>{}
	};
}

void print(arguments const &args) {
	for(auto &&arg: args) {
		cout << arg << " ";
	}
	cout << endl;
}


int main() {
	commands cmds = {
		{"print", print}
	};
	
	string input;
	while(getline(cin, input)) {
		if(input.empty()) {
			continue;
		}
		auto tokens = str_to_tokens(input);
		auto cmd = tokens[0];
		cmds.at(cmd)(arguments{begin(tokens)+1, end(tokens)});
	}
	return 0;
}

Proste jak budowa cepa, a powinno wystarczyć na twoje potrzeby.
http://ideone.com/bOrLAL
**input: **
print Ala ma dziwnego kota.
**output: **
Ala ma dziwnego kota.

Z takim niewydajnym podejściem możesz szybko stać się dość kreatywny, patrz np.

#include <iostream>
#include <functional>
#include <vector>
#include <string>
#include <sstream>
#include <algorithm>
#include <unordered_map>
#include <iterator>
#include <cassert>
using namespace std;

//boring helper functions
inline std::string trim(const std::string &s) {
   auto  wsfront=std::find_if_not(s.begin(),s.end(),[](int c){return std::isspace(c);});
   return std::string(wsfront,std::find_if_not(s.rbegin(),std::string::const_reverse_iterator(wsfront),[](int c){return std::isspace(c);}).base());
}

vector<string> str_to_tokens(string const &src) {
	istringstream stream(src);
	return {
		istream_iterator<string>{stream},
		istream_iterator<string>{}
	};
}

string join(vector<string> const &vec, string const &delim) {
    stringstream res;
    copy(vec.begin(), vec.end(), ostream_iterator<string>(res, delim.c_str()));
    return res.str();
}

//scaffolding
using arguments = vector<string>;
using command = function<void(arguments const &)>;
using commands = unordered_map<string, command>;
using middleware = function<void(arguments &)>;
using middlewares = vector<middleware>;

//bullshit that matters
void print(arguments const &args) {
	for(auto &&arg: args) {
		cout << arg << " ";
	}
	cout << endl;
}

struct data_store {
	using vname = string;
	using vval = string;
	unordered_map<vname, vval> vars;
	
	void store(vname const &name, vval const &val) {
		vars[name] = val;
	}
	
	void store_cmd(arguments const &args) {
		enum { VNAME, VEQSIGN, VVAL_BEG };
		assert(args[VEQSIGN] == "=");
		auto val = join({begin(args)+VVAL_BEG, end(args)}, " ");
		store(args[VNAME], trim(val));
	}
	
	vval get(vname const &name) const {
		return vars.at(name);
	}
};

void replace_vars_with_values(data_store const &store, string const &var_prefix, arguments &args) {
	for(auto &arg: args) {
		if(arg.rfind(var_prefix, 0) == 0) {
			arg = store.get(string{begin(arg)+var_prefix.size(), end(arg)});
		}
	}
}

void apply_middlewares(middlewares const &mdws, arguments &args) {
	for(auto &&mdw: mdws) {
		mdw(args);
	}
}

int main() {
	string var_prefix = "$";
	data_store dstore;
	
	middlewares mdws = {
		[&](arguments &args) { return replace_vars_with_values(dstore, var_prefix, args); }
	};
	
	commands cmds = {
		{"print", print},
		{"var", [&dstore](arguments const &args) { dstore.store_cmd(args); }}
	};
	
	string input;
	while(getline(cin, input)) {
		if(input.empty()) {
			continue;
		}
		auto tokens = str_to_tokens(input);
		apply_middlewares(mdws, tokens);
		
		enum { FUNC_POS, ARGS_START };
		
		cmds.at(tokens[FUNC_POS])(arguments{begin(tokens)+ARGS_START, end(tokens)});
	}
	return 0;
}

https://ideone.com/kdaGqy
input:

var person = Ala
var state = ma dziwnego kota.
var f = print

$f $person $state

output:
Ala ma dziwnego kota.

0

Dodam tylko, że jeżeli chcesz mapować polecenia na funkcje już istniejące w programie podczas wykonania tegoż programu, bez predefiniowania wszystkich dostępnych funkcji już podczas kompilacji, to powinieneś skorzystać z biblioteki systemowej do obsługi kontenerów kodu lub do ładowania dynamicznego. Tzn. możesz użyć biblioteki libelf (zakładając, że korzystasz z linuxa), otworzyć samego siebie, przeiterować się przez wszystkie sekcje i tablice symboli wewnątrz tej sekcji i znaleźć odpowiednią funkcję. Łatwiejszym sposobem jest użycie dlopen aby dostać wskaźnik na dowolną funkcję w bibliotece współdzielonej albo programie aktualnie wykonywanym ( http://man7.org/linux/man-pages/man3/dlopen.3.html ). Pamiętaj jednak, że w przypadku c++ nazwy będą zamanglowane (extern "C" pomoże Ci uzyskać nazwy funkcji w stylu C).

Pozostaje problem przekazania parametrów do funkcji i przeżutowania wskaźnika na odpowiedni typ (zakładam, że własnego kodu asm do wołania funkcji nie zamierzasz pisać). Nie wiem na ile rozumiesz ABI i wygenerowany kod, ale upraszczając, funkcje napisane w C/C++ mają przypisane przejmowane i zwracane typy podczas kompilacji. Podczas uruchomienia programu, o ile to nie jest wersja debugowa, tej informacji już nie ma. Ewentualnym rozwiązaniem jest założenie, że wszystkie funkcje będą przyjmowały taki sam "opisowy" typ parametru, z którym bedą w stanie sobie poradzić. Takie dynamiczne typowanie dla ubogich ;).

Oczywiście można opisać wszystkie funkcje, zdefiniować sposoby ich wołania z poziomu parsera już podczas pisania programu. Ale w takim wypadku w ogóle nie potrzebujesz tego dynamicznego pobierania wskaźników na funckje. Zależy, czy chcesz mieć dynamiczne wołanie funkcji "natywnych" czy interpreter predefiniowanych poleceń.

P.S.
Inne pojęcie, które ewentualnie może Cię zainteresować to FFI ( https://en.wikipedia.org/wiki/Foreign_function_interface ).

0

Widzę, że pojawiło się kilka pytań. Korzystam z linuxa i nie zależy mi jakoś specjalnie, żeby program działał na Windowsie. Dodatkowo zapomniałem zaznaczyć, że jestem zmuszony do używania ANSI C89, także mechanizmy wprowadzone później lub w C++ odpadają.

Wielki Pomidor napisał(a):

Może zrób obiekt kompilatora, który w locie sparsuje polecenie na bajtkod, który wyegzekujesz.

Hah

spartanPAGE napisał(a):

Spróbuj coś takiego

Wygląda naprawdę fajnie, widzę tylko, że co druga konstrukcja nie jest dostępna w moim "ukochanym" C89 :D Jeśli mogę się spytać - czy warto studiować ten kod i spróbować go przełożyć na C?

nalik napisał(a):

Pozostaje problem przekazania parametrów do funkcji i przeżutowania wskaźnika na odpowiedni typ

Można przyjać, że wszystkie funkcje zwracają typ int, jednak parametry to już jest problem

Oczywiście można opisać wszystkie funkcje, zdefiniować sposoby ich wołania z poziomu parsera już podczas pisania programu.

I chyba wgłębiając się w temat tak zrobię. Miałem nadzieję, że problem jest dosyć powszechny i istnieją gotowe rozwiązania. Jak w takim razie są pisane programy, które pozwalają na wpisywanie poleceń? (dla przykładu gnuplot) Porównują pierwszy łańcuch, a potem mają zakodowane ileś scanf, żeby na koniec przekazać argumenty do funkcji? I tak dla każdego polecenia?

Wielkie dzięki za pomoc!

0

Nie wiem jak działa gnuplot, ale podejrzewam, że jednak parsuje kod, tworzy AST i to intepretuje albo generuje z AST bajtkod czy też inną formę pośrednią. Parser można napisać z ręki, ale można i go w większości wygenerować (choćby najbardziej znane narzędzia jak flex, bison, yacc, ...).

0

Wygląda naprawdę fajnie, widzę tylko, że co druga konstrukcja nie jest dostępna w moim "ukochanym" C89 :D Jeśli mogę się spytać - czy warto studiować ten kod i spróbować go przełożyć na C?

Przełożenie tego na C nie powinno być problemem. Tutaj krótko jak działa pierwszy kod:

Trzymamy zbiór funkcji o ściśle określonej sygnaturze - dzięki temu możemy wrzucić je do kontenera i doczepić do nich nazwę.
W C wystarczy Ci coś takiego: 

typedef int (*func_t)(int argc, const char * const argv[]);
typedef struct { const char *name; func_pointer func; } named_func_t;

Później zostaje Ci wczytać linię, rozdzielić je, znaleźć funkcję o nazwie pierwszego argumentu i przekazać resztę jako faktyczne argumenty.

1 użytkowników online, w tym zalogowanych: 0, gości: 1