commit cf874cee330db7996e879891b7be0ffa3bd6a535
parent 6d73bf48861c13d190f3085eb54380fa76cb1a11
Author: Sanzhar Kuandyk <92693103+SanzharKuandyk@users.noreply.github.com>
Date: Wed, 25 Feb 2026 13:38:08 +0500
feat(startup): provide v:argf for file arguments #35889
Problem:
- `:args` and `argv()` can change after startup.
- `v:arg` includes options/commands, not just files.
- Plugins (e.g. Oil) may rewrite directory args.
Solution:
- New read-only var `v:argf`: snapshot of file/dir args at startup.
- Unaffected by `:args` or plugins.
- Unlike `v:argv`, excludes options/commands.
- Paths are resolved to absolute paths when possible
Example:
nvim file1.txt dir1 file2.txt
:echo v:argf
" ['/home/user/project/file1.txt', '/home/user/project/dir1', '/home/user/project/file2.txt']
Diffstat:
7 files changed, 152 insertions(+), 0 deletions(-)
diff --git a/runtime/doc/vvars.txt b/runtime/doc/vvars.txt
@@ -11,6 +11,21 @@ be mentioned at the variable description below. The type cannot be changed.
Type |gO| to see the table of contents.
+ *v:argf* *argf-variable*
+v:argf
+ The list of file arguments passed on the command line at startup.
+
+ Each filename is expanded to an absolute path, so that v:argf
+ remains valid even if the current working directory changes later.
+
+ Unlike |v:argv|, this does not include option arguments
+ such as `-u`, `--cmd`, or `+cmd`. Unlike |argv()|, it is not
+ affected by later |:args|, |:argadd|, or plugin modifications.
+ It also handles the `--` separator correctly, including only
+ files specified after it.
+
+ This is a read-only snapshot of the original startup file arguments.
+
*v:argv* *argv-variable*
v:argv
The command line arguments Vim was invoked with. This is a
diff --git a/runtime/lua/vim/_meta/vvars.lua b/runtime/lua/vim/_meta/vvars.lua
@@ -6,6 +6,21 @@ error('Cannot require a meta file')
--- @class vim.v
vim.v = ...
+--- The list of file arguments passed on the command line at startup.
+---
+--- Each filename is expanded to an absolute path, so that v:argf
+--- remains valid even if the current working directory changes later.
+---
+--- Unlike `v:argv`, this does not include option arguments
+--- such as `-u`, `--cmd`, or `+cmd`. Unlike `argv()`, it is not
+--- affected by later `:args`, `:argadd`, or plugin modifications.
+--- It also handles the `--` separator correctly, including only
+--- files specified after it.
+---
+--- This is a read-only snapshot of the original startup file arguments.
+--- @type string[]
+vim.v.argf = ...
+
--- The command line arguments Vim was invoked with. This is a
--- list of strings. The first item is the Vim command.
--- See `v:progpath` for the command with full path.
diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c
@@ -198,6 +198,7 @@ static struct vimvar {
VV(VV_EVENT, "event", VAR_DICT, VV_RO),
VV(VV_VERSIONLONG, "versionlong", VAR_NUMBER, VV_RO),
VV(VV_ECHOSPACE, "echospace", VAR_NUMBER, VV_RO),
+ VV(VV_ARGF, "argf", VAR_LIST, VV_RO),
VV(VV_ARGV, "argv", VAR_LIST, VV_RO),
VV(VV_COLLATE, "collate", VAR_STRING, VV_RO),
VV(VV_EXITING, "exiting", VAR_NUMBER, VV_RO),
diff --git a/src/nvim/eval_defs.h b/src/nvim/eval_defs.h
@@ -118,6 +118,7 @@ typedef enum {
VV_EVENT,
VV_VERSIONLONG,
VV_ECHOSPACE,
+ VV_ARGF,
VV_ARGV,
VV_COLLATE,
VV_EXITING,
diff --git a/src/nvim/main.c b/src/nvim/main.c
@@ -301,6 +301,8 @@ int main(int argc, char **argv)
// argument list "global_alist".
command_line_scan(¶ms);
+ set_argf_var();
+
nlua_init(argv, argc, params.lua_arg0);
TIME_MSG("init lua interpreter");
@@ -1506,6 +1508,22 @@ scripterror:
TIME_MSG("parsing arguments");
}
+static void set_argf_var(void)
+{
+ list_T *list = tv_list_alloc(kListLenMayKnow);
+
+ for (int i = 0; i < GARGCOUNT; i++) {
+ char *fname = alist_name(&GARGLIST[i]);
+ if (fname != NULL) {
+ (void)vim_FullName(fname, NameBuff, sizeof(NameBuff), false);
+ tv_list_append_string(list, NameBuff, -1);
+ }
+ }
+
+ tv_list_set_lock(list, VAR_FIXED);
+ set_vim_var_list(VV_ARGF, list);
+}
+
// Many variables are in "params" so that we can pass them to invoked
// functions without a lot of arguments. "argc" and "argv" are also
// copied, so that they can be changed.
diff --git a/src/nvim/vvars.lua b/src/nvim/vvars.lua
@@ -1,6 +1,23 @@
local M = {}
M.vars = {
+ argf = {
+ type = 'string[]',
+ desc = [=[
+ The list of file arguments passed on the command line at startup.
+
+ Each filename is expanded to an absolute path, so that v:argf
+ remains valid even if the current working directory changes later.
+
+ Unlike |v:argv|, this does not include option arguments
+ such as `-u`, `--cmd`, or `+cmd`. Unlike |argv()|, it is not
+ affected by later |:args|, |:argadd|, or plugin modifications.
+ It also handles the `--` separator correctly, including only
+ files specified after it.
+
+ This is a read-only snapshot of the original startup file arguments.
+ ]=],
+ },
argv = {
type = 'string[]',
desc = [=[
diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua
@@ -1806,3 +1806,88 @@ describe('inccommand on ex mode', function()
]])
end)
end)
+
+describe('v:argf', function()
+ local files = {}
+
+ before_each(function()
+ clear()
+ files = {}
+
+ for _, f in ipairs({
+ 'Xargf_file1',
+ 'Xargf_file2',
+ 'Xargf_sep1',
+ 'Xargf_sep2',
+ 'Xargf_sep3',
+ }) do
+ os.remove(f)
+ end
+ end)
+
+ after_each(function()
+ for _, f in ipairs(files) do
+ os.remove(f)
+ end
+ end)
+
+ it('stores full path of file args', function()
+ local file1, file2 = 'Xargf_file1', 'Xargf_file2'
+ write_file(file1, '')
+ write_file(file2, '')
+ files = { file1, file2 }
+
+ local abs1 = fn.fnamemodify(file1, ':p')
+ local abs2 = fn.fnamemodify(file2, ':p')
+
+ local p = n.spawn_wait('--cmd', 'echo v:argf', '-c', 'qall', file1, file2)
+
+ eq(0, p.status)
+ matches(pesc(abs1), p.stderr)
+ matches(pesc(abs2), p.stderr)
+ end)
+
+ it('argadd does not affect v:argf', function()
+ local file1, file2 = 'Xargf_file1', 'Xargf_file2'
+ write_file(file1, '')
+ write_file(file2, '')
+ files = { file1, file2 }
+
+ local p = n.spawn_wait(
+ '--cmd',
+ 'argadd extrafile.txt',
+ '--cmd',
+ 'echo v:argf',
+ '-c',
+ 'qall',
+ file1,
+ file2
+ )
+
+ eq(0, p.status)
+ eq(nil, string.find(p.stderr, 'extrafile.txt'))
+ end)
+
+ it('handles -- separator correctly', function()
+ local file1, file2, file3 = 'Xargf_sep1', 'Xargf_sep2', 'Xargf_sep3'
+ write_file(file1, '')
+ write_file(file2, '')
+ write_file(file3, '')
+ files = { file1, file2, file3 }
+
+ local abs1 = fn.fnamemodify(file1, ':p')
+ local abs2 = fn.fnamemodify(file2, ':p')
+ local abs3 = fn.fnamemodify(file3, ':p')
+
+ local p = n.spawn_wait(file1, '--cmd', 'echo v:argf', '-c', 'qall', '--', file2, file3)
+
+ eq(0, p.status)
+ matches(pesc(abs1), p.stderr)
+ matches(pesc(abs2), p.stderr)
+ matches(pesc(abs3), p.stderr)
+ end)
+
+ it('is read-only', function()
+ matches('E46', t.pcall_err(command, "let v:argf = ['foo']"))
+ end)
+end)