Skip to content

Instantly share code, notes, and snippets.

@podhmo
Last active September 28, 2025 01:04
Show Gist options
  • Save podhmo/40361237fa3ef074498ea608b91e0acb to your computer and use it in GitHub Desktop.
Save podhmo/40361237fa3ef074498ea608b91e0acb to your computer and use it in GitHub Desktop.
goname

goname

goname is a simple and robust command-line utility to get the package name from a Go package path.

It is designed to work seamlessly whether the package is part of your current Go module or an external one. It intelligently falls back to a temporary module to resolve package names that are not in your module's dependency graph.

Features

  • Get Package Names: Fetches the official package name from one or more package paths.
  • Version Specifiers: Supports version specifiers like @latest or @v1.2.3 for fetching packages.

Installation

To install goname, use go install:

go install github.com/your-username/goname@latest

(Note: Replace github.com/your-username/goname with the actual repository path once you publish it.)

Usage

The basic syntax is:

goname [-debug] <package_path>[@version]...

Examples

1. Get a single package name

$ goname github.com/go-chi/chi/v5
chi

2. Get multiple package names

$ goname golang.org/x/text/language github.com/google/uuid
language
uuid

3. Use a version specifier

This will fetch the latest version of the package to determine its name.

$ goname github.com/spf13/cobra@latest
cobra

4. Use Debug Mode

When a package isn't in your local cache or go.mod, goname performs extra steps. The -debug flag shows you what's happening.

$ goname -debug github.com/some/new-package@latest
==> Executing in current directory: go list -f {{.Name}} github.com/some/new-package
`go list` command failed: exit status 1
--- stderr ---
go: go.mod file not found in current directory or any parent directory; see 'go help modules'

==> Initial 'go list' failed. Retrying in a temporary module...
==> Created temporary directory: /var/folders/xyz/..../T/goname-12345
==> Executing in /var/folders/xyz/..../T/goname-12345: go mod init m
go: creating new go.mod: module m
go: to add module requirements and sums:
	go mod tidy
==> Executing in /var/folders/xyz/..../T/goname-12345: go get github.com/some/new-package@latest
go: finding module for package github.com/some/new-package
... (go get output) ...
==> Executing in /var/folders/xyz/..../T/goname-12345: go list -f {{.Name}} github.com/some/new-package
newpackage

This ensures reliable name resolution regardless of your current project's context.

License

This project is licensed under the MIT License.

