zk icon indicating copy to clipboard operation
zk copied to clipboard

File paths w/ whitespace causes editors to open invalid paths

Open SneakyAlba opened this issue 3 weeks ago • 6 comments

Check if applicable

  • [x] I have searched the existing issues (required)
  • [ ] I'm willing to help fix the problem and contribute a pull request

Describe the bug

If zk opens or creates a file in a path that contains whitespace, e.g. "ZK Experiment", the command sent to the editor application doesn't get wrapped in quotes, causing undefined behavior.

Terminal editors like Helix or NeoVim seem to interpret the whitespace as path separators, resulting in multiple open buffers on invalid file paths. In specific file trees, this could theoretically open and/or write to valid file paths other than intended.

Example image of editor opened without quote-wrapped whitespace: Image

How to reproduce?

May be replicated if:

  • On Windows (assumed; possibly replicated on other systems)
  • With an editor specified in config.toml
  • In a notebook with whitespace in its path
  • Using a command that opens an editor e.g. the zk new without the -i option

zk configuration

# zk configuration file
#
# Uncomment the properties you want to customize.

# NOTE SETTINGS
#
# Defines the default options used when generating new notes.
[note]

# Language used when writing notes.
# This is used to generate slugs or with date formats.
#language = "en"

# The default title used for new note, if no `--title` flag is provided.
#default-title = "Untitled"

# Template used to generate a note's filename, without extension.
#filename = "{{id}}"

# The file extension used for the notes.
#extension = "md"

# Template used to generate a note's content.
# If not an absolute path or "~/unix/path", it's relative to .zk/templates/
template = "default.md"

# Path globs ignored while indexing existing notes.
#exclude = [
#    "drafts/*",
#    "log.md"
#]

# Configure random ID generation.

# The charset used for random IDs. You can use:
#   * letters: only letters from a to z.
#   * numbers: 0 to 9
#   * alphanum: letters + numbers
#   * hex: hexadecimal, from a to f and 0 to 9
#   * custom string: will use any character from the provided value
#id-charset = "alphanum"

# Length of the generated IDs.
#id-length = 4

# Letter case for the random IDs, among lower, upper or mixed.
#id-case = "mixed"


# EXTRA VARIABLES
#
# A dictionary of variables you can use for any custom values when generating
# new notes. They are accessible in templates with {{extra.<key>}}
[extra]

#key = "value"


# GROUP OVERRIDES
#
# You can override global settings from [note] and [extra] for a particular
# group of notes by declaring a [group."<name>"] section.
#
# Specify the list of directories which will automatically belong to the group
# with the optional `paths` property.
#
# Omitting `paths` is equivalent to providing a single path equal to the name of
# the group. This can be useful to quickly declare a group by the name of the
# directory it applies to.

#[group."<NAME>"]
#paths = ["<DIR1>", "<DIR2>"]
#[group."<NAME>".note]
#filename = "{{format-date now}}"
#[group."<NAME>".extra]
#key = "value"


# MARKDOWN SETTINGS
[format.markdown]

# Format used to generate links between notes.
# Either "wiki", "markdown" or a custom template. Default is "markdown".
link-format = "markdown"
# Indicates whether a link's path will be percent-encoded.
# Defaults to true for "markdown" format and false for "wiki" format.
#link-encode-path = true
# Indicates whether a link's path file extension will be removed.
# Defaults to true.
#link-drop-extension = true

# Enable support for #hashtags.
hashtags = true
# Enable support for :colon:separated:tags:.
colon-tags = false
# Enable support for Bear's #multi-word tags#
# Hashtags must be enabled for multi-word tags to work.
multiword-tags = false


# EXTERNAL TOOLS
[tool]

# Default editor used to open notes. When not set, the EDITOR or VISUAL
# environment variables are used.
editor = "hx"

# Pager used to scroll through long output. If you want to disable paging
# altogether, set it to an empty string "".
#pager = "less -FIRX"

# Command used to preview a note during interactive fzf mode.
# Set it to an empty string "" to disable preview.

# bat is a great tool to render Markdown document with syntax highlighting.
#https://github.com/sharkdp/bat
#fzf-preview = "bat -p --color always {-1}"


# LSP
#
#   Configure basic editor integration for LSP-compatible editors.
#   See https://github.com/zk-org/zk/blob/main/docs/editors-integration.md
#
[lsp]

[lsp.diagnostics]
# Each diagnostic can have for value: none, hint, info, warning, error

# Report titles of wiki-links as hints.
#wiki-title = "hint"
# Warn for dead links between notes.
dead-link = "error"

[lsp.completion]
# Customize the completion pop-up of your LSP client.

# Show the note title in the completion pop-up, or fallback on its path if empty.
#note-label = "{{title-or-path}}"
# Filter out the completion pop-up using the note title or its path.
#note-filter-text = "{{title}} {{path}}"
# Show the note filename without extension as detail.
#note-detail = "{{filename-stem}}"


