mirror of
https://github.com/containers/podman
synced 2024-10-19 08:44:11 +00:00
Merge pull request #14457 from Luap99/completion4
shell completion for paths inside the image/container
This commit is contained in:
commit
be527a358a
|
@ -4,6 +4,7 @@ import (
|
|||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -21,6 +22,7 @@ import (
|
|||
"github.com/containers/podman/v4/pkg/signal"
|
||||
systemdDefine "github.com/containers/podman/v4/pkg/systemd/define"
|
||||
"github.com/containers/podman/v4/pkg/util"
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -282,6 +284,61 @@ func getNetworks(cmd *cobra.Command, toComplete string, cType completeType) ([]s
|
|||
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
|
||||
// It utilizes the Args function from the cmd struct
|
||||
// In most cases the Args function validates the args length but it
|
||||
|
@ -523,8 +580,32 @@ func AutocompleteCreateRun(cmd *cobra.Command, args []string, toComplete string)
|
|||
}
|
||||
return getImages(cmd, toComplete)
|
||||
}
|
||||
// TODO: add path completion for files in the image
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
// Mount the image and provide path completion
|
||||
engine, err := setupImageEngine(cmd)
|
||||
if err != nil {
|
||||
cobra.CompErrorln(err.Error())
|
||||
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.
|
||||
|
@ -572,14 +653,39 @@ func AutocompleteCpCommand(cmd *cobra.Command, args []string, toComplete string)
|
|||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
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)
|
||||
for _, container := range containers {
|
||||
// TODO: Add path completion for inside the container if possible
|
||||
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
|
||||
}
|
||||
// don't complete more than 2 args
|
||||
|
|
|
@ -8,6 +8,16 @@
|
|||
|
||||
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
|
||||
function _can_pause() {
|
||||
# Even though we're just trying completion, not an actual unpause,
|
||||
|
@ -88,8 +98,14 @@ function check_shell_completion() {
|
|||
continue 2
|
||||
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[@]}" ""
|
||||
is "$output" ".*-$random_container_name${nl}" \
|
||||
is "$output" ".*-$name${nl}" \
|
||||
"$* $cmd: actual container listed in suggestions"
|
||||
|
||||
match=true
|
||||
|
@ -175,7 +191,7 @@ function check_shell_completion() {
|
|||
_check_completion_end NoSpace
|
||||
else
|
||||
_check_completion_end Default
|
||||
assert "${#lines[@]}" -eq 2 "$* $cmd: Suggestions are in the output"
|
||||
_check_no_suggestions
|
||||
fi
|
||||
;;
|
||||
|
||||
|
@ -205,16 +221,7 @@ function check_shell_completion() {
|
|||
if [[ ! ${args##* } =~ "..." ]]; then
|
||||
run_completion "$@" $cmd "${extra_args[@]}" ""
|
||||
_check_completion_end NoFileComp
|
||||
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
|
||||
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
|
||||
_check_no_suggestions
|
||||
fi
|
||||
|
||||
done
|
||||
|
@ -231,6 +238,24 @@ function _check_completion_end() {
|
|||
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" {
|
||||
|
||||
|
@ -280,11 +305,6 @@ function _check_completion_end() {
|
|||
# create secret
|
||||
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
|
||||
# recurse for any subcommands.
|
||||
check_shell_completion
|
||||
|
@ -316,3 +336,41 @@ function _check_completion_end() {
|
|||
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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue