How to Run Terminal Command on the Active File on Write?

I want Kakoune to run go fmt this-file when I write to a file ending in .go.

I’m currently trying this:

hook global BufWritePre .*\.go %{
	evaluate-commands %sh{ go fmt $kak_buffile }
}

but getting this in :buffer *debug* when I do :w (I’m testing it on ./src/main.go):

Things I’m unsure on:

  1. Would buffer scope be more appropriate? It seems more fitting, but I’ve only been using global scope in my kakrc so far.
  2. Should I use evaluate-commands? I’m not sure what it’s doing that the %sh isn’t.
  3. Should I use $kak_buffile or %val{buffile}? I was using the second, but switched to the first, but I’m not sure on the difference.
  4. If none of that is the problem, what is the problem?
1 Like

Would buffer scope be more appropriate?

The conventional approach is to hook BufSetOption filetype=go, and set your hooks and options at buffer-scope within that hook. That way, you get Go-like behaviour whenever you edit a file that is Go-like, not just specifically .go files.

Should I use evaluate-commands? I’m not sure what it’s doing that the %sh isn’t.

Probably not. What’s going on here is:

  • when the hook fires, its body is evaluated
  • when the body is evaluated, Kakoune sees a %sh{} block, runs the content of that block as a shell-script, and replaces the %sh{} block with the output of that script
  • apparently, that command prints src/main.go possibly followed by some other text
  • evaluate-commands tries to evaluate its arguments (src/main.go ...) as a Kakoune command, but that isn’t a valid Kakoune command, it’s a filename, so it prints an error

A better choice here might be nop %sh{ go fmt $kak_buffile } since nop is a command that ignores its arguments and does nothing - you don’t want it to do anything, you just want the side-effect of running a shell-script.

Should I use $kak_buffile or %val{buffile}?

Generally, you use $kak_buffile inside a %sh{} block, and %val{buffile} outside one. There’s more specific rules (for example, if you’re using a shell-script to build up a command that will be evaluated outside the shell-script, such as by evaluate-commands or kak -p) but those don’t apply here.

Also, when you use $kak_buffile (or any other shell variable) it’s a good idea to wrap it in double-quotes so that unexpected space characters don’t make things go haywire.

There’s one more problem with your solution: you’re running go fmt on the file on disk, before Kakoune writes out the content of the buffer. So whatever work go fmt does, will be immediately overwritten.

All that aside, though, Kakoune comes with a format plugin that can be used to run a formatter on the current buffer.

Gluing together my personal configuration for Python files, and the wiki’s suggestion for formatting Go, I’d recommend something like:

# When we edit a Go-like file,
# either auto-detected
# or because the user overrode the filetype manually....
hook global BufSetOption filetype=go %{
    # Tell the format plugin
    # to use "gofmt" to format this buffer
    set-option buffer formatcmd "gofmt"
    # Automatically format the buffer
    # before writing it to disk
    hook buffer -group go-auto-format BufWritePre .* format
    # If, after we've set these options and hooks,
    # the user overrides the filetype to something else...
    hook buffer BufSetOption filetype=.* %{
        # remove the auto-format hook,
        # since it probably won't be
        # appropriate for the new filetype
        remove-hooks buffer go-auto-format
        # We leave the formatcmd option alone,
        # since the user might have manually overridden it
        # and we don't have anything better to set it to.
    }
}
3 Likes

Cool; I’ll go with that, then.

This is helpful.

Ah, for some reason I was under the mistaken impression that it was a terminal command, not a Kakoune command. Now that error makes more sense.

That makes sense; I wasn’t sure exactly when and where to use nop, but it makes a lot more sense now in conjunction with what %sh{} and evaluate-commands are doing.

Cool; I wasn’t sure if it was that or if one was deprecated or something.

Good to know; I’m not used to variables being good things to surround in ", lol.

Good catch; that was a total blind spot. I must have been conflating the buffer and the disk in my mind.

