View on GitHub

bash-funk

bash-funk is a collection of useful commands for Bash 3.2 or higher.

Bash-Funk “filesystem” module

The following commands are available when this module is loaded:

  1. -abspath
  2. -cd-down
  3. -cd-hist
  4. -cd-up
  5. -count-words
  6. -du
  7. -extract
  8. -find-up
  9. -findfiles
  10. -ll
  11. -mkcd
  12. -modified
  13. -owner
  14. -realpath
  15. -sudo-append
  16. -sudo-write
  17. -tail-reverse
  18. -test-all-filesystem

License

SPDX-FileCopyrightText: © Vegard IT GmbH (https://vegardit.com)
SPDX-License-Identifier: Apache-2.0

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

-abspath

Usage: -abspath [OPTION]... [PATH]

Prints the normalized path of the given path WITHOUT resolving symbolic links. The path is not required to exist.

Parameters:
  PATH (default: '.')
      The path to normalize.

Options:
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:


# use realpath if available
if hash realpath &>/dev/null; then
   realpath -m $_PATH

# use python as last resort
else
   python -c "import os
print(os.path.abspath('$_PATH'))"
fi

-cd-down

Usage: -cd-down [OPTION]... [START_AT] DIR_NAME

Jumps down in the tree of the current directory to the first sub directory found with the given name.

Parameters:
  START_AT (default: '.')
      The start directory.
  DIR_NAME (required)
      The name of the subdirectory to locate and cd into.

Options:
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

local path=$(find $_START_AT -name "$_DIR_NAME" -type d -print -quit 2>/dev/null || true);
if [[ $path ]]; then
   echo "$path"
   cd $path
else
   echo "-cd-down: $_DIR_NAME: No such directory"
   return 1
fi

-cd-hist

Usage: -cd-hist [OPTION]... [STEPS_OR_DIRNAME]

Navigates back in the directory history which can be managed via pushd/popd/dirs and is automatically populated if the Bash Funk bash-prompt is installed.

Parameters:
  STEPS_OR_DIRNAME
      The name of the subdirectory to locate and cd into. If not specified a list of the last 20 entries is displayed.

Options:
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

if [[ ! $_STEPS_OR_DIRNAME ]]; then
   echo "Directory history:"
   for (( __idx=2; __idx<${#DIRSTACK[*]}; __idx++ )); do
      echo "$(( __idx - 1 )) cd ${DIRSTACK[$__idx]}"
      [[ $__idx -eq 22 ]] && break || true
   done
   return 0
fi

if [[ $_STEPS_OR_DIRNAME == "-" ]]; then
   cd - && return 0 || return 1
fi

if [[ $_STEPS_OR_DIRNAME =~ ^[0-9]+$ ]]; then
   local path="${DIRSTACK[@]:$(( _STEPS_OR_DIRNAME + 1 )):1}"
   echo "$path"
   cd $path
else
   local path
   for path in "${DIRSTACK[@]}"; do
      case "${path}" in
         *"/"$_STEPS_OR_DIRNAME)
            echo "$path"
            cd "$path"
            return 0;
          ;;
      esac
   done
   echo "-cd-hist: $_STEPS_OR_DIRNAME: No such directory in history"
   return 1
fi

-cd-up

Usage: -cd-up [OPTION]... [LEVEL_OR_PATTERN]

Navigates up in the current directory tree to the first parent directory found with the given namen or the given number of levels. Bash completion will auto-complete the names of the parent directories.

Parameters:
  LEVEL_OR_PATTERN (default: '..')
      The number of directories to navigate up in the directory tree or the glob pattern to find a matching directory.

Options:
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

if [[ $_LEVEL_OR_PATTERN == ".." ]]; then
   cd ..
   return 0
fi

# check if value is numeric
if [[ $_LEVEL_OR_PATTERN =~ ^[0-9]+$ ]]; then
   local path
   for (( i = 0; i < _LEVEL_OR_PATTERN; i++ )); do
      path="../$path"
   done
   echo "$path"
   cd "$path"

else
   local elem path=()

   # read current path elements into array 'path'
   IFS=/ read -r -a path <<< "$PWD"

   # iterate reverse through the array and check for matching directory
   for (( idx=${#path[@]}-2; idx>=0; idx-- )); do
      case "${path[idx]}" in
         $_LEVEL_OR_PATTERN)
            # join the path
            IFS="/" eval 'path="${path[*]:0:$((idx+1))}"'
            echo "$path"
            cd "$path"
            return 0;
           ;;
      esac
   done
   echo "-cd-up: $_LEVEL_OR_PATTERN: No such directory"
   return 1
fi

-count-words

Usage: -count-words [OPTION]... WORD1 [WORD]...

Counts the number of occurences of the word(s) in the text read from stdin.

Parameters:
  WORD (1 or more required)
      The word to count.

Options:
-f, --file PATH (file)
        Count the words in the given file instead of reading from stdin.
-s, --sort MODE (one of: [count,word])
        Specifies how to sort the output.
    -----------------------------
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

local sedCmds grepCmds
for word in "${_WORD[@]}"; do
   sedCmds="s/$word/\n$word\n/g; $sedCmds"
   grepCmds="$grepCmds -e $word"
done

if [[ $_file ]]; then
   if [[ $_sort == "count" ]]; then
      sed "$sedCmds" "$_file" | grep $grepCmds | sort | uniq -c | sort -r
   else
      sed "$sedCmds" "$_file" | grep $grepCmds | sort | uniq -c
   fi
else
   # check if stdin is opend on terminal (and thus not on a pipe)
   if [[ -t 0 ]]; then
      return 0
   fi

   if [[ $_sort == "count" ]]; then
      cat /dev/fd/0 | sed "$sedCmds" | grep $grepCmds | sort | uniq -c | sort -r
   else
      cat /dev/fd/0 | sed "$sedCmds" | grep $grepCmds | sort | uniq -c
   fi
fi

-du

Usage: -du [OPTION]... [PATH]...

Prints disk usage information.

Parameters:
  PATH
      The path to check.

Options:
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

[[ ! $_PATH ]] && _PATH=(.) || true

du -s -h "${_PATH[@]}"

-extract

Usage: -extract [OPTION]... ARCHIVE [TO_DIR]

Extracts the given archive using the compatible extractor.

Parameters:
  ARCHIVE (required, file)
      The archive to extract.
  TO_DIR
      The target folder.

Options:
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

if [[ $_TO_DIR ]]; then
   local origPWD="$PWD"
   mkdir "$_TO_DIR"
   cd "$_TO_DIR"
fi

if [[ ! -w "$PWD" ]]; then
   echo "-extract: Error: Path [$PWD] is not writeable."
   return 1
fi

local tmpDir=$(mktemp -d -p "$PWD")

case "$_FILE" in
   *.bz2)            bunzip2    "$_ARCHIVE" ;;
   *.gz)             gunzip     "$_ARCHIVE" ;;
   *.rar)            unrar x    "$_ARCHIVE" ;;
   *.tar)            tar xvf    "$_ARCHIVE" ;;
   *.tbz2|*.tar.bz2) tar xvjf   "$_ARCHIVE" ;;
   *.tgz|*.tar.gz)   tar xvzf   "$_ARCHIVE" ;;
   *.zip)            unzip      "$_ARCHIVE" ;;
   *.Z)              uncompress "$_ARCHIVE" ;;
   *.7z)             7z x       "$_ARCHIVE" ;;
   *) echo "-extract: Error: Unsupported archive format '$_ARCHIVE'"; return 1 ;;