module github.com/podhmo/goname
go 1.24.3
/**
* goname: A Deno script to get the package name from a Go package path.
*
* This tool replicates the functionality of its Go counterpart, providing a
* robust way to resolve Go package names even for packages not present in
* the current module's dependency graph.
*
* install:
* deno install --global --allow-write --allow-run=go goname.ts
*/
import { parseArgs } from "jsr:@std/cli@^0.224.0/parse-args";
/**
* Strips the version suffix (e.g., "@latest") from a package path.
* If no '@' is found, it returns the original string. This logic is
* designed to ignore scoped packages from private registries (e.g., scope@host/pkg).
*/
function stripVersion(pkgPath: string): string {
const atIndex = pkgPath.lastIndexOf("@");
if (atIndex !== -1) {
// If a '/' exists after the '@', it's part of the host, not a version.
if (!pkgPath.substring(atIndex).includes("/")) {
return pkgPath.substring(0, atIndex);
}
}
return pkgPath;
}
/**
* Executes 'go list' and returns the stdout (package names).
* Throws an error if the command fails.
*/
async function goList(
cwd: string,
pkgPaths: string[],
debug: boolean,
): Promise<string> {
const args = ["list", "-f", "{{.Name}}", ...pkgPaths];
if (debug) {
const dir = cwd || "current directory";
console.error(`==> Executing in ${dir}: go ${args.join(" ")}`);
}
const cmd = new Deno.Command("go", { args, cwd });
const { success, stdout, stderr } = await cmd.output();
// In debug mode, always print stderr for full transparency.
if (debug && stderr.length > 0) {
console.error("--- stderr ---");
await Deno.stderr.write(stderr);
console.error("--- end stderr ---");
}
if (!success) {
const errorText = new TextDecoder().decode(stderr);
throw new Error(`'go list' command failed:\n${errorText}`);
}
return new TextDecoder().decode(stdout);
}
/**
* A generic helper for running side-effect Go commands like 'mod init' or 'get'.
* Throws an error if the command fails.
*/
async function executeGoCommand(
cwd: string,
args: string[],
debug: boolean,
): Promise<void> {
if (debug) {
const dir = cwd || "current directory";
console.error(`==> Executing in ${dir}: go ${args.join(" ")}`);
}
const cmd = new Deno.Command("go", { args, cwd });
const { success, stdout, stderr } = await cmd.output();
// In debug mode, show all output from these commands to stderr.
if (debug) {
if (stdout.length > 0) await Deno.stderr.write(stdout);
if (stderr.length > 0) await Deno.stderr.write(stderr);
}
if (!success) {
const errorText = new TextDecoder().decode(stderr);
throw new Error(`Command 'go ${args[0]}' failed:\n${errorText}`);
}
}
/**
* Contains the main application logic.
*/
async function run(pkgPaths: string[], debug: boolean): Promise<void> {
// Paths for 'go list' must not have version specifiers.
const strippedPkgPaths = pkgPaths.map(stripVersion);
// --- Step 1: Attempt 'go list' in the current directory. ---
try {
const output = await goList(Deno.cwd(), strippedPkgPaths, debug);
// On success, print the output and we are done.
await Deno.stdout.write(new TextEncoder().encode(output));
return;
} catch (initialError) {
if (debug) {
console.error(
"==> Initial 'go list' failed. Retrying in a temporary module...",
);
// The detailed error is already printed by goList in debug mode.
if (!debug) console.error(initialError.message);
}
}
// --- Step 2: Fallback to a temporary directory. ---
const tmpDir = await Deno.makeTempDir({ prefix: "goname-" });
if (debug) {
console.error(`==> Created temporary directory: ${tmpDir}`);
}
try {
// Initialize a new module.
await executeGoCommand(tmpDir, ["mod", "init", "m"], debug);
// Fetch the packages using 'go get', preserving version specifiers.
await executeGoCommand(tmpDir, ["get", ...pkgPaths], debug);
// Retry 'go list' in the temporary module.
const finalOutput = await goList(tmpDir, strippedPkgPaths, debug);
await Deno.stdout.write(new TextEncoder().encode(finalOutput));
} finally {
// Ensure the temporary directory is always cleaned up.
await Deno.remove(tmpDir, { recursive: true });
if (debug) {
console.error(`==> Removed temporary directory: ${tmpDir}`);
}
}
}
/**
* Main entry point of the script.
*/
async function main() {
const flags = parseArgs(Deno.args, {
boolean: ["debug"],
string: ["_"],
});
const pkgPaths = flags._ as string[];
if (pkgPaths.length === 0) {
const scriptName = Deno.mainModule.split("/").pop() ?? "goname";
console.error(
`Usage: deno run ${scriptName} [--debug] <package_path>[@version]...`,
);
Deno.exit(1);
}
try {
await run(pkgPaths, flags.debug);
} catch (error) {
console.error(`Error: ${error.message}`);
Deno.exit(1);
}
}
// Run the script if it's the main module.
if (import.meta.main) {
await main();
}
package main
import (
"bytes"
"context"
"flag"
"fmt"
"io"
"os"
"os/exec"
"strings"
)
// debug holds the state of the debug flag.
var debug bool
func main() {
// Define and parse command-line flags.
// If -debug is present, the global 'debug' variable will be set to true.
flag.BoolVar(&debug, "debug", false, "Enable debug mode to show all underlying command outputs to stderr.")
flag.Parse()
// Get the non-flag arguments (package paths).
pkgPaths := flag.Args()
if len(pkgPaths) == 0 {
fmt.Fprintf(os.Stderr, "Usage: %s [-debug] <package_path>[@version]...\n", os.Args[0])
os.Exit(1)
}
// Delegate the main logic to the run function to simplify error handling.
if err := run(pkgPaths); err != nil {
// If an error occurs during execution, print it to stderr and exit.
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// run executes the main logic of the tool.
func run(pkgPaths []string) error {
ctx := context.Background()
// Create a slice of package paths with any version suffixes stripped off.
// This is necessary for 'go list'.
strippedPkgPaths := make([]string, len(pkgPaths))
for i, p := range pkgPaths {
strippedPkgPaths[i] = stripVersion(p)
}
// --- Step 1: Try running 'go list' in the current directory first. ---
// We use the stripped paths here.
output, err := goList(ctx, "", strippedPkgPaths)
if err == nil {
// If successful, write the result to stdout and exit cleanly.
fmt.Print(string(output))
return nil
}
// If it fails, notify the user in debug mode.
if debug {
fmt.Fprintf(os.Stderr, "==> Initial 'go list' failed. Retrying in a temporary module...\n")
// The detailed error is already printed inside the goList function.
}
// --- Step 2: Since it failed, create a temporary directory and retry. ---
tmpDir, err := os.MkdirTemp("", "goname-")
if err != nil {
return fmt.Errorf("error creating temp directory: %w", err)
}
// Schedule the removal of the temporary directory when this function returns.
defer os.RemoveAll(tmpDir)
if debug {
fmt.Fprintf(os.Stderr, "==> Created temporary directory: %s\n", tmpDir)
}
// Run 'go mod init m' inside the temporary directory.
if err := goModInit(ctx, tmpDir); err != nil {
return fmt.Errorf("failed to run 'go mod init': %w", err)
}
// Download the target packages using 'go get'.
// We use the original paths here to respect any version specifiers.
if err := goGet(ctx, tmpDir, pkgPaths); err != nil {
return fmt.Errorf("failed to run 'go get': %w", err)
}
// Run 'go list' again to get the package names.
// We use the stripped paths again.
finalOutput, err := goList(ctx, tmpDir, strippedPkgPaths)
if err != nil {
return fmt.Errorf("failed to run 'go list' in temp directory: %w", err)
}
// Write the final result to stdout.
fmt.Print(string(finalOutput))
return nil
}
// goList executes 'go list' in the specified directory and returns the package names (stdout).
func goList(ctx context.Context, dir string, pkgPaths []string) ([]byte, error) {
args := []string{"list", "-f", `{{.Name}}`}
args = append(args, pkgPaths...)
cmd := exec.CommandContext(ctx, "go", args...)
cmd.Dir = dir // Set the execution directory (current directory if empty).
var stdoutBuf, stderrBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
// In debug mode, pipe the command's stderr to this process's stderr as well.
if debug {
// Use io.MultiWriter to write to both the buffer and os.Stderr.
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
fmt.Fprintf(os.Stderr, "==> Executing in %s: go %s\n", dirForLog(dir), strings.Join(args, " "))
} else {
cmd.Stderr = &stderrBuf
}
if err := cmd.Run(); err != nil {
// If the command fails, return an error that includes the stderr output.
return nil, fmt.Errorf("`go list` command failed: %w\n--- stderr ---\n%s", err, stderrBuf.String())
}
return stdoutBuf.Bytes(), nil
}
// goModInit runs 'go mod init m' in the specified directory.
func goModInit(ctx context.Context, dir string) error {
args := []string{"mod", "init", "m"}
cmd := exec.CommandContext(ctx, "go", args...)
cmd.Dir = dir
return executeSideEffectCommand(cmd)
}
// goGet runs 'go get' in the specified directory.
func goGet(ctx context.Context, dir string, pkgPaths []string) error {
args := []string{"get"}
args = append(args, pkgPaths...)
cmd := exec.CommandContext(ctx, "go", args...)
cmd.Dir = dir
return executeSideEffectCommand(cmd)
}
// executeSideEffectCommand is a helper for running commands like 'go mod init' or 'go get',
// which are executed for their side effects (e.g., creating files) and don't need stdout capture.
func executeSideEffectCommand(cmd *exec.Cmd) error {
var stderrBuf bytes.Buffer
if debug {
// In debug mode, stream stdout and stderr directly to the process's stderr.
cmd.Stdout = os.Stderr
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) // Also capture for error reporting
fmt.Fprintf(os.Stderr, "==> Executing in %s: %s\n", dirForLog(cmd.Dir), cmd.String())
} else {
// In normal mode, only capture stderr.
cmd.Stderr = &stderrBuf
}
if err := cmd.Run(); err != nil {
return fmt.Errorf("command `%s` failed: %w\n--- stderr ---\n%s", cmd.Args[0], err, stderrBuf.String())
}
return nil
}
// stripVersion removes the version suffix (e.g., "@latest") from a package path.
// If no '@' is found, it returns the original string.
func stripVersion(pkgPath string) string {
if i := strings.LastIndex(pkgPath, "@"); i != -1 {
// Check if there is a '/' after the '@'. If so, it's likely a scoped package
// from a private registry and not a version string.
// e.g., [email protected]/pkg
if !strings.Contains(pkgPath[i:], "/") {
return pkgPath[:i]
}
}
return pkgPath
}
// dirForLog returns "current directory" if the dir path is empty, for logging purposes.
func dirForLog(dir string) string {
if dir == "" {
return "current directory"
}
return dir
}