Bash programming/Function Usage
Functions allows you to group pieces of code and reuse it along your programs. [1]
Pre-requisites
[edit | edit source]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 | edit source]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 | edit source]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 | edit source]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 | edit source]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 | edit source]Save functions
[edit | edit source]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 | edit source]Functions can be stored in any .sh
file. To load the functions from that file, run the command . filename.sh
.
load at login
[edit | edit source]Functions can be stored in the file named .bashrc
. It is typically located in the home directory (shortcut: ~/.bashrc
).
Reload functions
[edit | edit source]Examples
[edit | edit source]- 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; }
- Count the files in a directory and its subdirectories:
filecount-total() { find "$@" |wc -l; }
- Count the folders only:
subfoldercount() { find "$@" -type d |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 | edit source]- Create a file list for the concatenation feature of
ffmpeg
from thefind
command:fffile() { sed -r "s/(.*)/file '\1'/g"; }
- Example use with pipe:
find DCIM/Camera/VID_20241213*.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.