Bash programming/Function Usage

Functions allows you to group pieces of code and reuse it along your programs. [1]


Pre-requisites

edit

Now is the time to note, while some concepts are assumed as a pre-requisite, it's reasonable to expect not every student will be familiar with all concepts. Therefore, where a concept is listed here in the pre-requisites, it will be introduced with a link to a more detailed explanation.

Environment:

  • have completed the course Introduction
  • use ENVIRONMENT variables
  • stdin, stdout
  • re-direct outputs

Commands:

  • how to define a function
  • expr -- does arithmetic,
  • tail -- delivers last output lines
  • history -- delivers command history

New Concepts

edit

commands:

  • declare -- to view functions
  • history -- to view command history
  • set -- positional parameters
  • source -- to re-use, or re-load functions

shell features

  • positional parameters
  • environment variables
  • sub-shell execution

Use function arguments

edit

In this exercise, we will write two functions which use command arguments. The first function will display a function body or bodies. The second will display our command history [2].. It will have an optional single argument, a number, which will default to the number of LINES, an Environment variable, in your terminal window. In the course of the exercise we will:

  • use a single function argument
  • usa a variable number of arguments
  • use a default argument
  • use a function to display our function repertoire.

View a function body or bodies:

First, from our last section, recall:

$ helloWorld () { echo "Hello, World!"; }
$ declare -f helloWorld
 ... shows the function body,   

Does your output meet your expectations. If this is the first time you've tried it, you will see something different than you entered.

Let's write a function to do this:

$ fbdy () { declare -f $*; }
$ fbdy helloWorld fbdy

Now you are less surprised by the output. What's the $* doing? It says "when this command (or function) is executed, put all the blank-separated positional parameters right here". You observe the function is little different from using the built-in command declare with it's -f option. Like an alias, but the function allows upward-compatible evolution. In this case, a version of fbdy may return one-line functions as one-liners, another may routinely add (or remove) function tracing to the function body.

Make sure you've executed the examples above.

Use command history

edit

How many lines is our terminal displaying?

An Environment variable, LINES is often set by the shell to the number of lines the terminal window is displaying.

$ echo $LINES

If you see nothing, then variable is not set on your terminal. We'll supply a default. It is useful to define a function, again almost an alias th, standing for Tail History. The reason for this function goes a bit beyond our needs here. However it's useful as a short-hand, if not not to provide a consistent interface to different shell's with a different sets of options for a single command.

$ set $(expr ${LINES:-27} - 3);   history | tail -$1

execute that command; if needed, look ahead to subshells here is a sample of results:

