Skip to content

Commit 2604e91

Browse files
committed
config: Expand leading ~ in paths
cm_dir and CM_CONFIG do not expand a leading ~, which forces absolute $HOME paths in shell configuration and scripts. Expand leading ~ to $HOME for path settings and document the behaviour, with integration coverage for CM_DIR and CM_CONFIG.
1 parent 374b564 commit 2604e91

3 files changed

Lines changed: 75 additions & 19 deletions

File tree

man/clipmenu.conf.5

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,14 @@ browsers). Set to 0 to disable partial merging entirely. Default: 2.
6464
Overrides the default directory for the clip store. This is by default at a
6565
subdirectory inside XDG_RUNTIME_DIR, TMPDIR, or if both are unset, inside /tmp.
6666
The path must be absolute.
67+
A leading ~ expands to $HOME.
6768
.SH FILE LOCATION
6869
Typically, clipmenu.conf is located at
6970
.BR ~/.config/clipmenu/clipmenu.conf
7071
.
7172
The file location can be overridden with $CM_CONFIG, which must be an absolute
7273
path.
74+
A leading ~ expands to $HOME.
7375
.SH SEE ALSO
7476
.BR clipctl (1),
7577
.BR clipdel (1),

src/config.c

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,40 @@ static bool is_absolute_path(const char *path) {
5353
return path && path[0] == '/';
5454
}
5555

56+
static int resolve_path(const char *input, void *output) {
57+
char **out_path = output;
58+
if (!input) {
59+
return -EINVAL;
60+
}
61+
62+
if (input[0] == '~') {
63+
if (input[1] != '\0' && input[1] != '/') {
64+
return -EINVAL;
65+
}
66+
const char *home = getenv("HOME");
67+
if (!home) {
68+
return -EINVAL;
69+
}
70+
size_t len = strlen(home) + strlen(input);
71+
char *expanded = malloc(len);
72+
expect(expanded);
73+
snprintf_safe(expanded, len, "%s%s", home, input + 1);
74+
*out_path = expanded;
75+
} else {
76+
char *dup = strdup(input);
77+
expect(dup);
78+
*out_path = dup;
79+
}
80+
81+
if (!is_absolute_path(*out_path)) {
82+
free(*out_path);
83+
*out_path = NULL;
84+
return -EINVAL;
85+
}
86+
87+
return 0;
88+
}
89+
5690
/**
5791
* This whole section consists of conversion functions to go from a string in
5892
* the config file to the type we expect for `struct Config`.
@@ -105,13 +139,7 @@ static int convert_cm_dir(const char *str, void *output) {
105139
if (!str) {
106140
str = get_runtime_directory();
107141
}
108-
if (!is_absolute_path(str)) {
109-
return -EINVAL;
110-
}
111-
char *rtd = strdup(str);
112-
expect(rtd);
113-
*(char **)output = rtd;
114-
return 0;
142+
return resolve_path(str, output);
115143
}
116144

117145
static int _nonnull_ convert_launcher(const char *str, void *output) {
@@ -175,29 +203,39 @@ static int get_config_file(char *config_path, size_t config_path_len) {
175203
const char *cm_config = getenv("CM_CONFIG");
176204
const char *xdg_config_home = getenv("XDG_CONFIG_HOME");
177205
const char *home = getenv("HOME");
206+
_drop_(free) char *resolved = NULL;
178207

179208
if (cm_config) {
180-
if (!is_absolute_path(cm_config)) {
181-
fprintf(stderr, "Error parsing config file path\n");
182-
return -EINVAL;
209+
int ret = resolve_path(cm_config, &resolved);
210+
if (ret != 0) {
211+
fprintf(stderr,
212+
"Invalid config path from $CM_CONFIG: expected absolute "
213+
"path or leading ~\n");
214+
return ret;
183215
}
184-
snprintf_safe(config_path, config_path_len, "%s", cm_config);
216+
snprintf_safe(config_path, config_path_len, "%s", resolved);
185217
} else if (xdg_config_home) {
186-
if (!is_absolute_path(xdg_config_home)) {
187-
fprintf(stderr, "Error parsing config file path\n");
188-
return -EINVAL;
218+
int ret = resolve_path(xdg_config_home, &resolved);
219+
if (ret != 0) {
220+
fprintf(stderr,
221+
"Invalid config path from $XDG_CONFIG_HOME: expected "
222+
"absolute path or leading ~\n");
223+
return ret;
189224
}
190225
snprintf_safe(config_path, config_path_len, "%s/clipmenu/clipmenu.conf",
191-
xdg_config_home);
226+
resolved);
192227
} else {
193228
die_on(!home,
194229
"None of $CM_CONFIG, $XDG_CONFIG_HOME, or $HOME is set\n");
195-
if (!is_absolute_path(home)) {
196-
fprintf(stderr, "Error parsing config file path\n");
197-
return -EINVAL;
230+
int ret = resolve_path(home, &resolved);
231+
if (ret != 0) {
232+
fprintf(stderr,
233+
"Invalid config path from $HOME: expected absolute path or "
234+
"leading ~\n");
235+
return ret;
198236
}
199237
snprintf_safe(config_path, config_path_len,
200-
"%s/.config/clipmenu/clipmenu.conf", home);
238+
"%s/.config/clipmenu/clipmenu.conf", resolved);
201239
}
202240

203241
return 0;

tests/x_integration_tests

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ EOF
5757
chmod a+x "$launcher"
5858
export CM_LAUNCHER="$launcher"
5959

60+
tilde_home=$(mktemp -d)
61+
tilde_dir="$tilde_home/clipmenu_tilde"
62+
mkdir -p "$tilde_dir"
63+
uid=$(id -u)
64+
HOME="$tilde_home" CM_DIR="~/clipmenu_tilde" CM_CONFIG="$(mktemp)" \
65+
clipmenu || true
66+
[[ -d "$tilde_dir/clipmenu.7.$uid" ]]
67+
rm -rf "$tilde_home"
68+
69+
tilde_conf_home=$(mktemp -d)
70+
printf 'max_clips nope\n' > "$tilde_conf_home/bad.conf"
71+
err=$(HOME="$tilde_conf_home" CM_CONFIG="~/bad.conf" CM_DIR="$(mktemp -d)" \
72+
clipmenu 2>&1 || true)
73+
[[ "$err" == *"for max_clips"* ]]
74+
rm -rf "$tilde_conf_home"
75+
6076
kill_background_jobs() {
6177
local -a bg
6278
readarray -t bg < <(jobs -p)

0 commit comments

Comments
 (0)