xaizek / vifm (License: GPLv2+) (since 2018-12-07)
Vifm is a file manager with curses interface, which provides Vi[m]-like environment for managing objects within file systems, extended with some useful ideas from mutt.
Commit a17658e7c7d813035bf1a878f5b8f4d536940b18

Add -deep parameter to :copy command
It makes copying resolve symbolic links.

Thanks to Jose Riha (a.k.a. jose1711) and an anonymous at Vifm Q2A site.

See
https://q2a.vifm.info/1237/how-can-i-use-yy-and-p-but-with-dereferenced-links
Author: xaizek
Author date (UTC): 2026-02-16 23:16
Committer name: xaizek
Committer date (UTC): 2026-02-19 09:45
Parent(s): 48759ef05d1f28ce9c145ad36c10ee7ae1e5a81c
Signing key: 99DC5E4DB05F6BE2
Tree: 0d90b73d3d6fa547739d7f82768cc606d2c7b606
File Lines added Lines deleted
ChangeLog 4 0
data/man/vifm.1 21 4
data/vim/doc/app/vifm-app.txt 18 4
src/cmd_completion.c 29 4
src/cmd_handlers.c 15 0
src/tags.c 1 0
tests/commands/completion.c 5 1
tests/commands/cpmv.c 30 0
File ChangeLog changed (mode: 100644) (index e2deeab6f..7caecc4dd)
34 34 Added example of handling .deb-files to sample vifmrc. Patch by Kirill Added example of handling .deb-files to sample vifmrc. Patch by Kirill
35 35 Rekhov. Rekhov.
36 36
37 Added -deep parameter to :copy command to resolve symbolic links while
38 copying. Thanks to Jose Riha (a.k.a. jose1711) and an anonymous at Vifm
39 Q2A site.
40
37 41 Updated utf8proc to v2.11.2. Updated utf8proc to v2.11.2.
38 42
39 43 Made documentation on which :commands can have comments a bit more Made documentation on which :commands can have comments a bit more
File data/man/vifm.1 changed (mode: 100644) (index 35f366e2f..88abd8222)
1 .TH VIFM 1 "23 January 2026" "vifm 0.15"
1 .TH VIFM 1 "17 February 2026" "vifm 0.15"
2 2 .\" --------------------------------------------------------------------------- .\" ---------------------------------------------------------------------------
3 3 .SH NAME .SH NAME
4 4 .\" --------------------------------------------------------------------------- .\" ---------------------------------------------------------------------------
 
... ... directory of other view). "!" forces overwrite.
1939 1939 copy files to directory of other view giving each next file a corresponding name copy files to directory of other view giving each next file a corresponding name
1940 1940 from the argument list. "!" forces overwrite from the argument list. "!" forces overwrite
1941 1941 .TP .TP
1942 .BI ":[range]co[py][!?] -deep ...[ &]"
1943 see "\-deep parameter" below.
1944 .TP
1942 1945 .BI ":[range]co[py][!?] -skip ...[ &]" .BI ":[range]co[py][!?] -skip ...[ &]"
1943 1946 see "\-skip parameter" below. see "\-skip parameter" below.
1944 1947 .LP .LP
 
... ... Some of the command-line commands accept parameters in the form of
3620 3623 parameters and positional arguments. Items from the two groups cannot be parameters and positional arguments. Items from the two groups cannot be
3621 3624 interleaved and parameters always come first. List of parameters is interleaved and parameters always come first. List of parameters is
3622 3625 terminated implicitly by the first argument that doesn't start with a terminated implicitly by the first argument that doesn't start with a
3623 dash ("-") or explicitly via "--" separator (needs to be a separate
3624 argument), which is just discarded. These strict rules allow arbitrary
3625 positional arguments, such as file names that start with a dash.
3626 dash ("-") or explicitly via "--" separator (must be a separate argument),
3627 which is just discarded. These strict rules allow arbitrary positional
3628 arguments, such as file names that start with a dash.
3629
3630 .TP
3631 .BI "\-deep parameter"
3632 This parameter makes :copy resolve symbolic links in the source.
3633
3634 Note that when 'syscalls' is on, the behaviour differs from that of
3635 `cp \-L` (same as `cp \-\-dereference`) in cases when a symbolic link's target
3636 can't be meaningfully copied:
3637 - when link's target doesn't exist (broken link)
3638 - when link to a directory creates a cycle
3639 .br
3640 In both cases `cp` prints an error and doesn't create anything at the
3641 destination but vifm copies symbolic links as is to preserve file structure.
3642 This implementation detail may change in the future or become configurable.
3626 3643
3627 3644 .TP .TP
3628 3645 .BI "\-skip parameter" .BI "\-skip parameter"
File data/vim/doc/app/vifm-app.txt changed (mode: 100644) (index b625b5d9f..32dafcfd6)
1 *vifm-app.txt* For Vifm version 0.15 Last change: 2026 January 23
1 *vifm-app.txt* For Vifm version 0.15 Last change: 2026 February 17
2 2
3 3 Email for bugs and suggestions: <xaizek@posteo.net> Email for bugs and suggestions: <xaizek@posteo.net>
4 4
 
