Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 130 additions & 6 deletions dvc/cli/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,27 @@
_dvc_compgen_config_vars() {
compgen -W "${_dvc_config_vars[*]}" -- $1
}

_dvc_compgen_remote_config_vars() {
local cur prev words cword remote_name filtered
_init_completion || return

# last non-option word before cursor
local nonopts=()
for w in "${words[@]:1:$((cword-1))}"; do
[[ $w == -* ]] || nonopts+=("$w")
done

remote_name="${nonopts[-1]}"
if [[ -n $remote_name ]]; then
local _dvc_remote_config_vars=($(
dvc config --available-options -q 2> /dev/null \
| grep "^remote\\.${remote_name}\\." \
| sed "s/^remote\\.${remote_name}\\.//"
))
compgen -W "${_dvc_remote_config_vars[*]}" -- $1
fi
}
"""

ZSH_PREAMBLE = """
Expand Down Expand Up @@ -69,27 +90,111 @@
_describe 'remotes' "($(dvc remote list | cut -d' ' -f1))"
}

_dvc_config_config_vars_for_completion() {
dvc config --available-options -q 2> /dev/null
}

_dvc_compadd_config_vars() {
_describe 'config_vars' _dvc_config_vars
}

_dvc_compadd_remote_config_vars() {
local remote_name filtered

# last non-option word before cursor
remote_name=${${words[1,CURRENT-1]##-*}[-1]}
if [[ -n $remote_name ]]; then
filtered=$(_dvc_config_config_vars_for_completion \\
| grep "^remote.${remote_name}\\\\." \\
| sed "s/^remote\\\\.${remote_name}\\\\.//"
)
compadd -- $=filtered
fi
}
"""

FISH_PREAMBLE = """
function __fish_complete_dvc_files
__fish_complete_path | string match -re '\\*?.dvc|Dvcfile|dvc\\.yaml'
end

function __fish_complete_dvc_stages
for line in (dvc stage list -q)
set -l parts (string split -m1 ' ' -- $line)
set -l name $parts[1]
set -l desc (string trim $parts[2])
echo -e "$name $desc"
end
end

function __fish_complete_dvc_stages_and_files
__fish_complete_dvc_stages
__fish_complete_dvc_files
end

function __fish_complete_dvc_experiments
dvc exp list -q --all-commits --names-only
end

function __fish_complete_dvc_remotes
dvc remote list | cut -d' ' -f1
end

function __fish_complete_dvc_remote_config_vars
# last non-option word before cursor
set -l remote_name (commandline -opc | string match -rv '^-' | tail -n 1)

if test -n "$remote_name"
dvc config --available-options -q 2>/dev/null |
string match -re "^remote\\.$remote_name\\." |
string replace -r "^remote\\.$remote_name\\." ''
end
end
"""

PREAMBLE = {
"bash": BASH_PREAMBLE,
"zsh": ZSH_PREAMBLE,
"fish": FISH_PREAMBLE,
}

FILE = shtab.FILE
DIR = shtab.DIRECTORY
DVC_FILE = {"bash": "_dvc_compgen_DVCFiles", "zsh": "_dvc_compadd_DVCFiles"}
STAGE = {"bash": "_dvc_compgen_stages", "zsh": "_dvc_compadd_stages"}
DVC_FILE = {
"bash": "_dvc_compgen_DVCFiles",
"zsh": "_dvc_compadd_DVCFiles",
"fish": '-f -a "(__fish_complete_dvc_files)"',
}
STAGE = {
"bash": "_dvc_compgen_stages",
"zsh": "_dvc_compadd_stages",
"fish": '-f -a "(__fish_complete_dvc_stages)"',
}
DVCFILES_AND_STAGE = {
"bash": "_dvc_compgen_stages_and_files",
"zsh": "_dvc_compadd_stages_and_files",
"fish": '-f -a "(__fish_complete_dvc_stages_and_files)"',
}
EXPERIMENT = {
"bash": "_dvc_compgen_exps",
"zsh": "_dvc_compadd_exps",
"fish": '-f -a "(__fish_complete_dvc_experiments)"',
}
REMOTE = {
"bash": "_dvc_compgen_remotes",
"zsh": "_dvc_compadd_remotes",
"fish": '-f -a "(__fish_complete_dvc_remotes)"',
}
CONFIG_VARS = {
"bash": "_dvc_compgen_config_vars",
"zsh": "_dvc_compadd_config_vars",
"fish": '-f -a "(__fish_complete_dvc_config_vars)"',
}
REMOTE_CONFIG_VARS = {
"bash": "_dvc_compgen_remote_config_vars",
"zsh": "_dvc_compadd_remote_config_vars",
"fish": '-f -a "(__fish_complete_dvc_remote_config_vars)"',
}
EXPERIMENT = {"bash": "_dvc_compgen_exps", "zsh": "_dvc_compadd_exps"}
REMOTE = {"bash": "_dvc_compgen_remotes", "zsh": "_dvc_compadd_remotes"}
CONFIG_VARS = {"bash": "_dvc_compgen_config_vars", "zsh": "_dvc_compadd_config_vars"}


def get_preamble() -> dict[str, str]:
Expand All @@ -103,7 +208,26 @@ def get_preamble() -> dict[str, str]:
_dvc_config_vars=(
{nl.join(config_vars)}
)
"""
indent = "\t\t".expandtabs(4) # 8 spaces
lines = (
"\n".join(
f"{indent}{c} \\"
for c in config_vars[:-1] # all but last
)
+ "\n"
+ f"{indent}{config_vars[-1]}"
) # last line without backslash
config_vars_arr_fish = f"""
function __fish_complete_dvc_config_vars
set -l _dvc_config_vars \\
{lines}
printf %s\\n $_dvc_config_vars
end
"""
for shell, preamble in PREAMBLE.items():
ret[shell] = config_vars_arr + preamble
if shell != "fish":
ret[shell] = config_vars_arr + preamble
else:
ret[shell] = config_vars_arr_fish + preamble
return ret
2 changes: 1 addition & 1 deletion dvc/commands/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
logger = logger.getChild(__name__)


SUPPORTED_SHELLS = ["bash", "zsh"]
SUPPORTED_SHELLS = ["bash", "zsh", "fish"]


class CmdCompletion(CmdBaseNoRepo):
Expand Down
12 changes: 12 additions & 0 deletions dvc/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ def __init__(self, args):
self.config = Config.from_cwd(validate=False)

def run(self):
if self.args.available_options:
from dvc.config_schema import contextual_config_vars_for_completion

ui.write(
"\n".join(list(contextual_config_vars_for_completion(self.config))),
force=True,
)
return 0

if self.args.show_origin and (self.args.value or self.args.unset):
logger.error(
"--show-origin can't be used together with any of these "
Expand Down Expand Up @@ -212,6 +221,9 @@ def add_parser(subparsers, parent_parser):
help=CONFIG_HELP,
formatter_class=formatter.RawDescriptionHelpFormatter,
)
config_parser.add_argument(
"--available-options", default=False, action="store_true"
)
config_parser.add_argument(
"-u",
"--unset",
Expand Down
4 changes: 3 additions & 1 deletion dvc/commands/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,9 @@ def add_parser(subparsers, parent_parser):
remote_modify_parser.add_argument(
"name", help="Name of the remote"
).complete = completion.REMOTE
remote_modify_parser.add_argument("option", help="Name of the option to modify.")
remote_modify_parser.add_argument(
"option", help="Name of the option to modify."
).complete = completion.REMOTE_CONFIG_VARS
remote_modify_parser.add_argument(
"value", nargs="?", help="(optional) Value of the option."
)
Expand Down
48 changes: 34 additions & 14 deletions dvc/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,27 @@ def Choices(*choices): # noqa: N802
return Any(*choices, msg="expected one of {}".format(", ".join(choices)))


def _get_schema_from_url(url: str) -> str:
parsed = urlparse(url)
# Windows absolute paths should really have scheme == "" (local)
if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc:
return ""
if not parsed.netloc:
return ""
return parsed.scheme


def ByUrl(mapping): # noqa: N802
schemas = walk_values(Schema, mapping)
schemas: dict[str, Schema] = walk_values(Schema, mapping)

def validate(data):
if "url" not in data:
raise Invalid("expected 'url'")

parsed = urlparse(data["url"])
# Windows absolute paths should really have scheme == "" (local)
if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc:
return schemas[""](data)
if not parsed.netloc:
return schemas[""](data)
if parsed.scheme not in schemas:
raise Invalid(f"Unsupported URL type {parsed.scheme}://")

return schemas[parsed.scheme](data)
scheme = _get_schema_from_url(data["url"])
if scheme not in schemas:
raise Invalid(f"Unsupported URL type {scheme}://")
return schemas[scheme](data)

return validate

Expand Down Expand Up @@ -396,9 +400,25 @@ def config_vars_for_completion(d: dict = SCHEMA, path: str = "") -> "Iterator[st
k = k.schema
if not isinstance(k, str):
continue

keypath = path + k
keypath = f"{path}.{k}" if path else k
if isinstance(v, dict):
yield from config_vars_for_completion(v, keypath + ".")
yield from config_vars_for_completion(v, keypath)
else:
yield keypath


def contextual_config_vars_for_completion(config) -> "Iterator[str]":
yield from config_vars_for_completion()
for name, data in config["remote"].items():
if "url" not in data:
continue
scheme = _get_schema_from_url(data["url"])
if scheme not in REMOTE_SCHEMAS:
continue
schema = REMOTE_SCHEMAS[scheme]
if schema:
yield from config_vars_for_completion(schema, f"remote.{name}") # type: ignore[arg-type]

db_schema = SCHEMA["db"][str] # type: ignore[index]
for name in config["db"]:
yield from config_vars_for_completion(db_schema, f"db.{name}") # type: ignore[arg-type]
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ dependencies = [
"ruamel.yaml>=0.17.11",
"scmrepo>=3.5.2,<4",
"shortuuid>=0.5",
"shtab<2,>=1.3.4",
# "shtab<2,>=1.3.4",
"shtab @ git+https://github.com/skshetry/shtab.git@fish-shell-v2",
"tabulate>=0.8.7",
"tomlkit>=0.11.1",
"tqdm<5,>=4.63.1",
Expand Down
Loading