# fscrypt_bash_completion # # Copyright 2017 Google Inc. # Author: Henry-Joseph Audéoud (h.audeoud@gmail.com) # # 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. # # bash completion scripts require exercising some unusual shell script # features/quirks, so we have to disable some shellcheck warnings: # # Disable SC2016 ("Expressions don't expand in single quotes, use double quotes # for that") because the 'compgen' built-in expands the argument passed to -W, # so that argument *must* be single-quoted to avoid command injection. # shellcheck disable=SC2016 # # Disable SC2034 ("{Variable} appears unused. Verify use (or export if used # externally)") because of the single quoting mentioned above as well as the # fact that we have to declare "local" variables used only by a called function # (_init_completion()) and not by the function itself. # shellcheck disable=SC2034 # # Disable SC2207 ("Prefer mapfile or read -a to split command output (or quote # to avoid splitting)") because bash completion scripts conventionally use # COMPREPLY=($(...)) assignments. # shellcheck disable=SC2207 # true # To apply the above shellcheck directives to the entire file # Generate the completion list for possible mountpoints. # # We need to be super careful here because mountpoints can contain whitespace # and shell meta-characters. To avoid most problems, we do the following: # # 1.) To avoid parsing ambiguities, 'fscrypt status' replaces the space, tab, # newline, and backslash characters with octal escape sequences -- like # what /proc/self/mountinfo does. To properly process its output, we need # to split lines on space only (and not on other whitespace which might # not be escaped), and unescape these characters. Exception: we don't # unescape newlines, as we need to reserve newline as the separator for # the words passed to compgen. (This causes mountpoints containing # newlines to not be completed correctly, which we have to tolerate.) # # 2.) We backslash-escape all shell meta-characters, and single-quote the # argument passed to compgen -W. Without either step, command injection # would be possible. Without both steps, completions would be incorrect. # The list of shell meta-characters used comes from that used by the # completion script for umount, which has to solve this same problem. # _fscrypt_compgen_mountpoints() { local IFS=$'\n' compgen -W '$(_fscrypt_mountpoints_internal)' -- "${cur}" } _fscrypt_mountpoints_internal() { fscrypt status 2>/dev/null | command awk -F " " \ 'substr($0, 1, 1) == "/" && $5 == "Yes" { gsub(/\\040/, " ", $1) gsub(/\\011/, "\t", $1) gsub(/\\134/, "\\", $1) gsub(/[\]\[(){}<>",:;^&!$=?`|'\''\\ \t\f\n\r\v]/, "\\\\&", $1) print $1 }' } # Complete with all possible mountpoints _fscrypt_complete_mountpoint() { COMPREPLY=($(_fscrypt_compgen_mountpoints)) } # Output list of possible policy or protector IDs # $1: the mount point on which policies are looked for. # $2: the section (policy or protector) to retrieve _fscrypt_status_section() { local section=${2^^} fscrypt status "$1" 2>/dev/null | \ command awk '/^[[:xdigit:]]{16}/ && section == "'"$section"'" { print $1; next; } { section = $1 }' } # Complete with policies or protectors _fscrypt_complete_policy_or_protector() { local status_section="$1" if [[ $cur = *:* ]]; then # Complete with IDs of the given mountpoint local mountpoint="${cur%:*}" id="${cur#*:}" # Note: compgen expands the argument to -W, so it *must* be single-quoted. COMPREPLY=($(compgen \ -W '$(_fscrypt_status_section "${mountpoint}" "${status_section}")' \ -- "${id}")) else # Complete with mountpoints, with colon and without ending space COMPREPLY=($(_fscrypt_compgen_mountpoints | sed s/\$/:/)) compopt -o nospace fi } # Complete with all arguments of that function _fscrypt_complete_word() { # Note: compgen expands the argument to -W, so it *must* be single-quoted. COMPREPLY=($(compgen -W '$*' -- "${cur}")) } # Complete with all arguments of that function, plus global options _fscrypt_complete_option() { local additional_opts=( "$@" ) # Add global options, always correct additional_opts+=( --verbose --quiet --help ) # Note: compgen expands the argument to -W, so it *must* be single-quoted. COMPREPLY=($(compgen -W '${additional_opts[*]}' -- "${cur}")) } _fscrypt() { # Initialize completion: compute some local variables to easily # detect what is written on the command line. -s is for splitting # long options on `=`, and -n is for splitting them also on `:` # (used in the protectors/policies `MOUNTPOINT:ID` forms). # # `split` is set by `_init_completion -s`, we must declare it local # even if we don't use it, not to modify the environment. local cur prev words cword split _init_completion -s -n : || return # Complete the options with argument here, if previous word were such # an option. It would be too difficult to check if they take place in # the correct command (such as `fscrypt status # --key ...`)—and that # is the command's job—so just complete them first. case $prev in --key) # Any file is accepted _filedir return ;; --name) # New value, nothing to complete return ;; --policy|--protector|--unlock-with) local p_or_p="${prev#--}" [[ $p_or_p = unlock-with ]] && p_or_p=protector _fscrypt_complete_policy_or_protector "${p_or_p}" return ;; --source) # Complete with keywords _fscrypt_complete_word \ pam_passphrase custom_passphrase raw_key return ;; --time) # It's a time, hard to complete a number… return ;; --user) # Complete with a user COMPREPLY=($(compgen -u -- "${cur}")) return ;; esac # Fetch positional arguments (i.e. subcommands) local positional positional=() local iword for ((iword = 1; iword < ${#words[@]} - 1; iword++)); do [[ ${words[iword - 1]} == --@(key|name|policy|protector|unlock-with|source|time|user) ]] \ && continue # Argument of previous option, skip [[ ${words[iword]} == -* ]] && continue # Option, skip positional+=("${words[iword]}") done # If completing the first positional, complete with all possible commands if [[ ${#positional[@]} == 0 ]]; then if [[ $cur == -* ]]; then _fscrypt_complete_option else _fscrypt_complete_word \ encrypt lock metadata purge setup status unlock fi return fi # Complete according to that provided case ${positional[0]-} in encrypt) # Directory or option if [[ $cur == -* ]]; then _fscrypt_complete_option \ --policy= --unlock-with= --protector= --source= \ --user= --name= --key= --skip-unlock --no-recovery else _filedir -d fi ;; lock) # Directory or option if [[ $cur == -* ]]; then _fscrypt_complete_option --user= --all-users else _filedir -d fi ;; purge) # Mountpoint or options if [[ $cur == -* ]]; then _fscrypt_complete_option --user= --force else _fscrypt_complete_mountpoint fi ;; setup) # Mountpoint or options if [[ $cur == -* ]]; then _fscrypt_complete_option --time= --force else _fscrypt_complete_mountpoint fi ;; status) # Directory (only global options for this command) if [[ $cur == -* ]]; then _fscrypt_complete_option else _filedir -d fi ;; unlock) # Directory or option if [[ $cur == -* ]]; then _fscrypt_complete_option --unlock-with= --user= --key= else _filedir -d fi ;; metadata) # This command has subcommands if [[ ${#positional[@]} = 1 ]]; then if [[ $cur = -* ]]; then _fscrypt_complete_option else # Still no subcommand, complete with them _fscrypt_complete_word \ add-protector-to-policy create change-passphrase \ destroy dump remove-protector-from-policy fi return fi # We have a subcommand, complete according to it case ${positional[1]-} in add-protector-to-policy) # Options only _fscrypt_complete_option \ --protector= --policy= --unlock-with= --key= ;; change-passphrase) # Options only _fscrypt_complete_option --protector= ;; destroy) # Mountpoint or option if [[ $cur == -* ]]; then _fscrypt_complete_option \ --protector= --policy= --force else _fscrypt_complete_mountpoint fi ;; dump) # Options only _fscrypt_complete_option --protector= --policy= ;; remove-protector-from-policy) # Options only _fscrypt_complete_option \ --protector= --policy= --force ;; create) # This subcommand has subsubcommands if [[ ${#positional[@]} = 2 ]]; then if [[ $cur = -* ]]; then _fscrypt_complete_option else # Still no subcommand, complete with them _fscrypt_complete_word protector policy fi return fi # We have a subsubcommand, complete according to it case ${positional[2]-} in policy) # Mountpoint or option if [[ $cur = -* ]]; then _fscrypt_complete_option --protector= --key= else _fscrypt_complete_mountpoint fi ;; protector) # Mountpoint or option if [[ $cur = -* ]]; then _fscrypt_complete_option \ --source= --name= --key= --user= else _fscrypt_complete_mountpoint fi ;; *) # Unrecognized subsubcommand… Suppose a new # unknown subsubcommand and complete with # global options only _fscrypt_complete_option ;; esac ;; *) # Unrecognized subcommand… Suppose a new unknown # subcommand and complete with global options only _fscrypt_complete_option ;; esac ;; *) # Unrecognized command… Suppose a new unknown command and # complete with global options only _fscrypt_complete_option ;; esac # When the sole offered completion is --*=, do not put a space after # the equal sign as we wait for the argument value. [[ ${#COMPREPLY[@]} == 1 ]] && [[ ${COMPREPLY[0]} == "--"*"=" ]] \ && compopt -o nospace } && complete -F _fscrypt fscrypt # ex: filetype=bash