summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Stapelberg <stapelberg@users.noreply.github.com>2021-06-02 21:01:43 +0200
committerGitHub <noreply@github.com>2021-06-02 21:01:43 +0200
commiteaa5e636f96815ddf7f236d35aa9b8686388aa76 (patch)
treeeec740574101a42b4733c6c53483338efb130a0f
parent4c93f61353ffa905a4f15cb98524280cd92c53cb (diff)
downloadi3-eaa5e636f96815ddf7f236d35aa9b8686388aa76.tar.gz
i3-eaa5e636f96815ddf7f236d35aa9b8686388aa76.zip
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
-rw-r--r--RELEASE-NOTES-next1
-rw-r--r--docs/userguide84
-rwxr-xr-xgenerate-command-parser.pl10
-rw-r--r--include/config_directives.h1
-rw-r--r--include/config_parser.h66
-rw-r--r--include/configuration.h12
-rw-r--r--parser-specs/config.spec8
-rw-r--r--src/bindings.c60
-rw-r--r--src/commands_parser.c99
-rw-r--r--src/config.c44
-rw-r--r--src/config_directives.c79
-rw-r--r--src/config_parser.c319
-rw-r--r--src/display_version.c60
-rw-r--r--src/ipc.c11
-rw-r--r--testcases/t/201-config-parser.t1
-rw-r--r--testcases/t/313-include.t338
16 files changed, 919 insertions, 274 deletions
diff --git a/RELEASE-NOTES-next b/RELEASE-NOTES-next
index 53fe2329..e6a892d6 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 cd48e7ef..944f7b39 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 <pattern>
+-----------------
+
+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 77502db7..cef4eda5 100755
--- a/generate-command-parser.pl
+++ b/generate-command-parser.pl
@@ -133,7 +133,7 @@ close($enumfh);
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 @@ for my $state (@keys) {
# 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 <number>s,
# literal numbers or state IDs and %s for NULL, <string>s and literal
@@ -175,9 +175,9 @@ for my $state (@keys) {
say $callfh '#ifndef TEST_PARSER';
my $real_cmd = $cmd;
if ($real_cmd =~ /\(\)/) {
- $real_cmd =~ s/\(/(&current_match, result/;
+ $real_cmd =~ s/\(/(current_match, result/;
} else {
- $real_cmd =~ s/\(/(&current_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 06fbd3b0..e0057097 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 009538f2..7cdb5a19 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,16 +67,16 @@ 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.
@@ -40,6 +84,18 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context);
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 11cdde0d..1e41893a 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
@@ -70,6 +72,16 @@ struct Variable {
};
/**
+ * 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
* currently active set of bindings by using the "mode <name>" command.
diff --git a/parser-specs/config.spec b/parser-specs/config.spec
index 7d7b9989..1b6d7340 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 <pattern>
+state INCLUDE:
+ pattern = string
+ -> call cfg_include($pattern)
+
# floating_minimum_size <width> x <height>
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 d6255e73..0aa960d3 100644
--- a/src/bindings.c
+++ b/src/bindings.c
@@ -718,6 +718,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
* stderr and the has_errors variable is set to true, which will start
@@ -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 6c791415..fd02293d 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(&current_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 ecc154c6..7f7e0257 100644
--- a/src/config.c
+++ b/src/config.c
@@ -10,6 +10,9 @@
*/
#include "all.h"
+#include <libgen.h>
+#include <unistd.h>
+
#include <xkbcommon/xkbcommon.h>
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 c039e35f..1e792fe0 100644
--- a/src/config_directives.c
+++ b/src/config_directives.c
@@ -9,6 +9,85 @@
*/
#include "all.h"
+#include <wordexp.h>
+
+/*******************************************************************************
+ * 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 f78e75f8..ff1132f6 100644
--- a/src/config_parser.c
+++ b/src/config_parser.c
@@ -35,18 +35,14 @@
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
+#include <libgen.h>
#include <xcb/xcb_xrm.h>
-// 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(&current_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(&current_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 <error> 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 <command>\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 = "<stdin>";
- 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) {
@@ -878,35 +838,66 @@ static char *get_resource(char *name) {
}
/*
+ * 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 32250c15..bced4c19 100644
--- a/src/display_version.c
+++ b/src/display_version.c
@@ -14,22 +14,34 @@
#include <time.h>
#include <unistd.h>
-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 a7ea8494..b21d79a1 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 e2d885ba..a87a7b89 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: <end>, '#', '" . 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 00000000..3a511e51
--- /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 = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+EOT
+
+is(launch_get_border($config), 'normal', 'normal border');
+
+#####################################################################
+# now use a variable and for_window
+#####################################################################
+
+my ($fh, $filename) = tempfile(UNLINK => 1);
+print $fh <<'EOT';
+set $vartest special title
+for_window [title="$vartest"] border none
+EOT
+$fh->flush;
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+include $filename
+EOT
+
+is(launch_get_border($config), 'none', 'no border');
+
+################################################################################
+# nested includes
+################################################################################
+
+my ($indirectfh, $indirectfilename) = tempfile(UNLINK => 1);
+print $indirectfh <<EOT;
+include $filename
+EOT
+$indirectfh->flush;
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+include $indirectfilename
+EOT
+
+is(launch_get_border($config), 'none', 'no border');
+
+################################################################################
+# nested includes with relative paths
+################################################################################
+
+my $relative = basename($filename);
+my ($indirectfh2, $indirectfilename2) = tempfile(UNLINK => 1);
+print $indirectfh2 <<EOT;
+include $relative
+EOT
+$indirectfh2->flush;
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+include $indirectfilename2
+EOT
+
+is(launch_get_border($config), 'none', 'no border');
+
+################################################################################
+# command substitution
+################################################################################
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+include `echo $filename`
+EOT
+
+is(launch_get_border($config), 'none', 'no border');
+
+################################################################################
+# failing command substitution
+################################################################################
+
+$config = <<'EOT';
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+include i3-`false`.conf
+
+set $vartest special title
+for_window [title="$vartest"] border none
+EOT
+
+is(launch_get_border($config), 'none', 'no border');
+
+################################################################################
+# permission denied
+################################################################################
+
+my ($permissiondeniedfh, $permissiondenied) = tempfile(UNLINK => 1);
+$permissiondeniedfh->flush;
+my $mode = 0055;
+chmod($mode, $permissiondenied);
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+include $permissiondenied
+include $filename
+EOT
+
+is(launch_get_border($config), 'none', 'no border');
+
+################################################################################
+# dangling symlink
+################################################################################
+
+my ($danglingfh, $dangling) = tempfile(UNLINK => 1);
+unlink($dangling);
+symlink("/dangling", $dangling);
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+include $dangling
+set \$vartest special title
+for_window [title="\$vartest"] border none
+EOT
+
+is(launch_get_border($config), 'none', 'no border');
+
+################################################################################
+# variables defined in the main file and used in the included file
+################################################################################
+
+my ($varfh, $var) = tempfile(UNLINK => 1);
+print $varfh <<'EOT';
+for_window [title="$vartest"] border none
+
+EOT
+$varfh->flush;
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+set \$vartest special title
+include $var
+EOT
+
+is(launch_get_border($config), 'none', 'no border');
+
+SKIP: {
+ skip "not implemented";
+
+################################################################################
+# variables defined in the included file and used in the main file
+################################################################################
+
+($varfh, $var) = tempfile(UNLINK => 1);
+print $varfh <<'EOT';
+set $vartest special title
+EOT
+$varfh->flush;
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+include $var
+for_window [title="\$vartest"] border none
+EOT
+
+is(launch_get_border($config), 'none', 'no border');
+}
+
+################################################################################
+# workspace names are loaded in the correct order (before reorder_bindings)
+################################################################################
+
+# The included config can be empty, the issue lies with calling parse_file
+# multiple times.
+my ($wsfh, $ws) = tempfile(UNLINK => 1);
+$wsfh->flush;
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+bindsym 1 workspace 1: eggs
+bindsym Mod4+Shift+1 workspace 11: tomatoes
+
+include $var
+EOT
+
+# starts i3 with the given config, opens a window, returns its border style
+sub launch_get_workspace_name {
+ my ($config) = @_;
+
+ my $pid = launch_with_config($config);
+
+ my $i3 = i3(get_socket_path(0));
+ my $name = $i3->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 <<EOT;
+include $loopname2
+EOT
+$loopfh1->flush;
+
+print $loopfh2 <<EOT;
+include $loopname1
+EOT
+$loopfh2->flush;
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+# loop
+include $loopname1
+
+set \$vartest special title
+for_window [title="\$vartest"] border none
+EOT
+
+is(launch_get_border($config), 'none', 'no border');
+
+################################################################################
+# Verify the GET_VERSION IPC reply contains all included files
+################################################################################
+
+$config = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+include $indirectfilename2
+EOT
+
+my $pid = launch_with_config($config);
+
+my $i3 = i3(get_socket_path(0));
+my $version = $i3->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 = <<EOT;
+# i3 config file (v4)
+font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
+
+include $indirectfilename2
+
+ipc-socket $socketpath
+EOT
+
+my $pid = launch_with_config($config, dont_add_socket_path => 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;