diff --git a/README.md b/README.md index 1bd62a1..b35adf9 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,24 @@ -# SQLcl Connection Alias Generators +# SQLcl Utilities ## Purpose -Generate BASH or ZSH shell aliases for connecting via [SQLcl](https://www.oracle.com/database/sqldeveloper/technologies/sqlcl/) to Oracle databases defined in TNS files or OID LDAP servers. +This plugin provides different utility functions to help you interact with [Oracle SQLcl](https://www.oracle.com/database/sqldeveloper/technologies/sqlcl/). + +One function, `sqlclUpdater` helps you install and update SQLcl. + +Another function, `sqlclConnectHelper` wraps a script around SQLcl to allow for unix style parameters to be passed to SQLcl. + +`sqlclGenerateTNSAliases` and `sqlclGenerateOIDAliases` generate BASH/ZSH shell aliases for connecting via SQLcl to Oracle databases defined in TNS files or OID LDAP servers. By default, the generated aliases take the format of `sql.database_name`. However, the alias name is completely customizable via a prefix parameter or an alias name format function (or both!). -### About this plugin -This plugin provides 3 seperate functions: +### Utility Functions + +#### sqlclUpdater +
+This function will download the latest version of Oracle SQLcl and unzip it into a given directory. The specific version of SQLcl downloaded is unzipped into a child directory or the given directory with the name of the SQLcl version. It additionally creates a symlink called 'latest' that points to the most recently downloaded version of SQLcl. You can also specify to create a symlink called 'live' that points to the most recently downloaded version of SQLcl as well as specify the number of versions of SQLcl to keep. + +If you do not specify the directory to use for SQLcl downloads, `/opt/sqlcl` will be used. The `/opt` directory usually requires root permissions. Best practice would be to create the `/opt/sqlcl` directory and set the group permissions to allow for write and make sure you are a member of the group. Then you can work on SQLcl versions (update symlinks, download new versions, etc) without needing to use `sudo`. +
#### sqlclConnectHelper
@@ -60,27 +72,27 @@ The function accepts either a tnsnames.ora file in the standard oracle location, ### Manual installation (ZSH) ```shell -git clone 'https://github.com/jasonlyle88/sqlcl-connection-alias-generators' "${XDG_CONFIG_HOME:-${HOME}}/sqlcl-connection-alias-generators" -echo 'source "${XDG_CONFIG_HOME:-${HOME}}/sqlcl-connection-alias-generators/sqlcl-connection-alias-generators.plugin.zsh"' >> "${HOME}/.zshrc" -source "${XDG_CONFIG_HOME:-${HOME}}/sqlcl-connection-alias-generators/sqlcl-connection-alias-generators.plugin.zsh" +git clone 'https://github.com/jasonlyle88/sqlcl-utilities' "${XDG_CONFIG_HOME:-${HOME}}/sqlcl-utilities" +echo 'source "${XDG_CONFIG_HOME:-${HOME}}/sqlcl-utilities/sqlcl-utilities.plugin.zsh"' >> "${HOME}/.zshrc" +source "${XDG_CONFIG_HOME:-${HOME}}/sqlcl-utilities/sqlcl-utilities.plugin.zsh" ``` ### Manual installation (BASH) ```shell -git clone 'https://github.com/jasonlyle88/sqlcl-connection-alias-generators' "${XDG_CONFIG_HOME:-${HOME}}/sqlcl-connection-alias-generators" -echo 'source "${XDG_CONFIG_HOME:-${HOME}}/sqlcl-connection-alias-generators/sqlcl-connection-alias-generators.plugin.bash"' >> "${HOME}/.bashrc" -source "${XDG_CONFIG_HOME:-${HOME}}/sqlcl-connection-alias-generators/sqlcl-connection-alias-generators.plugin.bash" +git clone 'https://github.com/jasonlyle88/sqlcl-utilities' "${XDG_CONFIG_HOME:-${HOME}}/sqlcl-utilities" +echo 'source "${XDG_CONFIG_HOME:-${HOME}}/sqlcl-utilities/sqlcl-utilities.plugin.bash"' >> "${HOME}/.bashrc" +source "${XDG_CONFIG_HOME:-${HOME}}/sqlcl-utilities/sqlcl-utilities.plugin.bash" ``` ### Installation with ZSH package managers #### [Antidote](https://getantidote.github.io/) -Add `jasonlyle88/sqlcl-connection-alias-generators` to your plugins file (default is `~/.zsh_plugins.txt`) +Add `jasonlyle88/sqlcl-utilities` to your plugins file (default is `~/.zsh_plugins.txt`) #### [Oh-My-Zsh](https://ohmyz.sh/) ```shell -git clone 'https://github.com/jasonlyle88/sqlcl-connection-alias-generators' "${ZSH_CUSTOM:-${HOME}/.oh-my-zsh/custom}/plugins/sqlcl-connection-alias-generators" -omz plugin enable sqlcl-connection-alias-generators +git clone 'https://github.com/jasonlyle88/sqlcl-utilities' "${ZSH_CUSTOM:-${HOME}/.oh-my-zsh/custom}/plugins/sqlcl-utilities" +omz plugin enable sqlcl-utilities ``` #### Others diff --git a/bin/sqlclUpdater.sh b/bin/sqlclUpdater.sh new file mode 100644 index 0000000..398de18 --- /dev/null +++ b/bin/sqlclUpdater.sh @@ -0,0 +1,347 @@ +#shellcheck shell=bash + +function sqlclUpdater() { + ############################################################################ + # + # Functions + # + ############################################################################ + function usage() { + printf -- 'This script is used to install/update Oracle SQLcl\n' + printf -- '\n' + printf -- 'This script installs or updates Oracle SQLcl in a given directory.\n' + printf -- 'It also creates/updates a symlink called "latest" to point to the\n' + printf -- 'most recently downloaded version of Oracle SQLcl.\n' + printf -- 'This script does not alter your PATH, you must do that manually.\n' + printf -- '\n' + printf -- 'The following arguments are recognized (* = required)\n' + printf -- '\n' + printf -- ' -d {dir} -- Sets the directory into which SQLcl should be installed.\n' + printf -- ' Defaults to "/opt/sqlcl"\n' + printf -- ' -k {num} -- Sets the number of SQLcl versions to keep. Must be >=1.\n' + printf -- ' All versions are kept if this is not specified.\n' + printf -- ' -l -- Creates/updates a symbolic link called "live" that points\n' + printf -- ' to the most recent version of Oracle SQLcl.\n' + printf -- ' -q -- Quiet, only output errors.\n' + printf -- ' -h -- Show this help.\n' + printf -- '\n' + printf -- 'Example:\n' + # shellcheck disable=2016 + printf -- ' %s -sld "$HOME/sqlcl" -k 3\n' "${scriptName}" + printf -- '\n' + + return 0 + } # usage + + function toUpperCase() { + ######################################################################## + # toUpperCase + # + # Return a string with all upper case letters + # + # All parameters are taken as a single string to get in all upper case + # + # upperCaseVar="$(toUpperCase "${var}")" + ######################################################################## + local string="$*" + + printf -- '%s' "${string}" | tr '[:lower:]' '[:upper:]' + + return 0 + } # toUpperCase + + function toLowerCase() { + ######################################################################## + # toLowerCase + # + # Return a string with all lower case letters + # + # All parameters are taken as a single string to get in all lower case + # + # toLowerCase="$(toLowerCase "${var}")" + ######################################################################## + local string="${*}" + + printf -- '%s' "${string}" | tr '[:upper:]' '[:lower:]' + + return 0 + } # toLowerCase + + function getCanonicalPath() { + ######################################################################## + # getCanonicalPath + # + # Return a path that is both absolute and does not contain any + # symbolic links. Always returns without a trailing slash. + # + # The first parameter is the path to canonicalize + # + # canonicalPath="$(getCanonicalPath "${somePath}")" + ######################################################################## + local target="${1}" + + if [ -d "${target}" ]; then + # dir + (cd "${target}" || exit; pwd -P) + elif [ -f "${target}" ]; then + # file + if [[ "${target}" = /* ]]; then + # Absolute file path + (cd "$(dirname "${target}")" || exit; printf -- '%s/%s\n' "$(pwd -P)" "$(basename "${target}")") + elif [[ "${target}" == */* ]]; then + # Relative file with path + printf -- '%s\n' "$(cd "${target%/*}" || exit; pwd -P)/${target##*/}" + else + # Relative file without path + printf -- '%s\n' "$(pwd -P)/${target}" + fi + fi + } # getCanonicalPath + + function printVerbose() { + local silentFlag="${1}" + + shift + + if [[ "${silentFlag}" == "false" ]]; then + # shellcheck disable=2059 + printf -- "$@" + fi + } # printVerbose + + ############################################################################ + # + # Variables + # + ############################################################################ + + ############################################################################ + ## Configurable variables + ############################################################################ + + ############################################################################ + ## Script info + ############################################################################ + local scriptBase + local scriptName + local scriptPath + + # BASH and ZSH safe method for obtaining the current script + # shellcheck disable=SC2296 + scriptBase="${BASH_SOURCE[0]:-${(%):-%x}}" + # shellcheck disable=SC2034 + scriptName="$(basename -- "${scriptBase}" '.sh')" + # shellcheck disable=SC2034 + scriptPath="$(getCanonicalPath "$(dirname -- "${scriptBase}")")" + + ############################################################################ + ## Parameter variables + ############################################################################ + local OPTIND + local OPTARG + local opt + + local dFlag='false' + local kFlag='false' + local lFlag='false' + local qFlag='false' + + local localDirectory + local keepNumVersions + + ############################################################################ + ## Constants + ############################################################################ + # shellcheck disable=SC2034,SC2155 + local h1="$(printf "%0.s-" {1..80})" + # shellcheck disable=SC2034,SC2155 + local h2="$(printf "%0.s-" {1..60})" + # shellcheck disable=SC2034,SC2155 + local h3="$(printf "%0.s-" {1..40})" + # shellcheck disable=SC2034,SC2155 + local h4="$(printf "%0.s-" {1..20})" + # shellcheck disable=SC2034,SC2155 + local hs="$(printf "%0.s-" {1..2})" + # shellcheck disable=SC2034 + local originalPWD="${PWD}" + # shellcheck disable=SC2034 + local originalIFS="${IFS}" + + local integerRegularExpression='^[0-9]+$' + local defaultLocalDirectory="/opt/sqlcl" + local remoteUrl="https://download.oracle.com/otn_software/java/sqldeveloper/sqlcl-latest.zip" + local zipFilename="sqlcl-latest.zip" + local unzippedFolder="sqlcl" + local etagFilename="sqlcl.etag" + + ############################################################################ + ## Procedural variables + ############################################################################ + local rawEtag + local sqlclEtag + local localEtag + local newVersionNumber + local idx + + ############################################################################ + # + # Option parsing + # + ############################################################################ + while getopts ':d:k:lqh' opt + do + + case "${opt}" in + 'd') + dFlag='true' + localDirectory="${OPTARG}" + ;; + 'k') + kFlag='true' + keepNumVersions="${OPTARG}" + ;; + 'l') + lFlag='true' + ;; + 'q') + qFlag='true' + ;; + 'h') + usage + return $? + ;; + '?') + printf -- 'ERROR: Invalid option -%s\n\n' "${OPTARG}" >&2 + usage >&2 + return 3 + ;; + ':') + printf -- 'ERROR: Option -%s requires an argument.\n' "${OPTARG}" >&2 + return 4 + ;; + esac + + done + + ############################################################################ + # + # Parameter handling + # + ############################################################################ + if [[ "${dFlag}" == 'false' ]]; then + localDirectory="${defaultLocalDirectory}" + fi + + if [[ "${kFlag}" == 'true' ]]; then + if ! [[ "${keepNumVersions}" =~ ${integerRegularExpression} ]]; then + printf -- 'ERROR: Argument to -k must be an positive integer.\n' >&2 + return 4 + fi + + if [[ "${keepNumVersions}" -lt 1 ]]; then + printf -- 'ERROR: Argument to -k must be >=1\n' >&2 + return 5 + fi + else + keepNumVersions=-1 + fi + + ############################################################################ + # + # Function Logic + # + ############################################################################ + # Ensure localDirectory exists + if [[ ! -d "${localDirectory}" ]]; then + if ! mkdir -p "${localDirectory}"; then + printf -- 'ERROR: Cannot create specified directory "%s"\n' "${localDirectory}" >&2 + return 6 + fi + fi + + # Get the raw ETag for the remote sqlcl + printVerbose "${qFlag}" 'Getting server ETag....\n' + if ! rawEtag=$(curl -Isf "${remoteUrl}"); then + printf -- 'ERROR: Unable to download ETag information\n' >&2 + return 7 + fi + + # Remove any carriage returns in headers + rawEtag="$(printf '%s' "${rawEtag}" | tr -d '\r')" + + # Get the actual ETag value for SQLcl + if ! sqlclEtag="$(printf '%s' "${rawEtag}" | sed -En 's/^ETag: (.*)/\1/p')"; then + printf -- 'ERROR: Unable to parse ETag information\n' >&2 + return 8 + fi + + # Get the ETag for the local sqlcl + if [[ -e "${localDirectory}/${etagFilename}" ]]; then + localEtag=$(cat "${localDirectory}/${etagFilename}") + else + localEtag="none" + fi + + # Check if ETags match + if [[ "${sqlclEtag}" == "${localEtag}" ]]; then + printVerbose "${qFlag}" 'SQLcl is current\n' + else + # Download sqlcl zip file and save the newest Etag + printVerbose "${qFlag}" 'Downloading....\n' + if ! curl -sS -f \ + -o "${localDirectory}/${zipFilename}" \ + "${remoteUrl}" 2>/dev/null + then + printf -- 'ERROR: Unable to download new version\n' >&2 + return 9 + fi + + # Save the ETag for future reference + # NOTE: not using etag options of curl as they version is not widely available + printf -- '%s\n' "${sqlclEtag}" > "${localDirectory}/${etagFilename}" + + # Unzip sqlcl zip file + printVerbose "${qFlag}" 'Unzipping %s/%s....\n' "${localDirectory}" "${zipFilename}" + unzip -qq -d "${localDirectory}" "${localDirectory}/${zipFilename}" + + # Remove the zip file since it has been unzipped + printVerbose "${qFlag}" 'Remove ZIP file....\n' + rm -rf "${localDirectory:?}/${zipFilename:?}" + + # Get the sqlcl version number from the unzipped directory + printVerbose "${qFlag}" 'Getting version number...\n' + newVersionNumber=$(find "${localDirectory}/${unzippedFolder}" -maxdepth 1 -type f | awk -F/ '{print $NF}' | grep -E '^([0-9]+\.)+[0-9]+$') + + # Move the unzipped directory to a folder named as the sqlcl version number + printVerbose "${qFlag}" 'Moving unzipped directory to version number directory....\n' + mv "${localDirectory}/${unzippedFolder}" "${localDirectory}/${newVersionNumber}" + + # Update the latest symlink + printVerbose "${qFlag}" 'Update latest symlink....\n' + ln -sfn "${newVersionNumber}" "${localDirectory}/latest" + + # Conditionally update the live symlink + if [[ "${lFlag}" = 'true' ]]; then + printVerbose "${qFlag}" 'Update live symlink....\n' + ln -sfn "${newVersionNumber}" "${localDirectory}/live" + fi + + # Conditionally only keep the the previous N versions of sqlcl + if [[ "${keepNumVersions}" -ge "1" ]]; then + idx=0 + find "${localDirectory}" -mindepth 1 -maxdepth 1 -type d | awk -F/ '{print $NF}' | grep -E '^([0-9]+\.)+[0-9]+$' | sort --reverse --version-sort | while read -r versionDirectory; do + ((idx++)) + if [ "${idx}" -le "${keepNumVersions}" ]; then + continue + fi + + printVerbose "${qFlag}" 'Remove version %s\n' "${versionDirectory}" + rm -rf "${localDirectory:?}/${versionDirectory:?}" + done + fi + + printVerbose "${qFlag}" 'Done\n' + fi + + return 0 +} # main