diff --git a/Makefile b/Makefile index e21b5121..28170342 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,7 @@ lint: $(BIN)/golint $(BIN)/staticcheck $(BIN)/misspell go list ./... | xargs -L1 golint -set_exit_status staticcheck ./... misspell -source=text $(FILES) + shellcheck -s bash cmd/fscrypt/fscrypt_bash_completion ( cd cli-tests && shellcheck -x *.sh) clean: @@ -158,8 +159,8 @@ coverage.out: $(BIN)/gocovmerge $(COVERAGE_FILES) @go test -coverpkg=./... -covermode=count -coverprofile=$@ -p 1 ./$* 2> /dev/null ###### Installation Commands (require sudo) ##### -.PHONY: install install-bin install-pam uninstall -install: install-bin install-pam +.PHONY: install install-bin install-pam uninstall install-completion +install: install-bin install-pam install-completion PREFIX := /usr/local BINDIR := $(PREFIX)/bin @@ -181,10 +182,16 @@ install-pam: $(PAM_MODULE) install -d $(DESTDIR)$(PAM_CONFIG_DIR) install $(PAM_CONFIG) $(DESTDIR)$(PAM_CONFIG_DIR)/$(NAME) +COMPLETION_INSTALL_DIR := $(PREFIX)/share/bash-completion/completions + +install-completion: cmd/fscrypt/fscrypt_bash_completion + install -Dm644 $< $(DESTDIR)$(COMPLETION_INSTALL_DIR)/fscrypt + uninstall: rm -f $(DESTDIR)$(BINDIR)/$(NAME) \ $(DESTDIR)$(PAM_INSTALL_PATH) \ - $(DESTDIR)$(PAM_CONFIG_DIR)/$(NAME) + $(DESTDIR)$(PAM_CONFIG_DIR)/$(NAME) \ + $(DESTDIR)$(COMPLETION_INSTALL_DIR)/fscrypt #### Tool Building Commands #### TOOLS := $(addprefix $(BIN)/,protoc golint protoc-gen-go goimports staticcheck gocovmerge misspell) diff --git a/cmd/fscrypt/fscrypt_bash_completion b/cmd/fscrypt/fscrypt_bash_completion new file mode 100644 index 00000000..00ee490c --- /dev/null +++ b/cmd/fscrypt/fscrypt_bash_completion @@ -0,0 +1,286 @@ +# 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. + + +# Prefer completion script style COMPREPLY=($(...)) assignment. +# shellcheck disable=SC2207 +true # To apply shellcheck directive to all file + + +# Output list of possible mount points +_fscrypt_mountpoints() +{ + # shellcheck disable=SC2016 + fscrypt status 2>/dev/null | \ + command awk 'substr($0, 1, 1) == "/" && $5 == "Yes" { print $1 }' +} + + +# Complete with all possible mountpoints +_fscrypt_complete_mountpoint() +{ + COMPREPLY=($(compgen -W "$(_fscrypt_mountpoints)" -- "${cur}")) +} + + +# 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^^} + # shellcheck disable=SC2016 + 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#*:}" + COMPREPLY=($(compgen \ + -W "$(_fscrypt_status_section "${mountpoint}" "${status_section}")" \ + -- "${id}")) + else + # Complete with mountpoints, with colon and without ending space + COMPREPLY=($(compgen -W "$(_fscrypt_mountpoints)" \ + -- "${cur}" | sed s/\$/:/)) + compopt -o nospace + fi +} + + +# Complete with all arguments of that function +_fscrypt_complete_word() +{ + 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 ) + 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. + # shellcheck disable=SC2034 + 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