From a532f5ac392ba8c527fc8337bcf15a78ddb6aefa Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sat, 14 Jan 2012 19:53:29 +0000 Subject: [PATCH] Implement a new parser for commands. (+test) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit On the rationale of using a custom parser instead of a lex/yacc one, see this quote from src/commands_parser.c: We use a hand-written parser instead of lex/yacc because our commands are easy for humans, not for computers. Thus, it’s quite hard to specify a context-free grammar for the commands. A PEG grammar would be easier, but there’s downsides to every PEG parser generator I have come accross so far. This parser is basically a state machine which looks for literals or strings and can push either on a stack. After identifying a literal or string, it will either transition to the current state, to a different state, or call a function (like cmd_move()). Special care has been taken that error messages are useful and the code is well testable (when compiled with -DTEST_PARSER it will output to stdout instead of actually calling any function). During the migration phase (I plan to completely switch to this parser before 4.2 will be released), the new parser will parse every command you send to i3 and save the resulting call stack. Then, the old parser will parse your input and actually execute the commands. Afterwards, both call stacks will be compared and any differences will be logged. The new parser works with 100% of the test suite and produces identical call stacks. --- Makefile | 23 +- generate-command-parser.pl | 204 +++++++++++++++ include/all.h | 1 + include/commands.h | 6 + include/commands_parser.h | 15 ++ parser-specs/commands.spec | 233 ++++++++++++++++++ parser-specs/highlighting.vim | 20 ++ src/cmdparse.l | 1 + src/cmdparse.y | 12 +- src/commands.c | 329 ++++++++++++++++++++++++- src/commands_parser.c | 397 ++++++++++++++++++++++++++++++ testcases/t/187-commands-parser.t | 149 +++++++++++ 12 files changed, 1380 insertions(+), 10 deletions(-) create mode 100755 generate-command-parser.pl create mode 100644 include/commands_parser.h create mode 100644 parser-specs/commands.spec create mode 100644 parser-specs/highlighting.vim create mode 100644 src/commands_parser.c create mode 100644 testcases/t/187-commands-parser.t diff --git a/Makefile b/Makefile index 8a34ecfc..4e55b2e1 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ AUTOGENERATED:=src/cfgparse.tab.c src/cfgparse.yy.c src/cmdparse.tab.c src/cmdpa FILES:=$(filter-out $(AUTOGENERATED),$(wildcard src/*.c)) FILES:=$(FILES:.c=.o) HEADERS:=$(filter-out include/loglevels.h,$(wildcard include/*.h)) +CMDPARSE_HEADERS:=include/GENERATED_call.h include/GENERATED_enums.h include/GENERATED_tokens.h # Recursively generate loglevels.h by explicitly calling make # We need this step because we need to ensure that loglevels.h will be @@ -53,6 +54,22 @@ loglevels.h: done; \ echo "};") > include/loglevels.h; +# The GENERATED_* files are actually all created from a single pass, so all +# files just depend on the first one. +include/GENERATED_call.h: generate-command-parser.pl parser-specs/commands.spec + echo "[i3] Generating command parser" + (cd include; ../generate-command-parser.pl) +include/GENERATED_enums.h: include/GENERATED_call.h +include/GENERATED_tokens.h: include/GENERATED_call.h + +# This target compiles the command parser twice: +# Once with -DTEST_PARSER, creating a stand-alone executable used for tests, +# and once as an object file for i3. +src/commands_parser.o: src/commands_parser.c ${HEADERS} ${CMDPARSE_HEADERS} + echo "[i3] CC $<" + $(CC) $(CPPFLAGS) $(CFLAGS) -DTEST_PARSER -DLOGLEVEL="((uint64_t)1 << $(shell awk '/$(shell basename $< .c)/ { print NR; exit 0; }' loglevels.tmp))" -o test.commands_parser $< + $(CC) $(CPPFLAGS) $(CFLAGS) -DLOGLEVEL="((uint64_t)1 << $(shell awk '/$(shell basename $< .c)/ { print NR; exit 0; }' loglevels.tmp))" -c -o $@ $< + src/cfgparse.yy.o: src/cfgparse.l src/cfgparse.y.o ${HEADERS} echo "[i3] LEX $<" $(FLEX) -i -o$(@:.o=.c) $< @@ -99,8 +116,8 @@ dist: distclean [ ! -d i3-${VERSION} ] || rm -rf i3-${VERSION} [ ! -e i3-${VERSION}.tar.bz2 ] || rm i3-${VERSION}.tar.bz2 mkdir i3-${VERSION} - cp i3-migrate-config-to-v4 i3-sensible-* i3.config.keycodes DEPENDS GOALS LICENSE PACKAGE-MAINTAINER RELEASE-NOTES-${VERSION} i3.config i3.desktop i3.welcome pseudo-doc.doxygen i3-wsbar Makefile i3-${VERSION} - cp -r src libi3 i3-msg i3-nagbar i3-config-wizard i3bar i3-dump-log yajl-fallback include man i3-${VERSION} + cp i3-migrate-config-to-v4 generate-command-parser.pl i3-sensible-* i3.config.keycodes DEPENDS GOALS LICENSE PACKAGE-MAINTAINER RELEASE-NOTES-${VERSION} i3.config i3.desktop i3.welcome pseudo-doc.doxygen i3-wsbar Makefile i3-${VERSION} + cp -r src libi3 i3-msg i3-nagbar i3-config-wizard i3bar i3-dump-log yajl-fallback include man parser-specs i3-${VERSION} # Only copy toplevel documentation (important stuff) mkdir i3-${VERSION}/docs # Pre-generate documentation @@ -121,7 +138,7 @@ dist: distclean rm -rf i3-${VERSION} clean: - rm -f src/*.o src/*.gcno src/cfgparse.tab.{c,h} src/cfgparse.yy.c src/cfgparse.{output,dot} src/cmdparse.tab.{c,h} src/cmdparse.yy.c src/cmdparse.{output,dot} loglevels.tmp include/loglevels.h + rm -f src/*.o src/*.gcno src/cfgparse.tab.{c,h} src/cfgparse.yy.c src/cfgparse.{output,dot} src/cmdparse.tab.{c,h} src/cmdparse.yy.c src/cmdparse.{output,dot} loglevels.tmp include/loglevels.h include/GENERATED_* (which lcov >/dev/null 2>&1 && lcov -d . --zerocounters) || true $(MAKE) -C libi3 clean $(MAKE) -C docs clean diff --git a/generate-command-parser.pl b/generate-command-parser.pl new file mode 100755 index 00000000..9220b30e --- /dev/null +++ b/generate-command-parser.pl @@ -0,0 +1,204 @@ +#!/usr/bin/env perl +# vim:ts=4:sw=4:expandtab +# +# i3 - an improved dynamic tiling window manager +# © 2009-2012 Michael Stapelberg and contributors (see also: LICENSE) +# +# generate-command-parser.pl: script to generate parts of the command parser +# from its specification file parser-specs/commands.spec. +# +# Requires only perl >= 5.10, no modules. + +use strict; +use warnings; +use Data::Dumper; +use v5.10; + +# reads in a whole file +sub slurp { + open my $fh, '<', shift; + local $/; + <$fh>; +} + +# Stores the different states. +my %states; + +# XXX: don’t hardcode input and output +my $input = '../parser-specs/commands.spec'; +my @raw_lines = split("\n", slurp($input)); +my @lines; + +# XXX: In the future, we might switch to a different way of parsing this. The +# parser is in many ways not good — one obvious one is that it is hand-crafted +# without a good reason, also it preprocesses lines and forgets about line +# numbers. Luckily, this is just an implementation detail and the specification +# for the i3 command parser is in-tree (not user input). +# -- michael, 2012-01-12 + +# First step of preprocessing: +# Join token definitions which are spread over multiple lines. +for my $line (@raw_lines) { + next if $line =~ /^\s*#/ || $line =~ /^\s*$/; + + if ($line =~ /^\s+->/) { + # This is a continued token definition, append this line to the + # previous one. + $lines[$#lines] = $lines[$#lines] . $line; + } else { + push @lines, $line; + next; + } +} + +# First step: We build up the data structure containing all states and their +# token rules. + +my $current_state; + +for my $line (@lines) { + if (my ($state) = ($line =~ /^state ([A-Z_]+):$/)) { + #say "got a new state: $state"; + $current_state = $state; + } else { + # Must be a token definition: + # [identifier = ] -> + #say "token definition: $line"; + + my ($identifier, $tokens, $action) = + ($line =~ / + ^\s* # skip leading whitespace + ([a-z_]+ \s* = \s*|) # optional identifier + (.*?) -> \s* # token + (.*) # optional action + /x); + + # Cleanup the identifier (if any). + $identifier =~ s/^\s*(\S+)\s*=\s*$/$1/g; + + # Cleanup the tokens (remove whitespace). + $tokens =~ s/\s*//g; + + # The default action is to stay in the current state. + $action = $current_state if length($action) == 0; + + #say "identifier = *$identifier*, token = *$tokens*, action = *$action*"; + for my $token (split(',', $tokens)) { + my $store_token = { + token => $token, + identifier => $identifier, + next_state => $action, + }; + if (exists $states{$current_state}) { + push $states{$current_state}, $store_token; + } else { + $states{$current_state} = [ $store_token ]; + } + } + } +} + +# Second step: Generate the enum values for all states. + +# It is important to keep the order the same, so we store the keys once. +my @keys = keys %states; + +open(my $enumfh, '>', 'GENERATED_enums.h'); + +# XXX: we might want to have a way to do this without a trailing comma, but gcc +# seems to eat it. +say $enumfh 'typedef enum {'; +my $cnt = 0; +for my $state (@keys, '__CALL') { + say $enumfh " $state = $cnt,"; + $cnt++; +} +say $enumfh '} cmdp_state;'; +close($enumfh); + +# Third step: Generate the call function. +open(my $callfh, '>', 'GENERATED_call.h'); +say $callfh 'static char *GENERATED_call(const int call_identifier) {'; +say $callfh ' char *output = NULL;'; +say $callfh ' switch (call_identifier) {'; +my $call_id = 0; +for my $state (@keys) { + my $tokens = $states{$state}; + for my $token (@$tokens) { + next unless $token->{next_state} =~ /^call /; + my ($cmd) = ($token->{next_state} =~ /^call (.*)/); + my ($next_state) = ($cmd =~ /; ([A-Z_]+)$/); + $cmd =~ s/; ([A-Z_]+)$//; + # Go back to the INITIAL state unless told otherwise. + $next_state ||= 'INITIAL'; + my $fmt = $cmd; + # Replace the references to identified literals (like $workspace) with + # calls to get_string(). + $cmd =~ s/\$([a-z_]+)/get_string("$1")/g; + # Used only for debugging/testing. + $fmt =~ s/\$([a-z_]+)/%s/g; + $fmt =~ s/"([a-z0-9_]+)"/%s/g; + + say $callfh " case $call_id:"; + say $callfh '#ifndef TEST_PARSER'; + my $real_cmd = $cmd; + if ($real_cmd =~ /\(\)/) { + $real_cmd =~ s/\(/(¤t_match/; + } else { + $real_cmd =~ s/\(/(¤t_match, /; + } + say $callfh " output = $real_cmd;"; + say $callfh '#else'; + # debug + $cmd =~ s/[^(]+\(//; + $cmd =~ s/\)$//; + $cmd = ", $cmd" if length($cmd) > 0; + say $callfh qq| printf("$fmt\\n"$cmd);|; + say $callfh '#endif'; + say $callfh " state = $next_state;"; + say $callfh " break;"; + $token->{next_state} = "call $call_id"; + $call_id++; + } +} +say $callfh ' default:'; +say $callfh ' printf("BUG in the parser. state = %d\n", call_identifier);'; +say $callfh ' }'; +say $callfh ' return output;'; +say $callfh '}'; +close($callfh); + +# Fourth step: Generate the token datastructures. + +open(my $tokfh, '>', 'GENERATED_tokens.h'); + +for my $state (@keys) { + my $tokens = $states{$state}; + say $tokfh 'cmdp_token tokens_' . $state . '[' . scalar @$tokens . '] = {'; + for my $token (@$tokens) { + my $call_identifier = 0; + my $token_name = $token->{token}; + if ($token_name =~ /^'/) { + # To make the C code simpler, we leave out the trailing single + # quote of the literal. We can do strdup(literal + 1); then :). + $token_name =~ s/'$//; + } + my $next_state = $token->{next_state}; + if ($next_state =~ /^call /) { + ($call_identifier) = ($next_state =~ /^call ([0-9]+)$/); + $next_state = '__CALL'; + } + my $identifier = $token->{identifier}; + say $tokfh qq| { "$token_name", "$identifier", $next_state, { $call_identifier } }, |; + } + say $tokfh '};'; +} + +say $tokfh 'cmdp_token_ptr tokens[' . scalar @keys . '] = {'; +for my $state (@keys) { + my $tokens = $states{$state}; + say $tokfh ' { tokens_' . $state . ', ' . scalar @$tokens . ' },'; +} +say $tokfh '};'; + +close($tokfh); diff --git a/include/all.h b/include/all.h index 9942fed9..648b0e0a 100644 --- a/include/all.h +++ b/include/all.h @@ -74,5 +74,6 @@ #include "startup.h" #include "scratchpad.h" #include "commands.h" +#include "commands_parser.h" #endif diff --git a/include/commands.h b/include/commands.h index a28b799f..73812451 100644 --- a/include/commands.h +++ b/include/commands.h @@ -22,6 +22,12 @@ typedef struct owindow { typedef TAILQ_HEAD(owindows_head, owindow) owindows_head; +void cmd_MIGRATION_enable(); +void cmd_MIGRATION_disable(); +void cmd_MIGRATION_save_new_parameters(Match *current_match, ...); +void cmd_MIGRATION_save_old_parameters(Match *current_match, ...); +void cmd_MIGRATION_validate(); + char *cmd_criteria_init(Match *current_match); char *cmd_criteria_match_windows(Match *current_match); char *cmd_criteria_add(Match *current_match, char *ctype, char *cvalue); diff --git a/include/commands_parser.h b/include/commands_parser.h new file mode 100644 index 00000000..77569ec5 --- /dev/null +++ b/include/commands_parser.h @@ -0,0 +1,15 @@ +/* + * vim:ts=4:sw=4:expandtab + * + * i3 - an improved dynamic tiling window manager + * © 2009-2012 Michael Stapelberg and contributors (see also: LICENSE) + * + * commands.c: all command functions (see commands_parser.c) + * + */ +#ifndef _COMMANDS_PARSER_H +#define _COMMANDS_PARSER_H + +char *parse_command(const char *input); + +#endif diff --git a/parser-specs/commands.spec b/parser-specs/commands.spec new file mode 100644 index 00000000..0fba3e31 --- /dev/null +++ b/parser-specs/commands.spec @@ -0,0 +1,233 @@ +# vim:ts=2:sw=2:expandtab +# +# i3 - an improved dynamic tiling window manager +# © 2009-2012 Michael Stapelberg and contributors (see also: LICENSE) +# +# parser-specs/commands.spec: Specification file for generate-command-parser.pl +# which will generate the appropriate header files for our C parser. +# +# Use :source highlighting.vim in vim to get syntax highlighting +# for this file. + +state INITIAL: + # We have an end token here for all the commands which just call some + # function without using an explicit 'end' token. + end -> + '[' -> call cmd_criteria_init(); CRITERIA + 'move' -> MOVE + 'exec' -> EXEC + 'exit' -> call cmd_exit() + 'restart' -> call cmd_restart() + 'reload' -> call cmd_reload() + 'border' -> BORDER + 'layout' -> LAYOUT + 'append_layout' -> APPEND_LAYOUT + 'workspace' -> WORKSPACE + 'focus' -> FOCUS + 'kill' -> KILL + 'open' -> call cmd_open() + 'fullscreen' -> FULLSCREEN + 'split' -> SPLIT + 'floating' -> FLOATING + 'mark' -> MARK + 'resize' -> RESIZE + 'nop' -> NOP + 'scratchpad' -> SCRATCHPAD + 'mode' -> MODE + +state CRITERIA: + ctype = 'class' -> CRITERION + ctype = 'instance' -> CRITERION + ctype = 'window_role' -> CRITERION + ctype = 'con_id' -> CRITERION + ctype = 'id' -> CRITERION + ctype = 'con_mark' -> CRITERION + ctype = 'title' -> CRITERION + ']' -> call cmd_criteria_match_windows(); INITIAL + +state CRITERION: + '=' -> CRITERION_STR + +state CRITERION_STR: + cvalue = word + -> call cmd_criteria_add($ctype, $cvalue); CRITERIA + +# exec [--no-startup-id] +state EXEC: + nosn = '--no-startup-id' + -> + command = string + -> call cmd_exec($nosn, $command) + +# border +state BORDER: + border_style = 'normal', 'none', '1pixel', 'toggle' + -> call cmd_border($border_style) + +# layout +state LAYOUT: + layout_mode = 'default', 'stacked', 'stacking', 'tabbed' + -> call cmd_layout($layout_mode) + +# append_layout +state APPEND_LAYOUT: + path = string -> call cmd_append_layout($path) + +# workspace next|prev|next_on_output|prev_on_output +# workspace back_and_forth +# workspace +state WORKSPACE: + direction = 'next_on_output', 'prev_on_output', 'next', 'prev' + -> call cmd_workspace($direction) + 'back_and_forth' + -> call cmd_workspace_back_and_forth() + workspace = string + -> call cmd_workspace_name($workspace) + +# focus left|right|up|down +# focus output +# focus tiling|floating|mode_toggle +# focus parent|child +# focus +state FOCUS: + direction = 'left', 'right', 'up', 'down' + -> call cmd_focus_direction($direction) + 'output' + -> FOCUS_OUTPUT + window_mode = 'tiling', 'floating', 'mode_toggle' + -> call cmd_focus_window_mode($window_mode) + level = 'parent', 'child' + -> call cmd_focus_level($level) + end + -> call cmd_focus() + +state FOCUS_OUTPUT: + output = string + -> call cmd_focus_output($output) + +# kill window|client +# kill +state KILL: + kill_mode = 'window', 'client' + -> call cmd_kill($kill_mode) + end + -> call cmd_kill($kill_mode) + +# fullscreen global +# fullscreen +state FULLSCREEN: + fullscreen_mode = 'global' + -> call cmd_fullscreen($fullscreen_mode) + end + -> call cmd_fullscreen($fullscreen_mode) + +# split v|h|vertical|horizontal +state SPLIT: + direction = 'v', 'h', 'vertical', 'horizontal' + -> call cmd_split($direction) + +# floating enable|disable|toggle +state FLOATING: + floating = 'enable', 'disable', 'toggle' + -> call cmd_floating($floating) + +# mark +state MARK: + mark = string + -> call cmd_mark($mark) + +# resize +state RESIZE: + way = 'grow', 'shrink' + -> RESIZE_DIRECTION + +state RESIZE_DIRECTION: + direction = 'up', 'down', 'left', 'right' + -> RESIZE_PX + +state RESIZE_PX: + resize_px = word + -> RESIZE_TILING + end + -> call cmd_resize($way, $direction, "10", "10") + +state RESIZE_TILING: + 'px' + -> + 'or' + -> RESIZE_TILING_OR + end + -> call cmd_resize($way, $direction, $resize_px, "10") + +state RESIZE_TILING_OR: + 'ppt' + -> + resize_ppt = word + -> + end + -> call cmd_resize($way, $direction, $resize_px, $resize_ppt) + +# move [ [px]] +# move [window|container] [to] workspace +# move [window|container] [to] output +# move [window|container] [to] scratchpad +# move workspace to [output] +# move scratchpad +state MOVE: + 'window' + -> + 'container' + -> + 'to' + -> + 'workspace' + -> MOVE_WORKSPACE + 'output' + -> MOVE_TO_OUTPUT + 'scratchpad' + -> call cmd_move_scratchpad() + direction = 'left', 'right', 'up', 'down' + -> MOVE_DIRECTION + +state MOVE_DIRECTION: + pixels = word + -> MOVE_DIRECTION_PX + end + -> call cmd_move_direction($direction, "10") + +state MOVE_DIRECTION_PX: + 'px' + -> call cmd_move_direction($direction, $pixels) + end + -> call cmd_move_direction($direction, $pixels) + +state MOVE_WORKSPACE: + 'to' + -> MOVE_WORKSPACE_TO_OUTPUT + workspace = 'next', 'prev', 'next_on_output', 'prev_on_output' + -> call cmd_move_con_to_workspace($workspace) + workspace = string + -> call cmd_move_con_to_workspace_name($workspace) + +state MOVE_TO_OUTPUT: + output = string + -> call cmd_move_con_to_output($output) + +state MOVE_WORKSPACE_TO_OUTPUT: + 'output' + -> + output = string + -> call cmd_move_workspace_to_output($output) + +# mode +state MODE: + mode = string + -> call cmd_mode($mode) + +state NOP: + comment = string + -> call cmd_nop($comment) + +state SCRATCHPAD: + 'show' + -> call cmd_scratchpad_show() diff --git a/parser-specs/highlighting.vim b/parser-specs/highlighting.vim new file mode 100644 index 00000000..f3d1aaba --- /dev/null +++ b/parser-specs/highlighting.vim @@ -0,0 +1,20 @@ +set filetype=i3cmd +syntax case match +syntax clear + +syntax keyword i3specStatement state call +highlight link i3specStatement Statement + +syntax match i3specComment /#.*/ +highlight link i3specComment Comment + +syntax region i3specLiteral start=/'/ end=/'/ +syntax keyword i3specToken string word end +highlight link i3specLiteral String +highlight link i3specToken String + +syntax match i3specState /[A-Z_]\{3,}/ +highlight link i3specState PreProc + +syntax match i3specSpecial /[->]/ +highlight link i3specSpecial Special diff --git a/src/cmdparse.l b/src/cmdparse.l index c333f7ae..47a4f4e0 100644 --- a/src/cmdparse.l +++ b/src/cmdparse.l @@ -96,6 +96,7 @@ back_and_forth { BEGIN(INITIAL); return TOK_BACK_AND_FORTH; } container { /* eat this token */ } workspace { yy_pop_state(); yy_push_state(MOVE_WS); yy_push_state(EAT_WHITESPACE); return TOK_WORKSPACE; } scratchpad { yy_pop_state(); return TOK_SCRATCHPAD; } +output { yy_pop_state(); return TOK_OUTPUT; } up { yy_pop_state(); return TOK_UP; } down { yy_pop_state(); return TOK_DOWN; } left { yy_pop_state(); return TOK_LEFT; } diff --git a/src/cmdparse.y b/src/cmdparse.y index 5400d765..4a2c6ab3 100644 --- a/src/cmdparse.y +++ b/src/cmdparse.y @@ -55,6 +55,14 @@ int cmdyywrap() { } char *parse_cmd(const char *new) { + cmd_MIGRATION_enable(); + char *output = parse_command(new); + if (output != NULL) { + printf("MIGRATION: new output != NULL: %s\n", output); + free(output); + } + cmd_MIGRATION_disable(); + json_output = NULL; LOG("COMMAND: *%s*\n", new); cmdyy_scan_string(new); @@ -73,6 +81,8 @@ char *parse_cmd(const char *new) { } printf("done, json output = %s\n", json_output); + cmd_MIGRATION_validate(); + cmdyylex_destroy(); FREE(context->line_copy); FREE(context->compact_error); @@ -276,7 +286,7 @@ operation: exec: TOK_EXEC optional_no_startup_id STR { - json_output = cmd_exec(¤t_match, ($2 ? "nosn" : NULL), $3); + json_output = cmd_exec(¤t_match, ($2 ? "--no-startup-id" : NULL), $3); free($3); } ; diff --git a/src/commands.c b/src/commands.c index 7c4b9a61..4071097f 100644 --- a/src/commands.c +++ b/src/commands.c @@ -8,6 +8,7 @@ * */ #include +#include #include "all.h" #include "cmdparse.tab.h" @@ -60,6 +61,272 @@ static Output *get_output_from_string(Output *current_output, const char *output return output; } +/******************************************************************************* + * Helper functions for the migration testing. We let the new parser call every + * function here and save the stack (current_match plus all parameters. Then we + * let the old parser call every function and actually execute the code. When + * there are differences between the first and the second invocation (or if + * there has not been a first invocation at all), we generate an error. + ******************************************************************************/ + +static bool migration_test = false; +typedef struct stackframe { + Match match; + int n_args; + char *args[10]; + TAILQ_ENTRY(stackframe) stackframes; +} stackframe; +static TAILQ_HEAD(stackframes_head, stackframe) old_stackframes = + TAILQ_HEAD_INITIALIZER(old_stackframes); +static struct stackframes_head new_stackframes = + TAILQ_HEAD_INITIALIZER(new_stackframes); +/* We use this char* to uniquely terminate the list of parameters to save. */ +static char *last_parameter = "0"; + +void cmd_MIGRATION_enable() { + migration_test = true; + /* clear the current stack */ + while (!TAILQ_EMPTY(&old_stackframes)) { + stackframe *current = TAILQ_FIRST(&old_stackframes); + for (int c = 0; c < current->n_args; c++) + if (current->args[c]) + free(current->args[c]); + TAILQ_REMOVE(&old_stackframes, current, stackframes); + free(current); + } + while (!TAILQ_EMPTY(&new_stackframes)) { + stackframe *current = TAILQ_FIRST(&new_stackframes); + for (int c = 0; c < current->n_args; c++) + if (current->args[c]) + free(current->args[c]); + TAILQ_REMOVE(&new_stackframes, current, stackframes); + free(current); + } +} + +void cmd_MIGRATION_disable() { + migration_test = false; +} + +void cmd_MIGRATION_save_new_parameters(Match *current_match, ...) { + va_list args; + + DLOG("saving parameters.\n"); + stackframe *frame = scalloc(sizeof(stackframe)); + match_copy(&(frame->match), current_match); + + /* All parameters are char*s */ + va_start(args, current_match); + while (true) { + char *parameter = va_arg(args, char*); + if (parameter == last_parameter) + break; + DLOG("parameter = %s\n", parameter); + if (parameter) + frame->args[frame->n_args] = sstrdup(parameter); + frame->n_args++; + } + va_end(args); + + TAILQ_INSERT_TAIL(&new_stackframes, frame, stackframes); +} + +void cmd_MIGRATION_save_old_parameters(Match *current_match, ...) { + va_list args; + + DLOG("saving new parameters.\n"); + stackframe *frame = scalloc(sizeof(stackframe)); + match_copy(&(frame->match), current_match); + + /* All parameters are char*s */ + va_start(args, current_match); + while (true) { + char *parameter = va_arg(args, char*); + if (parameter == last_parameter) + break; + DLOG("parameter = %s\n", parameter); + if (parameter) + frame->args[frame->n_args] = sstrdup(parameter); + frame->n_args++; + } + va_end(args); + + TAILQ_INSERT_TAIL(&old_stackframes, frame, stackframes); +} + +static bool re_differ(struct regex *new, struct regex *old) { + return ((new == NULL && old != NULL) || + (new != NULL && old == NULL) || + (new != NULL && old != NULL && + strcmp(new->pattern, old->pattern) != 0)); +} + +static bool str_differ(char *new, char *old) { + return ((new == NULL && old != NULL) || + (new != NULL && old == NULL) || + (new != NULL && old != NULL && + strcmp(new, old) != 0)); +} + +static pid_t migration_pid = -1; + +/* + * Handler which will be called when we get a SIGCHLD for the nagbar, meaning + * it exited (or could not be started, depending on the exit code). + * + */ +static void nagbar_exited(EV_P_ ev_child *watcher, int revents) { + ev_child_stop(EV_A_ watcher); + if (!WIFEXITED(watcher->rstatus)) { + fprintf(stderr, "ERROR: i3-nagbar did not exit normally.\n"); + return; + } + + int exitcode = WEXITSTATUS(watcher->rstatus); + printf("i3-nagbar process exited with status %d\n", exitcode); + if (exitcode == 2) { + fprintf(stderr, "ERROR: i3-nagbar could not be found. Is it correctly installed on your system?\n"); + } + + migration_pid = -1; +} + +/* We need ev >= 4 for the following code. Since it is not *that* important (it + * only makes sure that there are no i3-nagbar instances left behind) we still + * support old systems with libev 3. */ +#if EV_VERSION_MAJOR >= 4 +/* + * Cleanup handler. Will be called when i3 exits. Kills i3-nagbar with signal + * SIGKILL (9) to make sure there are no left-over i3-nagbar processes. + * + */ +static void nagbar_cleanup(EV_P_ ev_cleanup *watcher, int revent) { + if (migration_pid != -1) { + LOG("Sending SIGKILL (9) to i3-nagbar with PID %d\n", migration_pid); + kill(migration_pid, SIGKILL); + } +} +#endif + +void cmd_MIGRATION_start_nagbar() { + if (migration_pid != -1) { + fprintf(stderr, "i3-nagbar already running.\n"); + return; + } + fprintf(stderr, "Starting i3-nagbar, command parsing differs from expected output.\n"); + ELOG("Please report this on IRC or in the bugtracker. Make sure to include the full debug level logfile:\n"); + ELOG("i3-dump-log | gzip -9c > /tmp/i3.log.gz\n"); + ELOG("FYI: Your i3 version is " I3_VERSION "\n"); + migration_pid = fork(); + if (migration_pid == -1) { + warn("Could not fork()"); + return; + } + + /* child */ + if (migration_pid == 0) { + char *pageraction; + sasprintf(&pageraction, "i3-sensible-terminal -e i3-sensible-pager \"%s\"", errorfilename); + char *argv[] = { + NULL, /* will be replaced by the executable path */ + "-t", + "error", + "-m", + "You found a parsing error. Please, please, please, report it!", + "-b", + "show errors", + pageraction, + NULL + }; + exec_i3_utility("i3-nagbar", argv); + } + + /* parent */ + /* install a child watcher */ + ev_child *child = smalloc(sizeof(ev_child)); + ev_child_init(child, &nagbar_exited, migration_pid, 0); + ev_child_start(main_loop, child); + +/* We need ev >= 4 for the following code. Since it is not *that* important (it + * only makes sure that there are no i3-nagbar instances left behind) we still + * support old systems with libev 3. */ +#if EV_VERSION_MAJOR >= 4 + /* install a cleanup watcher (will be called when i3 exits and i3-nagbar is + * still running) */ + ev_cleanup *cleanup = smalloc(sizeof(ev_cleanup)); + ev_cleanup_init(cleanup, nagbar_cleanup); + ev_cleanup_start(main_loop, cleanup); +#endif +} + +void cmd_MIGRATION_validate() { + DLOG("validating the different stacks now\n"); + int old_count = 0; + int new_count = 0; + stackframe *current; + TAILQ_FOREACH(current, &new_stackframes, stackframes) + new_count++; + TAILQ_FOREACH(current, &old_stackframes, stackframes) + old_count++; + if (new_count != old_count) { + ELOG("FAILED, new_count == %d != old_count == %d\n", new_count, old_count); + cmd_MIGRATION_start_nagbar(); + return; + } + DLOG("parameter count matching, comparing one by one...\n"); + + stackframe *new_frame = TAILQ_FIRST(&new_stackframes), + *old_frame = TAILQ_FIRST(&old_stackframes); + for (int i = 0; i < new_count; i++) { + if (new_frame->match.dock != old_frame->match.dock || + new_frame->match.id != old_frame->match.id || + new_frame->match.con_id != old_frame->match.con_id || + new_frame->match.floating != old_frame->match.floating || + new_frame->match.insert_where != old_frame->match.insert_where || + re_differ(new_frame->match.title, old_frame->match.title) || + re_differ(new_frame->match.application, old_frame->match.application) || + re_differ(new_frame->match.class, old_frame->match.class) || + re_differ(new_frame->match.instance, old_frame->match.instance) || + re_differ(new_frame->match.mark, old_frame->match.mark) || + re_differ(new_frame->match.role, old_frame->match.role) ) { + ELOG("FAILED, new_frame->match != old_frame->match (frame %d)\n", i); + cmd_MIGRATION_start_nagbar(); + return; + } + if (new_frame->n_args != old_frame->n_args) { + ELOG("FAILED, new_frame->n_args == %d != old_frame->n_args == %d (frame %d)\n", + new_frame->n_args, old_frame->n_args, i); + cmd_MIGRATION_start_nagbar(); + return; + } + for (int j = 0; j < new_frame->n_args; j++) { + if (str_differ(new_frame->args[j], old_frame->args[j])) { + ELOG("FAILED, new_frame->args[%d] == %s != old_frame->args[%d] == %s (frame %d)\n", + j, new_frame->args[j], j, old_frame->args[j], i); + cmd_MIGRATION_start_nagbar(); + return; + } + } + new_frame = TAILQ_NEXT(new_frame, stackframes); + old_frame = TAILQ_NEXT(old_frame, stackframes); + } + DLOG("OK\n"); +} + +#define MIGRATION_init(x, ...) do { \ + if (migration_test) { \ + cmd_MIGRATION_save_new_parameters(current_match, __FUNCTION__, ##__VA_ARGS__ , last_parameter); \ + return NULL; \ + } else { \ + cmd_MIGRATION_save_old_parameters(current_match, __FUNCTION__, ##__VA_ARGS__ , last_parameter); \ + } \ +} while (0) + + +/******************************************************************************* + * Criteria functions. + ******************************************************************************/ + char *cmd_criteria_init(Match *current_match) { DLOG("Initializing criteria, current_match = %p\n", current_match); match_init(current_match); @@ -79,6 +346,13 @@ char *cmd_criteria_init(Match *current_match) { char *cmd_criteria_match_windows(Match *current_match) { owindow *next, *current; + /* The same as MIGRATION_init, but doesn’t return */ + if (migration_test) { + cmd_MIGRATION_save_new_parameters(current_match, __FUNCTION__, last_parameter); + } else { + cmd_MIGRATION_save_old_parameters(current_match, __FUNCTION__, last_parameter); + } + DLOG("match specification finished, matching...\n"); /* copy the old list head to iterate through it and start with a fresh * list which will contain only matching windows */ @@ -123,6 +397,13 @@ char *cmd_criteria_match_windows(Match *current_match) { } char *cmd_criteria_add(Match *current_match, char *ctype, char *cvalue) { + /* The same as MIGRATION_init, but doesn’t return */ + if (migration_test) { + cmd_MIGRATION_save_new_parameters(current_match, __FUNCTION__, last_parameter); + } else { + cmd_MIGRATION_save_old_parameters(current_match, __FUNCTION__, last_parameter); + } + DLOG("ctype=*%s*, cvalue=*%s*\n", ctype, cvalue); if (strcmp(ctype, "class") == 0) { @@ -189,6 +470,8 @@ char *cmd_criteria_add(Match *current_match, char *ctype, char *cvalue) { char *cmd_move_con_to_workspace(Match *current_match, char *which) { owindow *current; + MIGRATION_init(x, which); + DLOG("which=%s\n", which); HANDLE_EMPTY_MATCH; @@ -220,6 +503,8 @@ char *cmd_move_con_to_workspace(Match *current_match, char *which) { } char *cmd_move_con_to_workspace_name(Match *current_match, char *name) { + MIGRATION_init(x, name); + if (strncasecmp(name, "__i3_", strlen("__i3_")) == 0) { LOG("You cannot switch to the i3 internal workspaces.\n"); return sstrdup("{\"sucess\": false}"); @@ -250,6 +535,7 @@ char *cmd_move_con_to_workspace_name(Match *current_match, char *name) { } char *cmd_resize(Match *current_match, char *way, char *direction, char *resize_px, char *resize_ppt) { + MIGRATION_init(x, way, direction, resize_px, resize_ppt); /* resize [ px] [or ppt] */ DLOG("resizing in way %s, direction %s, px %s or ppt %s\n", way, direction, resize_px, resize_ppt); // TODO: We could either handle this in the parser itself as a separate token (and make the stack typed) or we need a better way to convert a string to a number with error checking @@ -348,6 +634,7 @@ char *cmd_resize(Match *current_match, char *way, char *direction, char *resize_ } char *cmd_border(Match *current_match, char *border_style_str) { + MIGRATION_init(x, border_style_str); DLOG("border style should be changed to %s\n", border_style_str); owindow *current; @@ -381,6 +668,7 @@ char *cmd_border(Match *current_match, char *border_style_str) { } char *cmd_nop(Match *current_match, char *comment) { + MIGRATION_init(x, comment); LOG("-------------------------------------------------\n"); LOG(" NOP: %s\n", comment); LOG("-------------------------------------------------\n"); @@ -389,6 +677,7 @@ char *cmd_nop(Match *current_match, char *comment) { } char *cmd_append_layout(Match *current_match, char *path) { + MIGRATION_init(x, path); LOG("Appending layout \"%s\"\n", path); tree_append_json(path); tree_render(); @@ -398,6 +687,7 @@ char *cmd_append_layout(Match *current_match, char *path) { } char *cmd_workspace(Match *current_match, char *which) { + MIGRATION_init(x, which); Con *ws; DLOG("which=%s\n", which); @@ -423,6 +713,7 @@ char *cmd_workspace(Match *current_match, char *which) { } char *cmd_workspace_back_and_forth(Match *current_match) { + MIGRATION_init(x); workspace_back_and_forth(); tree_render(); @@ -431,6 +722,7 @@ char *cmd_workspace_back_and_forth(Match *current_match) { } char *cmd_workspace_name(Match *current_match, char *name) { + MIGRATION_init(x, name); if (strncasecmp(name, "__i3_", strlen("__i3_")) == 0) { LOG("You cannot switch to the i3 internal workspaces.\n"); return sstrdup("{\"sucess\": false}"); @@ -459,6 +751,7 @@ char *cmd_workspace_name(Match *current_match, char *name) { } char *cmd_mark(Match *current_match, char *mark) { + MIGRATION_init(x, mark); DLOG("Clearing all windows which have that mark first\n"); Con *con; @@ -484,6 +777,7 @@ char *cmd_mark(Match *current_match, char *mark) { } char *cmd_mode(Match *current_match, char *mode) { + MIGRATION_init(x, mode); DLOG("mode=%s\n", mode); switch_mode(mode); @@ -492,6 +786,7 @@ char *cmd_mode(Match *current_match, char *mode) { } char *cmd_move_con_to_output(Match *current_match, char *name) { + MIGRATION_init(x, name); owindow *current; DLOG("should move window to output %s\n", name); @@ -543,6 +838,7 @@ char *cmd_move_con_to_output(Match *current_match, char *name) { } char *cmd_floating(Match *current_match, char *floating_mode) { + MIGRATION_init(x, floating_mode); owindow *current; DLOG("floating_mode=%s\n", floating_mode); @@ -571,6 +867,7 @@ char *cmd_floating(Match *current_match, char *floating_mode) { } char *cmd_move_workspace_to_output(Match *current_match, char *name) { + MIGRATION_init(x, name); DLOG("should move workspace to output %s\n", name); HANDLE_EMPTY_MATCH; @@ -619,6 +916,7 @@ char *cmd_move_workspace_to_output(Match *current_match, char *name) { } char *cmd_split(Match *current_match, char *direction) { + MIGRATION_init(x, direction); /* TODO: use matches */ LOG("splitting in direction %c\n", direction[0]); tree_split(focused, (direction[0] == 'v' ? VERT : HORIZ)); @@ -630,12 +928,15 @@ char *cmd_split(Match *current_match, char *direction) { } char *cmd_kill(Match *current_match, char *kill_mode_str) { + if (kill_mode_str == NULL) + kill_mode_str = "window"; + MIGRATION_init(x, kill_mode_str); owindow *current; DLOG("kill_mode=%s\n", kill_mode_str); int kill_mode; - if (kill_mode_str == NULL || strcmp(kill_mode_str, "window") == 0) + if (strcmp(kill_mode_str, "window") == 0) kill_mode = KILL_WINDOW; else if (strcmp(kill_mode_str, "client") == 0) kill_mode = KILL_CLIENT; @@ -661,6 +962,7 @@ char *cmd_kill(Match *current_match, char *kill_mode_str) { } char *cmd_exec(Match *current_match, char *nosn, char *command) { + MIGRATION_init(x, nosn, command); bool no_startup_id = (nosn != NULL); DLOG("should execute %s, no_startup_id = %d\n", command, no_startup_id); @@ -671,6 +973,7 @@ char *cmd_exec(Match *current_match, char *nosn, char *command) { } char *cmd_focus_direction(Match *current_match, char *direction) { + MIGRATION_init(x, direction); if (focused && focused->type != CT_WORKSPACE && focused->fullscreen_mode != CF_NONE) { @@ -700,6 +1003,7 @@ char *cmd_focus_direction(Match *current_match, char *direction) { } char *cmd_focus_window_mode(Match *current_match, char *window_mode) { + MIGRATION_init(x, window_mode); if (focused && focused->type != CT_WORKSPACE && focused->fullscreen_mode != CF_NONE) { @@ -735,6 +1039,7 @@ char *cmd_focus_window_mode(Match *current_match, char *window_mode) { } char *cmd_focus_level(Match *current_match, char *level) { + MIGRATION_init(x, level); if (focused && focused->type != CT_WORKSPACE && focused->fullscreen_mode != CF_NONE) { @@ -755,6 +1060,7 @@ char *cmd_focus_level(Match *current_match, char *level) { } char *cmd_focus(Match *current_match) { + MIGRATION_init(x); DLOG("current_match = %p\n", current_match); if (focused && focused->type != CT_WORKSPACE && @@ -769,14 +1075,12 @@ char *cmd_focus(Match *current_match) { ELOG("You have to specify which window/container should be focused.\n"); ELOG("Example: [class=\"urxvt\" title=\"irssi\"] focus\n"); - // TODO: json output char *json_output; sasprintf(&json_output, "{\"success\":false, \"error\":\"You have to " "specify which window/container should be focused\"}"); return json_output; } - LOG("here"); int count = 0; TAILQ_FOREACH(current, &owindows, owindows) { Con *ws = con_get_workspace(current->con); @@ -784,7 +1088,6 @@ char *cmd_focus(Match *current_match) { * Just skip it, you cannot focus dock windows. */ if (!ws) continue; - LOG("there"); /* If the container is not on the current workspace, * workspace_show() will switch to a different workspace and (if @@ -820,6 +1123,9 @@ char *cmd_focus(Match *current_match) { } char *cmd_fullscreen(Match *current_match, char *fullscreen_mode) { + if (fullscreen_mode == NULL) + fullscreen_mode = "output"; + MIGRATION_init(x, fullscreen_mode); DLOG("toggling fullscreen, mode = %s\n", fullscreen_mode); owindow *current; @@ -827,7 +1133,7 @@ char *cmd_fullscreen(Match *current_match, char *fullscreen_mode) { TAILQ_FOREACH(current, &owindows, owindows) { printf("matching: %p / %s\n", current->con, current->con->name); - con_toggle_fullscreen(current->con, (fullscreen_mode && strcmp(fullscreen_mode, "global") == 0 ? CF_GLOBAL : CF_OUTPUT)); + con_toggle_fullscreen(current->con, (strcmp(fullscreen_mode, "global") == 0 ? CF_GLOBAL : CF_OUTPUT)); } tree_render(); @@ -837,6 +1143,7 @@ char *cmd_fullscreen(Match *current_match, char *fullscreen_mode) { } char *cmd_move_direction(Match *current_match, char *direction, char *move_px) { + MIGRATION_init(x, direction, move_px); // TODO: We could either handle this in the parser itself as a separate token (and make the stack typed) or we need a better way to convert a string to a number with error checking int px = atoi(move_px); @@ -869,10 +1176,13 @@ char *cmd_move_direction(Match *current_match, char *direction, char *move_px) { } char *cmd_layout(Match *current_match, char *layout_str) { + if (strcmp(layout_str, "stacking") == 0) + layout_str = "stacked"; + MIGRATION_init(x, layout_str); DLOG("changing layout to %s\n", layout_str); owindow *current; int layout = (strcmp(layout_str, "default") == 0 ? L_DEFAULT : - (strcmp(layout_str, "stacked") == 0 || strcmp(layout_str, "stacking") == 0 ? L_STACKED : + (strcmp(layout_str, "stacked") == 0 ? L_STACKED : L_TABBED)); /* check if the match is empty, not if the result is empty */ @@ -892,6 +1202,7 @@ char *cmd_layout(Match *current_match, char *layout_str) { } char *cmd_exit(Match *current_match) { + MIGRATION_init(x); LOG("Exiting due to user command.\n"); exit(0); @@ -899,6 +1210,7 @@ char *cmd_exit(Match *current_match) { } char *cmd_reload(Match *current_match) { + MIGRATION_init(x); LOG("reloading\n"); kill_configerror_nagbar(false); load_configuration(conn, NULL, true); @@ -911,6 +1223,7 @@ char *cmd_reload(Match *current_match) { } char *cmd_restart(Match *current_match) { + MIGRATION_init(x); LOG("restarting i3\n"); i3_restart(false); @@ -919,6 +1232,7 @@ char *cmd_restart(Match *current_match) { } char *cmd_open(Match *current_match) { + MIGRATION_init(x); LOG("opening new container\n"); Con *con = tree_open_con(NULL, NULL); con_focus(con); @@ -931,6 +1245,7 @@ char *cmd_open(Match *current_match) { } char *cmd_focus_output(Match *current_match, char *name) { + MIGRATION_init(x, name); owindow *current; DLOG("name = %s\n", name); @@ -966,6 +1281,7 @@ char *cmd_focus_output(Match *current_match, char *name) { } char *cmd_move_scratchpad(Match *current_match) { + MIGRATION_init(x); DLOG("should move window to scratchpad\n"); owindow *current; @@ -983,6 +1299,7 @@ char *cmd_move_scratchpad(Match *current_match) { } char *cmd_scratchpad_show(Match *current_match) { + MIGRATION_init(x); DLOG("should show scratchpad window\n"); owindow *current; diff --git a/src/commands_parser.c b/src/commands_parser.c new file mode 100644 index 00000000..04130526 --- /dev/null +++ b/src/commands_parser.c @@ -0,0 +1,397 @@ +/* + * vim:ts=4:sw=4:expandtab + * + * i3 - an improved dynamic tiling window manager + * © 2009-2012 Michael Stapelberg and contributors (see also: LICENSE) + * + * commands_parser.c: hand-written parser to parse commands (commands are what + * you bind on keys and what you can send to i3 using the IPC interface, like + * 'move left' or 'workspace 4'). + * + * We use a hand-written parser instead of lex/yacc because our commands are + * easy for humans, not for computers. Thus, it’s quite hard to specify a + * context-free grammar for the commands. A PEG grammar would be easier, but + * there’s downsides to every PEG parser generator I have come accross so far. + * + * This parser is basically a state machine which looks for literals or strings + * and can push either on a stack. After identifying a literal or string, it + * will either transition to the current state, to a different state, or call a + * function (like cmd_move()). + * + * Special care has been taken that error messages are useful and the code is + * well testable (when compiled with -DTEST_PARSER it will output to stdout + * instead of actually calling any function). + * + */ +#include +#include +#include +#include +#include +#include + +#include "all.h" +#include "queue.h" + +/******************************************************************************* + * The data structures used for parsing. Essentially the current state and a + * list of tokens for that state. + * + * The GENERATED_* files are generated by generate-commands-parser.pl with the + * input parser-specs/commands.spec. + ******************************************************************************/ + +#include "GENERATED_enums.h" + +typedef struct token { + char *name; + char *identifier; + /* This might be __CALL */ + cmdp_state next_state; + union { + uint16_t call_identifier; + } extra; +} cmdp_token; + +typedef struct tokenptr { + cmdp_token *array; + int n; +} cmdp_token_ptr; + +#include "GENERATED_tokens.h" + +/******************************************************************************* + * The (small) stack where identified literals are stored during the parsing + * of a single command (like $workspace). + ******************************************************************************/ + +struct stack_entry { + /* Just a pointer, not dynamically allocated. */ + const char *identifier; + char *str; +}; + +/* 10 entries should be enough for everybody. */ +static struct stack_entry stack[10]; + +/* + * Pushes a string (identified by 'identifier') on the stack. We simply use a + * single array, since the number of entries we have to store is very small. + * + */ +static void push_string(const char *identifier, char *str) { + for (int c = 0; c < 10; c++) { + if (stack[c].identifier != NULL) + continue; + /* Found a free slot, let’s store it here. */ + stack[c].identifier = identifier; + stack[c].str = str; + return; + } + + /* When we arrive here, the stack is full. This should not happen and + * means there’s either a bug in this parser or the specification + * contains a command with more than 10 identified tokens. */ + printf("argh! stack full\n"); + exit(1); +} + +// XXX: ideally, this would be const char. need to check if that works with all +// called functions. +static char *get_string(const char *identifier) { + DLOG("Getting string %s from stack...\n", identifier); + for (int c = 0; c < 10; c++) { + if (stack[c].identifier == NULL) + break; + if (strcmp(identifier, stack[c].identifier) == 0) + return stack[c].str; + } + return NULL; +} + +static void clear_stack() { + DLOG("clearing stack.\n"); + for (int c = 0; c < 10; c++) { + if (stack[c].str != NULL) + free(stack[c].str); + stack[c].identifier = NULL; + stack[c].str = NULL; + } +} + +// TODO: remove this if it turns out we don’t need it for testing. +#if 0 +/******************************************************************************* + * A dynamically growing linked list which holds the criteria for the current + * command. + ******************************************************************************/ + +typedef struct criterion { + char *type; + char *value; + + TAILQ_ENTRY(criterion) criteria; +} criterion; + +static TAILQ_HEAD(criteria_head, criterion) criteria = + TAILQ_HEAD_INITIALIZER(criteria); + +/* + * Stores the given type/value in the list of criteria. + * Accepts a pointer as first argument, since it is 'call'ed by the parser. + * + */ +static void push_criterion(void *unused_criteria, const char *type, + const char *value) { + struct criterion *criterion = malloc(sizeof(struct criterion)); + criterion->type = strdup(type); + criterion->value = strdup(value); + TAILQ_INSERT_TAIL(&criteria, criterion, criteria); +} + +/* + * Clears the criteria linked list. + * Accepts a pointer as first argument, since it is 'call'ed by the parser. + * + */ +static void clear_criteria(void *unused_criteria) { + struct criterion *criterion; + while (!TAILQ_EMPTY(&criteria)) { + criterion = TAILQ_FIRST(&criteria); + free(criterion->type); + free(criterion->value); + TAILQ_REMOVE(&criteria, criterion, criteria); + free(criterion); + } +} +#endif + +/******************************************************************************* + * The parser itself. + ******************************************************************************/ + +static cmdp_state state; +#ifndef TEST_PARSER +static Match current_match; +#endif +static char *json_output; + +#include "GENERATED_call.h" + + +static void next_state(const cmdp_token *token) { + if (token->next_state == __CALL) { + DLOG("should call stuff, yay. call_id = %d\n", + token->extra.call_identifier); + json_output = GENERATED_call(token->extra.call_identifier); + clear_stack(); + return; + } + + state = token->next_state; + if (state == INITIAL) { + clear_stack(); + } +} + +/* TODO: Return parsing errors via JSON. */ +char *parse_command(const char *input) { + DLOG("new parser handling: %s\n", input); + state = INITIAL; + json_output = NULL; + + const char *walk = input; + const size_t len = strlen(input); + int c; + const cmdp_token *token; + bool token_handled; + + // TODO: make this testable +#ifndef TEST_PARSER + cmd_criteria_init(¤t_match); +#endif + + /* The "<=" operator is intentional: We also handle the terminating 0-byte + * explicitly by looking for an 'end' token. */ + while ((walk - input) <= len) { + /* skip whitespace before every token */ + while ((*walk == ' ' || *walk == '\t') && *walk != '\0') + walk++; + + DLOG("remaining input = %s\n", walk); + + cmdp_token_ptr *ptr = &(tokens[state]); + token_handled = false; + for (c = 0; c < ptr->n; c++) { + token = &(ptr->array[c]); + DLOG("trying token %d = %s\n", c, token->name); + + /* A literal. */ + if (token->name[0] == '\'') { + DLOG("literal\n"); + if (strncasecmp(walk, token->name + 1, strlen(token->name) - 1) == 0) { + DLOG("found literal, moving to next state\n"); + if (token->identifier != NULL) + push_string(token->identifier, strdup(token->name + 1)); + walk += strlen(token->name) - 1; + next_state(token); + token_handled = true; + break; + } + continue; + } + + if (strcmp(token->name, "string") == 0 || + strcmp(token->name, "word") == 0) { + DLOG("parsing this as a string\n"); + const char *beginning = walk; + /* Handle quoted strings (or words). */ + if (*walk == '"') { + beginning++; + walk++; + while (*walk != '"' || *(walk-1) == '\\') + walk++; + } else { + if (token->name[0] == 's') { + /* For a string (starting with 's'), the delimiters are + * comma (,) and semicolon (;) which introduce a new + * operation or command, respectively. */ + while (*walk != ';' && *walk != ',' && *walk != '\0') + walk++; + } else { + /* For a word, the delimiters are white space (' ' or + * '\t'), closing square bracket (]), comma (,) and + * semicolon (;). */ + while (*walk != ' ' && *walk != '\t' && *walk != ']' && + *walk != ',' && *walk != ';' && *walk != '\0') + walk++; + } + } + if (walk != beginning) { + char *str = calloc(walk-beginning + 1, 1); + strncpy(str, beginning, walk-beginning); + if (token->identifier) + push_string(token->identifier, str); + DLOG("str is \"%s\"\n", str); + /* If we are at the end of a quoted string, skip the ending + * double quote. */ + if (*walk == '"') + walk++; + next_state(token); + token_handled = true; + break; + } + } + + if (strcmp(token->name, "end") == 0) { + DLOG("checking for the end token.\n"); + if (*walk == '\0' || *walk == ',' || *walk == ';') { + DLOG("yes, indeed. end\n"); + walk++; + next_state(token); + token_handled = true; + /* To make sure we start with an appropriate matching + * datastructure for commands which do *not* specify any + * criteria, we re-initialize the criteria system after + * every command. */ + // TODO: make this testable +#ifndef TEST_PARSER + if (*walk == '\0' || *walk == ';') + cmd_criteria_init(¤t_match); +#endif + break; + } + } + } + + if (!token_handled) { + /* Figure out how much memory we will need to fill in the names of + * all tokens afterwards. */ + int tokenlen = 0; + for (c = 0; c < ptr->n; c++) + tokenlen += strlen(ptr->array[c].name) + strlen("'', "); + + /* Build up a decent error message. We include the problem, the + * full input, and underline the position where the parser + * currently is. */ + char *errormessage; + char *possible_tokens = malloc(tokenlen + 1); + char *tokenwalk = possible_tokens; + for (c = 0; c < ptr->n; c++) { + token = &(ptr->array[c]); + if (token->name[0] == '\'') { + /* A literal is copied to the error message enclosed with + * single quotes. */ + *tokenwalk++ = '\''; + strcpy(tokenwalk, token->name + 1); + tokenwalk += strlen(token->name + 1); + *tokenwalk++ = '\''; + } else { + /* Any other token is copied to the error message enclosed + * with angle brackets. */ + *tokenwalk++ = '<'; + strcpy(tokenwalk, token->name); + tokenwalk += strlen(token->name); + *tokenwalk++ = '>'; + } + if (c < (ptr->n - 1)) { + *tokenwalk++ = ','; + *tokenwalk++ = ' '; + } + } + *tokenwalk = '\0'; + asprintf(&errormessage, "Expected one of these tokens: %s", + possible_tokens); + free(possible_tokens); + + /* Contains the same amount of characters as 'input' has, but with + * the unparseable part highlighted using ^ characters. */ + char *position = malloc(len + 1); + for (const char *copywalk = input; *copywalk != '\0'; copywalk++) + position[(copywalk - input)] = (copywalk >= walk ? '^' : ' '); + position[len] = '\0'; + + printf("%s\n", errormessage); + printf("Your command: %s\n", input); + printf(" %s\n", position); + + free(position); + free(errormessage); + break; + } + } + + DLOG("json_output = %s\n", json_output); + return json_output; +} + +/******************************************************************************* + * Code for building the stand-alone binary test.commands_parser which is used + * by t/187-commands-parser.t. + ******************************************************************************/ + +#ifdef TEST_PARSER + +/* + * Logs the given message to stdout while prefixing the current time to it, + * but only if the corresponding debug loglevel was activated. + * This is to be called by DLOG() which includes filename/linenumber + * + */ +void debuglog(uint64_t lev, char *fmt, ...) { + va_list args; + + va_start(args, fmt); + fprintf(stdout, "# "); + vfprintf(stdout, fmt, args); + va_end(args); +} + +int main(int argc, char *argv[]) { + if (argc < 2) { + fprintf(stderr, "Syntax: %s \n", argv[0]); + return 1; + } + parse_command(argv[1]); +} +#endif diff --git a/testcases/t/187-commands-parser.t b/testcases/t/187-commands-parser.t new file mode 100644 index 00000000..35eaef73 --- /dev/null +++ b/testcases/t/187-commands-parser.t @@ -0,0 +1,149 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Tests the standalone parser binary to see if it calls the right code when +# confronted with various commands, if it prints proper error messages for +# wrong commands and if it terminates in every case. +# +use i3test i3_autostart => 0; + +sub parser_calls { + my ($command) = @_; + + # TODO: use a timeout, so that we can error out if it doesn’t terminate + # TODO: better way of passing arguments + my $stdout = qx(../test.commands_parser '$command'); + + # Filter out all debugging output. + my @lines = split("\n", $stdout); + @lines = grep { not /^# / } @lines; + + # The criteria management calls are irrelevant and not what we want to test + # in the first place. + @lines = grep { !(/cmd_criteria_init()/ || /cmd_criteria_match_windows/) } @lines; + return join("\n", @lines); +} + +################################################################################ +# 1: First that the parser properly recognizes commands which are ok. +################################################################################ + +is(parser_calls('move workspace 3'), + 'cmd_move_con_to_workspace_name(3)', + 'single number (move workspace 3) ok'); + +is(parser_calls('move to workspace 3'), + 'cmd_move_con_to_workspace_name(3)', + 'to (move to workspace 3) ok'); + +is(parser_calls('move window to workspace 3'), + 'cmd_move_con_to_workspace_name(3)', + 'window to (move window to workspace 3) ok'); + +is(parser_calls('move container to workspace 3'), + 'cmd_move_con_to_workspace_name(3)', + 'container to (move container to workspace 3) ok'); + +is(parser_calls('move workspace foobar'), + 'cmd_move_con_to_workspace_name(foobar)', + 'single word (move workspace foobar) ok'); + +is(parser_calls('move workspace 3: foobar'), + 'cmd_move_con_to_workspace_name(3: foobar)', + 'multiple words (move workspace 3: foobar) ok'); + +is(parser_calls('move workspace "3: foobar"'), + 'cmd_move_con_to_workspace_name(3: foobar)', + 'double quotes (move workspace "3: foobar") ok'); + +is(parser_calls('move workspace "3: foobar, baz"'), + 'cmd_move_con_to_workspace_name(3: foobar, baz)', + 'quotes with comma (move workspace "3: foobar, baz") ok'); + +is(parser_calls('move workspace 3: foobar, nop foo'), + "cmd_move_con_to_workspace_name(3: foobar)\n" . + "cmd_nop(foo)", + 'multiple ops (move workspace 3: foobar, nop foo) ok'); + +is(parser_calls('exec i3-sensible-terminal'), + 'cmd_exec((null), i3-sensible-terminal)', + 'exec ok'); + +is(parser_calls('exec --no-startup-id i3-sensible-terminal'), + 'cmd_exec(--no-startup-id, i3-sensible-terminal)', + 'exec --no-startup-id ok'); + +is(parser_calls('resize shrink left'), + 'cmd_resize(shrink, left, 10, 10)', + 'simple resize ok'); + +is(parser_calls('resize shrink left 25 px'), + 'cmd_resize(shrink, left, 25, 10)', + 'px resize ok'); + +is(parser_calls('resize shrink left 25 px or 33 ppt'), + 'cmd_resize(shrink, left, 25, 33)', + 'px + ppt resize ok'); + +is(parser_calls('resize shrink left 25 px or 33 ppt'), + 'cmd_resize(shrink, left, 25, 33)', + 'px + ppt resize ok'); + +is(parser_calls('resize shrink left 25 px or 33 ppt,'), + 'cmd_resize(shrink, left, 25, 33)', + 'trailing comma resize ok'); + +is(parser_calls('resize shrink left 25 px or 33 ppt;'), + 'cmd_resize(shrink, left, 25, 33)', + 'trailing semicolon resize ok'); + +is(parser_calls('resize shrink left 25'), + 'cmd_resize(shrink, left, 25, 10)', + 'resize early end ok'); + +is(parser_calls('[con_mark=yay] focus'), + "cmd_criteria_add(con_mark, yay)\n" . + "cmd_focus()", + 'criteria focus ok'); + +is(parser_calls("[con_mark=yay con_mark=bar] focus"), + "cmd_criteria_add(con_mark, yay)\n" . + "cmd_criteria_add(con_mark, bar)\n" . + "cmd_focus()", + 'criteria focus ok'); + +is(parser_calls("[con_mark=yay\tcon_mark=bar] focus"), + "cmd_criteria_add(con_mark, yay)\n" . + "cmd_criteria_add(con_mark, bar)\n" . + "cmd_focus()", + 'criteria focus ok'); + +is(parser_calls("[con_mark=yay\tcon_mark=bar]\tfocus"), + "cmd_criteria_add(con_mark, yay)\n" . + "cmd_criteria_add(con_mark, bar)\n" . + "cmd_focus()", + 'criteria focus ok'); + +is(parser_calls('[con_mark="yay"] focus'), + "cmd_criteria_add(con_mark, yay)\n" . + "cmd_focus()", + 'quoted criteria focus ok'); + +################################################################################ +# 2: Verify that the parser spits out the right error message on commands which +# are not ok. +################################################################################ + +is(parser_calls('unknown_literal'), + "Expected one of these tokens: , '[', 'move', 'exec', 'exit', 'restart', 'reload', 'border', 'layout', 'append_layout', 'workspace', 'focus', 'kill', 'open', 'fullscreen', 'split', 'floating', 'mark', 'resize', 'nop', 'scratchpad', 'mode'\n" . + "Your command: unknown_literal\n" . + " ^^^^^^^^^^^^^^^", + 'error for unknown literal ok'); + +is(parser_calls('move something to somewhere'), + "Expected one of these tokens: 'window', 'container', 'to', 'workspace', 'output', 'scratchpad', 'left', 'right', 'up', 'down'\n" . + "Your command: move something to somewhere\n" . + " ^^^^^^^^^^^^^^^^^^^^^^", + 'error for unknown literal ok'); + +done_testing; -- 2.39.5