Alright; looks like it’s time for me to install the plugin manager (Is GitHub - andreyorst/plug.kak: Plugin manager for Kakoune the one everyone uses?).

Why does formatting before writing work here? Is it because the format plugin is giving the formatting tool the buffer to edit instead of the file on disk? At least conceptually.

Is this just an arbitrary name to reference below?

PS: The above post is super thorough; thanks for taking the time to put it together.

To be clear, format.kak is part of Kakoune’s standard library, you don’t need to install it separately. Also, a plugin manager is mostly just automation for git clone into the autoload directory; some people find that really valuable, but if you’re happy to run git clone and git pull yourself you don’t really need a plugin manager.

Yes, it pipes the buffer through the formatting tool, just before Kakoune writes the buffer to disk.

Yes.

Glad to help!

2 Likes

Hi @Screwtapello !

Yes, it pipes the buffer through the formatting tool, just before Kakoune writes the buffer to disk.

That’s the issue with go format tool and why your snippet won’t work.
gofmt/gofumpt do not use stdin as their format source, but need a filepath :slightly_frowning_face:

To make those work with kakoune :format command, I created a small script that acts just like xargs but for files.

usage() { printf 'usage: fargs [-h/--help] [-q/--quiet] CMD...\n'; exit 1; }

file=$(mktemp fargs.tmp.XXXX)
trap 'rm "$file"' EXIT

case "$1" in
    -h|--help) usage ;;
    -q|--quiet) shift; exec 2>/dev/null ;;
esac

[ $# -le 0 ] && usage

cat - > "$file"
"$@" "$file"
cat "$file"

I then used this script in my formatcmd for go and python :blush:

# Need fargs to work
hook -group languagecmd global WinSetOption filetype=go %{
    set-option buffer formatcmd 'fargs gofumpt -w'
}

hook -group languagecmd global WinSetOption filetype=python %{
    set-option buffer formatcmd 'fargs black --line-length 150'
}

@samtcifihi, you can use the above solution, or some similar script, to format go code using the standard kakoune :format command.
As @Screwtapello and I mentioned previously, kakoune PIPES the whole buffer to formatcmd, it does not append the buffer path as an argument (whereas lintcmd does receive the buffer name as an argument).


You can always create a small custom command just for go, which will pass the current buffer filepath to gofmt/gofumpt if the external script is bothersome.

define-command -params 0 -docstring "Format go code, passing the buffer filepath to gofmt" \
    go-format nop %sh{ gofmt "$kak_buffile" }
2 Likes

That seems like a very useful script, and I’m sure it comes in handy. However, although I’m not a Go programmer myself, but this gofmt documentation I found says:

Without an explicit path, it processes the standard input.

gofumpt doesn’t seem to have corresponding documentation, but it says it was forked from gofmt and is intended as a drop-in replacement, so I assume it probably behaves similarly.

Meanwhile, the Python black formatting tool does require a path, but you can give it - to mean “read from stdin” and it works quite nicely as a Kakoune formatcmd.

@Screwtapello my bad… Don’t know where I got the idea stdin wasn’t supported, but I’m clearly wrong here. Thanks for correcting me !

Ah; thanks.

Cool.

Is this right? It works when I comment it out, but otherwise it doesn’t do anything. Either way; thanks for the help; this is way more convenient than running gofmt manually all the time.

I’ll keep this post in mind if I run into trouble in the future using Screwtapello’s solution for other languages.

Go may require all its source files to have the .go file extension, but it’s possible to have other filetypes with that extension, like a shell-script called give.me.a.fair.go or whatever. If you open such a file in Kakoune, it may be autodetected as a Go file (and so the autoformat hook will be installed) even though it’s not appropriate.

If that happens, you should be able to say :set buffer filetype=sh (or whatever the real filetype is) and then the autoformat hook will automatically be removed, since you wouldn’t get anything useful trying to pass a shell-script through gofmt.

That is to say, you won’t see that code do anything in normal operation, it’s just there to cover an unusual corner-case.

1 Like