... ... The builtin commands are:
1681 1681 :[range]co[py][!] name1 name2...[ &] :[range]co[py][!] name1 name2...[ &]
1682 1682 copy files to directory of other view giving each next file a copy files to directory of other view giving each next file a
1683 1683 corresponding name from the argument list. "!" forces overwrite. corresponding name from the argument list. "!" forces overwrite.
1684 :[range]co[py][!?] -deep ...
1685 see |vifm-deep-param|.
1684 1686 :[range]co[py][!?] -skip ... :[range]co[py][!?] -skip ...
1685 1687 see |vifm-skip-param|. see |vifm-skip-param|.
1686 1688
 
... ... Some of the command-line commands accept parameters in the form of
2975 2977 parameters and positional arguments. Items from the two groups cannot be parameters and positional arguments. Items from the two groups cannot be
2976 2978 interleaved and parameters always come first. List of parameters is interleaved and parameters always come first. List of parameters is
2977 2979 terminated implicitly by the first argument that doesn't start with a terminated implicitly by the first argument that doesn't start with a
2978 dash ("-") or explicitly via "--" separator (needs to be a separate
2979 argument), which is just discarded. These strict rules allow arbitrary
2980 positional arguments, such as file names that start with a dash.
2980 dash ("-") or explicitly via "--" separator (must be a separate argument),
2981 which is just discarded. These strict rules allow arbitrary positional
2982 arguments, such as file names that start with a dash.
2983
2984 -deep *vifm-deep-param*
2985 This parameter makes |vifm-:copy| resolve symbolic links in the source.
2986
2987 Note that when |vifm-'syscalls'| is on, the behaviour differs from that of
2988 `cp -L` (same as `cp --dereference`) in cases when a symbolic link's target
2989 can't be meaningfully copied:
2990 - when link's target doesn't exist (broken link)
2991 - when link to a directory creates a cycle
2992 In both cases `cp` prints an error and doesn't create anything at the
2993 destination but vifm copies symbolic links as is to preserve file structure.
2994 This implementation detail may change in the future or become configurable.
2981 2995
2982 2996 -skip *vifm-skip-param* -skip *vifm-skip-param*
2983 2997 This parameter makes |vifm-:copy|, |vifm-:move|, |vifm-:alink| and This parameter makes |vifm-:copy|, |vifm-:move|, |vifm-:alink| and
File src/cmd_completion.c changed (mode: 100644) (index e0bbe2c50..06b533bb8)
... ... static void complete_wingo(const char str[]);
119 119 static int wingo_sorter(const char a[], const char b[]); static int wingo_sorter(const char a[], const char b[]);
120 120 static const char * skip_number(const char str[]); static const char * skip_number(const char str[]);
121 121 static void complete_winrun(const char str[]); static void complete_winrun(const char str[]);
122 static void complete_option(int cmd_id, const char str[]);
122 123 static void complete_view(const char str[]); static void complete_view(const char str[]);
123 124 static void complete_from_string_list(const char str[], const char *items[][2], static void complete_from_string_list(const char str[], const char *items[][2],
124 125 size_t item_count, int ignore_case); size_t item_count, int ignore_case);
 
... ... non_path_completion(completion_data_t *data)
318 319 else if(is_option(data->cmd_info) && (id == COM_COPY || id == COM_MOVE || else if(is_option(data->cmd_info) && (id == COM_COPY || id == COM_MOVE ||
319 320 id == COM_ALINK || id == COM_RLINK)) id == COM_ALINK || id == COM_RLINK))
320 321 { {
321 static const char *lines[][2] = {
322 { "-skip", "skip files with conflicting names" }
323 };
324 complete_from_string_list(arg, lines, ARRAY_LEN(lines), /*ignore_case=*/0);
322 complete_option(id, arg);
325 323 } }
326 324 else if(id == COM_VIEW) else if(id == COM_VIEW)
327 325 { {
 
... ... complete_winrun(const char str[])
1132 1130 complete_from_string_list(str, win_marks, ARRAY_LEN(win_marks), 0); complete_from_string_list(str, win_marks, ARRAY_LEN(win_marks), 0);
1133 1131 } }
1134 1132
1133 /* Completes an option of :copy/:move/:alink/:rlink. */
1134 static void
1135 complete_option(int cmd_id, const char str[])
1136 {
1137 static const char *lines[][2] = {
1138 { "-deep", "resolve symbolic links while copying" },
1139 { "-skip", "skip files with conflicting names" },
1140 };
1141
1142 const char *(*opts)[2];
1143 int opt_count;
1144 switch(cmd_id)
1145 {
1146 case COM_COPY:
1147 opts = lines;
1148 opt_count = 2;
1149 break;
1150
1151 default:
1152 opts = &lines[1];
1153 opt_count = 1;
1154 break;
1155 }
1156
1157 complete_from_string_list(str, opts, opt_count, /*ignore_case=*/0);
1158 }
1159
1135 1160 /* Completes the first argument of :view. */ /* Completes the first argument of :view. */
1136 1161 static void static void
1137 1162 complete_view(const char str[]) complete_view(const char str[])
File src/cmd_handlers.c changed (mode: 100644) (index 7767adc49..57ce4b96c)
... ... cpmv_cmd(const cmd_info_t *cmd_info, int move)
3914 3914 { {
3915 3915 return CMDS_ERR_CUSTOM; return CMDS_ERR_CUSTOM;
3916 3916 } }
3917 if(move && (flags & CMLF_DEEP))
3918 {
3919 ui_sb_err("-deep doesn't apply to moving");
3920 return CMDS_ERR_CUSTOM;
3921 }
3917 3922
3918 3923 flags |= (cmd_info->emark ? CMLF_FORCE : CMLF_NONE); flags |= (cmd_info->emark ? CMLF_FORCE : CMLF_NONE);
3919 3924
 
