bin/sym.symit.bash

Summary

Maintainability
Test Coverage
#!/usr/bin/env bash
#
# (c) 2017-2018 Konstantin Gredeskoul
#
# MIT License, distributed as part of `sym` ruby gem.
# • https://github.com/kigster/sym
#
#==============================================================================
#
# The purpose of this script is to transparently edit application secrets in
# Rails apps or other projects. It simplifies the process of key import, as well
# as the direct editing, as well as multi-file encryption/decryption routines.
#
# The idea is that you set some of the variables below to values specific to your
# system and working with encrypted files will become very easy.
#
# SYMIT__FOLDER is a relative folder to your project root, under which you
# might keep ALL of your encrypted files.  Alternatively, if you keep encrypted
# files sprinkled around your project, just leave it out, because it defaults
# to "." — the current folder, and search anything beneath.
#
# Variables:
#
#     # only search ./config folder
#     export SYMIT__FOLDER="config"
#
#     # this will be the name of your key in OS-X KeyChain
#     export SYMIT__KEY="my-org.engineering.dev" # just a name
#
#     # This is the extension given to the encrypted files. Ideally, leave it
#     # be as ".enc"
#     export SYMIT__EXTENSION=".enc"
#
# And then
#
#     symit import key [ insecure ]           # import a key and password-protect it (or not)
#     symit auto application.yml.enc          # auto-decrypts it
#     symit auto application.yml              # auto-encrypts it
#     symit decrypt application.yml           # finds application.yml.enc and decrypts that.
#
#
# ...and vola! You are editing the encrypted file with sym from the root of
# your Rails application. Neat, no?
#


# Check if we are being sourced in, or run as a script:
(   [[ -n ${ZSH_EVAL_CONTEXT} && ${ZSH_EVAL_CONTEXT} =~ :file$ ]] || \
    [[ -n $BASH_VERSION && $0 != "$BASH_SOURCE" ]]) && _s_=1 || _s_=0

(( $_s_ )) && _is_sourced=1
(( $_s_ )) || _is_sourced=0

# Set all the defaults
function __symit::init()  {
  export SYMIT__EXTENSION=${SYMIT__EXTENSION:-'.enc'}
  export SYMIT__FOLDER=${SYMIT__FOLDER:-'.'}
  export SYMIT__KEY=${SYMIT__KEY}
  export SYMIT__MIN_VERSION='latest'
}

# Returns name of the current shell, eg 'bash'
function __lib::shell::name() {
  echo $(basename $(printf $SHELL))
}

# Returns 'yes' if current shell is BASH
function __lib::shell::is_bash() {
  [[ $(__lib::shell::name) == "bash" ]] && echo yes
}

# Returns a number representing shell version, eg.
# 3 or 4 for BASH v3 and v4 respectively.
function __lib::bash::version_number() {
  echo $BASH_VERSION | awk 'BEGIN{FS="."}{print $1}'
}

# Enable all colors, but only if the STDOUT is a terminal
function __lib::color::setup()  {
  if [[ -t 1 ]]; then
    export txtblk='\e[0;30m' # Black - Regular
    export txtred='\e[0;31m' # Red
    export txtgrn='\e[0;32m' # Green
    export txtylw='\e[0;33m' # Yellow
    export txtblu='\e[0;34m' # Blue
    export txtpur='\e[0;35m' # Purple
    export txtcyn='\e[0;36m' # Cyan
    export txtwht='\e[0;37m' # White

    export bldblk='\e[1;30m' # Black - Bold
    export bldred='\e[1;31m' # Red
    export bldgrn='\e[1;32m' # Green
    export bldylw='\e[1;33m' # Yellow
    export bldblu='\e[1;34m' # Blue
    export bldpur='\e[1;35m' # Purple
    export bldcyn='\e[1;36m' # Cyan
    export bldwht='\e[1;37m' # White

    export unkblk='\e[4;30m' # Black - Underline
    export undred='\e[4;31m' # Red
    export undgrn='\e[4;32m' # Green
    export undylw='\e[4;33m' # Yellow
    export undblu='\e[4;34m' # Blue
    export undpur='\e[4;35m' # Purple
    export undcyn='\e[4;36m' # Cyan
    export undwht='\e[4;37m' # White

    export bakblk='\e[40m' # Black - Background
    export bakred='\e[41m' # Red
    export bakgrn='\e[42m' # Green
    export bakylw='\e[43m' # Yellow
    export bakblu='\e[44m' # Blue
    export bakpur='\e[45m' # Purple
    export bakcyn='\e[46m' # Cyan
    export bakwht='\e[47m' # White

    export clr='\e[0m' # Text Reset
    export txtrst='\e[0m' # Text Reset
    export rst='\e[0m' # Text Reset
  fi
}

