From 143e8482204cb00d2d4a9bba9d4f4e920d3b6864 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 22 Oct 2026 12:26:53 -0400 Subject: [PATCH 01/10] add flag for custom user-agent suffix --- client.go | 12 +++++ client_test.go | 26 +++++++++++ cmd/dbc/main.go | 12 ++++- cmd/dbc/user_agent_test.go | 91 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 cmd/dbc/user_agent_test.go diff --git a/client.go b/client.go index 9f696954..1600b7ac 100644 --- a/client.go +++ b/client.go @@ -36,6 +36,7 @@ type clientConfig struct { // who pass WithRegistries retain full control over that baseline. registries []Registry userAgent string + userAgentSuffix string baseURL string credentialResolver func(*url.URL) (*auth.Credential, error) globalConfig *GlobalConfig @@ -74,6 +75,10 @@ func NewClient(opts ...Option) (*Client, error) { opt(cfg) } + if cfg.userAgentSuffix != "" { + cfg.userAgent += " " + cfg.userAgentSuffix + } + httpClient := cfg.httpClient if httpClient == nil { httpClient = &http.Client{ @@ -194,3 +199,10 @@ func WithBaseURL(u string) Option { func WithUserAgent(ua string) Option { return func(cfg *clientConfig) { cfg.userAgent = ua } } + +// WithUserAgentSuffix appends suffix, separated by a space, to the user agent +// string (the default or one set via WithUserAgent). Like WithUserAgent, it +// has no effect on a custom HTTP client supplied via WithHTTPClient. +func WithUserAgentSuffix(suffix string) Option { + return func(cfg *clientConfig) { cfg.userAgentSuffix = suffix } +} diff --git a/client_test.go b/client_test.go index cae9c987..085b210b 100644 --- a/client_test.go +++ b/client_test.go @@ -61,3 +61,29 @@ func TestNewClientWithUserAgent(t *testing.T) { require.NoError(t, err) assert.Equal(t, "custom-agent/1.0", c.UserAgent()) } + +func TestNewClientWithUserAgentSuffix(t *testing.T) { + base, err := dbc.NewClient() + require.NoError(t, err) + + c, err := dbc.NewClient(dbc.WithUserAgentSuffix("my-tool/2.0")) + require.NoError(t, err) + assert.Equal(t, base.UserAgent()+" my-tool/2.0", c.UserAgent()) + + c, err = dbc.NewClient(dbc.WithUserAgentSuffix("")) + require.NoError(t, err) + assert.Equal(t, base.UserAgent(), c.UserAgent(), "empty suffix must not add a trailing space") +} + +// The suffix is applied after all options, so it composes with WithUserAgent +// regardless of option order. +func TestNewClientUserAgentSuffixAppliesToCustomUserAgent(t *testing.T) { + for _, opts := range [][]dbc.Option{ + {dbc.WithUserAgent("custom-agent/1.0"), dbc.WithUserAgentSuffix("my-tool/2.0")}, + {dbc.WithUserAgentSuffix("my-tool/2.0"), dbc.WithUserAgent("custom-agent/1.0")}, + } { + c, err := dbc.NewClient(opts...) + require.NoError(t, err) + assert.Equal(t, "custom-agent/1.0 my-tool/2.0", c.UserAgent()) + } +} diff --git a/cmd/dbc/main.go b/cmd/dbc/main.go index d9409162..18bb9b7f 100644 --- a/cmd/dbc/main.go +++ b/cmd/dbc/main.go @@ -77,6 +77,8 @@ type NeedsRenderer interface { } var ( + userAgentSuffix string + dbcClient *dbc.Client dbcClientErr error dbcClientOnce = &sync.Once{} @@ -120,6 +122,11 @@ func newDBCClient(projectRegs []dbc.RegistryEntry, projectReplaceDefaults *bool) opts = append(opts, dbc.WithProjectRegistries(projectRegs, projectReplaceDefaults)) } } + + if userAgentSuffix != "" { + opts = append(opts, dbc.WithUserAgentSuffix(userAgentSuffix)) + } + return dbc.NewClient(opts...) } @@ -167,7 +174,6 @@ var getDriverRegistry = func() ([]dbc.Driver, error) { return dbcClient.Search(context.Background(), "") } - func findDriver(name string, drivers []dbc.Driver) (dbc.Driver, error) { idx := slices.IndexFunc(drivers, func(d dbc.Driver) bool { return d.Path == name @@ -245,6 +251,7 @@ type cmds struct { Auth *AuthCmd `arg:"subcommand" help:"Manage driver registry credentials"` Completion *completions.Cmd `arg:"subcommand,hidden"` Quiet bool `arg:"-q,--quiet" help:"Suppress all output"` + UserAgent string `arg:"-u,--user-agent" help:"Append a custom string to the User-Agent header"` } func (cmds) Version() string { @@ -464,6 +471,9 @@ func main() { if jm, ok := m.(interface{ IsJSONMode() bool }); ok && jm.IsJSONMode() { needsRenderer = false } + + userAgentSuffix = args.UserAgent + // Work around https://github.com/columnar-tech/dbc/issues/351 usedRenderer := false if !isatty.IsTerminal(os.Stdout.Fd()) || !needsRenderer { diff --git a/cmd/dbc/user_agent_test.go b/cmd/dbc/user_agent_test.go new file mode 100644 index 00000000..f4a91d47 --- /dev/null +++ b/cmd/dbc/user_agent_test.go @@ -0,0 +1,91 @@ +// Copyright 2026 Columnar Technologies Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "runtime" + "testing" + + "github.com/columnar-tech/dbc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func defaultUserAgent() string { + return fmt.Sprintf("dbc-cli/%s (%s; %s)", dbc.Version, runtime.GOOS, runtime.GOARCH) +} + +func TestUserAgentFlagParsing(t *testing.T) { + tests := []struct { + name string + argv []string + want string + }{ + {"short", []string{"-u", "my-tool/2.0", "search"}, "my-tool/2.0"}, + {"long", []string{"--user-agent", "my-tool/2.0", "search"}, "my-tool/2.0"}, + {"long equals", []string{"--user-agent=with spaces (x)", "search"}, "with spaces (x)"}, + {"absent", []string{"search"}, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var args cmds + p, err := newParser(&args) + require.NoError(t, err) + require.NoError(t, p.Parse(tt.argv)) + assert.Equal(t, tt.want, args.UserAgent) + }) + } +} + +// TestNewDBCClientSendsUserAgent verifies the header that actually reaches the +// server, with and without a custom suffix. +func TestNewDBCClientSendsUserAgent(t *testing.T) { + tests := []struct { + name string + suffix string + want string + }{ + {"default", "", defaultUserAgent()}, + {"custom", "my-tool/2.0", defaultUserAgent() + " my-tool/2.0"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got = r.Header.Get("User-Agent") + })) + t.Cleanup(srv.Close) + + t.Setenv("DBC_BASE_URL", srv.URL) + // Keep the expected header independent of the runner's CI env. + t.Setenv("CI", "") + prev := userAgentSuffix + t.Cleanup(func() { userAgentSuffix = prev }) + userAgentSuffix = tt.suffix + + c, err := newDBCClient(nil, nil) + require.NoError(t, err) + assert.Equal(t, tt.want, c.UserAgent()) + + resp, err := c.HTTPClient().Get(srv.URL) + require.NoError(t, err) + resp.Body.Close() + assert.Equal(t, tt.want, got) + }) + } +} From e5e26d79d9765a47a4a2ca7d7d61e6f35372959b Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 22 Oct 2026 12:30:34 -0400 Subject: [PATCH 02/10] Add autocomplete and docs --- cmd/dbc/completions/dbc.bash | 9 ++++++++- cmd/dbc/completions/dbc.fish | 1 + cmd/dbc/completions/dbc.zsh | 4 +++- docs/reference/cli.md | 24 ++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/cmd/dbc/completions/dbc.bash b/cmd/dbc/completions/dbc.bash index 5b604ba9..a5e4ed62 100644 --- a/cmd/dbc/completions/dbc.bash +++ b/cmd/dbc/completions/dbc.bash @@ -5,7 +5,14 @@ _dbc() { _init_completion || return local subcommands="install uninstall list init add sync search info docs remove completion auth" - local global_opts="--help -h --version --quiet -q" + local global_opts="--help -h --version --quiet -q --user-agent -u" + + # --user-agent takes a free-form value; offer no completions for it + case "$prev" in + --user-agent|-u) + return 0 + ;; + esac # If we're completing the first argument (subcommand) if [[ $cword -eq 1 ]]; then diff --git a/cmd/dbc/completions/dbc.fish b/cmd/dbc/completions/dbc.fish index 622d6f96..e9e00827 100644 --- a/cmd/dbc/completions/dbc.fish +++ b/cmd/dbc/completions/dbc.fish @@ -26,6 +26,7 @@ complete -c dbc -n '__fish_dbc_needs_command' -l help -d 'Show help' complete -c dbc -n '__fish_dbc_needs_command' -s h -d 'Show help' complete -c dbc -n '__fish_dbc_needs_command' -l version -d 'Show version' complete -c dbc -n '__fish_dbc_needs_command' -l quiet -s q -d 'Suppress all output' +complete -c dbc -n '__fish_dbc_needs_command' -l user-agent -s u -r -d 'Append a custom string to the User-Agent header' # Subcommands complete -f -c dbc -n '__fish_dbc_needs_command' -a 'install' -d 'Install a driver' diff --git a/cmd/dbc/completions/dbc.zsh b/cmd/dbc/completions/dbc.zsh index c6db9d2c..02acb717 100644 --- a/cmd/dbc/completions/dbc.zsh +++ b/cmd/dbc/completions/dbc.zsh @@ -29,7 +29,9 @@ function _dbc { '-h[Show help]' \ '--version[Show version]' \ '--quiet[Suppress all output]' \ - '-q[Suppress all output]' + '-q[Suppress all output]' \ + '--user-agent[Append a custom string to the User-Agent header]' \ + '-u[Append a custom string to the User-Agent header]' ;; args) case $line[1] in diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 9ad04e90..3c1beebe 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -84,6 +84,10 @@ $ dbc search [PATTERN] : Suppress all output +`--user-agent`, `-u` {{ since_version('v0.4.0') }} + +: Append a custom string to the `User-Agent` header sent with HTTP requests + ## install Install a driver. @@ -126,6 +130,10 @@ $ dbc install [OPTIONS] : Suppress all output +`--user-agent`, `-u` {{ since_version('v0.4.0') }} + +: Append a custom string to the `User-Agent` header sent with HTTP requests + ## uninstall Uninstall a driver. @@ -234,6 +242,10 @@ $ dbc add : Suppress all output +`--user-agent`, `-u` {{ since_version('v0.4.0') }} + +: Append a custom string to the `User-Agent` header sent with HTTP requests + ## remove Remove a driver from the current [driver list](../concepts/driver_list.md). @@ -291,6 +303,10 @@ dbc sync --file dbc.toml : Suppress all output +`--user-agent`, `-u` {{ since_version('v0.4.0') }} + +: Append a custom string to the `User-Agent` header sent with HTTP requests + ## info Get information about a driver. Shows information about the latest version of the driver with the given name. @@ -317,6 +333,10 @@ $ dbc info : Suppress all output +`--user-agent`, `-u` {{ since_version('v0.4.0') }} + +: Append a custom string to the `User-Agent` header sent with HTTP requests + ## docs {{ since_version('v0.2.0') }} @@ -346,6 +366,10 @@ $ dbc docs : Suppress all output +`--user-agent`, `-u` {{ since_version('v0.4.0') }} + +: Append a custom string to the `User-Agent` header sent with HTTP requests + ## auth {{ since_version('v0.2.0') }} From ab170c4cd65a1f2001b4da93ca87841f057aa4f2 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 22 Oct 2026 12:39:17 -0400 Subject: [PATCH 03/10] apply user-agent suffix to auth and download clients; skip global options in completions --- cmd/dbc/completions/dbc.bash | 35 +++++++++++++++++++++++++++++++---- cmd/dbc/completions/dbc.fish | 33 +++++++++++++++++++++++++++------ cmd/dbc/completions/dbc.zsh | 11 ++++++----- cmd/dbc/main.go | 25 +++++++++++++++++++++---- cmd/dbc/user_agent_test.go | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 19 deletions(-) diff --git a/cmd/dbc/completions/dbc.bash b/cmd/dbc/completions/dbc.bash index a5e4ed62..00b39d6f 100644 --- a/cmd/dbc/completions/dbc.bash +++ b/cmd/dbc/completions/dbc.bash @@ -14,14 +14,41 @@ _dbc() { ;; esac - # If we're completing the first argument (subcommand) - if [[ $cword -eq 1 ]]; then + # Skip leading global options (and the --user-agent value) to find the + # subcommand. COMP_WORDS splits "--user-agent=x" into "--user-agent" "=" "x". + local i=1 + while [[ $i -lt $COMP_CWORD ]]; do + case "${COMP_WORDS[i]}" in + --user-agent|-u) + if [[ "${COMP_WORDS[i+1]}" == "=" ]]; then + ((i += 3)) + else + ((i += 2)) + fi + ;; + -*) + ((i += 1)) + ;; + *) + break + ;; + esac + done + + # If we're completing the subcommand itself + if [[ $i -ge $COMP_CWORD ]]; then COMPREPLY=($(compgen -W "$subcommands $global_opts" -- "$cur")) return 0 fi - # Get the subcommand (first argument) - local subcommand="${words[1]}" + local subcommand="${COMP_WORDS[i]}" + + # The per-subcommand completers index COMP_WORDS positionally (subcommand + # at 1), so hand them a view with the global options removed. + local -a sub_words=("${COMP_WORDS[0]}" "${COMP_WORDS[@]:i}") + local sub_cword=$((COMP_CWORD - i + 1)) + local -a COMP_WORDS=("${sub_words[@]}") + local COMP_CWORD=$sub_cword case "$subcommand" in install) diff --git a/cmd/dbc/completions/dbc.fish b/cmd/dbc/completions/dbc.fish index e9e00827..4394273d 100644 --- a/cmd/dbc/completions/dbc.fish +++ b/cmd/dbc/completions/dbc.fish @@ -1,8 +1,29 @@ # fish completion for dbc -*- shell-script -*- +# Tokens before the cursor with leading global options (and the --user-agent +# value) removed, so the subcommand is always at index 2. +function __fish_dbc_tokens + set -l tokens (commandline -opc) + printf '%s\n' $tokens[1] + set -l i 2 + while test $i -le (count $tokens) + switch $tokens[$i] + case -u --user-agent + set i (math $i + 2) + case '-*' + set i (math $i + 1) + case '*' + break + end + end + if test $i -le (count $tokens) + printf '%s\n' $tokens[$i..-1] + end +end + # Helper function to check if a subcommand has been given function __fish_dbc_needs_command - set -l cmd (commandline -opc) + set -l cmd (__fish_dbc_tokens) set -e cmd[1] if test (count $cmd) -eq 0 return 0 @@ -12,7 +33,7 @@ end # Helper function to check if we're using a specific subcommand function __fish_dbc_using_subcommand - set -l cmd (commandline -opc) + set -l cmd (__fish_dbc_tokens) if test (count $cmd) -gt 1 if test $argv[1] = $cmd[2] return 0 @@ -112,7 +133,7 @@ complete -f -c dbc -n '__fish_dbc_using_subcommand completion' -a 'fish' -d 'Gen # Helper function to check if we're using auth subcommand and need a nested subcommand function __fish_dbc_auth_needs_subcommand - set -l cmd (commandline -opc) + set -l cmd (__fish_dbc_tokens) if test (count $cmd) -eq 2 if test $cmd[2] = "auth" return 0 @@ -123,7 +144,7 @@ end # Helper function to check if we're using a specific auth subcommand function __fish_dbc_auth_using_subcommand - set -l cmd (commandline -opc) + set -l cmd (__fish_dbc_tokens) if test (count $cmd) -gt 2 if test $cmd[2] = "auth" -a $argv[1] = $cmd[3] return 0 @@ -151,7 +172,7 @@ complete -f -c dbc -n '__fish_dbc_auth_using_subcommand logout' -l purge -d 'Rem # Helper function to check if we need a license subcommand function __fish_dbc_auth_license_needs_subcommand - set -l cmd (commandline -opc) + set -l cmd (__fish_dbc_tokens) if test (count $cmd) -eq 3 if test $cmd[2] = "auth" -a $cmd[3] = "license" return 0 @@ -162,7 +183,7 @@ end # Helper function to check if we're using a specific auth license subcommand function __fish_dbc_auth_license_using_subcommand - set -l cmd (commandline -opc) + set -l cmd (__fish_dbc_tokens) if test (count $cmd) -gt 3 if test $cmd[2] = "auth" -a $cmd[3] = "license" -a $argv[1] = $cmd[4] return 0 diff --git a/cmd/dbc/completions/dbc.zsh b/cmd/dbc/completions/dbc.zsh index 02acb717..abacc7e9 100644 --- a/cmd/dbc/completions/dbc.zsh +++ b/cmd/dbc/completions/dbc.zsh @@ -6,7 +6,12 @@ compdef _dbc dbc function _dbc { local line state + # Global options are declared here (not only listed as values below) so + # _arguments skips them and the --user-agent value when locating the + # subcommand. _arguments -C \ + '(-q --quiet)'{-q,--quiet}'[Suppress all output]' \ + '(-u --user-agent)'{-u,--user-agent}'[Append a custom string to the User-Agent header]:user agent suffix: ' \ "1: :->subcommand" \ "*::arg:->args" @@ -27,11 +32,7 @@ function _dbc { 'auth[Authenticate with a driver registry]' \ '--help[Show help]' \ '-h[Show help]' \ - '--version[Show version]' \ - '--quiet[Suppress all output]' \ - '-q[Suppress all output]' \ - '--user-agent[Append a custom string to the User-Agent header]' \ - '-u[Append a custom string to the User-Agent header]' + '--version[Show version]' ;; args) case $line[1] in diff --git a/cmd/dbc/main.go b/cmd/dbc/main.go index 18bb9b7f..c23e5f65 100644 --- a/cmd/dbc/main.go +++ b/cmd/dbc/main.go @@ -95,10 +95,15 @@ func newDefaultClient() (*dbc.Client, error) { // authHTTPClient returns an *http.Client suitable for auth flows (device-code // login, license fetch, etc.) that do not need driver registries resolved. -// It bypasses dbc.NewClient so registry misconfiguration — a valid state for -// a user who is about to run `dbc auth login` to fix it — cannot break login. +// It bypasses dbc.NewClient's registry resolution so registry misconfiguration +// — a valid state for a user who is about to run `dbc auth login` to fix it — +// cannot break login. It carries the --user-agent suffix, if any. func authHTTPClient() *http.Client { - c, err := dbc.NewClient(dbc.WithBaseURL("https://placeholder.invalid")) + opts := []dbc.Option{dbc.WithBaseURL("https://placeholder.invalid")} + if userAgentSuffix != "" { + opts = append(opts, dbc.WithUserAgentSuffix(userAgentSuffix)) + } + c, err := dbc.NewClient(opts...) if err != nil { // Impossible in practice: WithBaseURL short-circuits all registry // validation. Fall back to http.DefaultClient so auth still works. @@ -107,6 +112,18 @@ func authHTTPClient() *http.Client { return c.HTTPClient() } +// setUserAgentSuffix records the --user-agent suffix for every client the CLI +// builds. Must run before the program issues requests, as dbc.DefaultClient +// requires. +func setUserAgentSuffix(suffix string) { + userAgentSuffix = suffix + if suffix != "" { + // Package downloads (PkgInfo.DownloadPackage) use the package-level + // client rather than dbcClient. + dbc.DefaultClient = authHTTPClient() + } +} + // newDBCClient builds a client with the given project registry overrides. // Callers pass nil/nil for process-wide operations (search, info, install); // project commands (add, sync, remove) pass the values parsed from dbc.toml. @@ -472,7 +489,7 @@ func main() { needsRenderer = false } - userAgentSuffix = args.UserAgent + setUserAgentSuffix(args.UserAgent) // Work around https://github.com/columnar-tech/dbc/issues/351 usedRenderer := false diff --git a/cmd/dbc/user_agent_test.go b/cmd/dbc/user_agent_test.go index f4a91d47..32e69407 100644 --- a/cmd/dbc/user_agent_test.go +++ b/cmd/dbc/user_agent_test.go @@ -18,6 +18,9 @@ import ( "fmt" "net/http" "net/http/httptest" + "net/url" + "os" + "path/filepath" "runtime" "testing" @@ -89,3 +92,34 @@ func TestNewDBCClientSendsUserAgent(t *testing.T) { }) } } + +// TestSetUserAgentSuffixCoversAuthAndDownloads verifies that the HTTP paths +// outside dbcClient — the auth client and package downloads via +// PkgInfo.DownloadPackage — also send the suffix. +func TestSetUserAgentSuffixCoversAuthAndDownloads(t *testing.T) { + var got []string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got = append(got, r.Header.Get("User-Agent")) + _, _ = w.Write([]byte("pkg")) + })) + t.Cleanup(srv.Close) + t.Setenv("CI", "") + + prevSuffix, prevDefault := userAgentSuffix, dbc.DefaultClient + t.Cleanup(func() { userAgentSuffix, dbc.DefaultClient = prevSuffix, prevDefault }) + setUserAgentSuffix("my-tool/2.0") + want := defaultUserAgent() + " my-tool/2.0" + + resp, err := authHTTPClient().Get(srv.URL) + require.NoError(t, err) + resp.Body.Close() + + u, err := url.Parse(srv.URL + "/pkg.tar.gz") + require.NoError(t, err) + f, err := dbc.PkgInfo{Path: u}.DownloadPackage(func(int64, int64) {}) + require.NoError(t, err) + f.Close() + t.Cleanup(func() { os.RemoveAll(filepath.Dir(f.Name())) }) + + assert.Equal(t, []string{want, want}, got) +} From 501d42734da6b81bfed67859fc9df8cb2020dad5 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 22 Oct 2026 13:08:40 -0400 Subject: [PATCH 04/10] completions: offer nothing for the --user-agent value --- cmd/dbc/completions/dbc.bash | 8 ++++++++ cmd/dbc/completions/dbc.fish | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cmd/dbc/completions/dbc.bash b/cmd/dbc/completions/dbc.bash index 00b39d6f..169a3fa2 100644 --- a/cmd/dbc/completions/dbc.bash +++ b/cmd/dbc/completions/dbc.bash @@ -8,11 +8,19 @@ _dbc() { local global_opts="--help -h --version --quiet -q --user-agent -u" # --user-agent takes a free-form value; offer no completions for it + # (covers both "-u " and "--user-agent=") case "$prev" in --user-agent|-u) return 0 ;; esac + # COMP_WORDS splits "--user-agent=val" into "--user-agent" "=" "val"; the + # cursor is on "=" or on the value word after it. + local w1="${COMP_WORDS[COMP_CWORD-1]}" w2="${COMP_WORDS[COMP_CWORD-2]}" + if [[ "${COMP_WORDS[COMP_CWORD]}" == "=" && "$w1" == --user-agent ]] || + [[ "$w1" == "=" && "$w2" == --user-agent ]]; then + return 0 + fi # Skip leading global options (and the --user-agent value) to find the # subcommand. COMP_WORDS splits "--user-agent=x" into "--user-agent" "=" "x". diff --git a/cmd/dbc/completions/dbc.fish b/cmd/dbc/completions/dbc.fish index 4394273d..ff2e5501 100644 --- a/cmd/dbc/completions/dbc.fish +++ b/cmd/dbc/completions/dbc.fish @@ -47,7 +47,7 @@ complete -c dbc -n '__fish_dbc_needs_command' -l help -d 'Show help' complete -c dbc -n '__fish_dbc_needs_command' -s h -d 'Show help' complete -c dbc -n '__fish_dbc_needs_command' -l version -d 'Show version' complete -c dbc -n '__fish_dbc_needs_command' -l quiet -s q -d 'Suppress all output' -complete -c dbc -n '__fish_dbc_needs_command' -l user-agent -s u -r -d 'Append a custom string to the User-Agent header' +complete -c dbc -n '__fish_dbc_needs_command' -l user-agent -s u -x -d 'Append a custom string to the User-Agent header' # Subcommands complete -f -c dbc -n '__fish_dbc_needs_command' -a 'install' -d 'Install a driver' From 0c953e8550457701f3f2abda3e7ea3e6131bd54e Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 22 Oct 2026 13:13:18 -0400 Subject: [PATCH 05/10] hide the help for the new --user-agent flag --- cmd/dbc/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/dbc/main.go b/cmd/dbc/main.go index c23e5f65..1f4baf07 100644 --- a/cmd/dbc/main.go +++ b/cmd/dbc/main.go @@ -268,7 +268,7 @@ type cmds struct { Auth *AuthCmd `arg:"subcommand" help:"Manage driver registry credentials"` Completion *completions.Cmd `arg:"subcommand,hidden"` Quiet bool `arg:"-q,--quiet" help:"Suppress all output"` - UserAgent string `arg:"-u,--user-agent" help:"Append a custom string to the User-Agent header"` + UserAgent string `arg:"-u,--user-agent,hidden"` } func (cmds) Version() string { From 9cde3460bd235bdb5f2e2b5de4f1bd9628cc1405 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 22 Oct 2026 13:16:28 -0400 Subject: [PATCH 06/10] hide --user-agent from completions and docs --- cmd/dbc/completions/dbc.bash | 4 +++- cmd/dbc/completions/dbc.fish | 16 +++++++++++++++- cmd/dbc/completions/dbc.zsh | 5 +++-- docs/reference/cli.md | 24 ------------------------ 4 files changed, 21 insertions(+), 28 deletions(-) diff --git a/cmd/dbc/completions/dbc.bash b/cmd/dbc/completions/dbc.bash index 169a3fa2..4e12922e 100644 --- a/cmd/dbc/completions/dbc.bash +++ b/cmd/dbc/completions/dbc.bash @@ -5,7 +5,9 @@ _dbc() { _init_completion || return local subcommands="install uninstall list init add sync search info docs remove completion auth" - local global_opts="--help -h --version --quiet -q --user-agent -u" + # --user-agent/-u is a hidden option: never offered, but its value is + # skipped below so completion keeps working when it is used. + local global_opts="--help -h --version --quiet -q" # --user-agent takes a free-form value; offer no completions for it # (covers both "-u " and "--user-agent=") diff --git a/cmd/dbc/completions/dbc.fish b/cmd/dbc/completions/dbc.fish index ff2e5501..823527a8 100644 --- a/cmd/dbc/completions/dbc.fish +++ b/cmd/dbc/completions/dbc.fish @@ -21,8 +21,21 @@ function __fish_dbc_tokens end end +# True when the cursor sits on the value of the hidden --user-agent/-u option, +# which is deliberately not declared to fish (so it is never offered). +function __fish_dbc_in_user_agent_value + switch (commandline -opc)[-1] + case -u --user-agent + return 0 + end + return 1 +end + # Helper function to check if a subcommand has been given function __fish_dbc_needs_command + if __fish_dbc_in_user_agent_value + return 1 + end set -l cmd (__fish_dbc_tokens) set -e cmd[1] if test (count $cmd) -eq 0 @@ -47,7 +60,8 @@ complete -c dbc -n '__fish_dbc_needs_command' -l help -d 'Show help' complete -c dbc -n '__fish_dbc_needs_command' -s h -d 'Show help' complete -c dbc -n '__fish_dbc_needs_command' -l version -d 'Show version' complete -c dbc -n '__fish_dbc_needs_command' -l quiet -s q -d 'Suppress all output' -complete -c dbc -n '__fish_dbc_needs_command' -l user-agent -s u -x -d 'Append a custom string to the User-Agent header' +# The hidden --user-agent value is free-form: no file or other candidates +complete -c dbc -n '__fish_dbc_in_user_agent_value' -f # Subcommands complete -f -c dbc -n '__fish_dbc_needs_command' -a 'install' -d 'Install a driver' diff --git a/cmd/dbc/completions/dbc.zsh b/cmd/dbc/completions/dbc.zsh index abacc7e9..ec80b5a2 100644 --- a/cmd/dbc/completions/dbc.zsh +++ b/cmd/dbc/completions/dbc.zsh @@ -8,10 +8,11 @@ function _dbc { # Global options are declared here (not only listed as values below) so # _arguments skips them and the --user-agent value when locating the - # subcommand. + # subcommand. --user-agent/-u is hidden: "!" keeps it parsed but unoffered. _arguments -C \ '(-q --quiet)'{-q,--quiet}'[Suppress all output]' \ - '(-u --user-agent)'{-u,--user-agent}'[Append a custom string to the User-Agent header]:user agent suffix: ' \ + '!-u:user agent suffix: ' \ + '!--user-agent=:user agent suffix: ' \ "1: :->subcommand" \ "*::arg:->args" diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 3c1beebe..9ad04e90 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -84,10 +84,6 @@ $ dbc search [PATTERN] : Suppress all output -`--user-agent`, `-u` {{ since_version('v0.4.0') }} - -: Append a custom string to the `User-Agent` header sent with HTTP requests - ## install Install a driver. @@ -130,10 +126,6 @@ $ dbc install [OPTIONS] : Suppress all output -`--user-agent`, `-u` {{ since_version('v0.4.0') }} - -: Append a custom string to the `User-Agent` header sent with HTTP requests - ## uninstall Uninstall a driver. @@ -242,10 +234,6 @@ $ dbc add : Suppress all output -`--user-agent`, `-u` {{ since_version('v0.4.0') }} - -: Append a custom string to the `User-Agent` header sent with HTTP requests - ## remove Remove a driver from the current [driver list](../concepts/driver_list.md). @@ -303,10 +291,6 @@ dbc sync --file dbc.toml : Suppress all output -`--user-agent`, `-u` {{ since_version('v0.4.0') }} - -: Append a custom string to the `User-Agent` header sent with HTTP requests - ## info Get information about a driver. Shows information about the latest version of the driver with the given name. @@ -333,10 +317,6 @@ $ dbc info : Suppress all output -`--user-agent`, `-u` {{ since_version('v0.4.0') }} - -: Append a custom string to the `User-Agent` header sent with HTTP requests - ## docs {{ since_version('v0.2.0') }} @@ -366,10 +346,6 @@ $ dbc docs : Suppress all output -`--user-agent`, `-u` {{ since_version('v0.4.0') }} - -: Append a custom string to the `User-Agent` header sent with HTTP requests - ## auth {{ since_version('v0.2.0') }} From 2c6bf91db27377cc9160dacbdab487429c8a5181 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 23 Oct 2026 12:08:29 -0400 Subject: [PATCH 07/10] replace --user-agent flag with DBC_USER_AGENT_SUFFIX env var --- cmd/dbc/completions/dbc.bash | 27 +-------------------------- cmd/dbc/completions/dbc.fish | 21 ++------------------- cmd/dbc/completions/dbc.zsh | 5 +---- cmd/dbc/main.go | 11 +++++------ cmd/dbc/user_agent_test.go | 22 ---------------------- 5 files changed, 9 insertions(+), 77 deletions(-) diff --git a/cmd/dbc/completions/dbc.bash b/cmd/dbc/completions/dbc.bash index 4e12922e..a5faf992 100644 --- a/cmd/dbc/completions/dbc.bash +++ b/cmd/dbc/completions/dbc.bash @@ -5,37 +5,12 @@ _dbc() { _init_completion || return local subcommands="install uninstall list init add sync search info docs remove completion auth" - # --user-agent/-u is a hidden option: never offered, but its value is - # skipped below so completion keeps working when it is used. local global_opts="--help -h --version --quiet -q" - # --user-agent takes a free-form value; offer no completions for it - # (covers both "-u " and "--user-agent=") - case "$prev" in - --user-agent|-u) - return 0 - ;; - esac - # COMP_WORDS splits "--user-agent=val" into "--user-agent" "=" "val"; the - # cursor is on "=" or on the value word after it. - local w1="${COMP_WORDS[COMP_CWORD-1]}" w2="${COMP_WORDS[COMP_CWORD-2]}" - if [[ "${COMP_WORDS[COMP_CWORD]}" == "=" && "$w1" == --user-agent ]] || - [[ "$w1" == "=" && "$w2" == --user-agent ]]; then - return 0 - fi - - # Skip leading global options (and the --user-agent value) to find the - # subcommand. COMP_WORDS splits "--user-agent=x" into "--user-agent" "=" "x". + # Skip leading global options to find the subcommand. local i=1 while [[ $i -lt $COMP_CWORD ]]; do case "${COMP_WORDS[i]}" in - --user-agent|-u) - if [[ "${COMP_WORDS[i+1]}" == "=" ]]; then - ((i += 3)) - else - ((i += 2)) - fi - ;; -*) ((i += 1)) ;; diff --git a/cmd/dbc/completions/dbc.fish b/cmd/dbc/completions/dbc.fish index 823527a8..ef355dd0 100644 --- a/cmd/dbc/completions/dbc.fish +++ b/cmd/dbc/completions/dbc.fish @@ -1,15 +1,13 @@ # fish completion for dbc -*- shell-script -*- -# Tokens before the cursor with leading global options (and the --user-agent -# value) removed, so the subcommand is always at index 2. +# Tokens before the cursor with leading global options removed, so the +# subcommand is always at index 2. function __fish_dbc_tokens set -l tokens (commandline -opc) printf '%s\n' $tokens[1] set -l i 2 while test $i -le (count $tokens) switch $tokens[$i] - case -u --user-agent - set i (math $i + 2) case '-*' set i (math $i + 1) case '*' @@ -21,21 +19,8 @@ function __fish_dbc_tokens end end -# True when the cursor sits on the value of the hidden --user-agent/-u option, -# which is deliberately not declared to fish (so it is never offered). -function __fish_dbc_in_user_agent_value - switch (commandline -opc)[-1] - case -u --user-agent - return 0 - end - return 1 -end - # Helper function to check if a subcommand has been given function __fish_dbc_needs_command - if __fish_dbc_in_user_agent_value - return 1 - end set -l cmd (__fish_dbc_tokens) set -e cmd[1] if test (count $cmd) -eq 0 @@ -60,8 +45,6 @@ complete -c dbc -n '__fish_dbc_needs_command' -l help -d 'Show help' complete -c dbc -n '__fish_dbc_needs_command' -s h -d 'Show help' complete -c dbc -n '__fish_dbc_needs_command' -l version -d 'Show version' complete -c dbc -n '__fish_dbc_needs_command' -l quiet -s q -d 'Suppress all output' -# The hidden --user-agent value is free-form: no file or other candidates -complete -c dbc -n '__fish_dbc_in_user_agent_value' -f # Subcommands complete -f -c dbc -n '__fish_dbc_needs_command' -a 'install' -d 'Install a driver' diff --git a/cmd/dbc/completions/dbc.zsh b/cmd/dbc/completions/dbc.zsh index ec80b5a2..ce44633c 100644 --- a/cmd/dbc/completions/dbc.zsh +++ b/cmd/dbc/completions/dbc.zsh @@ -7,12 +7,9 @@ function _dbc { local line state # Global options are declared here (not only listed as values below) so - # _arguments skips them and the --user-agent value when locating the - # subcommand. --user-agent/-u is hidden: "!" keeps it parsed but unoffered. + # _arguments skips them when locating the subcommand. _arguments -C \ '(-q --quiet)'{-q,--quiet}'[Suppress all output]' \ - '!-u:user agent suffix: ' \ - '!--user-agent=:user agent suffix: ' \ "1: :->subcommand" \ "*::arg:->args" diff --git a/cmd/dbc/main.go b/cmd/dbc/main.go index 1f4baf07..5b2da69a 100644 --- a/cmd/dbc/main.go +++ b/cmd/dbc/main.go @@ -97,7 +97,7 @@ func newDefaultClient() (*dbc.Client, error) { // login, license fetch, etc.) that do not need driver registries resolved. // It bypasses dbc.NewClient's registry resolution so registry misconfiguration // — a valid state for a user who is about to run `dbc auth login` to fix it — -// cannot break login. It carries the --user-agent suffix, if any. +// cannot break login. It carries the DBC_USER_AGENT_SUFFIX suffix, if any. func authHTTPClient() *http.Client { opts := []dbc.Option{dbc.WithBaseURL("https://placeholder.invalid")} if userAgentSuffix != "" { @@ -112,9 +112,9 @@ func authHTTPClient() *http.Client { return c.HTTPClient() } -// setUserAgentSuffix records the --user-agent suffix for every client the CLI -// builds. Must run before the program issues requests, as dbc.DefaultClient -// requires. +// setUserAgentSuffix records the DBC_USER_AGENT_SUFFIX value for every client +// the CLI builds. Must run before the program issues requests, as +// dbc.DefaultClient requires. func setUserAgentSuffix(suffix string) { userAgentSuffix = suffix if suffix != "" { @@ -268,7 +268,6 @@ type cmds struct { Auth *AuthCmd `arg:"subcommand" help:"Manage driver registry credentials"` Completion *completions.Cmd `arg:"subcommand,hidden"` Quiet bool `arg:"-q,--quiet" help:"Suppress all output"` - UserAgent string `arg:"-u,--user-agent,hidden"` } func (cmds) Version() string { @@ -489,7 +488,7 @@ func main() { needsRenderer = false } - setUserAgentSuffix(args.UserAgent) + setUserAgentSuffix(os.Getenv("DBC_USER_AGENT_SUFFIX")) // Work around https://github.com/columnar-tech/dbc/issues/351 usedRenderer := false diff --git a/cmd/dbc/user_agent_test.go b/cmd/dbc/user_agent_test.go index 32e69407..ddd78358 100644 --- a/cmd/dbc/user_agent_test.go +++ b/cmd/dbc/user_agent_test.go @@ -33,28 +33,6 @@ func defaultUserAgent() string { return fmt.Sprintf("dbc-cli/%s (%s; %s)", dbc.Version, runtime.GOOS, runtime.GOARCH) } -func TestUserAgentFlagParsing(t *testing.T) { - tests := []struct { - name string - argv []string - want string - }{ - {"short", []string{"-u", "my-tool/2.0", "search"}, "my-tool/2.0"}, - {"long", []string{"--user-agent", "my-tool/2.0", "search"}, "my-tool/2.0"}, - {"long equals", []string{"--user-agent=with spaces (x)", "search"}, "with spaces (x)"}, - {"absent", []string{"search"}, ""}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var args cmds - p, err := newParser(&args) - require.NoError(t, err) - require.NoError(t, p.Parse(tt.argv)) - assert.Equal(t, tt.want, args.UserAgent) - }) - } -} - // TestNewDBCClientSendsUserAgent verifies the header that actually reaches the // server, with and without a custom suffix. func TestNewDBCClientSendsUserAgent(t *testing.T) { From bedc001379c986d0575148c878bcdac1861c0a6a Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 23 Oct 2026 12:10:14 -0400 Subject: [PATCH 08/10] docs: document DBC_USER_AGENT_SUFFIX --- docs/reference/cli.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 9ad04e90..69dfd755 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -50,6 +50,12 @@ $ dbc [OPTIONS]
dbc auth

Manage driver registry credentials

+

Environment Variables

+ +`DBC_USER_AGENT_SUFFIX` {{ since_version('v0.4.0') }} + +: Append a custom string to the `User-Agent` header sent with dbc's HTTP requests (driver registry lookups, package downloads, and `dbc auth login`) + ## search Search for a driver to install. From 7673dabc41a141e57d8bc5afd8fbb2d98adf3e5b Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 23 Oct 2026 12:12:32 -0400 Subject: [PATCH 09/10] Revert "docs: document DBC_USER_AGENT_SUFFIX" This reverts commit bedc001379c986d0575148c878bcdac1861c0a6a. --- docs/reference/cli.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 69dfd755..9ad04e90 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -50,12 +50,6 @@ $ dbc [OPTIONS]
dbc auth

Manage driver registry credentials

-

Environment Variables

- -`DBC_USER_AGENT_SUFFIX` {{ since_version('v0.4.0') }} - -: Append a custom string to the `User-Agent` header sent with dbc's HTTP requests (driver registry lookups, package downloads, and `dbc auth login`) - ## search Search for a driver to install. From 03d2a2f4eddebd9791001b4107992419d4339541 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 23 Oct 2026 12:54:37 -0400 Subject: [PATCH 10/10] revert completion changes (moved to separate PR) --- cmd/dbc/completions/dbc.bash | 27 ++++----------------------- cmd/dbc/completions/dbc.fish | 31 ++++++------------------------- cmd/dbc/completions/dbc.zsh | 7 +++---- 3 files changed, 13 insertions(+), 52 deletions(-) diff --git a/cmd/dbc/completions/dbc.bash b/cmd/dbc/completions/dbc.bash index a5faf992..5b604ba9 100644 --- a/cmd/dbc/completions/dbc.bash +++ b/cmd/dbc/completions/dbc.bash @@ -7,33 +7,14 @@ _dbc() { local subcommands="install uninstall list init add sync search info docs remove completion auth" local global_opts="--help -h --version --quiet -q" - # Skip leading global options to find the subcommand. - local i=1 - while [[ $i -lt $COMP_CWORD ]]; do - case "${COMP_WORDS[i]}" in - -*) - ((i += 1)) - ;; - *) - break - ;; - esac - done - - # If we're completing the subcommand itself - if [[ $i -ge $COMP_CWORD ]]; then + # If we're completing the first argument (subcommand) + if [[ $cword -eq 1 ]]; then COMPREPLY=($(compgen -W "$subcommands $global_opts" -- "$cur")) return 0 fi - local subcommand="${COMP_WORDS[i]}" - - # The per-subcommand completers index COMP_WORDS positionally (subcommand - # at 1), so hand them a view with the global options removed. - local -a sub_words=("${COMP_WORDS[0]}" "${COMP_WORDS[@]:i}") - local sub_cword=$((COMP_CWORD - i + 1)) - local -a COMP_WORDS=("${sub_words[@]}") - local COMP_CWORD=$sub_cword + # Get the subcommand (first argument) + local subcommand="${words[1]}" case "$subcommand" in install) diff --git a/cmd/dbc/completions/dbc.fish b/cmd/dbc/completions/dbc.fish index ef355dd0..622d6f96 100644 --- a/cmd/dbc/completions/dbc.fish +++ b/cmd/dbc/completions/dbc.fish @@ -1,27 +1,8 @@ # fish completion for dbc -*- shell-script -*- -# Tokens before the cursor with leading global options removed, so the -# subcommand is always at index 2. -function __fish_dbc_tokens - set -l tokens (commandline -opc) - printf '%s\n' $tokens[1] - set -l i 2 - while test $i -le (count $tokens) - switch $tokens[$i] - case '-*' - set i (math $i + 1) - case '*' - break - end - end - if test $i -le (count $tokens) - printf '%s\n' $tokens[$i..-1] - end -end - # Helper function to check if a subcommand has been given function __fish_dbc_needs_command - set -l cmd (__fish_dbc_tokens) + set -l cmd (commandline -opc) set -e cmd[1] if test (count $cmd) -eq 0 return 0 @@ -31,7 +12,7 @@ end # Helper function to check if we're using a specific subcommand function __fish_dbc_using_subcommand - set -l cmd (__fish_dbc_tokens) + set -l cmd (commandline -opc) if test (count $cmd) -gt 1 if test $argv[1] = $cmd[2] return 0 @@ -130,7 +111,7 @@ complete -f -c dbc -n '__fish_dbc_using_subcommand completion' -a 'fish' -d 'Gen # Helper function to check if we're using auth subcommand and need a nested subcommand function __fish_dbc_auth_needs_subcommand - set -l cmd (__fish_dbc_tokens) + set -l cmd (commandline -opc) if test (count $cmd) -eq 2 if test $cmd[2] = "auth" return 0 @@ -141,7 +122,7 @@ end # Helper function to check if we're using a specific auth subcommand function __fish_dbc_auth_using_subcommand - set -l cmd (__fish_dbc_tokens) + set -l cmd (commandline -opc) if test (count $cmd) -gt 2 if test $cmd[2] = "auth" -a $argv[1] = $cmd[3] return 0 @@ -169,7 +150,7 @@ complete -f -c dbc -n '__fish_dbc_auth_using_subcommand logout' -l purge -d 'Rem # Helper function to check if we need a license subcommand function __fish_dbc_auth_license_needs_subcommand - set -l cmd (__fish_dbc_tokens) + set -l cmd (commandline -opc) if test (count $cmd) -eq 3 if test $cmd[2] = "auth" -a $cmd[3] = "license" return 0 @@ -180,7 +161,7 @@ end # Helper function to check if we're using a specific auth license subcommand function __fish_dbc_auth_license_using_subcommand - set -l cmd (__fish_dbc_tokens) + set -l cmd (commandline -opc) if test (count $cmd) -gt 3 if test $cmd[2] = "auth" -a $cmd[3] = "license" -a $argv[1] = $cmd[4] return 0 diff --git a/cmd/dbc/completions/dbc.zsh b/cmd/dbc/completions/dbc.zsh index ce44633c..c6db9d2c 100644 --- a/cmd/dbc/completions/dbc.zsh +++ b/cmd/dbc/completions/dbc.zsh @@ -6,10 +6,7 @@ compdef _dbc dbc function _dbc { local line state - # Global options are declared here (not only listed as values below) so - # _arguments skips them when locating the subcommand. _arguments -C \ - '(-q --quiet)'{-q,--quiet}'[Suppress all output]' \ "1: :->subcommand" \ "*::arg:->args" @@ -30,7 +27,9 @@ function _dbc { 'auth[Authenticate with a driver registry]' \ '--help[Show help]' \ '-h[Show help]' \ - '--version[Show version]' + '--version[Show version]' \ + '--quiet[Suppress all output]' \ + '-q[Suppress all output]' ;; args) case $line[1] in