... ... link_cmd(const cmd_info_t *cmd_info, int absolute)
4365 4370 return CMDS_ERR_CUSTOM; return CMDS_ERR_CUSTOM;
4366 4371 } }
4367 4372
4373 if(flags & CMLF_DEEP)
4374 {
4375 ui_sb_err("-deep doesn't apply to making links");
4376 return CMDS_ERR_CUSTOM;
4377 }
4378
4368 4379 flags |= (cmd_info->emark ? CMLF_FORCE : CMLF_NONE); flags |= (cmd_info->emark ? CMLF_FORCE : CMLF_NONE);
4369 4380
4370 4381 flist_set_marking(curr_view, 0); flist_set_marking(curr_view, 0);
 
... ... parse_cpmv_flags(int *argc, char ***argv)
4409 4420 { {
4410 4421 flags |= CMLF_SKIP; flags |= CMLF_SKIP;
4411 4422 } }
4423 else if(strcmp(argv[0][i], "-deep") == 0)
4424 {
4425 flags |= CMLF_DEEP;
4426 }
4412 4427 else else
4413 4428 { {
4414 4429 ui_sb_errf("Unrecognized :command option: %s", argv[0][i]); ui_sb_errf("Unrecognized :command option: %s", argv[0][i]);
File src/tags.c changed (mode: 100644) (index 91a847c4f..cb159341a)
... ... const char *tags[] = {
629 629 "vifm-cw", "vifm-cw",
630 630 "vifm-d", "vifm-d",
631 631 "vifm-dd", "vifm-dd",
632 "vifm-deep-param",
632 633 "vifm-do", "vifm-do",
633 634 "vifm-dp", "vifm-dp",
634 635 "vifm-e", "vifm-e",
File tests/commands/completion.c changed (mode: 100644) (index 39d7f04c6..9e9e55b2a)
... ... TEST(highlight_columns_are_completed)
630 630
631 631 TEST(command_options_are_completed) TEST(command_options_are_completed)
632 632 { {
633 ASSERT_COMPLETION(L"copy -", L"copy -skip");
633 ASSERT_COMPLETION(L"move -", L"move -skip");
634 ASSERT_NEXT_MATCH("-skip");
635
636 ASSERT_COMPLETION(L"copy -", L"copy -deep");
637 ASSERT_NEXT_MATCH("-skip");
634 638
635 639 other_view = &rwin; other_view = &rwin;
636 640 #ifndef _WIN32 #ifndef _WIN32
File tests/commands/cpmv.c changed (mode: 100644) (index c1f2e2c13..75ce18831)
... ... TEST(cpmv_does_not_crash_on_wrong_list_access)
147 147 restore_cwd(saved_cwd); restore_cwd(saved_cwd);
148 148 } }
149 149
150 TEST(non_copy_cmds_reject_deep_option)
151 {
152 ui_sb_msg("");
153 assert_failure(cmds_dispatch1("%move -deep", &lwin, CIT_COMMAND));
154 assert_string_equal("-deep doesn't apply to moving", ui_sb_last());
155
156 ui_sb_msg("");
157 assert_failure(cmds_dispatch1("%alink -deep", &lwin, CIT_COMMAND));
158 assert_string_equal("-deep doesn't apply to making links", ui_sb_last());
159
160 ui_sb_msg("");
161 assert_failure(cmds_dispatch1("%rlink -deep", &lwin, CIT_COMMAND));
162 assert_string_equal("-deep doesn't apply to making links", ui_sb_last());
163 }
164
165 TEST(deep_copy, IF(not_windows))
166 {
167 assert_success(make_symlink("a", SANDBOX_PATH "/left/linked"));
168 assert_success(populate_dir_list(&lwin, /*reload=*/1));
169
170 lwin.list_pos = lwin.list_rows - 1;
171 assert_string_equal("linked", lwin.dir_entry[lwin.list_pos].name);
172
173 assert_failure(cmds_dispatch1("copy -deep", &lwin, CIT_COMMAND));
174 assert_false(is_symlink(SANDBOX_PATH "/right/linked"));
175
176 remove_file(SANDBOX_PATH "/left/linked");
177 remove_file(SANDBOX_PATH "/right/linked");
178 }
179
150 180 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
151 181 /* vim: set cinoptions+=t0 filetype=c : */ /* vim: set cinoptions+=t0 filetype=c : */
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/vifm

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/vifm

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master