# NAMED FILTERS
#
#    A named filter is a set of note filtering options used frequently together.
#
[filter]

# Matches the notes created the last two weeks. For example:
#    $ zk list recents --limit 15
#    $ zk edit recents --interactive
#recents = "--sort created- --created-after 'last two weeks'"


# COMMAND ALIASES
#
#   Aliases are user commands called with `zk <alias> [<flags>] [<args>]`.
#
#   The alias will be executed with `$SHELL -c`, please refer to your shell's
#   man page to see the available syntax. In most shells:
#     * $@ can be used to expand all the provided flags and arguments
#     * you can pipe commands together with the usual | character
#
[alias]
# Here are a few aliases to get you started.

# Shortcut to a command.
#ls = "zk list $@"

# Default flags for an existing command.
#list = "zk list --quiet $@"

# Edit the last modified note.
#editlast = "zk edit --limit 1 --sort modified- $@"

# Edit the notes selected interactively among the notes created the last two weeks.
# This alias doesn't take any argument, so we don't use $@.
#recent = "zk edit --sort created- --created-after 'last two weeks' --interactive"

# Print paths separated with colons for the notes found with the given
# arguments. This can be useful to expand a complex search query into a flag
# taking only paths. For example:
#   zk list --link-to "`zk path -m potatoe`"
#path = "zk list --quiet --format {{path}} --delimiter , $@"

# Show a random note.
#lucky = "zk list --quiet --format full --sort random --limit 1"

# Returns the Git history for the notes found with the given arguments.
# Note the use of a pipe and the location of $@.
#hist = "zk list --format path --delimiter0 --quiet $@ | xargs -t -0 git log --patch --"

# Edit this configuration file.
#conf = '$EDITOR "$ZK_NOTEBOOK_DIR/.zk/config.toml"'

Environment


SneakyAlba avatar Nov 26 '25 11:11 SneakyAlba

Opening files with spaces in their file name works fine for me in Linux (using neovim as an editor).

WhyNotHugo avatar Nov 26 '25 11:11 WhyNotHugo

Including the following in my config, filenames with spaces still open fine:

[tool]
editor="nvim"

WhyNotHugo avatar Nov 26 '25 11:11 WhyNotHugo

Editor.Open in internal/adapter/editor/editor.go individually escapes each filename, builds a string with a shell command, and runs the editor via a shell. At this point, the behaviour shall be dependant on the shell being used.

I'm not sure why it does this instead of using a regular command with explicit args (without would negate the need for quoting and also work fine with filenames with quotes and other exotic inputs). Is this for shell variable expansion?

Construction of the command is delegated to CommandFromString, which has unix-specific and windows-specific implementations. I don't know what language is used for the windows invocation, but the arguments are interpolated into a string with quotes, so I suspect that's not working as intended. I can't comment beyond this; it's outside my domain.

WhyNotHugo avatar Nov 26 '25 11:11 WhyNotHugo

Similar: #499 Which was fixed in #502

I will see to test on my windows machine when I'm on it.

So basically I have to make a notebook with white spaces in it (/home/zk experminent) and create a note with zk new?

tjex avatar Nov 30 '25 17:11 tjex

That's right.

On my end, just having spaces anywhere in the file path causes the path to be split when zk launches the editor. I used new as an example as that opens the editor after the file is created.

Exactly what issues this causes depends on the editor used. Helix interprets the splits as being provided multiple files to open and will open with a buffer on those paths, even if invalid. On Sunday, 30 November 2025 at 17:16, Tillman Jex @.***> wrote:

tjex left a comment (zk-org/zk#620)

Similar: #499 Which was fixed in #502

I will see to test on my windows machine when I'm on it.

So basically I have to make a notebook with white spaces in it (/home/zk experminent) and create a note with zk new?

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.

SneakyAlba avatar Nov 30 '25 18:11 SneakyAlba

Yep, also happening on my end on the windows machine. And using neovim as the editor.

Pretty sure it's coming from the CommandFromString implementation in exec_windows.go

// CommandFromString returns a Cmd running the given command.
func CommandFromString(command string, args ...string) *exec.Cmd {
	cmd := exec.Command("cmd")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		HideWindow:    false,
		CmdLine:       fmt.Sprintf(` /v:on/s/c '%s %s'`, command, strings.Join(args[:], " ")),
		CreationFlags: 0,
	}
	return cmd
}

I'v also noticed that CommandFromString() is not getting two arguments, and instead one large string. Not sure why golsp isn't complaining...?

	cmd := executil.CommandFromString(e.editor + " " + shellquote.Join(paths...) + CMD_SUFFIX)

tjex avatar Dec 05 '25 08:12 tjex