# Unset all the colors, in case we a being piped into
# something else.
function __lib::color::reset()  {
  export txtblk=
  export txtred=
  export txtgrn=
  export txtylw=
  export txtblu=
  export txtpur=
  export txtcyn=
  export txtwht=

  export bldblk=
  export bldred=
  export bldgrn=
  export bldylw=
  export bldblu=
  export bldpur=
  export bldcyn=
  export bldwht=

  export unkblk=
  export undred=
  export undgrn=
  export undylw=
  export undblu=
  export undpur=
  export undcyn=
  export undwht=

  export bakblk=
  export bakred=
  export bakgrn=
  export bakylw=
  export bakblu=
  export bakpur=
  export bakcyn=
  export bakwht=

  export clr=
  export txtrst=
  export rst=
}

# Enable or disable the colors based on whether the STDOUT
# is a proper terminal, or a pipe.
function __lib::stdout::configure() {
  if [[ -t 1 ]]; then
    __lib::color::setup
  else
    __lib::color::reset
  fi
}

__lib::stdout::configure

# Check if we are being run as a script, and if so — bail.
(( $_s_ )) || {
  printf "${bldred}This script is meant to be sourced into your environment,\n"
  printf "not run on a command line.${clr} \n\n"

  printf "Please add 'source $0' to your BASH initialization file,\n"
  printf "or run the following command:\n\n"

  printf "    \$ ${bldgrn}sym -B ~/.bash_profile${clr}\n\n"

  printf "${bldblu}Thanks for using Sym!${clr}\n"
  exit 1
}

# Horizontal line, width of the full terminal
function __lib::color::hr()  {
  local cols=${1:-${COLUMNS}}
  local char=${2:-"—"}
  local color=${3:-${txtylw}}

  printf "${color}"
  eval "printf \"%0.s${char}\" {1..${cols}}"
  printf "${clr}\n"
}

