#!/usr/bin/env bash # NOTE: This script works best when sourced, otherwise the current shell's aliases, keywords, and shell functions can't be detected. # **Platforms supported in principle**: darwin, linux, freebsd # Possibly others, but the issue is that non-POSIX features of utilities such as `grep` and `file` are used. # Also, note that bash is NOT preinstalled on FreeBSD. # **Shells that support sourcing this script: BASH, KSH, ZSH** - ENSURE CROSS-SHELL/PLATFORM COMPATIBILITY WHEN MAKING CHANGES: # * Use `typeset` instead of `local` and `function someName { ... }` to ensure that ksh's variables are truly local. # * Account for variations in syntax of builtins are taken into account and/or that what is an external command in one shell may be a builtin in another (e.g., `which`) # * Account for platform-specific variations in syntax of executables (GNU vs. BSD; e.g., `stat`). # **Avoid polluting the global namespace*, since this script will be sourced: # * Prefix all *auxiliary* function names with '__typex_' (sadly, they are in the global namespace even if *nested* in other functions). # * Avoid shell-global variables if possible; otherwise, also prefix them with '__typex_'. # Executing this script *directly* (which will run in bash) is still *somewhat* useful: it finds *executable files* - similar to `which` - # but NOT aliases, functions, builtins - a warning to that effect is printed. # -------- BEGIN: POSIX SYNTAX ONLY # !! The purpose of this block is to determine, when this script is being *sourced*, whether the sourcing shell is a *supported shell* and *fail gracefully, if not*. # !! Thus, the code in this block must execute in [mostly] POSIX-features-only shells (e.g., dash) too. # !! Therefore, # !! - only POSIX syntax is used, in particular: POSIX-style function declarations (e.g., foo() rather than function foo) # !! - NO variables at all, because - due to lack of local variables in POSIX shells - they'd end up in the global namespace. # IF SOURCED: is the current shell supported? __typex_isSupportedShell() { [ -n "$BASH_VERSION" ] && return 0 # !! On platforms where 'sh' is symlinked to bash or virtually identical to bash (OSX), sourcing will therefore succeed from 'sh' as well - CAVEAT: bash invoked as 'sh' behave slightly differently; notably: process substitution doesn't work. [ -n "$KSH_VERSION" ] && return 0 [ -n "$ZSH_VERSION" ] && return 0 return 1 } # IF NOT SOURCED: is the *parent* shell (i.e., the one that *invoked* this script) supported? # Note: this function isn't strictly needed up here, but defined here to keep all information about what shells are supported in close proximity. __typex_isParentSupportedShell() { # Examine the basename of the *parent* process. # !! OSX prepends "-" to login shells (e.g., "-bash"), so that must be stripped away. case "$(command basename -- "$(command ps -o comm= $PPID | command sed 's/^-//')")" in bash|ksh|zsh) return 0 ;; esac return 1 } # If this script is *sourced*, abort, if the *current* shell is not supported. # (If it isn't sourced - i.e., executed directly - it is by definition run by a supported shell, via the shebang line: bash) # FOR UNSUPPORTED SHELLS, EXECUTION ENDS HERE. __typex_isSupportedShell || { printf '%s\n' "ERROR: This script can only be sourced in bash, ksh, or zsh -- $(command basename "$(ps -o comm= $$)") is not supported." >&2; return 2; } # -------- END: POSIX SYNTAX ONLY # Store the absolute path of this script in a SHELL-GLOBAL variable. # In other words: if this script is being *sourced*, this WILL CREATE A GLOBAL VARIABLE FOR THE REMAINDER OF THE SOURCING (TYPICALLY INTERACTIVE) SHELL. # Note: This is necessary, because determining the originating script later, from the already-sourced typex() function, won't work in *nested* functions in zsh (as of 5.0.5). [ -n "$BASH_SOURCE" ] && __typex_scriptpath=$BASH_SOURCE [ -n "$KSH_VERSION" ] && __typex_scriptpath=${.sh.file} [ -n "$ZSH_VERSION" ] && __typex_scriptpath=${(%):-%x} # Ensure that the script path is absolute (not required for ksh, where ${.sh.file} always reports an absolute path). [ -n "$KSH_VERSION" ] || __typex_scriptpath=$(CDPATH= cd "$(command dirname -- "$__typex_scriptpath")" && command printf '%s' "${PWD%/}/")$(command basename -- "$__typex_scriptpath") # --- MAIN FUNCTION to be SOURCED. function typex # !! Note the non-POSIX function syntax to ensure that ksh variables declared with `typeset` are *local* variables. { # --- # Delegate handling STANDARD options, which don't require being sourced, to the *script* so as to keep this *function* small. # --- case $1 in -h|--help|--version|--man|--man-source|--home) "$__typex_scriptpath" "$@" return ;; esac # ---- BEGIN: NESTED HELPER FUNCTIONS # ---- Note that, despite being nested, they're still put in the GLOBAL namespace ON FIRST INVOCATION of the hosting function (at least in Bash). ## ------- # SYNOPSIS # __typex_rreadlinkchain # DESCRIPTION # Recursive readlink: prints the CHAIN OF SYMLINKS from the input # file to its ultimate target, as ABSOLUTE paths, with each path on a separate # line. # Only the ultimate target's path is canonical, though. # A broken symlink in the chain causes an error that reports the # non-existent target. # An input path that is not a symlink will print its own canonical path. # LIMITATIONS # - Won't work with filenames with embedded newlines or filenames containing # the string ' -> '. # COMPATIBILITY # Fully POSIX-compliant. # EXAMPLES # # Print the symlink chain of the `git` executable in the $PATH. # __typex_rreadlinkchain "$(which git)" # # Ditto, using single-line `ls -l`-style format ('a@ -> b') # __typex_rreadlinkchain "$(which git)" | sed -nE -e '$!{a\'$'\n''@ -> ' -e '}; p' | tr -d '\n' # THANKS # http://stackoverflow.com/a/1116890/45375 function __typex_rreadlinkchain { ( # execute in *subshell* to localize the effect of `cd`, ...; note: use of `function` syntax requires both `{` and `(`, unlike POSIX-style function declarations target=$1 targetDir= targetName= CDPATH= # Try to make the execution environment as predictable as possible: # All commands below are invoked via `command`, so we must make sure that # `command` itself is not redefined as an alias or shell function. # (Note that command is too inconsistent across shells, so we don't use it.) # `command` is a *builtin* in bash, dash, ksh, zsh, and some platforms do not # even have an external utility version of it (e.g, Ubuntu). # `command` bypasses aliases and shell functions and also finds builtins # in bash, dash, and ksh. In zsh, option POSIX_BUILTINS must be turned on for # that to happen. { \unalias command; \unset -f command; } >/dev/null 2>&1 [ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on # make zsh find *builtins* with `command` too. while :; do # Unless the file is a symlink OR exists, we report an error - note that using `-e` with a symlink reports the *target*'s existence, not the symlink's. [ -L "$target" ] || [ -e "$target" ] || { command printf '%s\n' "ERROR: '$target' does not exist." 1>&2; return 1; } # !! We use `cd` to change to the target's folder # !! so we can correctly resolve the full dir. path. command cd "$(command dirname -- "$target")" # note: cd "" is the same as cd . - i.e., a no-op. targetDir=$PWD targetName=$(command basename -- "$target") [ "$targetName" = '/' ] && targetName='' # !! curiously, `basename /` returns '/' done=0 if [ ! -L "$targetName" ]; then # We've found the ultimate target (or the input file wasn't a symlink to begin with). # For the *ultimate* target we want use `pwd -P` to make sure we use the actual, physical directory, # (not a symlink) to get the *canonical* path. targetDir=$(command pwd -P) done=1 fi # Print (next) path - note that we manually resolve paths ending # in /. and /.. to make sure we have a normalized path. if [ "$targetName" = '.' ]; then command printf '%s\n' "${targetDir%/}" elif [ "$targetName" = '..' ]; then # Caveat: something like /var/.. will resolve to /private (assuming # /var@ -> /private/var), i.e. the '..' is applied AFTER canonicalization. command printf '%s\n' "$(command dirname -- "${targetDir}")" else command printf '%s\n' "${targetDir%/}/$targetName" fi # Exit, if we've hit the non-symlink at the end of the chain. [ "$done" = 1 ] && break # File is symlink -> continue to resolve. # Parse `ls -l` output, which, unfortunately, is the only POSIX-compliant # way to determine a symlink's target. Hypothetically, this can break with # filenames containig literal ' -> ' and embedded newlines. target=$(command ls -l -- "$targetName") target=${target#* -> } done ) } # SYNOPSIS # getFileTimestamp file [kind [outputFormat]] # DESCRIPTION # Returns one of the specified file's timestamps: last-modified by default, as epoch time by default. # KIND specifies the type of timestamp m = last-modified (default), a = last-accessed, b = birth, c = (inode data last) changed. # OUTPUTFORMAT is a format string to pass to the `date` utility; without it, epoch time (a value in seconds) is printed. # COMPATIBILITY # Linux and BSD platforms (including OSX) function __typex_getFileTimestamp { command typeset file="$1" kind="${2:-m}" outFmt="$3" fmtOptChar= fmtChar= osNdx= # !! Due to use of command, variable values *must* be *double-quoted* here. osNdx=0 # BSD platforms [[ $(command uname) == 'Linux' ]] && osNdx=1 fmtOptChar='fc' case "$kind" in m) fmtChar='mY' # '{BSD-format-char}{GNU-format-char}' ;; a) fmtChar='aX' ;; b) fmtChar='BW' ;; c) fmtChar='cZ' ;; *) # unknown timestamp type return 1 ;; esac ts=$(command stat -${fmtOptChar:$osNdx:1} "%${fmtChar:$osNdx:1}" "$file") || return # !! zsh requires the '$' to reference variables in subscripts if [[ -n $outFmt ]]; then case $osNdx in 0) # BSD command date -j -f '%s' "$ts" +"$outFmt" || return ;; 1) # Linux command date -d @"$ts" +"$outFmt" || return ;; esac else command printf '%s\n' "$ts" fi } # SYNOPSIS # __typex_reMatch string regex [outVarName] # DESCRIPTION # Regex-matching routine that returns a BASH_REMATCH-style array in the specified output variable (named for the function by default). function __typex_reMatch { # Note the use of `function` syntax to ensure that ksh variables declared with typeset are truly local. command typeset ec= outVar="${3:-__typex_reMatch}" unset -v "$outVar" # initialize output variable [[ $1 =~ $2 ]] # perform the regex test ec=$? # save exit code if [[ $ec -eq 0 ]]; then # copy result to output variable [[ -n $BASH_VERSION ]] && eval "$outVar"'=( "${BASH_REMATCH[@]}" )' [[ -n $KSH_VERSION ]] && eval "$outVar"'=( "${.sh.match[@]}" )' [[ -n $ZSH_VERSION ]] && eval "$outVar"'=( "$MATCH" "${match[@]}" )' fi return $ec } # SYNOPSIS # __typex_reQuote # DESCRIPTION # Escapes the specified literal text for use in an *extended* regular expression. # EXAMPLE # __typex_reQuote 'Cost^(*): $3.' # -> '[C][o][s][t]\^[(][*][)][:][ ][$][3][.]' # Note that the input is assumed to be a literal to be escaped as is - no existing escaping is recognized. function __typex_reQuote { command sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$*"; } # SYNOPSIS # __typex_install # = this script's absolute file path # Installs/uninstalls sourcing of this script in the specified shell's user-specific initialization file. function __typex_install { command typeset targetShellFilename=$1 install=$2 runningStandalone=$3 scriptFileAbsPath=$4 command typeset verbose=1 scriptFileAbsPathQ= initFile= instLineRegEx= found=0 cmd= cmt= ec= newContents= command typeset dir=${TYPEX_TEST_INITFILE_DIR:-~} # all init files are in the user's home folder, unless overriden for testing with an env. var. case "$targetShellFilename" in 'bash') # !! Sadly, bash ONLY reads ~/.bash_profile in LOGIN shells, and on OSX (Darwin) ALL shells are login shells, so on OSX we must target ~/.bash_profile. [[ $(uname) == 'Darwin' ]] && initFile="$dir/.bash_profile" || initFile="$dir/.bashrc" ;; 'ksh') initFile="$dir/.kshrc" ;; 'zsh') initFile="$dir/.zshrc" ;; *) printf '%s\n' "ERROR: ${scriptFileAbsPath##*/} can only be sourced in bash, ksh, or zsh -- $targetShell is not supported." >&2 return 2 ;; esac # Synthesize the sourcing command. # Note: Replacing $HOME in $scriptFileAbsPath - if present - with '~' is nontrivial to get right when combined with quoting, so we don't bother. scriptFileAbsPathQ=$(command printf '%q' "$scriptFileAbsPath") # shell-quote the path so we can use it without single or double quotes below; caveat: different shells produce different `printf '%q'` output. cmd="[[ -f $scriptFileAbsPathQ ]] && . $scriptFileAbsPathQ" cmt="# Added by ${scriptFileAbsPath##*/} -i; ${scriptFileAbsPath##*/} -u to remove." # See if command is already present in target file. # Note: We only search by the naked command, not including the comment, to have the freedom to change the comment's wording later. # For some additional flexibility, we ignore any whitespace at the start of the line. instLineRegEx='^[[:space:]]*'"$(__typex_reQuote "$cmd")"'([[:space:]]|$)' [[ -f $initFile ]] && command grep -Eq "$instLineRegEx" "$initFile" && found=1 # Add to / remove the sourcing command from the initialization file. # !! We do not use `sed -i` to update the file, as that would replace the original file's inode and thereby # !! potentially destroy symlinks (and lose the original creation date and extended attributes on OSX). # !! `ed` is an option in principle, but, unfortunately, not all platforms have it (e.g., Fedora). # !! Therefore, we simply use >> and > to append to / rewrite the file, which preserves the original inode. ec=0 if (( install )); then if (( found )); then # already installed (( verbose )) && command printf '%s\n' "(Sourcing command for '$scriptFileAbsPath' already present in '$initFile'; nothing to do.)" else # Append line to file (create it, if necessary). printf '\n%s %s\n' "$cmd" "$cmt" >> "$initFile" ec=$? fi else # uninstall if (( ! found )); then # already uninstalled (( verbose )) && command printf '%s\n' "(Sourcing command for '$scriptFileAbsPath' not present in '$initFile'; nothing to do.)" else # Remove line from file. newContents=$(command grep -Ev "$instLineRegEx" "$initFile") && command printf '%s\n' "$newContents" > "$initFile" ec=$? # Also undefine the sourced *function*, if currently defined. unset -f ${scriptFileAbsPath##*/} 2>/dev/null fi fi # Output success / error message. if (( install )); then if (( ec == 0 )); then command printf '%s\n' "Sourcing of '$scriptFileAbsPath' installed in '$initFile'." else command printf '%s\n' "ERROR: Failed to install sourcing of '$scriptFileAbsPath' in '$initFile'; please manually add line '. $scriptFileAbsPath'." >&2 fi else if (( ec == 0 )); then command printf '%s\n' "Sourcing of '$scriptFileAbsPath' uninstalled from '$initFile'." else command printf '%s\n' "ERROR: Failed to uninstall sourcing of '$scriptFileAbsPath' from '$initFile'; please manually remove the line containing '. $scriptFileAbsPath'." >&2 fi fi return $ec } # Change shell options, if necessary, and save their state. # !! Shell-global variables must be used to save the state - prefix them with __typex_ to avoid name collisions. function __typex_init { [[ -n $ZSH_VERSION ]] && { # zsh only # Set configuration item that makes `command` invoke builtins, too. __typex_prevPosixBuiltinsOptState=${options[posixbuiltins]}; options[POSIX_BUILTINS]=on; # Set configuration items that makes Zsh arrays behave like Ksh / Bash items, notably to use base index *0*. __typex_prevKshArrayEmulationOptState=${options[ksharrays]}; options[ksharrays]=on; } || unset __typex_prevPosixBuiltinsOptState __typex_prevKshArrayEmulationSOpttate } # Restore changed configuration options, if any. # !! ALL EXIT POINTS OF THIS FUNCTION MUST CALL THIS. # !! Sadly, if the function BREAKS, we don't get a chance to do this. function __typex_restore { # Restore all options that were changed in __typex_init(). [[ -n $__typex_prevPosixBuiltinsOptState ]] && options[posixbuiltins]=$__typex_prevPosixBuiltinsOptState [[ -n $__typex_prevKshArrayEmulationOptState ]] && options[ksharrays]=$__typex_prevKshArrayEmulationOptState } # ---- # ---- END: NESTED HELPER FUNCTIONS # IMPORTANT: # !! In the code below, given that it is typically *sourced*, we try to *guard against # !! aliases and shell functions shadowing builtins and standard utilities*. # !! For standard utilities, `command someUtil ...` can be used. # !! In bash and ksh this also works for builtins, but not in zsh, by default - EXCEPT if configuration # !! item POSIX_BUILTINS (posixbuiltins) is set. # !! (Using `builtin` to invoke builtins is, unfortunately, not an option, because in ksh it serves a different purpose.) # !! Given that this function is typically *sourced*, we must *save and restore* changed shell options, which we do # !! with functions __typex_init() and __typex_restore() # ------------------- # !! UPSHOT: THROUGH THE REMAINDER OF THIS FUNCTION, # !! - *any standard utility or builtin must be called with prefix `command `*. # !! - { __typex_restore; return []; } must be used *everywhere* in lieu of just `return` to return from this function. # ------------------- __typex_init # Determine this function's name. # !! We do this here, because in ksh $FUNCNAME is NOT available inside NESTED functions. command typeset thisFuncName=$([[ -n $BASH_VERSION ]] && command printf %s "$FUNCNAME" || command printf %s "$0") # Look for the special, private option that the top-level script code passes in to signal # that the script was invoked directly rather than getting sourced. command typeset notSourced=0 [[ $1 == '--not-sourced' ]] && { shift; notSourced=1; } # Note: For convenience, we ALWAYS print ALL defined forms of a given name. command typeset all=1 # !! zsh requires that all declarations be assigned a value, otherwise it *prints* variable definitions. command typeset CDPATH= numNotFound=0 name= names= isExplicitPath=0 isKeyword=0 keywordOutput= def= found=0 isBuiltIn=0 specialFileType= brokenSymlink=0 extraOpt= files= file= thisfile= fileCategory= fileType= scriptType= ver= fileLinkChain= trueFile= errMsgSuffix= shell= # ----- BEGIN: OPTIONS PARSING: command typeset fileSystemOnly=0 noVersion=0 verbose=0 install=0 uninstall=0 command typeset allowOptsAfterOperands=1 i=0 optName= isLong=0 prefix= optArg= haveOptArgAttached=0 haveOptArgAsNextArg=0 acceptOptArg=0 needOptArg=0 command typeset -a operands # Note: For the sake of Ksh, explicitly declare this as an array, __typex_reMatch= # Array variable that receives regex-matching info; !! MUST NOT BE DECLARED WITH TYPESET, as it is assigned to in __typex_reMatch(), which in Ksh can only assign to a GLOBAL variable. while (( $# )); do if __typex_reMatch "$1" '^(-)[a-zA-Z0-9]+.*$' || __typex_reMatch "$1" '^(--)[a-zA-Z0-9]+.*$'; then # an option: either a short option / multiple short options in compressed form, or a long option # Note: In Zsh, options[ksharrays] is assumed to be ON so that array subscripts start with 0, as in Bash / Ksh. prefix=${__typex_reMatch[1]}; [[ $prefix == '--' ]] && isLong=1 || isLong=0 for (( i = 1; i < (isLong ? 2 : ${#1}); i++ )); do command typeset acceptOptArg=0 needOptArg=0 haveOptArgAttached=0 haveOptArgAsNextArg=0 optArgAttached= optArgOpt= optArgReq= if (( isLong )); then # long option: parse into name and, if present, argument optName=${1:2} __typex_reMatch "$optName" '^([^=]+)=(.*)$' && { optName=${__typex_reMatch[1]}; optArgAttached=${__typex_reMatch[2]}; haveOptArgAttached=1; } else # short option: *if* it takes an argument, the rest of the string, if any, is by definition the argument. # Note: For the sake of Zsh, use $-prefixed variable references as subscripts; e.g., `${1:$i:1}` rather than `${1:i:1}`. optName=${1:$i:1}; optArgAttached=${1:$i+1}; (( ${#optArgAttached} >= 1 )) && haveOptArgAttached=1 fi (( haveOptArgAttached )) && optArgOpt=$optArgAttached optArgReq=$optArgAttached || { (( $# > 1 )) && { optArgReq=$2; haveOptArgAsNextArg=1; }; } # ---- BEGIN: CUSTOMIZE HERE case $optName in p|files-only) fileSystemOnly=1 ;; v|verbose) verbose=1 ;; V|skip-version) noVersion=1 ;; i|install-sourcing) install=1 ;; u|uninstall-sourcing) uninstall=1 ;; *) { echo "$thisFuncName: ARGUMENT ERROR: Unknown option: ${prefix}${optName}" >&2; __typex_restore; return 2; } ;; esac # ---- END: CUSTOMIZE HERE (( needOptArg )) && { (( ! haveOptArgAttached && ! haveOptArgAsNextArg )) && { echo "$thisFuncName: ARGUMENT ERROR: Option ${prefix}${optName} is missing its argument." >&2; __typex_restore; return 2; } || (( haveOptArgAsNextArg )) && shift; } (( acceptOptArg || needOptArg )) && break done else # an operand if [[ $1 == '--' ]]; then shift; operands+=( "$@" ); break elif (( allowOptsAfterOperands )); then operands+=( "$1" ) # continue else operands=( "$@" ) break fi fi shift done (( ${#operands[@]} > 0 )) && set -- "${operands[@]}"; unset allowOptsAfterOperands operands i optName isLong prefix optArgAttached haveOptArgAttached haveOptArgAsNextArg acceptOptArg needOptArg # --- End: OPTIONS PARSING: "$@" now contains all operands (non-option arguments). if (( install || uninstall )); then # install in / uninstall from supported shells' user-specific initialization file. (( !fileSystemOnly && !noVersion && !(install && uninstall) && $# == 0 )) || { echo "$thisFuncName: ARGUMENT ERROR: Incompatible options specified." >&2; __typex_restore; return 2; } (( uninstall )) && install=0 # Install/uninstall sourcing for ALL supported shells, IF installed. # Note: By definition, at least *1* supported shell is present: bash - otherwise this script wouldn't execute (unless someone passes it to another shell as filename operand). command typeset shellCount=0 okCount=0 for shell in bash ksh zsh; do (( ++shellCount )) # Ignore, if the shell cannot be found on this system. if [[ $TYPEX_TEST_ALL_INITFILES != '1' ]]; then # override fore testing if [[ -n $ZSH_VERSION ]]; then command which -p $shell >/dev/null 2>&1 || continue else command which $shell >/dev/null 2>&1 || continue fi fi # !! We must *explicitly* pass the following information: # - whether this script is being sourced (nested functions in ksh don't see their parents' local variables). # - this script's absolute path, as determining it inside a *nested* function doesn't work reliably in zsh (as of 5.0.5). # Also, we don't fail right away, so that the command can be attempted for all shells, and at least error messages are shown for each. __typex_install $shell $install $notSourced "$__typex_scriptpath" && (( ++okCount )) done (( install && okCount )) && command printf '%s\n' 'Open a new shell tab or window for sourcing to take effect.' if (( okCount == shellCount )); then __typex_restore; return 0 else __typex_restore; return $(( shellCount - okCount )) fi fi if (( $# == 0 )); then # If NO NAMES were passed, examine the BINARY OF THE CURRENT SHELL (IF SOURCED) / THE PARENT PROCESS (IF NON-SOURCED). # Note that if the process name starts with '-', by convention indicating a *login* shell, the binary name that follow is a mere filename, e.g., '-bash' # In that event we assume that it's an instance of the current user's default shell, as reflected in $SHELL - this is not guaranteed to be the case # but very likely, and avoids a potential false positive from looking up the mere filename in the $PATH. names=( $(f=$(command ps -o comm= $( (( notSourced )) && command printf $PPID || command printf $$ )); [[ $f =~ ^- ]] && command printf %s "$SHELL" || command printf %s "$f") ) fileSystemOnly=1 else names=( "$@" ) fi if (( ! fileSystemOnly )); then # The -p option is *implied*, if all NAMEs are explicit paths (contain "/"). fileSystemOnly=1 for name in "${names[@]}"; do [[ $name == */* ]] || { fileSystemOnly=0; break; } done fi if (( ! fileSystemOnly && notSourced )); then fileSystemOnly=1 # !! If this function wasn't sourced, there's no point in looking for aliases, function, builtins, because the invoking shell's context is not accessible. # !! Additionally, if invoked standalone, it is always *bash* that runs this script, which may or may not be the same as the invoking shell - different shell # !! types have different builtins, for instance, so the results could be misleading. # !! - Aliases and functions (with the exception of the rarely used exported-functions feature in bash) are NOT inherited by child shells (as opposed to subshells), # !! so there is no point in trying to report them when non-sourced. # !! - Builtins: We could in theory make an effort to report builtins (either if we're found to already be running the same shell type as the invoking shell, # !! or even through explicit determination via a child shell of the same type as the invoking one), but this partial solution could be confusing and # !! is not worth the trouble. # !! Ergo: The cleaner and simpler solutionis to ignore aliases, function, builtins when non-sourced, and to make do with the warning that is printed. if __typex_isParentSupportedShell; then command cat >&2 <\` To install sourcing persistently for interactive shells, run \`${__typex_scriptpath##*/} -i\`. Use \`-p\` to suppress this warning if you're only looking for executable *files*. EOF else command cat >&2 </dev/null | command egrep -q ' keyword$' && isKeyword=1 elif [[ -n $ZSH_VERSION ]]; then command type -wa "$name" 2>/dev/null | command egrep -q ' reserved$' && isKeyword=1 else # bash command type -ta "$name" 2>/dev/null | command fgrep -qx 'keyword' && isKeyword=1 fi if (( isKeyword )); then found=1 keywordOutput=$(command printf '%-10s %s' 'KEYWORD:' "$name") fi # ksh (also dash): keyword has HIGHER precedence than alias. if [[ -n $keywordOutput && -n $KSH_VERSION ]]; then command printf '%s\n' "$keywordOutput" fi # Alias? def=$(command alias "$name" 2>/dev/null) if [[ -n $def ]]; then found=1 command printf '%-10s %s\n' 'ALIAS:' "${def#alias }" (( all )) || { __typex_restore; return 0; } # Extract the alias' first token. def=$(cut -d= -f2- <<<"$def" | tr -d \'\" | cut -d' ' -f1) # Recurse on the alias' first token to also print the underlying command's definition, # and indent the output to show that is subordinate information. # Note: # - Since aliases don't necessarily start with a command, we quietly ignore failure. # - To prevent bloat in the output, we do not pass through -v, if it was specified, to the recursive call. typex -- "$def" 2>/dev/null | sed 's/^/ /' fi # bash, zsh: keyword has LOWER precedence than alias. if [[ -n $keywordOutput && -z $KSH_VERSION ]]; then command printf '%s\n' "$keywordOutput" fi # Shell function? def=$(command typeset -f "$name" 2>/dev/null) # get function definition if [[ -n $def ]]; then # if a function, print its name followed by () found=1 command printf '%-10s %s' 'FUNCTION:' "$name()" if [[ -n $BASH_VERSION ]]; then # bash only: if it's an exported function, append ' # -x' command declare -F | command egrep -q -- "-fx $name\$" && command printf ' # -x' fi command printf '\n' # if in verbose mode: print function definition (source code) too. (( verbose )) && command sed 's/^/ /' <<<"$def" (( all )) || { __typex_restore; return 0; } fi # Builtin? if [[ -n $KSH_VERSION ]]; then # !! ksh: if a function shadows a builtin, `type -a` inexplicably doesn't report the builtin, so we must test differently. # !! In ksh - unlike in bash and zsh - running `builtin` without arguments lists all defined builtins, so we look in its output. command builtin | command fgrep -qx -- "$name" && isBuiltIn=1 elif [[ -n $ZSH_VERSION ]]; then # Check for builtins; note use of `-a` to print *all* types, in case a function shadows the builtin. # !! -w prints a more terse description command type -wa -- "$name" 2>/dev/null | command egrep -q ':\s+builtin$' && isBuiltIn=1 else # bash # Check for builtins; note use of `-a` to print *all* types, in case a function shadows the builtin. # -t prints one-word descriptions; without it, shell functions are printed with their full definition, which could lead to false positives. command type -ta -- "$name" 2>/dev/null | command fgrep -qx 'builtin' && isBuiltIn=1 fi if (( isBuiltIn )); then command printf '%-10s %s\n' 'BUILTIN:' "$name" (( all )) || { __typex_restore; return 0; } found=1 fi fi # -- END: look for NON-file command forms # [Executable] file or directory? files= if (( isExplicitPath )) || [[ $name == '.' ]]; then if [[ -d $name ]]; then # directory # ??? # # Convert to full path (without resolving symlinks YET). # if [[ $name =~ /../?$ ]]; then # # !! Exception: if the path ends in /.. or /../, we DO resolve symlinks # # !! right away to avoid unexpected behavior: consider the following OSX path: # # !! /tmp/.. - it *seemingly* effectively points to /, but given that /tmp is # # !! a symlink to /private/var, `ls /tmp/..` actually targets /private(!) - i.e., /private/var/.. # # !! `ls` clearly resolves the symlink /tmp first, before applying '..' - the same goes for # # !! executing a file: running /tmp/../foo would actually look for /private/foo(!) # # !! However, `ls` doesn't actually *display* the resolved dir. path when using -d, or when targeting a file: `ls /tmp/../foo` # # !! *targets* /private/foo, but *displays path* /tmp/../foo(!) # # !! The only exception is `cd` if you don't also use -P: `cd /tmp/..` actually changes to / (whereas cd -P /tmp/.. does change to /private). # files=$(command cd -P -- "$name" && command echo "$PWD") # else # files=$(command cd "$name" && command echo "$PWD") # fi files=$name # Further analysis happens below. elif [[ -e $name || -L $name ]]; then # file or symlink to file or special file (block device, character devie, pipe, socket) - we all treat them the same here and rely on `file` to provide type info below. # !! If -L $name is true, but -e $name isn't, $name is a broken (dangling) symlink. # !! We still proceed with inspecting the symlink itself. [[ ! -e $name ]] && brokenSymlink=1 # Convert to full path (without resolving symlinks). # !! Note: There is an obscure bug in ksh 93u+ when passing paths such as ../.foo to cd fails and complains about # !! /foo (!) not existing - the '.' in '.foo' is mistakenly dropped. # !! We work around it by prepending $PWD. [[ -n $KSH_VERSION && $name =~ ^'../.'. ]] && name="${PWD%/}/$name" # ??? # files=$(command cd -- "$(command dirname -- "$name")" && command echo "${PWD%/}/")$(command basename -- "$name") # [[ $files =~ / ]] || { __typex_restore; return 1; } # this should never happen, except if the ksh workaround above isn't effective files=$name # Further analysis happens below. else : # We assume this means the path doesn't exist and report it below. fi else # Get ALL files in the $PATH, using `which -a`. if [[ -n $ZSH_VERSION ]]; then # !! zsh: -p is needed to only consider FILES, not also OTHER command forms, such as functions (which would print as their full definitions) - not needed for bash and ksh (which do not even support -p). files=$(command which -ap -- "$name" 2>/dev/null) || files='' # !! `which -ap` under certain circumstances, e.g. `which -ap while`, prints its error message to *stdout*, so we make sure to only capture output in case of *success*. else files=$(command which -a -- "$name" 2>/dev/null) || files='' fi fi if [[ -n $files ]]; then # executables matching the name found found=1 while command read -r file; do # !! zsh: options[posixbuiltins] must be ON for `command read` to work as expected - see above. # Unless we're dealing with a broken symlink, we want to examine the *target* of a symlink. # !! BSD `file` detects a broken symlink even with -L, while GNU `file` does not: it treats the symlink like a non-existent file (and defaults to NOT following symlinks by default, unlike its BSD cousin). # Both BSD and GNU `file` report something like `broken symbolic link to ...` for broken symlinks. (( brokenSymlink )) || extraOpt='-L' fileType=$(command file -b $extraOpt -- "$file") # determine file-type info of the file, or, in case of a symlink, its ultimate target if [[ -d $file ]]; then fileCategory='DIRECTORY:' elif [[ -x $file ]]; then # See if the executable is a *script* (an executable text file, normally with a shebang line) scriptType=$(command egrep -o '(POSIX )?(\w+ )script' <<<"$fileType") # `file` reports shebang-less scripts as text files rather than a script - we create our own type description. [[ -z $scriptType ]] && command egrep -q '\' <<<"$fileType" && scriptType='script w/o shebang line' [[ -n $scriptType ]] && fileCategory='SCRIPT:' || fileCategory='BINARY:' else fileCategory='FILE:' fi # Print the full path and, if applicable, symlink chain. # Note that we always examine ALL paths, not just symlinks, because even non-symlinks # can have symlink components (directories) in their *path*. fileLinkChain=$(__typex_rreadlinkchain "$file") # !! with a broken symlink, this will output an error message if [[ ! $fileLinkChain =~ $'\n' ]]; then # $file is NOT a symlink, but may contain symlinks in its path. trueFile=$fileLinkChain if [[ "$file" != "$trueFile" ]]; then # $file has at least 1 symlink component (directory) in its path. # Note that, unlike with targeting a symlink directly, we only print the *ultimate* target path in this case. command printf '%-10s %s -> %s' "$fileCategory" "$file" "$fileLinkChain" else # $file has no symlink components in its path # Print the file's full path (only). command printf '%-10s %s' "$fileCategory" "$file" fi else # $file is itself a SYMLINK (possibly a broken one) # A symlink: resolve the *entire* chain of symlinks `ls -F`-style, but with *full* paths; e.g.: # /usr/local/bin/awk@ -> /usr/local/Cellar/gawk/4.1.0/bin/awk@ -> /usr/local/Cellar/gawk/4.1.0/bin/gawk # __typex_rreadlinkchain() reads the chain of links: from given link to (ultimate) target. trueFile=$(command tail -n 1 <<<"$fileLinkChain") # The `awk` command replaces the newlines with ' -> 'and terminates every field but the last with "@" to mark it as a symlink. # Note: `gsub("\n$", "")` makes sure that the last line has no trailing \n - gawk, for instance, apparently always appends one. command printf '%-10s %s' "$fileCategory" "$(command awk -v RS= -v sep=' -> ' '{ gsub("\n$", ""); gsub("\n", "@" sep); printf "%s", $0 }' <<<"$fileLinkChain")" fi # Append script-type/non-executable-file-type info and, for executables, version info, if available. if [[ -d $file ]]; then : # n/a elif [[ -x $file ]]; then # For executable *scripts*, append script-interpreter information (e.g., 'bash script') [[ -n $scriptType ]] && printf ' (%s)' "$scriptType" # For executables: Try to obtain version information, unless suppressed. ver= if (( ! noVersion )); then # Note that we only keep the 1st line (some utilities - e.g., gawk - output multiple lines). # !! Blindly invoking with --version is somewhat fragile, in that poorly designed executables that don't actually support --version # !! could ignore the option and do their own thing, including waiting for input. We try to avoid that by passing /dev/null to stdin. # !! Still, the invocation could block for other reasons or even perform unwanted tasks. # !! Even if it doesn't block and produces output, that output may or may not be version information. # !! Some utilities, such as ksh and /usr/bin/time, inexplicably report version information on std*err*. # !! Given the variety of version information, we can only perform a crude check to rule out obvious non-version strings. # !! Note that we only look at the first nonempty line. ver=$("$file" --version &1 | command sed '/^$/d; q' | command fgrep -v -- '--version' | command fgrep -vi -- $'unknown\ninvalid\nillegal\nunrecognized\nnot recognized' | command egrep -i -e '[0-9]\.' -e 'version' -e 'build' ) fi # If lookup of version information was suppressed or no version info was found, # use *the file's last-modified timestamp* as a substitute. # Note: We use the last-modified timestamp rather than the file-creation ('birth') date, because the latter is not always available on Linux platforms. [[ -z $ver ]] && ver=$(__typex_getFileTimestamp "$trueFile" m '%Y-%m-%d') # Print version / last-modified info. printf ' [%s]' "$ver" else # non-executable file, including broken symlinks # Append file-type information. printf ' (%s)' "$fileType" fi # If in verbose mode: if (( verbose )); then # Add file stats via `ls -l` (in case of a symlink: for each file in the symlink chain) while command read -r thisfile; do command printf '\n %s' "$(command ls -dFAhl -- "$thisfile" | command cut -d/ -f1)" done <<<"${fileLinkChain:-$file}" # Add additional information for executables. if [[ ! -d $file ]]; then if [[ -n $scriptType ]]; then # for a script, print the shebang line (for shebang-less files, this will simply be whatever the 1st line happens to be). command printf '\n %s' "$(command head -n 1 "$file")" elif [[ -x $file ]]; then # for an executable binary, output file-type information (architecture, ...) command printf '\n %s' "$(sed '1! s/^/ /' <<<"$fileType")" # some binaries produce multi-line input fi fi fi command printf '\n' (( all )) || break # not ALL paths requested? we're done after the first pass. done <<<"$files" fi # If not found, print error message. if (( ! found )); then (( ++numNotFound )) if (( isExplicitPath )); then errMsgSuffix=": No such file or directory, or insufficient permissions for inspection." else (( fileSystemOnly )) && errMsgSuffix=' is not an executable in the PATH.' || errMsgSuffix=' is neither an alias, keyword, function, builtin, nor an executable in the PATH.' [[ -e $name ]] && errMsgSuffix+=$'\nHowever, it does exist as a filesystem item in the current directory. To target that, prefix with \"./\"' fi command printf '%s\n' "$thisFuncName: \"${name}\"${errMsgSuffix}" >&2 fi done # for name in names __typex_restore # restore global settings return $numNotFound # exit code is the number of NAMEs that were NOT found; i.e., 0, if all were found. } # We only get here in 2 cases: # - The script is being *sourced* by the current shell without arguments, solely in order to define the function of the same name for later use. # Once sourced, invoking `typex` will invoke the *function directly*. # - Either the script is being invoked *directly* (and, due to its shebang line, run by *bash*) or the script filenname was passed to a shell executable. # Either way, the invoking shell's aliases and functions won't be visible. if [[ -n $ZSH_EVAL_CONTEXT && $ZSH_EVAL_CONTEXT =~ :file$ ]] || [[ -n $KSH_VERSION && $(cd "$(dirname -- "$0")" && printf '%s' "${PWD%/}/")$(basename -- "$0") != ${.sh.file} ]] || [[ -n $BASH_VERSION && $0 != $BASH_SOURCE ]]; then # script is being SOURCED if (( $# > 0 )); then # This script is being sourced *and* invoked with arguments at the same time - useful for ad-hoc sourcing, such as when using typex in a script or when running something like `shall -c '. ./typex ...'. typex() remains defined as a - desirable - side effect. typex "$@" # innvoke typex() return # return, passing typex()'s exit code through. else # This script is being sourced without arguments, solely to define the typex() function for later use. # Nothing more to do; return, indicate success return 0 fi else # This script is NOT being sourced; either handle standard options such as --version, or run in non-sourced mode, which prevents reporting on aliases, functions, shell builtins. # Handle STANDARD OPTIONS up front, HERE, WITHOUT INVOKING the FUNCTION typex(). # Note that an already-sourced typex() function indirectly calls this script # (non-sourced) and ends up here as well for handling standard options. # # !! EVEN THOUGH THIS BLOCK IS ONLY *EXECUTED* BY BASH, IT IS STILL *PARSED* # !! BY KSH AND ZSH, SO IT NEEDS TO BE VALID SYNTAX IN THOSE SHELLS TOO: # !! - use `typeset` instead of `local` # !! -use `>... 2>&1` instead of `&>` # --- STANDARD SCRIPT-GLOBAL CONSTANTS kTHIS_NAME=${BASH_SOURCE##*/} kTHIS_HOMEPAGE='https://github.com/mklement0/typex' kTHIS_VERSION='v0.4.3' # NOTE: This assignment is automatically updated by `make version VER=` - DO keep the 'v' prefix. # --- Begin: STANDARD HELPER FUNCTIONS # SYNOPSIS # openUrl # DESCRIPTION # Opens the specified URL in the system's default browser. openUrl() { typeset url=$1 platform=$(uname) cmd=() case $platform in 'Darwin') # OSX cmd=( open "$url" ) ;; 'CYGWIN_'*) # Cygwin on Windows; must call cmd.exe with its `start` builtin cmd=( cmd.exe /c start '' "$url " ) # !! Note the required trailing space. ;; 'MINGW32_'*) # MSYS or Git Bash on Windows; they come with a Unix `start` binary cmd=( start '' "$url" ) ;; *) # Otherwise, assume a Freedesktop-compliant OS, which includes many Linux distros, PC-BSD, OpenSolaris, ... cmd=( xdg-open "$url" ) ;; esac "${cmd[@]}" || { echo "Cannot locate or failed to open default browser; please go to '$url' manually." >&2; return 1; } } # Prints the embedded Markdown-formatted man-page source to stdout. printManPageSource() { sed -n -e $'/^: <<\'EOF_MAN_PAGE\'/,/^EOF_MAN_PAGE/ { s///; t\np;}' "$BASH_SOURCE" } # Opens the man page, if installed; otherwise, tries to display the embedded Markdown-formatted man-page source; if all else fails: tries to display the man page online. openManPage() { typeset pager embeddedText if ! man 1 "$kTHIS_NAME" 2>/dev/null; then # 2nd attempt: if present, display the embedded Markdown-formatted man-page source embeddedText=$(printManPageSource) if [[ -n $embeddedText ]]; then pager='more' command -v less >/dev/null 2>&1 && pager='less' # see if the non-standard `less` is available, because it's preferable to the POSIX utility `more` printf '%s\n' "$embeddedText" | "$pager" else # 3rd attempt: open the the man page on the utility's website openUrl "${kTHIS_HOMEPAGE}/doc/${kTHIS_NAME}.md" fi fi } # Prints the contents of the synopsis chapter of the embedded Markdown-formatted man-page source for quick reference. printUsage() { typeset embeddedText # Extract usage information from the SYNOPSIS chapter of the embedded Markdown-formatted man-page source. embeddedText=$(sed -n -e $'/^: <<\'EOF_MAN_PAGE\'/,/^EOF_MAN_PAGE/!d; /^## SYNOPSIS$/,/^#/{ s///; t\np; }' "$BASH_SOURCE") if [[ -n $embeddedText ]]; then # Print extracted synopsis chapter - remove backticks for uncluttered display. printf '%s\n\n' "$embeddedText" | tr -d '`' else # No SYNOPIS chapter found; fall back to displaying the man page. echo "WARNING: usage information not found; opening man page instead." >&2 openManPage fi } # --- End: STANDARD HELPER FUNCTIONS # --- PROCESS STANDARD, OUTPUT-INFO-THEN-EXIT OPTIONS. case $1 in --version) # Output version number and exit, if requested. echo "$kTHIS_NAME $kTHIS_VERSION"$'\nFor license information and more, visit '"$kTHIS_HOMEPAGE"; exit 0 ;; -h|--help) # Print usage information and exit. printUsage; exit ;; --man) # Display the manual page and exit, falling back to printing the embedded man-page source. openManPage; exit ;; --man-source) # private option, used by `make update-man` # Print raw, embedded Markdown-formatted man-page source and exit printManPageSource; exit ;; --home) # Open the home page and exit. openUrl "$kTHIS_HOMEPAGE"; exit ;; esac # --- NON-SOURCED INVOCATION WITH OPTIONS OTHER THAN STANDARD OPTIONS: # Pass all arguments through to the function, preceded by a private option # that tells typex() that it's being invoked NON-sourced. # Note that this will trigger a warning, unless -p is among the arguments or # all operands are filesystem paths. typex --not-sourced "$@" exit # exit, passing typex()'s exit code through fi # ---- END OF MAIN BODY #### # MAN PAGE MARKDOWN SOURCE # - Place a Markdown-formatted version of the man page for this script # inside the here-document below. # The document must be formatted to look good in all 3 viewing scenarios: # - as a man page, after conversion to ROFF with marked-man # - as plain text (raw Markdown source) # - as HTML (rendered Markdown) # Markdown formatting tips: # - GENERAL # To support plain-text rendering in the terminal, limit all lines to 80 chars., # and, for similar rendering as HTML, *end every line with 2 trailing spaces*. # - HEADINGS # - For better plain-text rendering, leave an empty line after a heading # marked-man will remove it from the ROFF version. # - The first heading must be a level-1 heading containing the utility # name and very brief description; append the manual-section number # directly to the CLI name; e.g.: # # foo(1) - does bar # - The 2nd, level-2 heading must be '## SYNOPSIS' and the chapter's body # must render reasonably as plain text, because it is printed to stdout # when `-h`, `--help` is specified: # Use 4-space indentation without markup for both the syntax line and the # block of brief option descriptions; represent option-arguments and operands # in angle brackets; e.g., '' # - All other headings should be level-2 headings in ALL-CAPS. # - TEXT # - Use NO indentation for regular chapter text; if you do, it will # be indented further than list items. # - Use 4-space indentation, as usual, for code blocks. # - Markup character-styling markup translates to ROFF rendering as follows: # `...` and **...** render as bolded (red) text # _..._ and *...* render as word-individually underlined text # - LISTS # - Indent list items by 2 spaces for better plain-text viewing, but note # that the ROFF generated by marked-man still renders them unindented. # - End every list item (bullet point) itself with 2 trailing spaces too so # that it renders on its own line. # - Avoid associating more than 1 paragraph with a list item, if possible, # because it requires the following trick, which hampers plain-text readability: # Use ' ' in lieu of an empty line. #### : <<'EOF_MAN_PAGE' # typex(1) - report salient information about commands, programs, and filesystem items ## SYNOPSIS Reports salient information about available commands, programs, and filesystem items, such as command or file type, (ultimate) location, and version. typex [-p] [-V] [-v] [...] -p look for files only (ignore shell aliases, keywords, functions, builtins) -V skip attempt to obtain executable version information -v verbose mode: report additional information Install / uninstall sourcing via shell-initialization files (required to detect aliases, functions, shell builtins - supported in Bash, Ksh, Zsh): typex -i typex -u ## DESCRIPTION `typex` combines and extends features found in the standard `type`, `which`, and `file` utilities. Not specying any operands prints information about the current shell. When given a command name (as opposed to a filesystem path), the following command forms are recognized: * shell aliases, shell keywords, shell functions, shell builtins, and executable files in the `$PATH` Alternatively, filesystem paths rather than mere (file)names may be given, in which case extended information about theses paths is reported. NOTE: For this utility to detect shell-based command forms, it must be *sourced*, which is supported in the following shells: `bash`, `ksh`, and `zsh` Sourcing this utility in your shell's per-user initialization file will define a function of the same name that will be available in interactive sessions: * `typex -i` installs this sourcing for all supported shells. * `typex -u` uninstalls it. Alternatively, you can source and invoke at the same time; e.g.: . typex read If you instead invoke this utility directly (as a script): * only executable *files* and filesystem items will be detected - no other command forms; * a warning to that effect is printed, unless only filesystem paths are supplied; suppress it with `-p`. Unless you specify a filesystem path, ALL defined forms of a given command name are reported, in order of precedence (highest first; if this utility is invoked directly or `-p` is specified, only executable *files* with that name are reported): * `bash`, `zsh`: alias > shell keyword > shell function > builtin > executable * `ksh` shell keyword > alias > shell function > builtin > executable The exit code is `0` if all operands were found, the number of non-existent operands otherwise. Note: `typex` is designed for interactive use, to provide humans with concise, salient information about a command or filesystem object. As such, its output is geared toward readability by humans, and should not be relied on for programmatic parsing. ## OPTIONS * `-p`, `--files-only` Only looks for (executable) *files* (ignores aliases, keywords, functions, and builtins). This option applies implicity if a given operand is a filesystem path rather than a mere command name. * `-V`, `--skip-version` Suppresses the attempt to obtain version information from executable files. Helpful if you know that the target file doesn't support `--version, or you're wary of what invocation with `--version` might do. * `-v`, `--verbose` Verbose mode: prints additional information - see below. * `-i`, `--install-sourcing` Updates the user-specific shell-initialization files of all supported shells, if present, to include a command to source this utility, which is the prerequisite for it to be able to detect shell-based definitions, namely shell aliases, keywords, functions, and builtins. * `-u`, `--uninstall-sourcing` Removes a previously installed sourcing command from the user-specific initialization files of supported shells. ## OUTPUT FORMAT All output starts with a capitalized headword identifying the command form / path type, as in the headings below. Output for each command form is single-line, except for the optional information added by `-v`; the latter is indented with 2 spaces. ### ALIAS Prints the shell alias's definition. If the first token of the alias's definition is itself a command, information about that command is printed as well, in indented form, potentially recursively. No attempt is made to detect additional commands inside the alias definition. `-v` is never applied to the recursive calls. ### KEYWORD A shell-language keyword, such as `while`; the name is printed. ### FUNCTION Prints the shell function name, followed by `()`; e.g., `foo()`. In Bash, if the function is also exported, ` # -x` is appended. With `-v` specified: The function definition (its source code) is printed, too. ### BUILTIN Prints the shell builtin's name. ### BINARY / SCRIPT / DIRECTORY / FILE Prints files' full path, and, in the case of symlinks, the entire symlink chain to the ultimate target file, using full paths. If the file is not itself a symlink, but has symlink components (directories) in its directory path, both the full form of the input path as well as the canonical path with all symlinks resolved are printed. If the file is an executable script, interpreter information is appended in parentheses; e.g., `(bash script)`. If the file is non-executable, file-type information obtained with `file` is appended in parentheses; e.g., `(ASCII text)`. If the file is a special file such as `/dev/null`, its type is also appended in parentheses; e.g., `(character special)`. For executable files, unless `-V` is specified, an attempt is made to obtain version information through invocation with `--version`. If `-V` was specified, or if no version information was found, the last-modified timestamp of the (ultimate target) file is used. Whatever information is available is appended in square brackets; e.g., `[ls (GNU coreutils) 8.21]` or `[2014-09-09]` With `-v` specified: * File statistics (permissions, owner, ...) are printed using `ls -l`. * For symlinks, this happens for every file in the symlink chain. Additional information printed for executables: * binaries: file-type information obtained with `file`. * scripts: the shebang line. ## STANDARD OPTIONS All standard options provide information only. * `-h, --help` Prints the contents of the synopsis chapter to stdout for quick reference. * `--man` Displays this manual page, which is a helpful alternative to using `man`, if the manual page isn't installed. * `--version` Prints version information. * `--home` Opens this utility's home page in the system's default web browser. ## COMPATIBILITY Platforms supported in principle: * OSX, Linux, BSD (with Bash installed) On those, sourcing the script (recommmended) works in the following shells: * Bash, Ksh, Zsh ## LICENSE For license information, bug reports, and more, visit this utility's home page by running `typex --home` ## EXAMPLES # Print info about the current shell. $ typex BINARY: /bin/bash [GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin14)] # Print info about utility 'awk' and its symlink chain: $ typex awk BINARY: /usr/bin/awk@ -> /etc/alternatives/awk -> /usr/bin/gawk [GNU Awk 4.0.1] # Print information about command forms named 'printf' in order of # precedence: $ typex printf BUILTIN: printf BINARY: /usr/bin/printf [printf (GNU coreutils) 8.21] # Print information about script 'npm': $ typex -p npm SCRIPT: /usr/local/bin/npm (node script) [2.1.7] # Print information about executables 'nawk' and 'mawk': $ typex -p nawk mawk BINARY: /usr/bin/nawk@ -> /etc/alternatives/nawk@ -> /usr/bin/gawk [GNU Awk 4.0.1] BINARY: /usr/bin/mawk [2014-03-24] # Define an alias and print information about it. $ alias lsx='ls -FAhl'; typex lsx ALIAS: lsx='ls -FAhl' BINARY: /bin/ls [ls (GNU coreutils) 8.21] # Print extended information about a filesystem object: $ typex -v '/User Guides And Information' DIRECTORY: /User Guides And Information@ -> /Library/Documentation/User Guides and Information.localized lrwxr-xr-x 1 root wheel 60B Jul 2 2012 drwxrwxr-x@ 9 root admin 306B Oct 16 2014 EOF_MAN_PAGE