esac

if [[ $_TO_DIR ]]; then
   cd "$origPWD"
fi

-find-up

Usage: -find-up [OPTION]... FILENAME

Traverses the directory upward to find the given file.

Parameters:
  FILENAME (required)
      The file or directory to find.

Options:
-t, --type TYPE (one of: [d,dir,f,file])
        File type.
    -----------------------------
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

local path=$PWD
while [[ $path ]]; do
   case $_type in
      d|dir)  if [[ -d "$path/$_FILENAME" ]]; then echo "$path/$_FILENAME"; return; fi ;;
      f|file) if [[ -f "$path/$_FILENAME" ]]; then echo "$path/$_FILENAME"; return; fi ;;
      *)      if [[ -e "$path/$_FILENAME" ]]; then echo "$path/$_FILENAME"; return; fi ;;
   esac
   path=${path%/*}
done
echo "-find-up: '$_FILENAME': No such file or directory"
return 1

-findfiles

Usage: -findfiles [OPTION]... [START_PATH] SEARCH_STRING

Recursively finds all files containing the given string and displays their path.

Parameters:
  START_PATH (default: '.')
      The path where to search.
  SEARCH_STRING (required)
      The string to search.

Options:
-l, --lines
        Show matching lines of the files that contain the given string.
    --maxdepth levels (integer: ?-?)
        The maximum number of levels to descend into the directory tree below the starting-point.
    --mindepth levels (integer: ?-?)
        The level of directory tree below the starting-point where to start the search.
    --name pattern
        Name pattern.
-u, --unpack
        Unpack supported archives (.zip, .jar, .war, .ear).
-v, --verbose
        Prints additional information during command execution.
    -----------------------------
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

if [[ ! -e "$_START_PATH" ]]; then
   echo "-findfiles: Error: Path [$_START_PATH] does not exist."
   return 1
fi

if [[ ! -r "$_START_PATH" ]]; then
   echo "-findfiles: Error: Path [$_START_PATH] is not readable by user '$USER'."
   return 1
fi

if [[ $_lines ]]; then
   local grepCmd="grep -n"
else
   local grepCmd="grep -l"
fi

local findOpts="-type f"
if [[ $_name ]];     then findOpts="$findOpts -name $_name"; fi
if [[ $_maxdepth ]]; then findOpts="$findOpts -maxdepth $_maxdepth"; fi
if [[ $_mindepth ]]; then findOpts="$findOpts -mindepth $_mindepth"; fi

# turn off verbose if part of pipe or subshell
[[ ! $__interactive ]] && _verbose= || true

if [[ $_verbose ]]; then
   if hash tput &>/dev/null; then
      cols=$(tput cols)
   else
      cols=$(stty size| cut -d' ' -f 2)
   fi

   find "$_START_PATH" $findOpts 2>/dev/null | while read file; do
      local message="Scanning $file ..."

      echo -en "\033[s${message:0:$cols}"
      if [[ $_unpack && ( $file == *.zip || $file == *.jar || $file == *.ear || $file == *.war ) ]]; then
         if unzip -p "$file" | LC_ALL=C $grepCmd "$_SEARCH_STRING" "$file" 2>&1 >/dev/null; then
            echo -e "\033[u\033[K$file" || echo -e "$file"
         else
            echo -en "\033[u\033[K"
         fi
      else
         if LC_ALL=C $grepCmd "$_SEARCH_STRING" "$file" 2>&1 >/dev/null; then
            echo -e "\033[u\033[K$file" || echo -e "$file"
         else
            echo -en "\033[u\033[K"
         fi
      fi
   done

else

   if [[ $_unpack ]]; then
      find "$_START_PATH" $findOpts 2>/dev/null | while read file; do
         if [[ $file == *.zip || $file == *.jar || $file == *.ear || $file == *.war ]]; then
            unzip -p "$file" | LC_ALL=C $grepCmd "$_SEARCH_STRING" "$file" 2>/dev/null || true
         else
            LC_ALL=C $grepCmd "$_SEARCH_STRING" "$file" 2>/dev/null
         fi
      done
   else
      find "$_START_PATH" $findOpts -print0 | LC_ALL=C xargs -r -0 -P2 $grepCmd "$_SEARCH_STRING" 2>/dev/null
   fi
fi

-ll

Usage: -ll [OPTION]... [PATH]...

Alternative version of 'ls -lt' that prints directories (and sym-links to directories) before files and hidden entries before non-hidden entries.

Parameters:
  PATH
      The path to list.

Options:
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

[[ ! $_PATH ]] && _PATH=(.) || true

local _ls="command ls -lAph \"${_PATH[@]}\""
[[ $OSTYPE == "darwin"* ]] && _ls="$_ls -G" || _ls="$_ls -I lost+found --color=always"
eval $_ls | awk '
   BEGIN { dotDirs = ""; dirs = ""; dotFiles = ""; files = "" }
   /^total/                                                                                       { total = $0 }                    # capture total line

   /^d[rwxXst+-]+.? .* ([0-9][0-9][0-9][0-9]|[0-9]+:[0-9]+) (\033\[[0-9;]+m)?[.].+/                 { dotDirs = dotDirs    "\n" $0 }; # capture hidden directories
   /^d[rwxXst+-]+.? .* ([0-9][0-9][0-9][0-9]|[0-9]+:[0-9]+) (\033\[[0-9;]+m[^.]|[^\033^.])/         { dirs    = dirs       "\n" $0 }; # capture normal directories
   /^l[rwxXst+-]+.? .* ([0-9][0-9][0-9][0-9]|[0-9]+:[0-9]+) (\033\[[0-9;]+m)?[.].+[\/]/             { dotDirs = dotDirs    "\n" $0 }; # capture hidden sym-links to directories
   /^l[rwxXst+-]+.? .* ([0-9][0-9][0-9][0-9]|[0-9]+:[0-9]+) (\033\[[0-9;]+m[^.]|[^\033^.]).*[\/]$/  { dirs    = dirs       "\n" $0 }; # capture normal sym-links to directories

   /^-[rwxXst+-]+.? .* ([0-9][0-9][0-9][0-9]|[0-9]+:[0-9]+) (\033\[[0-9;]+m)?[.].+/                 { dotFiles = dotFiles "\n" $0 };  # capture hidden files
   /^-[rwxXst+-]+.? .* ([0-9][0-9][0-9][0-9]|[0-9]+:[0-9]+) (\033\[[0-9;]+m[^.]|[^\033^.])/         { files    = files    "\n" $0 };  # capture normal files
   /^l[rwxXst+-]+.? .* ([0-9][0-9][0-9][0-9]|[0-9]+:[0-9]+) (\033\[[0-9;]+m)?[.].+[^\/]/            { dotFiles = dotFiles "\n" $0 };  # capture hidden sym-links to files
   /^l[rwxXst+-]+.? .* ([0-9][0-9][0-9][0-9]|[0-9]+:[0-9]+) (\033\[[0-9;]+m[^.]|[^\033^.]).*[^\/]$/ { files    = files    "\n" $0 };  # capture normal sym-links to files
   END { print total dotDirs dirs dotFiles files }'

-mkcd

Usage: -mkcd [OPTION]... PATH

Creates a directory and changes into it.

Parameters:
  PATH (required)
      The path to create.

Options:
-m, --mode MODE (pattern: "[0-7]{3}")
        The file mode for the new directory.
-p, --parents
        Automatically create missing parent directories.
-v, --verbose
        Prints additional information during command execution.
    -----------------------------
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

local mkdirOpts

[[ $_mode    ]] && mkdirOpts="$mkdirOpts -m $_mode" || true
[[ $_parents ]] && mkdirOpts="$mkdirOpts -p" || true
[[ $_verbose ]] && mkdirOpts="$mkdirOpts -v" || true

mkdir "$_PATH" && cd "$_PATH"

-modified

Usage: -modified [OPTION]... [PATH]

Prints the modification timestamp of the given file or directory.

Parameters:
  PATH (default: '.', path)
      The file or directory to check.

Options:
-f, --format FORMAT (one of: [locale,iso8601,human])
        Prints the timestamp in the given format.
    -----------------------------
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

local _format=${_format:-timestamp}

case $_format in
   human)  find "$_PATH" -maxdepth 0 -printf "%TY.%Tm.%Td %TT %TZ\n" ;;
   locale) find "$_PATH" -maxdepth 0 -printf "%Tc\n" ;;
   timestamp)
      # use stat if available
      if hash stat &>/dev/null; then
         stat -c %Y "$_PATH"

      # use perl if available
      elif hash perl &>/dev/null; then
         perl << EOF
use File::stat;
print stat("$_PATH")->mtime, "\n"
EOF

      # use python as last resort
      else
         python -c "import os, pwd
 print(str(int(os.path.getmtime('$_PATH'))))"
      fi
     ;;
   iso8601)

      # use stat if available
      if hash stat &>/dev/null; then
         local timestamp=$(stat -c %Y "$_PATH")
         date --iso-8601=seconds -d@$timestamp

      # use perl if available
      elif hash perl &>/dev/null; then
         local timestamp=$(perl << EOF
use File::stat;
print stat("$_PATH")->mtime, "\n"
EOF
         )
         date --iso-8601=seconds -d@$timestamp

      # use python as last resort
      else
         python -c "import os, pwd, datetime, time
print(datetime.datetime.fromtimestamp(int(os.path.getmtime('$_PATH'))).isoformat() + time.strftime('%z'))"
      fi
     ;;
esac

-owner

Usage: -owner [OPTION]... [PATH]

Prints the owner of the given file or directory.

Parameters:
  PATH (default: '.', path)
      The file or directory to check.

Options:
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

# use stat if available
if hash stat &>/dev/null; then
   echo $(stat -c %U "$_PATH")

# use perl if available
elif hash perl &>/dev/null; then
   perl << EOF
use File::stat;
print getpwuid(stat("$_PATH")->uid), "\n"
EOF

# use python as last resort
else
   python -c "import os, pwd
print(pwd.getpwuid(os.stat('$_PATH').st_uid).pw_name)"
fi

-realpath

Usage: -realpath [OPTION]... [PATH]

Prints the normalized path of the given path resolving any symbolic links. The path is not required to exist.

Parameters:
  PATH (default: '.', path)
      The path to normalize.

Options:
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

# use readlink if available
if hash readlink &>/dev/null; then
   readlink -m "$_PATH"

# use perl if available
elif hash perl &>/dev/null; then
   perl << EOF
use Cwd 'abs_path';
print abs_path('$_PATH'), "\n"
EOF

# use python as last resort
else
   python -c "import os
print(os.path.realpath('$_PATH'))"
fi

-sudo-append

Usage: -sudo-append [OPTION]... FILE_PATH CONTENT

Creates a file with the given content.

Requirements:
  + Sudo 'tee -a' is required.

Parameters:
  FILE_PATH (required)
      The path to the file to write.
  CONTENT (required)
      The content to append to the file.

Options:
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Examples:
$ -sudo-append /tmp/testfile.cfg 'foo=bar'
Appending to \[/tmp/testfile.cfg\]...

Implementation:

echo "Appending to [$_FILE_PATH]..."
echo "$_CONTENT" | sudo tee -a "$_FILE_PATH" > /dev/null

-sudo-write

Usage: -sudo-write [OPTION]... FILE_PATH OWNER CONTENT

Creates a file with the given content.

Requirements:
  + Sudo 'sh -c' is required.
  + Sudo 'sh chown' is required.

Parameters:
  FILE_PATH (required)
      The path to the file to write.
  OWNER (required)
      The owner and group to set.
  CONTENT (required)
      The content to write.

Options:
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Examples:
$ -sudo-write /tmp/testfile.cfg $USER:$GROUP 'foo=bar'
Writing \[/tmp/testfile.cfg\]...

Implementation:

echo "Writing [$_FILE_PATH]..."
sudo sh -c "echo '$_CONTENT' > '$_FILE_PATH'" && sudo chown "$_OWNER" "$_FILE_PATH"

-tail-reverse

Usage: -tail-reverse [OPTION]... FILE

Prints the last N lines of the given text file in reverse order.

Parameters:
  FILE (required, file)
      Path to the file.

Options:
-n, --lines N (integer: ?-?)
        The maximum number of lines to output.
-u, --unique
        Don't print duplicates.
    -----------------------------
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

if [[ $_unique ]]; then
   if [[ $_lines ]]; then
      awk "{lines[len++]=\$0} END {for(i=len-1;i>=0;i--) if (occurrences[lines[i]]++ == 0) { print lines[i]; count++; if (count>=$_lines) break}}" $_FILE
   else
      awk "{lines[len++]=\$0} END {for(i=len-1;i>=0;i--) if (occurrences[lines[i]]++ == 0) print lines[i]}" $_FILE
   fi
 else
   if [[ $_lines ]]; then
      awk "{lines[len++]=\$0} END {for(i=len-1;len-i<=$_lines;i--) print lines[i]}" $_FILE
   else
      awk "{lines[len++]=\$0} END {for(i=len-1;i>=0;i--) print lines[i]}" $_FILE
   fi
fi

-test-all-filesystem

Usage: -test-all-filesystem [OPTION]...

Performs a selftest of all functions of this module by executing each function with option '--selftest'.

Options:
    --help
        Prints this help.
    --tracecmd
        Enables bash debug mode (set -x).
    --selftest
        Performs a self-test.
    --
        Terminates the option list.

Implementation:

-abspath --selftest && echo || return 1
-cd-down --selftest && echo || return 1
-cd-hist --selftest && echo || return 1
-cd-up --selftest && echo || return 1
-count-words --selftest && echo || return 1
-du --selftest && echo || return 1
-extract --selftest && echo || return 1
-find-up --selftest && echo || return 1
-findfiles --selftest && echo || return 1
-ll --selftest && echo || return 1
-mkcd --selftest && echo || return 1
-modified --selftest && echo || return 1
-owner --selftest && echo || return 1
-realpath --selftest && echo || return 1
-sudo-append --selftest && echo || return 1
-sudo-write --selftest && echo || return 1
-tail-reverse --selftest && echo || return 1