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;