$ set $(expr ${LINES:-27} - 3);   history | tail -$1
 683  pushd $(which cmdlib)
 684  pushd $(which $(dirname cmdlib))
 685  pushd
 686  pushd $(dirname $(which  cmdlib))
 687  view cmdlib
 688  ff th
 689  history -24
 690  th
 691  th
 692  ff th
 693  set | grep HISTSIZE
 694  set | grep HIST
 695  th () { set $1 $(expr ${LINES:-27} - 3); history | tail -$1; }
 696  declare -f th
 697  echo $LINES
 698  clear
 699  declare -f th
 700  th 24
 701  th
 702  echo $LINES
 703  history 24
 704  history 45
 705  history -h
 706  history -T
 707  history -x
 708  uname -a
 709  https://www.gnu.org/software/bash/manual/html_node/Bash-History-Builtins.html#index-history-builtins
 710  th () { set $1 $(expr ${LINES:-27} - 3;   history | tail -$1; }
 711  th () { set $1 $(expr ${LINES:-27} - 3);   history | tail -$1; }
 712  echo $LINES
 713  set $(expr ${LINES:-27} - 3);   history | tail -$1
bin.$ 

Notice a number of things about that command:

  • number 713 is the last command itself
  • thirty-one commands were displayed

Now, convert that into a function, and test it.

$ th () { set $1 $(expr ${LINES:-27} - 3);   history | tail -$1; }
$ th     # returns your recent command history, filling up the screen

It turns out in the bash shell, history is a builtin, which shows a complete list of bash history builtins here.

What happened here?

The set command (ksh version) in this usage assigns the positional parameters. If the function is used with one, then its done like this:

$ th 24     #   24 -> $1 ...   so, the command becomes "history | tail -24" 

If no argument is used, then it becomes

$ th      # and if no lines are set, then:    expr 27 - 3  ( = 24 ) , so ... "history | tail -24" again

but if LINES was say, 34, then it's expr 34 - 3 (= 31) ... history | tail -31

The shell parameter substitution works for named variables (LINES, SHELL, ...) as well as the positional parameters (1, 2, ... *) and this expression is most useful to assign a default value, in this example:

 $ echo ${LINES:-27}

And this last feature introduced here is the ability to insert sub-shell results in the command. The general idea is:

$ command .. $( sub-shell command or commands... ) ...

where the results of sub-shell command or commands... is inserted into the command ... In our case then, the result of the shell arithmetic is inserted:

$ set "" $(expr $LINES - 3)       # $1 was empty, becomes
$ set  $(expr 34 - 3)             # to be evaluated,
$ set 31; history | tail -$1      # then becomes
$ history | tail -34

Display functions

edit

To collect our new and useful work, lets' see what we have:

$ fbdy fbdy th

For example:

bin.$ fbdy fbdy th 
fbdy () 
{ 
    declare -f $*
}
th () 
{ 
    set $1 $(expr ${LINES:-27} - 3);
    history | tail -$1
}
bin.$

This is progress. Do your results compare?


Edit functions

edit

Save functions

edit

You have at least two ways to keep a consistent set of functions available for your command line:

  • save them in a local file
  • save them to load when you login

We'll exercise both methods here. First the local file. You'll find that not all functions are needed in all instances.

In a local file

edit

Functions can be stored in any .sh file. To load the functions from that file, run the command . filename.sh.

load at login

edit

Functions can be stored in the file named .bashrc. It is typically located in the home directory (shortcut: ~/.bashrc).

Reload functions

edit

Examples

edit
  • Create a directory and enter it:
    • mkcd() { if [ ! -d "$@" ];then mkdir -p "$@" ;fi; cd "$@"; }
  • Change the window title in a terminal emulator:
    • window_title() { printf "\033]0;$*\007"; }
  • Prevent a hard drive that does not respond to hdparm from spinning down:
    • spindisk() { while : ; do (sudo dd if=/dev/$1 of=/dev/null iflag=direct ibs=4096 count=1; sleep 29); done }
    • Requires root access to run due to block-level disk access.
    • Adjust the time after sleep to just below your hard drive's default spin-down timeout.
    • Exit with CTRL+C.
  • Count the files in a directory: filecount() { ls "$@" |wc -l; }
  • Search for files in a directory: findfile() { find "$2" |grep -i "$1"; }
  • Search text inside 7z (7-Zip) archives: 7zgrep() { 7z e -so "$2" |grep -i "$1"; }
  • Find the newest or the oldest file in a given directory:
    • newestfile() { find "$@" -type f -printf '%T+ %p\n' |sort |tail -n 1; }
    • oldestfile() { find "$@" -type f -printf '%T+ %p\n' |sort |head -n 1; }
  • Add time stamps at the beginning of specified file names:
prepend_timestamps() {
	if [[ "$@" == "" ]]; then echo "No file name specified. Exiting."; return 1; fi;
	for filename in "$@"; do
		timestamp="$(date -r "$filename" +%Y-%m-%dT%H-%M-%S)"
		mv -nv -- "$filename" "$timestamp $filename";
	done;
}
alias addtimestamps=prepend_timestamps;
  • Generate check sums of all files in the current or a specified directory:
superMD5() { find "$@" -type f |sort |xargs  -d '\n' md5sum; }

Working with multimedia

edit
  • Create a file list for the concatenation feature of ffmpeg from the find command:
    • fffile() { sed -r "s/(.*)/file '\1'/g"; }
    • Example use with pipe: find DCIM/Camera/VID_20241118*.mp4 |fffile >>example.txt
  • Access the concatenation feature of ffmpeg with only two parameters:
    • ffconcat() { ffmpeg -f concat -safe 0 -i "$1" -c copy "$2"; }
    • Example use: ffconcat example.txt example.mp4
  • Verify the integrity of multimedia files using its decoding mechanism, independently from the file system: fferror() { ffmpeg -v error -i "$1" -f null - ;}
  • Generate a table of video resolutions and framerates by processing the text generated by the mediainfo tool.
    • mediainfotable() { mediainfo "$@" |grep -v "Frame rate.*SPF" |grep -P "(name|Width|Height|Frame rate )" |tr '\n' ' ' |sed -r 's/FPS/FPS\n/g' |sed -r "s/( )+//g"; }
  • Redact geolocation from video files before sharing for privacy. This will overwrite the specified video files in-place, so it is recommended to only use it on copies of video files.
    • gpsnull() { sed -i -r "s/\+[0-9][0-9]\.[0-9][0-9][0-9][0-9]\+[0-9][0-9][0-9]\.[0-9][0-9][0-9][0-9]\//+00.0000+000.0000\//g" "$@"; }
  • Mute the audio of a video (specify input and output file): ffmute() { ffmpeg -i "$1" -c:v copy -an "$2"; }
  • Extract the audio from a video file intp a separate file (specify input and output file): ffaudio() { ffmpeg -i "$1" -c:a copy -vn "$2"; }
  • Find out the total size of a selection of files: totalsize() { du -sh -c "$@" |tail -n 1; }
  • Get the frame rate of a video: getfps() { ffprobe -v 0 -of csv=p=0 -select_streams v:0 -show_entries stream=r_frame_rate "$1"; }
  • Get the resolution of a video: getRes() { echo $(ffprobe -v 0 -of csv=p=0 -select_streams v:0 -show_entries stream=height "$1"; )p; }
  • Stick all videos from one folder into one video file without re-encoding (only works for files from the same device with the same width and height and frame rate):
ffconcat-dir() {
	# edge case handlers
	if [[ "$1" == "" ]]; then echo "No directory specified. Exiting." ; return 1; fi
	if [[ "$1" == "ffconcat-dir.txt" ]]; then echo "This name is reserved. Please choose a different directory."; return 1; fi
	if [ -d "ffconcat-dir.txt" ]; then mv ffconcat-dir.txt "ffconcat-dir (usurped-$(date +%Y%m%d%H%M%S))"; fi
	
	# main part
	truncate -s 0 ffconcat-dir.txt # blanking temporary file from last run
	find "$1" -maxdepth 1 -type f |sed -r "s/(.*)/file '\1'/g"  >ffconcat-dir.txt
	if [[ "$2" != "" ]]; then outfile="$2"; else outfile=tmp.mp4;fi
	ffmpeg -f concat -safe 0 -i ffconcat-dir.txt -c copy "$outfile"
}

Example use: ffconcat-dir folder_name output_video_name.mp4

This works for other file types too, but not with mixed file types. The extension specified file type has to match the type of the source files. The default name is tmp.mp4 if none is specified.

References

edit
  1. http://tldp.org/HOWTO/Bash-Prog-Intro-HOWTO-8.html
  2. https://en.wikipedia.org/wiki/History_(Unix)