# Large header, all caps
function __lib::color::h1()  {
  local title=$(echo "$*" | tr 'a-z' 'A-Z')
  len=${#title}
  printf "${bldylw}${title}\n"
  __lib::color::hr ${len} '─'
}

# Smaller header
function __lib::color::h2()  {
  printf "${bldpur}$*${clr}\n"
}

# Shift cursor by N positions to the right
function __lib::color::cursor-right-by()  {
  position=$1
  printf "\e[${position}C"
}

# Shift cursor by N positions to the left
function __lib::color::cursor-left-by()  {
  position=$1
  printf "\e[${position}D"
}

# Shift cursor by N positions up
function __lib::color::cursor-up-by()  {
  position=$1
  printf "\e[${position}A"
}

# Shift cursor by N positions down
function __lib::color::cursor-down-by()  {
  position=$1
  printf "\e[${position}B"
}

# Convert a version string such as "1.50.17" to an integer
# 101050017 for numeric comparison:
function __lib::ver-to-i() {
  version=${1}
  echo ${version} | awk 'BEGIN{FS="."}{ printf "1%02d%03.3d%03.3d", $1, $2, $3}'
}

# Convert a result of __lib::ver-to-i() back to a regular version.
function __lib::i-to-ver() {
  version=${1}
  /usr/bin/env ruby -e "ver='${version}'; printf %Q{%d.%d.%d}, ver[1..2].to_i, ver[3..5].to_i, ver[6..8].to_i"
}

# Prints Usage
function __symit::usage()  {
  echo
  __lib::color::h1 "symit"

  printf "
  ${bldylw}symit${bldgrn} is a powerful BASH helper, that enhances the CLI encryption
  tool called ${bldred}Sym${clr}, which is a Ruby Gem.

  Sym has an extensive CLI interface, but it only handles one
  encryption/decryption operation per invocation. With this script, you can
  auto decrypt all files in a given folder, you can import the key in a
  simpler way, and you can save into the environment sym configuration that
  will be used. It also streamlines editing of encrypted files in a given
  folder. Symit can be configured either with the ENV variables, or using
  the CLI flags.\n"

  printf "
  The recommended way to use ${bldred}symit${clr} is to set the following
  environment variables, which removes the need to pass these values via the
  flags. These variables default to the shown values if not set elsewhere:

  Perhaps the most critically important variable to set is ${txtylw}SYMIT__KEY${clr}:
  ${txtylw}
       export SYMIT__KEY='my-org.my-app.dev'
   eg: export SYMIT__KEY='github.web.development' 
  ${clr}
  The ${txtcya}key${clr} can resolve to a file name, or a name of ENV variable,
  a keychain entry, or be the actual key (not recommended!). See the following
  link for more info:

  ${undblu}https://github.com/kigster/sym#resolving-the--k-argument${clr}

  Additional configuration is available through these variables:
  ${txtylw}
     export SYMIT__EXTENSION='${SYMIT__EXTENSION}'
     export SYMIT__FOLDER='${SYMIT__FOLDER}'
     export SYMIT__MIN_VERSION='latest'
  ${clr}
  The last variable defines the minimum Sym version desired. Set it to
  'latest' to have symit auto-upgrade Sym every time it is invoked.

  ${clr}\n"

  __lib::color::h2 "Usage:"
  printf "    ${bldgrn}symit [ action ] [ file-path/pattern ] [ flags ]${clr}\n\n"

  __lib::color::h2 "Actions:"
  printf "    Action is the first word that defaults to ${bldylw}edit${clr}.\n\n"
  printf "    ${bldcya}Valid actions are below, starting with the Key import or creation:${clr}\n\n"
  printf "    ${bldylw}— generate      ${clr}create a new secure key, and copies it to the\n"
  printf "                    clipboard (if supported), otherwise prints to STDOUT\n"
  printf "                    Key name (set via SYMIT__KEY or -k flag) is required,\n"
  printf "                    and is used as the KeyChain entry name for the new key.\n\n"
  printf "    ${bldylw}— import [insecure]\n"
  printf "                    ${clr}imports the key from clipboard and adds password\n"
  printf "                    encryption unless 'insecure' is passed in. Same as above\n"
  printf "                    in relation with the key parameter.\n\n"
  printf "    ${bldcya}The following actions require the file pattern/path argument:${clr}\n"
  printf "    ${bldylw}— edit          ${clr}Finds all files, and opens them in $EDITOR\n"
  printf "    ${bldylw}— encrypt       ${clr}Encrypts files matching file-path\n"
  printf "    ${bldylw}— decrypt       ${clr}Adds the extension to file pattern and decrypts\n"
  printf "    ${bldylw}— auto          ${clr}encrypts decrypted file, and vice versa\n"

  echo
  __lib::color::h2 "Flags:"
  printf "    -f | --folder    DIR   ${clr}Top level folder to search.${clr}\n"
  printf "    -k | --key       KEY   ${clr}Key identifier${clr}\n"
  printf "    -x | --extension EXT   ${clr}Default extension of encrypted files.${clr}\n"
  printf "    -n | --dry-run         ${clr}Print stuff, but dont do it${clr}\n"
  printf "    -a | --all-files       ${clr}If provided ALL FILES are operated on${clr}\n"
  printf "                           ${clr}Use with CAUTION!${clr}\n"
  printf "    -v | --verbose         ${clr}Print more stuff${clr}\n"
  printf "    -q | --quiet           ${clr}Print less stuff${clr}\n"
  printf "    -h | --help            ${clr}Show this help message${clr}\n"

  echo
  __lib::color::h2 'Encryption key identifier can be:'
  printf "${clr}"

  printf '
  1. name of the keychain item storing the keychain (secure)
  2. name of the environment variable storing the Key (*)
  3. name of the file storing the key (*)
  4. the key itself (*)'

  echo
  printf "${bldred}"
  printf '
  (*) 2-4 are insecure UNLESS the key is encrypted with a password.'; echo
  printf "${clr}\
  Please refer to README about generating password protected keys:\n
  ${bldblu}${undblu}https://github.com/kigster/sym#generating-the-key--examples${clr}\n\n"
  echo

  __lib::color::h1 'Examples:'

  printf "  To import a key securely, first copy the key to your clipboard,\n"
  printf "  and then run the following command, pasting the key when asked:\n\n"
  printf "     ❯ ${bldgrn}symit${bldblu} import key ${clr}\n\n"

  printf "  To encrypt or decrypt ALL files in the 'config' directory:${clr}\n\n"
  printf "     ❯ ${bldgrn}symit${bldblu} encrypt|decrypt -a -f config ${clr}\n\n"

  printf "  To decrypt all *.yml.enc files in the 'config' directory:${clr}\n\n"
  printf "     ❯ ${bldgrn}symit${bldblu} decrypt '*.yml' -f config ${clr}\n\n"

  printf "  To edit an encrypted file ${txtblu}config/application.yml.enc${clr}\n\n"
  printf "     ❯ ${bldgrn}symit${bldblu} application.yml${clr}\n\n"

  printf "  To auto decrypt a file ${txtblu}config/settings/crypt/pass.yml.enc${clr}\n\n"
  printf "     ❯ ${bldgrn}symit${bldblu} auto config/settings/crypt/pass.yml.enc${clr}\n\n"

  printf "  To automatically decide to either encrypt or decrypt a file,\n"
  printf "  based on the file extension use 'auto' command. The first line below\n"
  printf "  encrypts the file, second decrypts it, because the file extension is .enc:${clr}\n\n"

  printf "     ❯ ${bldgrn}symit${bldblu} auto config/settings/crypt/pass.yml${clr}\n"
  printf "     ❯ ${bldgrn}symit${bldblu} auto config/settings/crypt/pass.yml.enc${clr}\n\n"

  printf "  To encrypt a file ${txtblu}config/settings.yml${clr}\n"
  printf "     ❯ ${bldgrn}symit${bldblu} encrypt config/settings.yml${clr}\n\n"
}

function __datum()   {
  date +"%m/%d/%Y.%H:%M:%S"
}

function __warn()  {
  __lib::color::cursor-left-by 1000
  printf "${bldylw}$* ${bldylw}\n"
}
function __err()  {
  __lib::color::cursor-left-by 1000
  printf "${bldred}ERROR: ${txtred}$* ${bldylw}\n"
}

function __inf()  {
  [[ ${cli__opts__quiet} ]] && return
  __lib::color::cursor-left-by 1000
  printf "${txtblu}$*${clr}\n"
}

function __dbg()  {
  [[ ${cli__opts__verbose} ]] || return
  __lib::color::cursor-left-by 1000
  printf "${txtgrn}$*${clr}\n"
}

function __lib::command::print() {
  __inf "${bldylw}❯ ${bldcya}$*${clr}"
}

function __symit::sym::installed_version() {
  __lib::ver-to-i $(gem list | grep sym | awk '{print $2}' | sed 's/(//g;s/)//g')
}

function __symit::sym::latest_version() {
  __lib::ver-to-i $(gem query --remote -n '^sym$' | awk '{print $2}' | sed 's/(//g;s/)//g')
}

function __symit::install::update() {
  local desired_version=$1
  shift
  local current_version=$2
  shift
  local version_args=$*

  __inf "updating sym to version ${bldylw}$(__lib::i-to-ver ${desired_version})${clr}..."
  printf "${bldblu}" >&1
  echo y | gem uninstall sym --force -x  2>/dev/null
  printf "${clr}" >&1

  command="gem install sym ${version_args} "
  eval "${command}" >/dev/null
  code=$?
  printf "${clr}" >&2
  if [[ ${code} != 0 ]]; then
    __err "gem install returned ${code}, with command ${bldylw}${command}"
    return 127
  fi
  current_version=$(__symit::sym::installed_version)
  __inf "sym version ${bldylw}$(__lib::i-to-ver ${current_version}) was successfully installed."
}

function __symit::install::gem()  {
  if [[ -n ${__symit_last_checked_at} ]]; then
    now=$(date +'%s')
    if [[ $(( $now - ${__symit_last_checked_at} )) -lt 3600 ]]; then
      return
    fi
  fi

  export __symit_last_checked_at=${now:-$(date +'%s')}

  __inf "Verifying current sym version, please wait..."
  current_version=$(__symit::sym::installed_version)
  if [[ -n ${SYMIT__MIN_VERSION} ]]; then
    if [[ ${SYMIT__MIN_VERSION} -eq 'latest' ]]; then
      desired_version=$(__symit::sym::latest_version)
      version_args=''
    else
      desired_version=$( __lib::ver-to-i ${SYMIT__MIN_VERSION})
      version_args=" --version ${SYMIT__MIN_VERSION}"
    fi

    if [[ "${desired_version}" != "${current_version}" ]]; then
      __symit::install::update "${desired_version}" "${current_version}" "${version_args}"
    else
      __inf "${bldgrn}sym${clr} ${txtblu}is on the correct version ${bldylw}$(__lib::i-to-ver ${desired_version})${txtblu} already"
    fi
  else
    if [[ -z ${current_version} ]] ; then
      __dbg "installing latest version of ${bldylw}sym..."
    fi
  fi
}

function __symit::files()  {
  eval $(__symit::files::cmd)
}

function __symit::files::cmd() {
  if [[ -n ${cli__opts__file} && -n ${cli__opts__extension} ]]; then

    local folder=${cli__opts__folder}
    local file="${cli__opts__file}"
    local ext="${cli__opts__extension}"

    if [[ ${file} =~ '/' ]]; then
      if [[ ${folder} == '.' ]]; then
        folder="$(dirname ${file})"
      else
        folder="${folder}/$(dirname ${file})"
      fi
      file="$(basename ${file})"
    fi

    if [[ "${cli__opts__action}" == "encrypt" ]] ; then
      printf "find ${folder} -name '${file}' -and -not -name '*${ext}'"
    elif [[ "${cli__opts__action}" == "auto" ]] ; then
      printf "find ${folder} -name '${file}'"
    else # edit, decrypt
      [[ ${file} =~ "${ext}" ]] || file="${file}${ext}"
      printf "find ${folder} -name '${file}'"
    fi
  fi
}

function __symit::command()  {
  file=${1}
  if [[ -n "${cli__opts__key}" && -n "${cli__opts__extension}" ]]; then
    action="${cli__opts__action}"
    v="sym__actions__${action}"
    flags="${!v}"
    if [[ ${action} =~ "key" ]]; then
      [[ -n ${cli__opts__verbose} ]] && printf "processing key import action ${bldylw}${action}${clr}\n" >&2
      printf "sym ${flags} ${cli__opts__key} "
    elif [[ ${action} =~ "generate" ]] ; then
      [[ -n ${cli__opts__verbose} ]] && printf "processing generate key action ${bldylw}${action}${clr}\n" >&2
      if [[ -n $(which pbcopy) ]]; then
        out_key=/tmp/outkey
        command="sym ${flags} ${cli__opts__key} -q -o ${out_key}; cat ${out_key} | pbcopy; rm -f ${out_key}"
        printf "${command}"
      else
        printf "sym ${flags} ${cli__opts__key} "
      fi
    elif [[ -n ${file} ]] ; then
      ext="${cli__opts__extension}"
      [[ -z ${ext} ]] && ext='.enc'
      ext=$(echo ${ext} | sed -E 's/[\*\/,.]//g')
      if [[ ${action} =~ "encrypt" ]]; then
        printf "sym ${flags} ${file} -ck ${cli__opts__key} -o ${file}.${ext}"
      elif [[ ${action} =~ "decrypt" ]]; then
        new_name=$(echo ${file} | sed "s/\.${ext}//g")
        [[ "${new_name}" == "${file}" ]] && name="${file}.decrypted"
        printf "sym ${flags} ${file} -ck ${cli__opts__key} -o ${new_name}"
      else
        printf "sym ${flags} ${file} -ck ${cli__opts__key} "
      fi
    else
      printf "printf \"ERROR: not sure how to generate a correct command\\n\""
    fi
  fi
}

function __symit::cleanup()  {
  unset sym__actions
  unset cli__opts
}

function __symit::exit()  {
  code=${1:-0}
  __symit::cleanup
  echo -n ${code}
}

function __symit::print_cli_args()  {
  __dbg "action     ${bldylw}: ${cli__opts__action}${clr}"
  __dbg "key        ${bldylw}: ${cli__opts__key}${clr}"
  __dbg "file       ${bldylw}: ${cli__opts__file}${clr}"
  __dbg "extension  ${bldylw}: ${cli__opts__extension}${clr}"
  __dbg "folder     ${bldylw}: ${cli__opts__folder}${clr}"
  __dbg "verbose    ${bldylw}: ${cli__opts__verbose}${clr}"
  __dbg "dry_run    ${bldylw}: ${cli__opts__dry_run}${clr}"
}

function __symit::args::needs_file()  {
  if [[ "${cli__opts__action}" == 'edit' || \
  "${cli__opts__action}" == 'auto' || \
  "${cli__opts__action}" == 'encrypt' || \
  "${cli__opts__action}" == 'decrypt' ]]; then
    printf 'yes'
  fi
}

function __symit::validate_args()  {
  if [[ -n $(__symit::args::needs_file) && -z ${cli__opts__file} ]]; then
    __err "missing file argument, config/application.yml"
    return $(__symit::exit 2)
  fi

  if [[ -z "${cli__opts__key}" ]]; then
    __err "Key was not defined, pass it with ${bldblu}-k KEY_ID${bldred}"
    __err "or set it via ${bldgrn}\$SYMIT__KEY${bldred} variable."
    return $(__symit::exit 4)
  fi

  if [[ -z ${cli__opts__extension} ]]; then
    cli__opts__extension='.enc'
  fi
}

function __symit::run()  {
  __symit::cleanup
  __symit::init

  cli__opts__verbose=''
  cli__opts__quiet=''
  cli__opts__key=${SYMIT__KEY}
  cli__opts__extension=${SYMIT__EXTENSION}
  cli__opts__folder=${SYMIT__FOLDER}
  cli__opts__dry_run=''
  cli__opts__action=edit
  cli__opts__file=''

  sym__actions__generate=' -cpgx '
  sym__actions__edit=' -t '
  sym__actions__encrypt='-e -f '
  sym__actions__decrypt='-d -f '
  sym__actions__auto=' -n '
  sym__actions__key_secure=' -iqcpx '
  sym__actions__key_insecure=' -iqcx '
  sym__actions__install='install'

  if [[ -z $1 ]]; then
    __symit::usage
    return $(__symit::exit 0)
  fi

  while :; do
    case $1 in
      -h|-\?|--help)
          shift
          __symit::usage
          __symit::cleanup
          return $(__symit::exit 0)
          ;;

      -k|--key)
          shift
          if [[ -z $1 ]]; then
            __err "-k/--key requires an argument" && return $(__symit::exit 1)
          else
            cli__opts__key=$1
            shift
          fi
          ;;

      -x|--extension)
          shift
          if [[ -z $1 ]]; then
            __err "-x/--extension requires an argument" && return $(__symit::exit 1)
          else
            cli__opts__extension=${1}
            shift
          fi
          ;;

      -f|--folder)
          shift
          if [[ -z $1 ]]; then
            __err "-f/--folder requires an argument" && return $(__symit::exit 1)
          else
            cli__opts__folder=${1}
            shift
          fi
          ;;

      -a|--all-files)
          shift
          cli__opts__file="'*'"
          ;;

      -n|--dry-run)
          shift
          cli__opts__dry_run="yes"
          ;;

      -v|--verbose)
          shift
          cli__opts__verbose="yes"
          ;;

      -q|--quiet)
          shift
          cli__opts__quiet="yes"
          ;;

      import|key)
          shift
          cli__opts__action="key_secure"
          ;;

      insecure)
          shift
          if [[ "${cli__opts__action}" == 'key_secure' ]] ; then
            cli__opts__action="key_insecure"
          fi
          ;;

      --) # End of all options.
          shift
          break
          ;;

      -?*)
          __err 'WARN: Unknown option: %s\n' "$1" >&2
          return $(__symit::exit 127)
          shift
          ;;


      ?*)
          param=$1
          v="sym__actions__${param}"
          if [[ ! ${param} =~ '.' && -n "${!v}" ]]; then
            __dbg "Action ${bldylw}${param}${clr} is a valid action."
            cli__opts__action=${param}
          else
            __dbg "Parameter ${bldylw}${param}${clr} is not a valid action,"
            __dbg "therefore it must be a file pattern."
            cli__opts__file=${1}
          fi
          shift
          ;;

      *) # Default case: If no more options then break out of the loop.
          break
          shift
    esac
  done

  [[ -n "${cli__opts__verbose}" ]] && __symit::print_cli_args

  if [[ "${cli__opts__action}" == 'install' ]]; then
    if [[ -n ${cli__opts__dry_run} ]]; then
      __dbg "This command verifies that Sym is properly installed,"
      __dbg "and if not found — installs it."
      return $(__symit::exit 0)
    else
      __symit::install::gem
      return $(__symit::exit 0)
    fi
  fi

  __symit::validate_args

  code=$?
  if [[ ${code} != 0 ]]; then
    return $(__symit::exit ${code})
  fi

  __symit::install::gem

  changed_count=0

  if [[ -n "${cli__opts__dry_run}" ]] ; then
    __lib::color::h1 "DRY RUN"
    for file in $(__symit::files); do
      printf "   \$ ${bldblu}$(__symit::command ${file})${clr}\n"
    done
  else
    if [[ -n "${cli__opts__file}" ]]; then
      [[ -n ${cli__opts__verbose} ]] && __dbg $(__symit::files)
      declare -a file_list

      for file in $(__symit::files); do
        local cmd="$(__symit::command ${file})"
        __lib::command::print "${cmd}"
        eval "${cmd}"
        code=$?; [[ ${code} != 0 ]] && __err "command '${bldblu}${cmd}${bldred}' exited with code ${bldylw}${code}"
        changed_count=$(( ${changed_count} + 1))
      done

      if [[ ${changed_count} == 0 ]]; then
        printf "${undylw}Bad news:${clr}\n\n"
        __warn "  No files matched your specification. The following 'find' command"
        __warn "  ran to find the file you requested. Please change the name, and "
        __warn "  try again.\n"
        __warn "   ${bldblu}$(__symit::files::cmd)${clr}\n\n"
        return $(__symit::exit 5)
      fi

    else # opts[file]
      cmd=$(__symit::command)
      __lib::command::print "${cmd}"
      eval "${cmd}"
      code=$?; [[ ${code} != 0 ]] && return $(__symit::exit ${code})
      changed_count=$(( ${changed_count} + 1))
    fi
  fi
}

function symit()  {
  __lib::stdout::configure
  __symit::run $@
}