mirror of
https://github.com/containers/podman.git
synced 2025-06-23 18:59:30 +08:00
shell completion for paths inside the image/container
Add shell completion for paths inside the container or image. Currently podman run IMAGE [TAB] only uses the default shell completion which suggests paths on the host. This is fine for some cases but often the user wants a path which only exists in the image/container. This commits adds support for that. Both podman create/run can now complete the paths from the image, podman cp ctr:... now completes paths from the actual container. Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
@ -4,6 +4,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -21,6 +22,7 @@ import (
|
|||||||
"github.com/containers/podman/v4/pkg/signal"
|
"github.com/containers/podman/v4/pkg/signal"
|
||||||
systemdDefine "github.com/containers/podman/v4/pkg/systemd/define"
|
systemdDefine "github.com/containers/podman/v4/pkg/systemd/define"
|
||||||
"github.com/containers/podman/v4/pkg/util"
|
"github.com/containers/podman/v4/pkg/util"
|
||||||
|
securejoin "github.com/cyphar/filepath-securejoin"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -282,6 +284,61 @@ func getNetworks(cmd *cobra.Command, toComplete string, cType completeType) ([]s
|
|||||||
return suggestions, cobra.ShellCompDirectiveNoFileComp
|
return suggestions, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPathCompletion(root string, toComplete string) []string {
|
||||||
|
if toComplete == "" {
|
||||||
|
toComplete = "/"
|
||||||
|
}
|
||||||
|
// Important: securejoin is required to make sure we never leave the root mount point
|
||||||
|
userpath, err := securejoin.SecureJoin(root, toComplete)
|
||||||
|
if err != nil {
|
||||||
|
cobra.CompErrorln(err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var base string
|
||||||
|
f, err := os.Open(userpath)
|
||||||
|
if err != nil {
|
||||||
|
// Do not use path.Dir() since this cleans the paths which
|
||||||
|
// then no longer matches the user input.
|
||||||
|
userpath, base = path.Split(userpath)
|
||||||
|
toComplete, _ = path.Split(toComplete)
|
||||||
|
f, err = os.Open(userpath)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stat, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
cobra.CompErrorln(err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !stat.IsDir() {
|
||||||
|
// nothing to complete since it is no dir
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entries, err := f.ReadDir(-1)
|
||||||
|
if err != nil {
|
||||||
|
cobra.CompErrorln(err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
completions := make([]string, 0, len(entries))
|
||||||
|
for _, e := range entries {
|
||||||
|
if strings.HasPrefix(e.Name(), base) {
|
||||||
|
completions = append(completions, simplePathJoinUnix(toComplete, e.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return completions
|
||||||
|
}
|
||||||
|
|
||||||
|
// simplePathJoinUnix joins to path components by adding a slash only if p1 doesn't end with one.
|
||||||
|
// We cannot use path.Join() for the completions logic because this one always calls Clean() on
|
||||||
|
// the path which changes it from the input.
|
||||||
|
func simplePathJoinUnix(p1, p2 string) string {
|
||||||
|
if p1[len(p1)-1] == '/' {
|
||||||
|
return p1 + p2
|
||||||
|
}
|
||||||
|
return p1 + "/" + p2
|
||||||
|
}
|
||||||
|
|
||||||
// validCurrentCmdLine validates the current cmd line
|
// validCurrentCmdLine validates the current cmd line
|
||||||
// It utilizes the Args function from the cmd struct
|
// It utilizes the Args function from the cmd struct
|
||||||
// In most cases the Args function validates the args length but it
|
// In most cases the Args function validates the args length but it
|
||||||
@ -523,10 +580,34 @@ func AutocompleteCreateRun(cmd *cobra.Command, args []string, toComplete string)
|
|||||||
}
|
}
|
||||||
return getImages(cmd, toComplete)
|
return getImages(cmd, toComplete)
|
||||||
}
|
}
|
||||||
// TODO: add path completion for files in the image
|
// Mount the image and provide path completion
|
||||||
|
engine, err := setupImageEngine(cmd)
|
||||||
|
if err != nil {
|
||||||
|
cobra.CompErrorln(err.Error())
|
||||||
return nil, cobra.ShellCompDirectiveDefault
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resp, err := engine.Mount(registry.Context(), []string{args[0]}, entities.ImageMountOptions{})
|
||||||
|
if err != nil {
|
||||||
|
cobra.CompErrorln(err.Error())
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_, err := engine.Unmount(registry.Context(), []string{args[0]}, entities.ImageUnmountOptions{})
|
||||||
|
if err != nil {
|
||||||
|
cobra.CompErrorln(err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if len(resp) != 1 {
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
// So this uses ShellCompDirectiveDefault to also still provide normal shell
|
||||||
|
// completion in case no path matches. This is useful if someone tries to get
|
||||||
|
// completion for paths that are not available in the image, e.g. /proc/...
|
||||||
|
return getPathCompletion(resp[0].Path, toComplete), cobra.ShellCompDirectiveDefault | cobra.ShellCompDirectiveNoSpace
|
||||||
|
}
|
||||||
|
|
||||||
// AutocompleteRegistries - Autocomplete registries.
|
// AutocompleteRegistries - Autocomplete registries.
|
||||||
func AutocompleteRegistries(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
func AutocompleteRegistries(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if !validCurrentCmdLine(cmd, args, toComplete) {
|
if !validCurrentCmdLine(cmd, args, toComplete) {
|
||||||
@ -572,14 +653,39 @@ func AutocompleteCpCommand(cmd *cobra.Command, args []string, toComplete string)
|
|||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
|
if i := strings.IndexByte(toComplete, ':'); i > -1 {
|
||||||
|
// Looks like the user already set the container.
|
||||||
|
// Lets mount it and provide path completion for files in the container.
|
||||||
|
engine, err := setupContainerEngine(cmd)
|
||||||
|
if err != nil {
|
||||||
|
cobra.CompErrorln(err.Error())
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := engine.ContainerMount(registry.Context(), []string{toComplete[:i]}, entities.ContainerMountOptions{})
|
||||||
|
if err != nil {
|
||||||
|
cobra.CompErrorln(err.Error())
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_, err := engine.ContainerUnmount(registry.Context(), []string{toComplete[:i]}, entities.ContainerUnmountOptions{})
|
||||||
|
if err != nil {
|
||||||
|
cobra.CompErrorln(err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if len(resp) != 1 {
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
|
return prefixSlice(toComplete[:i+1], getPathCompletion(resp[0].Path, toComplete[i+1:])), cobra.ShellCompDirectiveDefault | cobra.ShellCompDirectiveNoSpace
|
||||||
|
}
|
||||||
|
// Suggest containers when they match the input otherwise normal shell completion is used
|
||||||
containers, _ := getContainers(cmd, toComplete, completeDefault)
|
containers, _ := getContainers(cmd, toComplete, completeDefault)
|
||||||
for _, container := range containers {
|
for _, container := range containers {
|
||||||
// TODO: Add path completion for inside the container if possible
|
|
||||||
if strings.HasPrefix(container, toComplete) {
|
if strings.HasPrefix(container, toComplete) {
|
||||||
return containers, cobra.ShellCompDirectiveNoSpace
|
return suffixCompSlice(":", containers), cobra.ShellCompDirectiveNoSpace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// else complete paths
|
// else complete paths on the host
|
||||||
return nil, cobra.ShellCompDirectiveDefault
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
}
|
}
|
||||||
// don't complete more than 2 args
|
// don't complete more than 2 args
|
||||||
|
@ -8,6 +8,16 @@
|
|||||||
|
|
||||||
load helpers
|
load helpers
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
# $PODMAN may be a space-separated string, e.g. if we include a --url.
|
||||||
|
local -a podman_as_array=($PODMAN)
|
||||||
|
# __completeNoDesc must be the first arg if we running the completion cmd
|
||||||
|
# set the var for the run_completion function
|
||||||
|
PODMAN_COMPLETION="${podman_as_array[0]} __completeNoDesc ${podman_as_array[@]:1}"
|
||||||
|
|
||||||
|
basic_setup
|
||||||
|
}
|
||||||
|
|
||||||
# Returns true if we are able to podman-pause
|
# Returns true if we are able to podman-pause
|
||||||
function _can_pause() {
|
function _can_pause() {
|
||||||
# Even though we're just trying completion, not an actual unpause,
|
# Even though we're just trying completion, not an actual unpause,
|
||||||
@ -88,8 +98,14 @@ function check_shell_completion() {
|
|||||||
continue 2
|
continue 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
name=$random_container_name
|
||||||
|
# special case podman cp suggest containers names with a colon
|
||||||
|
if [[ $cmd = "cp" ]]; then
|
||||||
|
name="$name:"
|
||||||
|
fi
|
||||||
|
|
||||||
run_completion "$@" $cmd "${extra_args[@]}" ""
|
run_completion "$@" $cmd "${extra_args[@]}" ""
|
||||||
is "$output" ".*-$random_container_name${nl}" \
|
is "$output" ".*-$name${nl}" \
|
||||||
"$* $cmd: actual container listed in suggestions"
|
"$* $cmd: actual container listed in suggestions"
|
||||||
|
|
||||||
match=true
|
match=true
|
||||||
@ -175,7 +191,7 @@ function check_shell_completion() {
|
|||||||
_check_completion_end NoSpace
|
_check_completion_end NoSpace
|
||||||
else
|
else
|
||||||
_check_completion_end Default
|
_check_completion_end Default
|
||||||
assert "${#lines[@]}" -eq 2 "$* $cmd: Suggestions are in the output"
|
_check_no_suggestions
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
@ -205,16 +221,7 @@ function check_shell_completion() {
|
|||||||
if [[ ! ${args##* } =~ "..." ]]; then
|
if [[ ! ${args##* } =~ "..." ]]; then
|
||||||
run_completion "$@" $cmd "${extra_args[@]}" ""
|
run_completion "$@" $cmd "${extra_args[@]}" ""
|
||||||
_check_completion_end NoFileComp
|
_check_completion_end NoFileComp
|
||||||
if [ ${#lines[@]} -gt 2 ]; then
|
_check_no_suggestions
|
||||||
# checking for line count is not enough since we may include additional debug output
|
|
||||||
# lines starting with [Debug] are allowed
|
|
||||||
i=0
|
|
||||||
length=$(( ${#lines[@]} - 2 ))
|
|
||||||
while [[ i -lt length ]]; do
|
|
||||||
assert "${lines[$i]:0:7}" == "[Debug]" "Suggestions are in the output"
|
|
||||||
i=$(( i + 1 ))
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
done
|
done
|
||||||
@ -231,6 +238,24 @@ function _check_completion_end() {
|
|||||||
is "${lines[-1]}" "Completion ended with directive: ShellCompDirective$1" "Completion has wrong ShellCompDirective set"
|
is "${lines[-1]}" "Completion ended with directive: ShellCompDirective$1" "Completion has wrong ShellCompDirective set"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check that there are no suggestions in the output.
|
||||||
|
# We could only check stdout and not stderr but this is not possible with bats.
|
||||||
|
# By default we always have two extra lines at the end for the ShellCompDirective.
|
||||||
|
# Then we could also have other extra lines for debugging, they will always start
|
||||||
|
# with [Debug], e.g. `[Debug] [Error] no container with name or ID "t12" found: no such container`.
|
||||||
|
function _check_no_suggestions() {
|
||||||
|
if [ ${#lines[@]} -gt 2 ]; then
|
||||||
|
# Checking for line count is not enough since we may include additional debug output.
|
||||||
|
# Lines starting with [Debug] are allowed.
|
||||||
|
local i=0
|
||||||
|
length=$((${#lines[@]} - 2))
|
||||||
|
while [[ i -lt length ]]; do
|
||||||
|
assert "${lines[$i]:0:7}" == "[Debug]" "Unexpected non-Debug output line: ${lines[$i]}"
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@test "podman shell completion test" {
|
@test "podman shell completion test" {
|
||||||
|
|
||||||
@ -280,11 +305,6 @@ function _check_completion_end() {
|
|||||||
# create secret
|
# create secret
|
||||||
run_podman secret create $random_secret_name $secret_file
|
run_podman secret create $random_secret_name $secret_file
|
||||||
|
|
||||||
# $PODMAN may be a space-separated string, e.g. if we include a --url.
|
|
||||||
local -a podman_as_array=($PODMAN)
|
|
||||||
# __completeNoDesc must be the first arg if we running the completion cmd
|
|
||||||
PODMAN_COMPLETION="${podman_as_array[0]} __completeNoDesc ${podman_as_array[@]:1}"
|
|
||||||
|
|
||||||
# Called with no args -- start with 'podman --help'. check_shell_completion() will
|
# Called with no args -- start with 'podman --help'. check_shell_completion() will
|
||||||
# recurse for any subcommands.
|
# recurse for any subcommands.
|
||||||
check_shell_completion
|
check_shell_completion
|
||||||
@ -316,3 +336,41 @@ function _check_completion_end() {
|
|||||||
done <<<"$output"
|
done <<<"$output"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "podman shell completion for paths in container/image" {
|
||||||
|
skip_if_remote "mounting via remote does not work"
|
||||||
|
for cmd in create run; do
|
||||||
|
run_completion $cmd $IMAGE ""
|
||||||
|
assert "$output" =~ ".*^/etc\$.*^/home\$.*^/root\$.*" "root directories suggested (cmd: podman $cmd)"
|
||||||
|
|
||||||
|
# check completion for subdirectory
|
||||||
|
run_completion $cmd $IMAGE "/etc"
|
||||||
|
# It should be safe to assume the os-release file always exists in $IMAGE
|
||||||
|
assert "$output" =~ ".*^/etc/os-release\$.*" "/etc files suggested (cmd: podman $cmd /etc)"
|
||||||
|
# check completion for partial file name
|
||||||
|
run_completion $cmd $IMAGE "/etc/os-"
|
||||||
|
assert "$output" =~ ".*^/etc/os-release\$.*" "/etc files suggested (cmd: podman $cmd /etc/os-)"
|
||||||
|
|
||||||
|
# check completion with relative path components
|
||||||
|
# It is important the we will still use the image root and not escape to the host
|
||||||
|
run_completion $cmd $IMAGE "../../"
|
||||||
|
assert "$output" =~ ".*^../../etc\$.*^../../home\$.*" "relative root directories suggested (cmd: podman $cmd ../../)"
|
||||||
|
done
|
||||||
|
|
||||||
|
random_name=$(random_string 30)
|
||||||
|
random_file=$(random_string 30)
|
||||||
|
run_podman run --name $random_name $IMAGE touch /tmp/$random_file
|
||||||
|
|
||||||
|
# check completion for podman cp
|
||||||
|
run_completion cp ""
|
||||||
|
assert "$output" =~ ".*^$random_name\:\$.*" "podman cp suggest container names"
|
||||||
|
|
||||||
|
run_completion cp "$random_name:"
|
||||||
|
assert "$output" =~ ".*^$random_name\:/etc\$.*" "podman cp suggest paths in container"
|
||||||
|
|
||||||
|
run_completion cp "$random_name:/tmp"
|
||||||
|
assert "$output" =~ ".*^$random_name\:/tmp/$random_file\$.*" "podman cp suggest custom file in container"
|
||||||
|
|
||||||
|
# cleanup container
|
||||||
|
run_podman rm $random_name
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user