From eaa5e636f96815ddf7f236d35aa9b8686388aa76 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Wed, 2 Jun 2021 21:01:43 +0200 Subject: [PATCH] Implement include config directive (#4420) The implementation uses wordexp(3) just like sway: https://github.com/i3/i3/issues/1197#issuecomment-226844106 Thanks to jajm for their implementation at https://github.com/jajm/i3/commit/bb55709d0aa0731f7b3c641871731a992ababb1a This required refactoring the config parser to be re-entrant (no more global state) and to return an error instead of dying. In case a file cannot be opened, i3 reports an error but proceeds with the remaining configuration. Key bindings can be overwritten or removed using the new --remove flag of the bindsym/bindcode directive. All files that were successfully included are displayed in i3 --moreversion. One caveat is i3 config file variable expansion, see the note in the userguide. fixes #4192 --- RELEASE-NOTES-next | 1 + docs/userguide | 84 ++++++++ generate-command-parser.pl | 10 +- include/config_directives.h | 1 + include/config_parser.h | 66 ++++++- include/configuration.h | 12 ++ parser-specs/config.spec | 8 + src/bindings.c | 60 +++--- src/commands_parser.c | 99 ++++------ src/config.c | 44 ++++- src/config_directives.c | 79 ++++++++ src/config_parser.c | 319 +++++++++++++++--------------- src/display_version.c | 60 ++++-- src/ipc.c | 11 ++ testcases/t/201-config-parser.t | 1 + testcases/t/313-include.t | 338 ++++++++++++++++++++++++++++++++ 16 files changed, 919 insertions(+), 274 deletions(-) create mode 100644 testcases/t/313-include.t diff --git a/RELEASE-NOTES-next b/RELEASE-NOTES-next index 53fe23293..e6a892d6a 100644 --- a/RELEASE-NOTES-next +++ b/RELEASE-NOTES-next @@ -35,6 +35,7 @@ option is enabled and only then sets a screenshot as background. • i3bar: use first bar config by default • i3-dump-log -f now uses UNIX sockets instead of pthreads. The UNIX socket approach should be more reliable and also more portable. + • Implement the include config directive • Allow for_window to match against WM_CLIENT_MACHINE • Add %machine placeholder (WM_CLIENT_MACHINE) to title_format • Allow multiple output names in 'move container|workspace to output' diff --git a/docs/userguide b/docs/userguide index cd48e7ef3..944f7b398 100644 --- a/docs/userguide +++ b/docs/userguide @@ -319,6 +319,90 @@ include the following line in your config file: # i3 config file (v4) --------------------- +[[include]] +=== Include directive + +Since i3 v4.20, it is possible to include other configuration files from your i3 +configuration. + +*Syntax*: +----------------- +include +----------------- + +i3 expands `pattern` using shell-like word expansion, specifically using the +https://manpages.debian.org/wordexp.3[`wordexp(3)` C standard library function]. + +*Examples*: +-------------------------------------------------------------------------------- +# Tilde expands to the user’s home directory: +include ~/.config/i3/assignments.conf + +# Environment variables are expanded: +include $HOME/.config/i3/assignments.conf + +# Wildcards are expanded: +include ~/.config/i3/config.d/*.conf + +# Command substitution: +include ~/.config/i3/`hostname`.conf + +# i3 loads each path only once, so including the i3 config will not result +# in an endless loop, but in an error: +include ~/.config/i3/config + +# i3 changes the working directory while parsing a config file +# so that relative paths are interpreted relative to the directory +# of the config file that contains the path: +include assignments.conf +-------------------------------------------------------------------------------- + +If a specified file cannot be read, for example because of a lack of file +permissions, or because of a dangling symlink, i3 will report an error and +continue processing your remaining configuration. + +To list all loaded configuration files, run `i3 --moreversion`: + +-------------------------------------------------------------------------------- +% i3 --moreversion +Binary i3 version: 4.19.2-87-gfcae64f7+ © 2009 Michael Stapelberg and contributors +Running i3 version: 4.19.2-87-gfcae64f7+ (pid 963940) +Loaded i3 config: + /tmp/i3.cfg (main) (last modified: 2021-05-13T16:42:31 CEST, 463 seconds ago) + /tmp/included.cfg (included) (last modified: 2021-05-13T16:42:43 CEST, 451 seconds ago) + /tmp/another.cfg (included) (last modified: 2021-05-13T16:42:46 CEST, 448 seconds ago) +-------------------------------------------------------------------------------- + +Variables are shared between all config files, but beware of the following limitation: + +* You can define a variable and use it within an included file. +* You cannot use (in the parent file) a variable that was defined within an included file. + +This is a technical limitation: variable expansion happens in a separate stage +before parsing include directives. + +Conceptually, included files can only add to the configuration, not undo the +effects of already-processed configuration. For example, you can only add new +key bindings, not overwrite or remove existing key bindings. This means: + +* The `include` directive is suitable for organizing large configurations into + separate files, possibly selecting files based on conditionals. + +* The `include` directive is not suitable for expressing “use the default + configuration with the following changes”. For that case, we still recommend + copying and modifying the default config. + +[NOTE] +==== +Implementation-wise, i3 does not currently construct one big configuration from +all `include` directives. Instead, i3’s config file parser interprets all +configuration directives in its `parse_file()` function. When processing an +`include` configuration directive, the parser recursively calls `parse_file()`. + +This means the evaluation order of files forms a tree, or one could say i3 uses +depth-first traversal. +==== + === Comments It is possible and recommended to use comments in your configuration file to diff --git a/generate-command-parser.pl b/generate-command-parser.pl index 77502db7e..cef4eda50 100755 --- a/generate-command-parser.pl +++ b/generate-command-parser.pl @@ -133,7 +133,7 @@ sub slurp { open(my $callfh, '>', "GENERATED_${prefix}_call.h"); my $resultname = uc(substr($prefix, 0, 1)) . substr($prefix, 1) . 'ResultIR'; say $callfh '#pragma once'; -say $callfh "static void GENERATED_call(const int call_identifier, struct $resultname *result) {"; +say $callfh "static void GENERATED_call(Match *current_match, struct stack *stack, const int call_identifier, struct $resultname *result) {"; say $callfh ' switch (call_identifier) {'; my $call_id = 0; for my $state (@keys) { @@ -150,8 +150,8 @@ sub slurp { # calls to get_string(). Also replaces state names (like FOR_WINDOW) # with their ID (useful for cfg_criteria_init(FOR_WINDOW) e.g.). $cmd =~ s/$_/$statenum{$_}/g for @keys; - $cmd =~ s/\$([a-z_]+)/get_string("$1")/g; - $cmd =~ s/\&([a-z_]+)/get_long("$1")/g; + $cmd =~ s/\$([a-z_]+)/get_string(stack, "$1")/g; + $cmd =~ s/\&([a-z_]+)/get_long(stack, "$1")/g; # For debugging/testing, we print the call using printf() and thus need # to generate a format string. The format uses %d for s, # literal numbers or state IDs and %s for NULL, s and literal @@ -175,9 +175,9 @@ sub slurp { say $callfh '#ifndef TEST_PARSER'; my $real_cmd = $cmd; if ($real_cmd =~ /\(\)/) { - $real_cmd =~ s/\(/(¤t_match, result/; + $real_cmd =~ s/\(/(current_match, result/; } else { - $real_cmd =~ s/\(/(¤t_match, result, /; + $real_cmd =~ s/\(/(current_match, result, /; } say $callfh " $real_cmd;"; say $callfh '#else'; diff --git a/include/config_directives.h b/include/config_directives.h index 06fbd3b06..e00570979 100644 --- a/include/config_directives.h +++ b/include/config_directives.h @@ -39,6 +39,7 @@ CFGFUN(criteria_init, int _state); CFGFUN(criteria_add, const char *ctype, const char *cvalue); CFGFUN(criteria_pop_state); +CFGFUN(include, const char *pattern); CFGFUN(font, const char *font); CFGFUN(exec, const char *exectype, const char *no_startup_id, const char *command); CFGFUN(for_window, const char *command); diff --git a/include/config_parser.h b/include/config_parser.h index 009538f2d..7cdb5a193 100644 --- a/include/config_parser.h +++ b/include/config_parser.h @@ -16,6 +16,50 @@ SLIST_HEAD(variables_head, Variable); extern pid_t config_error_nagbar_pid; +struct stack_entry { + /* Just a pointer, not dynamically allocated. */ + const char *identifier; + enum { + STACK_STR = 0, + STACK_LONG = 1, + } type; + union { + char *str; + long num; + } val; +}; + +struct stack { + struct stack_entry stack[10]; +}; + +struct parser_ctx { + bool use_nagbar; + bool assume_v4; + + int state; + Match current_match; + + /* A list which contains the states that lead to the current state, e.g. + * INITIAL, WORKSPACE_LAYOUT. + * When jumping back to INITIAL, statelist_idx will simply be set to 1 + * (likewise for other states, e.g. MODE or BAR). + * This list is used to process the nearest error token. */ + int statelist[10]; + /* NB: statelist_idx points to where the next entry will be inserted */ + int statelist_idx; + + /******************************************************************************* + * The (small) stack where identified literals are stored during the parsing + * of a single config directive (like $workspace). + ******************************************************************************/ + struct stack *stack; + + struct variables_head variables; + + bool has_errors; +}; + /** * An intermediate reprsentation of the result of a parse_config call. * Currently unused, but the JSON output will be useful in the future when we @@ -23,22 +67,34 @@ extern pid_t config_error_nagbar_pid; * */ struct ConfigResultIR { - /* The JSON generator to append a reply to. */ - yajl_gen json_gen; + struct parser_ctx *ctx; /* The next state to transition to. Passed to the function so that we can * determine the next state as a result of a function call, like * cfg_criteria_pop_state() does. */ int next_state; -}; -struct ConfigResultIR *parse_config(const char *input, struct context *context); + /* Whether any error happened while processing this config directive. */ + bool has_errors; +}; /** * launch nagbar to indicate errors in the configuration file. */ void start_config_error_nagbar(const char *configpath, bool has_errors); +/** + * Releases the memory of all variables in ctx. + * + */ +void free_variables(struct parser_ctx *ctx); + +typedef enum { + PARSE_FILE_FAILED = -1, + PARSE_FILE_SUCCESS = 0, + PARSE_FILE_CONFIG_ERRORS = 1, +} parse_file_result_t; + /** * Parses the given file by first replacing the variables, then calling * parse_config and launching i3-nagbar if use_nagbar is true. @@ -47,4 +103,4 @@ void start_config_error_nagbar(const char *configpath, bool has_errors); * parsing. * */ -bool parse_file(const char *f, bool use_nagbar); +parse_file_result_t parse_file(struct parser_ctx *ctx, const char *f); diff --git a/include/configuration.h b/include/configuration.h index 11cdde0dd..1e41893a7 100644 --- a/include/configuration.h +++ b/include/configuration.h @@ -15,6 +15,7 @@ #include "queue.h" #include "i3.h" +typedef struct IncludedFile IncludedFile; typedef struct Config Config; typedef struct Barconfig Barconfig; extern char *current_configpath; @@ -22,6 +23,7 @@ extern char *current_config; extern Config config; extern SLIST_HEAD(modes_head, Mode) modes; extern TAILQ_HEAD(barconfig_head, Barconfig) barconfigs; +extern TAILQ_HEAD(includedfiles_head, IncludedFile) included_files; /** * Used during the config file lexing/parsing to keep the state of the lexer @@ -69,6 +71,16 @@ struct Variable { SLIST_ENTRY(Variable) variables; }; +/** + * List entry struct for an included file. + * + */ +struct IncludedFile { + char *path; + + TAILQ_ENTRY(IncludedFile) files; +}; + /** * The configuration file can contain multiple sets of bindings. Apart from the * default set (name == "default"), you can specify other sets and change the diff --git a/parser-specs/config.spec b/parser-specs/config.spec index 7d7b99899..1b6d73409 100644 --- a/parser-specs/config.spec +++ b/parser-specs/config.spec @@ -20,6 +20,7 @@ state INITIAL: 'set ' -> IGNORE_LINE 'set ' -> IGNORE_LINE 'set_from_resource' -> IGNORE_LINE + 'include' -> INCLUDE bindtype = 'bindsym', 'bindcode', 'bind' -> BINDING 'bar' -> BARBRACE 'font' -> FONT @@ -63,6 +64,11 @@ state IGNORE_LINE: line -> INITIAL +# include +state INCLUDE: + pattern = string + -> call cfg_include($pattern) + # floating_minimum_size x state FLOATING_MINIMUM_SIZE_WIDTH: width = number @@ -394,6 +400,8 @@ state BINDCOMMAND: -> command = string -> call cfg_binding($bindtype, $modifiers, $key, $release, $border, $whole_window, $exclude_titlebar, $command) + end + -> call cfg_binding($bindtype, $modifiers, $key, $release, $border, $whole_window, $exclude_titlebar, $command) ################################################################################ # Mode configuration diff --git a/src/bindings.c b/src/bindings.c index d6255e735..0aa960d31 100644 --- a/src/bindings.c +++ b/src/bindings.c @@ -717,6 +717,40 @@ void reorder_bindings(void) { } } +/* + * Returns true if a is a key binding for the same key as b. + * + */ +static bool binding_same_key(Binding *a, Binding *b) { + /* Check if the input types are different */ + if (a->input_type != b->input_type) { + return false; + } + + /* Check if one is using keysym while the other is using bindsym. */ + if ((a->symbol == NULL && b->symbol != NULL) || + (a->symbol != NULL && b->symbol == NULL)) { + return false; + } + + /* If a is NULL, b has to be NULL, too (see previous conditional). + * If the keycodes differ, it can't be a duplicate. */ + if (a->symbol != NULL && + strcasecmp(a->symbol, b->symbol) != 0) { + return false; + } + + /* Check if the keycodes or modifiers are different. If so, they + * can't be duplicate */ + if (a->keycode != b->keycode || + a->event_state_mask != b->event_state_mask || + a->release != b->release) { + return false; + } + + return true; +} + /* * Checks for duplicate key bindings (the same keycode or keysym is configured * more than once). If a duplicate binding is found, a message is printed to @@ -730,31 +764,13 @@ void check_for_duplicate_bindings(struct context *context) { TAILQ_FOREACH (bind, bindings, bindings) { /* Abort when we reach the current keybinding, only check the * bindings before */ - if (bind == current) + if (bind == current) { break; + } - /* Check if the input types are different */ - if (bind->input_type != current->input_type) - continue; - - /* Check if one is using keysym while the other is using bindsym. - * If so, skip. */ - if ((bind->symbol == NULL && current->symbol != NULL) || - (bind->symbol != NULL && current->symbol == NULL)) - continue; - - /* If bind is NULL, current has to be NULL, too (see above). - * If the keycodes differ, it can't be a duplicate. */ - if (bind->symbol != NULL && - strcasecmp(bind->symbol, current->symbol) != 0) - continue; - - /* Check if the keycodes or modifiers are different. If so, they - * can't be duplicate */ - if (bind->keycode != current->keycode || - bind->event_state_mask != current->event_state_mask || - bind->release != current->release) + if (!binding_same_key(bind, current)) { continue; + } context->has_errors = true; if (current->keycode != 0) { diff --git a/src/commands_parser.c b/src/commands_parser.c index 6c7914151..fd02293dc 100644 --- a/src/commands_parser.c +++ b/src/commands_parser.c @@ -56,40 +56,19 @@ typedef struct tokenptr { #include "GENERATED_command_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; - enum { - STACK_STR = 0, - STACK_LONG = 1, - } type; - union { - char *str; - long num; - } val; -}; - -/* 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) { +static void push_string(struct stack *stack, const char *identifier, char *str) { for (int c = 0; c < 10; c++) { - if (stack[c].identifier != NULL) + if (stack->stack[c].identifier != NULL) continue; /* Found a free slot, let’s store it here. */ - stack[c].identifier = identifier; - stack[c].val.str = str; - stack[c].type = STACK_STR; + stack->stack[c].identifier = identifier; + stack->stack[c].val.str = str; + stack->stack[c].type = STACK_STR; return; } @@ -103,15 +82,15 @@ static void push_string(const char *identifier, char *str) { } // TODO move to a common util -static void push_long(const char *identifier, long num) { +static void push_long(struct stack *stack, const char *identifier, long num) { for (int c = 0; c < 10; c++) { - if (stack[c].identifier != NULL) { + if (stack->stack[c].identifier != NULL) { continue; } - stack[c].identifier = identifier; - stack[c].val.num = num; - stack[c].type = STACK_LONG; + stack->stack[c].identifier = identifier; + stack->stack[c].val.num = num; + stack->stack[c].type = STACK_LONG; return; } @@ -125,36 +104,36 @@ static void push_long(const char *identifier, long num) { } // TODO move to a common util -static const char *get_string(const char *identifier) { +static const char *get_string(struct stack *stack, const char *identifier) { for (int c = 0; c < 10; c++) { - if (stack[c].identifier == NULL) + if (stack->stack[c].identifier == NULL) break; - if (strcmp(identifier, stack[c].identifier) == 0) - return stack[c].val.str; + if (strcmp(identifier, stack->stack[c].identifier) == 0) + return stack->stack[c].val.str; } return NULL; } // TODO move to a common util -static long get_long(const char *identifier) { +static long get_long(struct stack *stack, const char *identifier) { for (int c = 0; c < 10; c++) { - if (stack[c].identifier == NULL) + if (stack->stack[c].identifier == NULL) break; - if (strcmp(identifier, stack[c].identifier) == 0) - return stack[c].val.num; + if (strcmp(identifier, stack->stack[c].identifier) == 0) + return stack->stack[c].val.num; } return 0; } // TODO move to a common util -static void clear_stack(void) { +static void clear_stack(struct stack *stack) { for (int c = 0; c < 10; c++) { - if (stack[c].type == STACK_STR) - free(stack[c].val.str); - stack[c].identifier = NULL; - stack[c].val.str = NULL; - stack[c].val.num = 0; + if (stack->stack[c].type == STACK_STR) + free(stack->stack[c].val.str); + stack->stack[c].identifier = NULL; + stack->stack[c].val.str = NULL; + stack->stack[c].val.num = 0; } } @@ -163,9 +142,12 @@ static void clear_stack(void) { ******************************************************************************/ static cmdp_state state; -#ifndef TEST_PARSER static Match current_match; -#endif +/******************************************************************************* + * The (small) stack where identified literals are stored during the parsing + * of a single command (like $workspace). + ******************************************************************************/ +static struct stack stack; static struct CommandResultIR subcommand_output; static struct CommandResultIR command_output; @@ -176,19 +158,19 @@ static void next_state(const cmdp_token *token) { subcommand_output.json_gen = command_output.json_gen; subcommand_output.client = command_output.client; subcommand_output.needs_tree_render = false; - GENERATED_call(token->extra.call_identifier, &subcommand_output); + GENERATED_call(¤t_match, &stack, token->extra.call_identifier, &subcommand_output); state = subcommand_output.next_state; /* If any subcommand requires a tree_render(), we need to make the * whole parser result request a tree_render(). */ if (subcommand_output.needs_tree_render) command_output.needs_tree_render = true; - clear_stack(); + clear_stack(&stack); return; } state = token->next_state; if (state == INITIAL) { - clear_stack(); + clear_stack(&stack); } } @@ -296,8 +278,9 @@ CommandResult *parse_command(const char *input, yajl_gen gen, ipc_client *client /* A literal. */ if (token->name[0] == '\'') { if (strncasecmp(walk, token->name + 1, strlen(token->name) - 1) == 0) { - if (token->identifier != NULL) - push_string(token->identifier, sstrdup(token->name + 1)); + if (token->identifier != NULL) { + push_string(&stack, token->identifier, sstrdup(token->name + 1)); + } walk += strlen(token->name) - 1; next_state(token); token_handled = true; @@ -319,8 +302,9 @@ CommandResult *parse_command(const char *input, yajl_gen gen, ipc_client *client if (end == walk) continue; - if (token->identifier != NULL) - push_long(token->identifier, num); + if (token->identifier != NULL) { + push_long(&stack, token->identifier, num); + } /* Set walk to the first non-number character */ walk = end; @@ -333,8 +317,9 @@ CommandResult *parse_command(const char *input, yajl_gen gen, ipc_client *client strcmp(token->name, "word") == 0) { char *str = parse_string(&walk, (token->name[0] != 's')); if (str != NULL) { - if (token->identifier) - push_string(token->identifier, str); + if (token->identifier) { + push_string(&stack, token->identifier, str); + } /* If we are at the end of a quoted string, skip the ending * double quote. */ if (*walk == '"') @@ -436,7 +421,7 @@ CommandResult *parse_command(const char *input, yajl_gen gen, ipc_client *client y(map_close); free(position); - clear_stack(); + clear_stack(&stack); break; } } diff --git a/src/config.c b/src/config.c index ecc154c61..7f7e0257e 100644 --- a/src/config.c +++ b/src/config.c @@ -10,6 +10,9 @@ */ #include "all.h" +#include +#include + #include char *current_configpath = NULL; @@ -17,6 +20,7 @@ char *current_config = NULL; Config config; struct modes_head modes; struct barconfig_head barconfigs = TAILQ_HEAD_INITIALIZER(barconfigs); +struct includedfiles_head included_files = TAILQ_HEAD_INITIALIZER(included_files); /* * Ungrabs all keys, to be called before re-grabbing the keys because of a @@ -225,8 +229,42 @@ bool load_configuration(const char *override_configpath, config_load_t load_type "$XDG_CONFIG_HOME/i3/config, ~/.i3/config, $XDG_CONFIG_DIRS/i3/config " "and " SYSCONFDIR "/i3/config)"); } - LOG("Parsing configfile %s\n", current_configpath); - const bool result = parse_file(current_configpath, load_type != C_VALIDATE); + + IncludedFile *file; + while (!TAILQ_EMPTY(&included_files)) { + file = TAILQ_FIRST(&included_files); + FREE(file->path); + TAILQ_REMOVE(&included_files, file, files); + FREE(file); + } + + char resolved_path[PATH_MAX] = {'\0'}; + if (realpath(current_configpath, resolved_path) == NULL) { + die("realpath(%s): %s", current_configpath, strerror(errno)); + } + + file = scalloc(1, sizeof(IncludedFile)); + file->path = sstrdup(resolved_path); + TAILQ_INSERT_TAIL(&included_files, file, files); + + LOG("Parsing configfile %s\n", resolved_path); + struct stack stack; + memset(&stack, '\0', sizeof(struct stack)); + struct parser_ctx ctx = { + .use_nagbar = (load_type != C_VALIDATE), + .assume_v4 = false, + .stack = &stack, + }; + SLIST_INIT(&(ctx.variables)); + FREE(current_config); + const int result = parse_file(&ctx, resolved_path); + free_variables(&ctx); + if (result == -1) { + die("Could not open configuration file: %s\n", strerror(errno)); + } + + extract_workspace_names_from_bindings(); + reorder_bindings(); if (config.font.type == FONT_TYPE_NONE && load_type != C_VALIDATE) { ELOG("You did not specify required configuration option \"font\"\n"); @@ -245,5 +283,5 @@ bool load_configuration(const char *override_configpath, config_load_t load_type xcb_flush(conn); } - return result; + return result == 0; } diff --git a/src/config_directives.c b/src/config_directives.c index c039e35f2..1e792fe07 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -9,6 +9,85 @@ */ #include "all.h" +#include + +/******************************************************************************* + * Include functions. + ******************************************************************************/ + +CFGFUN(include, const char *pattern) { + DLOG("include %s\n", pattern); + + wordexp_t p; + const int ret = wordexp(pattern, &p, 0); + if (ret != 0) { + ELOG("wordexp(%s): error %d\n", pattern, ret); + result->has_errors = true; + return; + } + char **w = p.we_wordv; + for (size_t i = 0; i < p.we_wordc; i++) { + char resolved_path[PATH_MAX] = {'\0'}; + if (realpath(w[i], resolved_path) == NULL) { + ELOG("realpath(%s): %s\n", w[i], strerror(errno)); + result->has_errors = true; + continue; + } + + bool skip = false; + IncludedFile *file; + TAILQ_FOREACH (file, &included_files, files) { + if (strcmp(file->path, resolved_path) == 0) { + skip = true; + break; + } + } + if (skip) { + LOG("Skipping file %s (already included)\n", resolved_path); + continue; + } + + LOG("Including config file %s\n", resolved_path); + + file = scalloc(1, sizeof(IncludedFile)); + file->path = sstrdup(resolved_path); + TAILQ_INSERT_TAIL(&included_files, file, files); + + struct stack stack; + memset(&stack, '\0', sizeof(struct stack)); + struct parser_ctx ctx = { + .use_nagbar = result->ctx->use_nagbar, + /* The include mechanism was added in v4, so we can skip the + * auto-detection and get rid of the risk of detecting the wrong + * version in potentially very short include fragments: */ + .assume_v4 = true, + .stack = &stack, + .variables = result->ctx->variables, + }; + switch (parse_file(&ctx, resolved_path)) { + case PARSE_FILE_SUCCESS: + break; + + case PARSE_FILE_FAILED: + ELOG("including config file %s: %s\n", resolved_path, strerror(errno)); + /* fallthrough */ + + case PARSE_FILE_CONFIG_ERRORS: + result->has_errors = true; + TAILQ_REMOVE(&included_files, file, files); + FREE(file->path); + FREE(file); + break; + + default: + /* missing case statement */ + assert(false); + break; + } + } + wordfree(&p); +} + /******************************************************************************* * Criteria functions. ******************************************************************************/ diff --git a/src/config_parser.c b/src/config_parser.c index f78e75f83..ff1132f68 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -35,18 +35,14 @@ #include #include #include +#include #include -// Macros to make the YAJL API a bit easier to use. -#define y(x, ...) yajl_gen_##x(command_output.json_gen, ##__VA_ARGS__) -#define ystr(str) yajl_gen_string(command_output.json_gen, (unsigned char *)str, strlen(str)) - xcb_xrm_database_t *database = NULL; #ifndef TEST_PARSER pid_t config_error_nagbar_pid = -1; -static struct context *context; #endif /******************************************************************************* @@ -76,46 +72,25 @@ typedef struct tokenptr { #include "GENERATED_config_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; - enum { - STACK_STR = 0, - STACK_LONG = 1, - } type; - union { - char *str; - long num; - } val; -}; - -/* 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, const char *str) { +static void push_string(struct stack *ctx, const char *identifier, const char *str) { for (int c = 0; c < 10; c++) { - if (stack[c].identifier != NULL && - strcmp(stack[c].identifier, identifier) != 0) + if (ctx->stack[c].identifier != NULL && + strcmp(ctx->stack[c].identifier, identifier) != 0) continue; - if (stack[c].identifier == NULL) { + if (ctx->stack[c].identifier == NULL) { /* Found a free slot, let’s store it here. */ - stack[c].identifier = identifier; - stack[c].val.str = sstrdup(str); - stack[c].type = STACK_STR; + ctx->stack[c].identifier = identifier; + ctx->stack[c].val.str = sstrdup(str); + ctx->stack[c].type = STACK_STR; } else { /* Append the value. */ - char *prev = stack[c].val.str; - sasprintf(&(stack[c].val.str), "%s,%s", prev, str); + char *prev = ctx->stack[c].val.str; + sasprintf(&(ctx->stack[c].val.str), "%s,%s", prev, str); free(prev); } return; @@ -130,14 +105,15 @@ static void push_string(const char *identifier, const char *str) { exit(EXIT_FAILURE); } -static void push_long(const char *identifier, long num) { +static void push_long(struct stack *ctx, const char *identifier, long num) { for (int c = 0; c < 10; c++) { - if (stack[c].identifier != NULL) + if (ctx->stack[c].identifier != NULL) { continue; + } /* Found a free slot, let’s store it here. */ - stack[c].identifier = identifier; - stack[c].val.num = num; - stack[c].type = STACK_LONG; + ctx->stack[c].identifier = identifier; + ctx->stack[c].val.num = num; + ctx->stack[c].type = STACK_LONG; return; } @@ -150,33 +126,33 @@ static void push_long(const char *identifier, long num) { exit(EXIT_FAILURE); } -static const char *get_string(const char *identifier) { +static const char *get_string(struct stack *ctx, const char *identifier) { for (int c = 0; c < 10; c++) { - if (stack[c].identifier == NULL) + if (ctx->stack[c].identifier == NULL) break; - if (strcmp(identifier, stack[c].identifier) == 0) - return stack[c].val.str; + if (strcmp(identifier, ctx->stack[c].identifier) == 0) + return ctx->stack[c].val.str; } return NULL; } -static long get_long(const char *identifier) { +static long get_long(struct stack *ctx, const char *identifier) { for (int c = 0; c < 10; c++) { - if (stack[c].identifier == NULL) + if (ctx->stack[c].identifier == NULL) break; - if (strcmp(identifier, stack[c].identifier) == 0) - return stack[c].val.num; + if (strcmp(identifier, ctx->stack[c].identifier) == 0) + return ctx->stack[c].val.num; } return 0; } -static void clear_stack(void) { +static void clear_stack(struct stack *ctx) { for (int c = 0; c < 10; c++) { - if (stack[c].type == STACK_STR) - free(stack[c].val.str); - stack[c].identifier = NULL; - stack[c].val.str = NULL; - stack[c].val.num = 0; + if (ctx->stack[c].type == STACK_STR) + free(ctx->stack[c].val.str); + ctx->stack[c].identifier = NULL; + ctx->stack[c].val.str = NULL; + ctx->stack[c].val.num = 0; } } @@ -184,50 +160,42 @@ static void clear_stack(void) { * The parser itself. ******************************************************************************/ -static cmdp_state state; -static Match current_match; -static struct ConfigResultIR subcommand_output; -static struct ConfigResultIR command_output; - -/* A list which contains the states that lead to the current state, e.g. - * INITIAL, WORKSPACE_LAYOUT. - * When jumping back to INITIAL, statelist_idx will simply be set to 1 - * (likewise for other states, e.g. MODE or BAR). - * This list is used to process the nearest error token. */ -static cmdp_state statelist[10] = {INITIAL}; -/* NB: statelist_idx points to where the next entry will be inserted */ -static int statelist_idx = 1; - #include "GENERATED_config_call.h" -static void next_state(const cmdp_token *token) { +static void next_state(const cmdp_token *token, struct parser_ctx *ctx) { cmdp_state _next_state = token->next_state; //printf("token = name %s identifier %s\n", token->name, token->identifier); //printf("next_state = %d\n", token->next_state); if (token->next_state == __CALL) { - subcommand_output.json_gen = command_output.json_gen; - GENERATED_call(token->extra.call_identifier, &subcommand_output); + struct ConfigResultIR subcommand_output = { + .ctx = ctx, + }; + GENERATED_call(&(ctx->current_match), ctx->stack, token->extra.call_identifier, &subcommand_output); + if (subcommand_output.has_errors) { + ctx->has_errors = true; + } _next_state = subcommand_output.next_state; - clear_stack(); + clear_stack(ctx->stack); } - state = _next_state; - if (state == INITIAL) { - clear_stack(); + ctx->state = _next_state; + if (ctx->state == INITIAL) { + clear_stack(ctx->stack); } /* See if we are jumping back to a state in which we were in previously * (statelist contains INITIAL) and just move statelist_idx accordingly. */ - for (int i = 0; i < statelist_idx; i++) { - if (statelist[i] != _next_state) + for (int i = 0; i < ctx->statelist_idx; i++) { + if ((cmdp_state)(ctx->statelist[i]) != _next_state) { continue; - statelist_idx = i + 1; + } + ctx->statelist_idx = i + 1; return; } /* Otherwise, the state is new and we add it to the list */ - statelist[statelist_idx++] = _next_state; + ctx->statelist[ctx->statelist_idx++] = _next_state; } /* @@ -257,7 +225,7 @@ static char *single_line(const char *start) { return result; } -struct ConfigResultIR *parse_config(const char *input, struct context *context) { +static void parse_config(struct parser_ctx *ctx, const char *input, struct context *context) { /* Dump the entire config file into the debug log. We cannot just use * DLOG("%s", input); because one log message must not exceed 4 KiB. */ const char *dumpwalk = input; @@ -273,13 +241,11 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) } linecnt++; } - state = INITIAL; - statelist_idx = 1; - - /* A YAJL JSON generator used for formatting replies. */ - command_output.json_gen = yajl_gen_alloc(NULL); - - y(array_open); + ctx->state = INITIAL; + for (int i = 0; i < 10; i++) { + ctx->statelist[i] = INITIAL; + } + ctx->statelist_idx = 1; const char *walk = input; const size_t len = strlen(input); @@ -290,7 +256,10 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) // TODO: make this testable #ifndef TEST_PARSER - cfg_criteria_init(¤t_match, &subcommand_output, INITIAL); + struct ConfigResultIR subcommand_output = { + .ctx = ctx, + }; + cfg_criteria_init(&(ctx->current_match), &subcommand_output, INITIAL); #endif /* The "<=" operator is intentional: We also handle the terminating 0-byte @@ -303,7 +272,7 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) //printf("remaining input: %s\n", walk); - cmdp_token_ptr *ptr = &(tokens[state]); + cmdp_token_ptr *ptr = &(tokens[ctx->state]); token_handled = false; for (c = 0; c < ptr->n; c++) { token = &(ptr->array[c]); @@ -311,10 +280,11 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) /* A literal. */ if (token->name[0] == '\'') { if (strncasecmp(walk, token->name + 1, strlen(token->name) - 1) == 0) { - if (token->identifier != NULL) - push_string(token->identifier, token->name + 1); + if (token->identifier != NULL) { + push_string(ctx->stack, token->identifier, token->name + 1); + } walk += strlen(token->name) - 1; - next_state(token); + next_state(token, ctx); token_handled = true; break; } @@ -334,12 +304,13 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) if (end == walk) continue; - if (token->identifier != NULL) - push_long(token->identifier, num); + if (token->identifier != NULL) { + push_long(ctx->stack, token->identifier, num); + } /* Set walk to the first non-number character */ walk = end; - next_state(token); + next_state(token, ctx); token_handled = true; break; } @@ -382,14 +353,15 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) inpos++; str[outpos] = beginning[inpos]; } - if (token->identifier) - push_string(token->identifier, str); + if (token->identifier) { + push_string(ctx->stack, token->identifier, str); + } free(str); /* If we are at the end of a quoted string, skip the ending * double quote. */ if (*walk == '"') walk++; - next_state(token); + next_state(token, ctx); token_handled = true; break; } @@ -398,7 +370,7 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) if (strcmp(token->name, "line") == 0) { while (*walk != '\0' && *walk != '\n' && *walk != '\r') walk++; - next_state(token); + next_state(token, ctx); token_handled = true; linecnt++; walk++; @@ -408,7 +380,7 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) if (strcmp(token->name, "end") == 0) { //printf("checking for end: *%s*\n", walk); if (*walk == '\0' || *walk == '\n' || *walk == '\r') { - next_state(token); + next_state(token, ctx); token_handled = true; /* To make sure we start with an appropriate matching * datastructure for commands which do *not* specify any @@ -416,7 +388,7 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) * every command. */ // TODO: make this testable #ifndef TEST_PARSER - cfg_criteria_init(¤t_match, &subcommand_output, INITIAL); + cfg_criteria_init(&(ctx->current_match), &subcommand_output, INITIAL); #endif linecnt++; walk++; @@ -515,41 +487,24 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) context->has_errors = true; - /* Format this error message as a JSON reply. */ - y(map_open); - ystr("success"); - y(bool, false); - /* We set parse_error to true to distinguish this from other - * errors. i3-nagbar is spawned upon keypresses only for parser - * errors. */ - ystr("parse_error"); - y(bool, true); - ystr("error"); - ystr(errormessage); - ystr("input"); - ystr(input); - ystr("errorposition"); - ystr(position); - y(map_close); - /* Skip the rest of this line, but continue parsing. */ while ((size_t)(walk - input) <= len && *walk != '\n') walk++; free(position); free(errormessage); - clear_stack(); + clear_stack(ctx->stack); /* To figure out in which state to go (e.g. MODE or INITIAL), * we find the nearest state which contains an token * and follow that one. */ bool error_token_found = false; - for (int i = statelist_idx - 1; (i >= 0) && !error_token_found; i--) { - cmdp_token_ptr *errptr = &(tokens[statelist[i]]); + for (int i = ctx->statelist_idx - 1; (i >= 0) && !error_token_found; i--) { + cmdp_token_ptr *errptr = &(tokens[ctx->statelist[i]]); for (int j = 0; j < errptr->n; j++) { if (strcmp(errptr->array[j].name, "error") != 0) continue; - next_state(&(errptr->array[j])); + next_state(&(errptr->array[j]), ctx); error_token_found = true; break; } @@ -558,10 +513,6 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context) assert(error_token_found); } } - - y(array_close); - - return &command_output; } /******************************************************************************* @@ -612,9 +563,17 @@ int main(int argc, char *argv[]) { fprintf(stderr, "Syntax: %s \n", argv[0]); return 1; } + struct stack stack; + memset(&stack, '\0', sizeof(struct stack)); + struct parser_ctx ctx = { + .use_nagbar = false, + .assume_v4 = false, + .stack = &stack, + }; + SLIST_INIT(&(ctx.variables)); struct context context; context.filename = ""; - parse_config(argv[1], &context); + parse_config(&ctx, argv[1], &context); } #else @@ -636,6 +595,7 @@ static int detect_version(char *buf) { /* check for some v4-only statements */ if (strncasecmp(line, "bindcode", strlen("bindcode")) == 0 || + strncasecmp(line, "include", strlen("include")) == 0 || strncasecmp(line, "force_focus_wrapping", strlen("force_focus_wrapping")) == 0 || strncasecmp(line, "# i3 config file (v4)", strlen("# i3 config file (v4)")) == 0 || strncasecmp(line, "workspace_layout", strlen("workspace_layout")) == 0) { @@ -877,36 +837,67 @@ static char *get_resource(char *name) { return resource; } +/* + * Releases the memory of all variables in ctx. + * + */ +void free_variables(struct parser_ctx *ctx) { + struct Variable *current; + while (!SLIST_EMPTY(&(ctx->variables))) { + current = SLIST_FIRST(&(ctx->variables)); + FREE(current->key); + FREE(current->value); + SLIST_REMOVE_HEAD(&(ctx->variables), variables); + FREE(current); + } +} + /* * Parses the given file by first replacing the variables, then calling * parse_config and possibly launching i3-nagbar. * */ -bool parse_file(const char *f, bool use_nagbar) { - struct variables_head variables = SLIST_HEAD_INITIALIZER(&variables); +parse_file_result_t parse_file(struct parser_ctx *ctx, const char *f) { int fd; struct stat stbuf; char *buf; FILE *fstr; char buffer[4096], key[512], value[4096], *continuation = NULL; - if ((fd = open(f, O_RDONLY)) == -1) - die("Could not open configuration file: %s\n", strerror(errno)); + char *old_dir = get_current_dir_name(); + char *dir = NULL; + /* dirname(3) might modify the buffer, so make a copy: */ + char *dirbuf = sstrdup(f); + if ((dir = dirname(dirbuf)) != NULL) { + LOG("Changing working directory to config file directory %s\n", dir); + if (chdir(dir) == -1) { + ELOG("chdir(%s) failed: %s\n", dir, strerror(errno)); + return PARSE_FILE_FAILED; + } + } + free(dirbuf); - if (fstat(fd, &stbuf) == -1) - die("Could not fstat file: %s\n", strerror(errno)); + if ((fd = open(f, O_RDONLY)) == -1) { + return PARSE_FILE_FAILED; + } + + if (fstat(fd, &stbuf) == -1) { + return PARSE_FILE_FAILED; + } buf = scalloc(stbuf.st_size + 1, 1); - if ((fstr = fdopen(fd, "r")) == NULL) - die("Could not fdopen: %s\n", strerror(errno)); + if ((fstr = fdopen(fd, "r")) == NULL) { + return PARSE_FILE_FAILED; + } - FREE(current_config); - current_config = scalloc(stbuf.st_size + 1, 1); - if ((ssize_t)fread(current_config, 1, stbuf.st_size, fstr) != stbuf.st_size) { - die("Could not fread: %s\n", strerror(errno)); + if (current_config == NULL) { + current_config = scalloc(stbuf.st_size + 1, 1); + if ((ssize_t)fread(current_config, 1, stbuf.st_size, fstr) != stbuf.st_size) { + return PARSE_FILE_FAILED; + } + rewind(fstr); } - rewind(fstr); bool invalid_sets = false; @@ -916,7 +907,7 @@ bool parse_file(const char *f, bool use_nagbar) { if (fgets(continuation, sizeof(buffer) - (continuation - buffer), fstr) == NULL) { if (feof(fstr)) break; - die("Could not read configuration file\n"); + return PARSE_FILE_FAILED; } if (buffer[strlen(buffer) - 1] != '\n' && !feof(fstr)) { ELOG("Your line continuation is too long, it exceeds %zd bytes\n", sizeof(buffer)); @@ -960,7 +951,7 @@ bool parse_file(const char *f, bool use_nagbar) { continue; } - upsert_variable(&variables, v_key, v_value); + upsert_variable(&(ctx->variables), v_key, v_value); continue; } else if (strcasecmp(key, "set_from_resource") == 0) { char res_name[512] = {'\0'}; @@ -993,7 +984,7 @@ bool parse_file(const char *f, bool use_nagbar) { res_value = sstrdup(fallback); } - upsert_variable(&variables, v_key, res_value); + upsert_variable(&(ctx->variables), v_key, res_value); FREE(res_value); continue; } @@ -1014,7 +1005,7 @@ bool parse_file(const char *f, bool use_nagbar) { * variables (otherwise we will count them twice, which is bad when * 'extra' is negative) */ char *bufcopy = sstrdup(buf); - SLIST_FOREACH (current, &variables, variables) { + SLIST_FOREACH (current, &(ctx->variables), variables) { int extra = (strlen(current->value) - strlen(current->key)); char *next; for (next = bufcopy; @@ -1034,12 +1025,12 @@ bool parse_file(const char *f, bool use_nagbar) { destwalk = new; while (walk < (buf + stbuf.st_size)) { /* Find the next variable */ - SLIST_FOREACH (current, &variables, variables) { + SLIST_FOREACH (current, &(ctx->variables), variables) { current->next_match = strcasestr(walk, current->key); } nearest = NULL; int distance = stbuf.st_size; - SLIST_FOREACH (current, &variables, variables) { + SLIST_FOREACH (current, &(ctx->variables), variables) { if (current->next_match == NULL) continue; if ((current->next_match - walk) < distance) { @@ -1064,7 +1055,10 @@ bool parse_file(const char *f, bool use_nagbar) { /* analyze the string to find out whether this is an old config file (3.x) * or a new config file (4.x). If it’s old, we run the converter script. */ - int version = detect_version(buf); + int version = 4; + if (!ctx->assume_v4) { + version = detect_version(buf); + } if (version == 3) { /* We need to convert this v3 configuration */ char *converted = migrate_config(new, strlen(new)); @@ -1090,17 +1084,16 @@ bool parse_file(const char *f, bool use_nagbar) { } } - context = scalloc(1, sizeof(struct context)); + struct context *context = scalloc(1, sizeof(struct context)); context->filename = f; + parse_config(ctx, new, context); + if (ctx->has_errors) { + context->has_errors = true; + } - struct ConfigResultIR *config_output = parse_config(new, context); - yajl_gen_free(config_output->json_gen); - - extract_workspace_names_from_bindings(); check_for_duplicate_bindings(context); - reorder_bindings(); - if (use_nagbar && (context->has_errors || context->has_warnings || invalid_sets)) { + if (ctx->use_nagbar && (context->has_errors || context->has_warnings || invalid_sets)) { ELOG("FYI: You are using i3 version %s\n", i3_version); if (version == 3) ELOG("Please convert your configfile first, then fix any remaining errors (see above).\n"); @@ -1108,22 +1101,22 @@ bool parse_file(const char *f, bool use_nagbar) { start_config_error_nagbar(f, context->has_errors || invalid_sets); } - bool has_errors = context->has_errors; + const bool has_errors = context->has_errors; FREE(context->line_copy); free(context); free(new); free(buf); - while (!SLIST_EMPTY(&variables)) { - current = SLIST_FIRST(&variables); - FREE(current->key); - FREE(current->value); - SLIST_REMOVE_HEAD(&variables, variables); - FREE(current); + if (chdir(old_dir) == -1) { + ELOG("chdir(%s) failed: %s\n", old_dir, strerror(errno)); + return PARSE_FILE_FAILED; } - - return !has_errors; + free(old_dir); + if (has_errors) { + return PARSE_FILE_CONFIG_ERRORS; + } + return PARSE_FILE_SUCCESS; } #endif diff --git a/src/display_version.c b/src/display_version.c index 32250c155..bced4c191 100644 --- a/src/display_version.c +++ b/src/display_version.c @@ -14,22 +14,34 @@ #include #include -static bool human_readable_key, loaded_config_file_name_key; -static char *human_readable_version, *loaded_config_file_name; +static bool human_readable_key; +static bool loaded_config_file_name_key; +static bool included_config_file_names; + +static char *human_readable_version; +static char *loaded_config_file_name; static int version_string(void *ctx, const unsigned char *val, size_t len) { - if (human_readable_key) + if (human_readable_key) { sasprintf(&human_readable_version, "%.*s", (int)len, val); - if (loaded_config_file_name_key) + } + if (loaded_config_file_name_key) { sasprintf(&loaded_config_file_name, "%.*s", (int)len, val); + } + if (included_config_file_names) { + IncludedFile *file = scalloc(1, sizeof(IncludedFile)); + sasprintf(&(file->path), "%.*s", (int)len, val); + TAILQ_INSERT_TAIL(&included_files, file, files); + } return 1; } static int version_map_key(void *ctx, const unsigned char *stringval, size_t stringlen) { - human_readable_key = (stringlen == strlen("human_readable") && - strncmp((const char *)stringval, "human_readable", strlen("human_readable")) == 0); - loaded_config_file_name_key = (stringlen == strlen("loaded_config_file_name") && - strncmp((const char *)stringval, "loaded_config_file_name", strlen("loaded_config_file_name")) == 0); +#define KEY_MATCHES(x) (stringlen == strlen(x) && strncmp((const char *)stringval, x, strlen(x)) == 0) + human_readable_key = KEY_MATCHES("human_readable"); + loaded_config_file_name_key = KEY_MATCHES("loaded_config_file_name"); + included_config_file_names = KEY_MATCHES("included_config_file_names"); +#undef KEY_MATCHES return 1; } @@ -38,6 +50,22 @@ static yajl_callbacks version_callbacks = { .yajl_map_key = version_map_key, }; +static void print_config_path(const char *path, const char *role) { + struct stat sb; + time_t now; + char mtime[64]; + + printf(" %s (%s)", path, role); + if (stat(path, &sb) == -1) { + printf("\n"); + ELOG("Cannot stat config file \"%s\"\n", path); + } else { + strftime(mtime, sizeof(mtime), "%c", localtime(&(sb.st_mtime))); + time(&now); + printf(" (last modified: %s, %.f seconds ago)\n", mtime, difftime(now, sb.st_mtime)); + } +} + /* * Connects to i3 to find out the currently running version. Useful since it * might be different from the version compiled into this binary (maybe the @@ -98,17 +126,11 @@ void display_running_version(void) { printf("Running i3 version: %s (pid %s)\n", human_readable_version, pid_from_atom); if (loaded_config_file_name) { - struct stat sb; - time_t now; - char mtime[64]; - printf("Loaded i3 config: %s", loaded_config_file_name); - if (stat(loaded_config_file_name, &sb) == -1) { - printf("\n"); - ELOG("Cannot stat config file \"%s\"\n", loaded_config_file_name); - } else { - strftime(mtime, sizeof(mtime), "%c", localtime(&(sb.st_mtime))); - time(&now); - printf(" (Last modified: %s, %.f seconds ago)\n", mtime, difftime(now, sb.st_mtime)); + printf("Loaded i3 config:\n"); + print_config_path(loaded_config_file_name, "main"); + IncludedFile *file; + TAILQ_FOREACH (file, &included_files, files) { + print_config_path(file->path, "included"); } } diff --git a/src/ipc.c b/src/ipc.c index a7ea84943..b21d79a10 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -1040,6 +1040,17 @@ IPC_HANDLER(get_version) { ystr("loaded_config_file_name"); ystr(current_configpath); + ystr("included_config_file_names"); + y(array_open); + IncludedFile *file; + TAILQ_FOREACH (file, &included_files, files) { + if (file == TAILQ_FIRST(&included_files)) { + /* Skip the first file, which is current_configpath. */ + continue; + } + ystr(file->path); + } + y(array_close); y(map_close); const unsigned char *payload; diff --git a/testcases/t/201-config-parser.t b/testcases/t/201-config-parser.t index e2d885bad..a87a7b891 100644 --- a/testcases/t/201-config-parser.t +++ b/testcases/t/201-config-parser.t @@ -506,6 +506,7 @@ EOT my $expected_all_tokens = "ERROR: CONFIG: Expected one of these tokens: , '#', '" . join("', '", 'set ', 'set ', qw( set_from_resource + include bindsym bindcode bind diff --git a/testcases/t/313-include.t b/testcases/t/313-include.t new file mode 100644 index 000000000..3a511e516 --- /dev/null +++ b/testcases/t/313-include.t @@ -0,0 +1,338 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • https://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • https://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • https://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Verifies the include directive. + +use File::Temp qw(tempfile tempdir); +use File::Basename qw(basename); +use i3test i3_autostart => 0; + +# starts i3 with the given config, opens a window, returns its border style +sub launch_get_border { + my ($config) = @_; + + my $pid = launch_with_config($config); + + my $i3 = i3(get_socket_path(0)); + my $tmp = fresh_workspace; + + my $window = open_window(name => 'special title'); + + my @content = @{get_ws_content($tmp)}; + cmp_ok(@content, '==', 1, 'one node on this workspace now'); + my $border = $content[0]->{border}; + + exit_gracefully($pid); + + return $border; +} + +##################################################################### +# test thet windows get the default border +##################################################################### + +my $config = < 1); +print $fh <<'EOT'; +set $vartest special title +for_window [title="$vartest"] border none +EOT +$fh->flush; + +$config = < 1); +print $indirectfh <flush; + +$config = < 1); +print $indirectfh2 <flush; + +$config = < 1); +$permissiondeniedfh->flush; +my $mode = 0055; +chmod($mode, $permissiondenied); + +$config = < 1); +unlink($dangling); +symlink("/dangling", $dangling); + +$config = < 1); +print $varfh <<'EOT'; +for_window [title="$vartest"] border none + +EOT +$varfh->flush; + +$config = < 1); +print $varfh <<'EOT'; +set $vartest special title +EOT +$varfh->flush; + +$config = < 1); +$wsfh->flush; + +$config = <get_workspaces->recv->[0]->{name}; + + exit_gracefully($pid); + + return $name; +} + +is(launch_get_workspace_name($config), '1: eggs', 'workspace name'); + +################################################################################ +# loop prevention +################################################################################ + +my ($loopfh1, $loopname1) = tempfile(UNLINK => 1); +my ($loopfh2, $loopname2) = tempfile(UNLINK => 1); + +print $loopfh1 <flush; + +print $loopfh2 <flush; + +$config = <get_version()->recv; +my $included = $version->{included_config_file_names}; + +is_deeply($included, [ $indirectfilename2, $filename ], 'included config file names correct'); + +exit_gracefully($pid); + +################################################################################ +# Verify the GET_CONFIG IPC reply returns the top-level config +################################################################################ + +my $tmpdir = tempdir(CLEANUP => 1); +my $socketpath = $tmpdir . "/config.sock"; +ok(! -e $socketpath, "$socketpath does not exist yet"); + +$config = < 1, dont_create_temp_dir => 1); + +my $i3 = i3(get_socket_path(0)); +my $config_reply = $i3->get_config()->recv; + +is($config_reply->{config}, $config, 'GET_CONFIG returns the top-level config file'); + +exit_gracefully($pid); + + +done_testing;