refactor: delegate -l output to findmnt
Update PKGBUILD version / update-pkgver (push) Successful in 2s

The custom one-alias-per-line output was useless — no mountpoint, no
source, no options. Reinventing a table format when findmnt from
util-linux already produces a familiar fuse.sshfs view was the wrong
call. -l now shells out to findmnt -t fuse.sshfs.
This commit is contained in:
2026-05-04 10:24:53 +02:00
parent e6a02e5bf7
commit 4306170626
4 changed files with 45 additions and 49 deletions
+30
View File
@@ -1,5 +1,35 @@
# Decisions # Decisions
## 2026-05-04 Delegate `-l` output to `findmnt -t fuse.sshfs`
- **Who**: Dom, ClaudeCode
- **Why**: The custom `-l` output ("just the alias, one per line") was
uselessly minimal — no mountpoint, no source, no options. Reinventing a
table format for a single command was the wrong call when `findmnt` from
`util-linux` already produces a familiar, well-formatted view of fuse.sshfs
mounts.
- **Tradeoffs**:
- External dependency on `findmnt` (part of `util-linux`, present on every
Arch and most other Linux systems by default — no real adoption cost).
- Output is mountpoint-first, not alias-first. The alias is only visible as
the last path segment under the SOURCE column / TARGET basename.
- Lists *all* fuse.sshfs mounts on the system, not just sshfsc-managed
ones under `$XDG_RUNTIME_DIR/sshfs/`. In practice this is what the user
wants ("show me all sshfs mounts"); a future `-r` (restrict) flag could
narrow it if needed.
- `findmnt` exits 1 when nothing matches its filter — we treat that as
success-with-empty-output, not a failure.
- **How**:
- `list_mounts` rewritten as a thin wrapper around `exec.Command("findmnt",
"-t", "fuse.sshfs")` with stdout piped to the caller's writer.
- Exit-code 1 from `findmnt` is swallowed via `errors.As(err, *exec.ExitError)`
+ `ExitCode() == 1`.
- `errors.Is(err, exec.ErrNotFound)` produces a clear "install util-linux"
message instead of a cryptic exec error.
- Tests `TestListMountsMissingBase` and `TestListMountsFiltersUnmounted`
removed: they covered the old in-Go enumeration logic that no longer
exists. Mocking `findmnt` would be alibi-testing — the wrapper is two
branches over `cmd.Run()`'s error.
## 2026-05-04 Detect mounts via `/proc/self/mountinfo` instead of `stat` ## 2026-05-04 Detect mounts via `/proc/self/mountinfo` instead of `stat`
- **Who**: Dom, ClaudeCode - **Who**: Dom, ClaudeCode
- **Why**: `mountinfo.Mounted(path)` from `github.com/moby/sys/mountinfo` works - **Why**: `mountinfo.Mounted(path)` from `github.com/moby/sys/mountinfo` works
+2 -1
View File
@@ -22,6 +22,7 @@ install -Dm755 sshfsc /usr/local/bin/sshfsc
# Dependencies # Dependencies
- [SSHFS](https://wiki.archlinux.org/title/SSHFS) - [SSHFS](https://wiki.archlinux.org/title/SSHFS)
- `findmnt` from `util-linux` (for `-l`)
- [Go](https://wiki.archlinux.org/title/Go) >= 1.25 (build-time) - [Go](https://wiki.archlinux.org/title/Go) >= 1.25 (build-time)
# Usage # Usage
@@ -39,7 +40,7 @@ sshfsc -l # list active mounts
| `-e` | open mountpoint in your editor | | `-e` | open mountpoint in your editor |
| `-v` | verbose: print resolved ssh_config fields (HostName, User, Port, IdentityFile) | | `-v` | verbose: print resolved ssh_config fields (HostName, User, Port, IdentityFile) |
| `-r`, `--remote-dir <path>` | remote directory to mount (default: remote home) | | `-r`, `--remote-dir <path>` | remote directory to mount (default: remote home) |
| `-l` | list active mounts under `$XDG_RUNTIME_DIR/sshfs/` and exit | | `-l` | list active fuse.sshfs mounts via `findmnt` and exit |
| `-u` | unmount the given `<Host>` and exit (mutually exclusive with `-l`) | | `-u` | unmount the given `<Host>` and exit (mutually exclusive with `-l`) |
By default only the resolved mount path is printed. Use `-v` for the full By default only the resolved mount path is printed. Use `-v` for the full
+13 -17
View File
@@ -281,26 +281,22 @@ func is_mounted_at(path string) (bool, error) {
} }
func list_mounts(out io.Writer) error { func list_mounts(out io.Writer) error {
base, err := mount_base(false) cmd := exec.Command("findmnt", "-t", "fuse.sshfs")
if err != nil { cmd.Stdout = out
if errors.Is(err, fs.ErrNotExist) { cmd.Stderr = os.Stderr
err := cmd.Run()
if err == nil {
return nil return nil
} }
return err // findmnt exits 1 when no matching mount exists — not an error here.
} var ee *exec.ExitError
mounts, err := mountinfo.GetMounts(mountinfo.PrefixFilter(base)) if errors.As(err, &ee) && ee.ExitCode() == 1 {
if err != nil {
return fmt.Errorf("read mountinfo: %w", err)
}
prefix := base + string(os.PathSeparator)
for _, m := range mounts {
rel := strings.TrimPrefix(m.Mountpoint, prefix)
if rel == "" || strings.Contains(rel, string(os.PathSeparator)) {
continue
}
fmt.Fprintln(out, rel)
}
return nil return nil
}
if errors.Is(err, exec.ErrNotFound) {
return fmt.Errorf("findmnt not found (install util-linux)")
}
return fmt.Errorf("findmnt: %w", err)
} }
func run_fusermount(flag, mount string) error { func run_fusermount(flag, mount string) error {
-31
View File
@@ -4,7 +4,6 @@
package main package main
import ( import (
"bytes"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -140,36 +139,6 @@ func TestVerifyMountDirRejectsSymlink(t *testing.T) {
} }
} }
func TestListMountsMissingBase(t *testing.T) {
runtime := t.TempDir()
t.Setenv("XDG_RUNTIME_DIR", runtime)
var buf bytes.Buffer
if err := list_mounts(&buf); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if buf.Len() != 0 {
t.Fatalf("want empty output, got %q", buf.String())
}
}
func TestListMountsFiltersUnmounted(t *testing.T) {
runtime := t.TempDir()
t.Setenv("XDG_RUNTIME_DIR", runtime)
base := filepath.Join(runtime, "sshfs")
for _, name := range []string{"stale1", "stale2"} {
if err := os.MkdirAll(filepath.Join(base, name), 0700); err != nil {
t.Fatalf("setup stale dir: %v", err)
}
}
var buf bytes.Buffer
if err := list_mounts(&buf); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if buf.Len() != 0 {
t.Fatalf("want stale dirs filtered, got %q", buf.String())
}
}
func TestUnmountRejectsBadAlias(t *testing.T) { func TestUnmountRejectsBadAlias(t *testing.T) {
runtime := t.TempDir() runtime := t.TempDir()
t.Setenv("XDG_RUNTIME_DIR", runtime) t.Setenv("XDG_RUNTIME_DIR", runtime)