Linux - Shells
Linux shell is command-line interpreter or command processor. A shell is a simple domain-specific programming language. Shell is the traditional Unix user interface, writing commands. Shells can also load prewritten commands from files called scripts.
Bourne-again Shell (bash)
- Most widely used Unix shell.
- Installed by default on Linux and OS X.
- Can be used on Windows with Cygwin and MinGW.
- Can be used on Android with GNU Bash Installer.
- Script files use
.sh
extension or no extension at all. - You can execute scripts by specifying relative or absolute path to the script.
Script Basics
Scripts start with shebang #!
that specifies what interpreter to use. Full syntax is #!interpreter [optional-arguments]
#!/bin/bash
#!/usr/bin/php (execute using PHP)
#!/usr/bin/ruby (execute using Ruby)
#!/usr/bin/python -O (execute using Python with optimizations)
Shebangs must specify absolute path to the interpreter. You can try to use env
utility to get the absolute path.
#!/usr/bin/env sh
Understand script search paths. Scripts are searched from directories specified in an environmental variable named PATH
.
# Show what is your $PATH.
echo $PATH
Scripts need to have execution permissions to run.
# Give user permission to read and execute the script.
chmod u+rx <SCRIPT_FILE>
# Give group and other permission to execute the script.
chmod 711 <SCRIPT_FILE>
Do batch processing using non-global scripts. For example, store a script in the project folder and run from there.
#!/bin/csh
echo compiling...
cc -c foo.c
cc -c bar.c
cc -c qux.c
cc -o myprog foo.o bar.o qux.o
echo done.
Use functions for readability.
# bad
[[ -z $dir ]] \
&& do_something...
# good
is_empty() {
local var=$1
[[ -z $var ]]
}
is_empty $dir \
&& do_something...
# good
ExtractBashComments() {
egrep "^#"
}
cat myscript.sh | ExtractBashComments | wc
comments=$(ExtractBashComments < myscript.sh)
Favor $()
over backticks ` in scripts. I personally use backticks while writing to command line as it's faster.
# both commands below print out: A-B-C-D
echo "A-`echo B-\`echo C-\\\`echo D\\\`\``"
echo "A-$(echo B-$(echo C-$(echo D)))"
Utilize $()
as part of a command.
sleep 30 &
sleep 30 &
sleep 30 &
pgrep sleep # => returns the process id by process name
kill `pgrep sleep` # => kills all matching
kill `pgrep -n sleep` # => kills the newest matching command
Always quote variables with double quotation marks. For consistency.
VAR="tmp/*"
echo "${VAR}a"
Scripts are not programs. If your script is over hundred lines, you are doing something wrong.
Bash Configuration
There are multiple configuration files that affect how your bash behaves.
Login Shell: When logging in with console e.g. SSH session, you start a login shell. Login shells execute .profile
configuration file in your home directory. That may then include .bashrc
inside the .profile
. Login shell will try to find .bash_profile
> .bash_login
> .profile
and only executes the first one it finds.
Non-login Shell: When you open a terminal application e.g. xterm, you start a non-login shell, except in OS X, the it is always login shell. Non-login shells execute .bashrc
configuration file in your home directory.
All in all, .bashrc
is the best place for all bash configurations as it is always loaded. You should use .profile
to all non-bash related settings e.g. environmental variables. You should use .bash_profile
to load .profile
on OS X for consistency.
# .profile
# Add path to Python scripts directory.
PATH=/usr/local/share/python:$PATH
Bash configuration file examples.
# .bashrc
# General
alias editbash="open ~/.bashrc"
alias ps?="ps aux | grep"
# Sublime
alias s.="subl ."
# Git
alias gl='git log --pretty=format:"%h%x09%an%x09%s"'
alias gs="git status"
alias ga="git add ."
alias gb="git branch"
alias gc="git commit"
alias gcm="git commit -m"
alias gca="git commit --amend"
alias gpp="git pull; git push"
alias gpl="git pull"
alias gps="git push"
alias grh="git reset --hard HEAD"
# Usage: coco <COMMIT_MESSAGE>;
coco() {
git add .;
git commit -m "$1";
git pull;
git push;
}
# PHP
alias tphp="tail -f /Applications/MAMP/logs/php_error.log"
# SSH
alias pubkey="more ~/.ssh/id_rsa.pub | pbcopy | printf \
'=> SSH public key copied to clipboard.\n'";
Bash Script Examples
General
#!/bin/bash
# Only thing global are readonly values, functions and the main function call.
readonly SCRIPT_NAME=$(basename $0)
readonly SCRIPT_DIR=$(cd $(dirname "$0"); pwd)
readonly SCRIPT_ARGS="$@"
is_file() {
local file=$1
[[ -f $file ]]
}
is_dir() {
local dir=$1
[[ -d $dir ]]
}
print_importants() {
echo "Here are the script variables:"
echo ${SCRIPT_NAME}
echo ${SCRIPT_DIR}
echo ${SCRIPT_ARGS}
}
main() {
local file
local files_in_same_dir=${SCRIPT_DIR}/*
for file in $files_in_same_dir
do
# Max one thing per line, use `/` to separate the lines.
is_dir $file \
&& echo "${file} is a directory."
is_file $file \
&& echo "${file} is a file."
done
print_importants
}
main
Script
#!/bin/bash
echo "Last program return value: $?"
echo "Script's PID: $$"
echo "Number of arguments: $#"
echo "Scripts arguments: $@"
echo "Scripts arguments separeted in different variables: $1 $2..."
Default value if nothing provided.
URL=${URL:-http://localhost:8080}
URL=${URL:-http://localhost:80}
Conditionals
#!/bin/bash
if [ $PWD = "/" ]
then
echo "You are at the root.";
# elif [ toinen ehto ]
else
echo "You are at $PWD";
fi
#!/bin/bash
VARIABLE="0"
case "$VARIABLE" in
0) echo "There is a zero.";;
1) echo "There is a one.";;
*) echo "It is not null.";;
esac
Loops
#!/bin/bash
for i in `seq 3`
do
echo "$i"
done
#!/bin/bash
# From 1 to 5.
for i in {1..5}
do
echo "Welcome $i times"
done
#!/bin/bash
# C-style for-loop from 1 to 5.
for (( c=1; c<=5; c++ ))
do
echo "Welcome $c times"
done
#!/bin/bash
# For each file.
for file_path in `ls ./`
do
echo $file_path
file_basename=`basename "$file_path" .txt`
echo $file_basename
done
#!/bin/bash
# Create backup on each specified file if no .bak exists.
FILES="$@"
for f in $FILES
do
# If .bak backup file exists, read next file.
if [ -f ${f}.bak ]
then
echo "Skipping $f file..."
continue
fi
# No backup file exists, use cp command to copy file.
/bin/cp $f $f.bak
done
Functions
function foo ()
{
echo "Arguments work just like script arguments: $@"
echo "And: $1 $2..."
echo "This is a function"
return 0
}
foo "My name is" $NAME
Convert JPGs to PNGs
#!/bin/bash
# Use $jpg in place of each filename given.
for jpg; do
# Find the PNG version of the filename by replacing .jpg with .png.
png="${jpg%.jpg}.png"
echo converting "$jpg" ...
#Uuse the common convert program to create the PNG.
if convert "$jpg" jpg.to.png
then
# If it worked, rename the temporary PNG image to the correct name.
mv jpg.to.png "$png"
else
# Otherwise complain and exit from the script.
echo 'Error: failed output saved in "jpg.to.png".' >&2
exit 1
fi
done
echo "All conversions successful"
String Substitution
echo ${VARIABLE/Some/A}
# Replaces first occurrence of "Some" with "A".
String Interpolation
# `` makes string a command and is replaced with response.
echo "Hello, `whoami`!"
# $ means "this is a variable"
NAAAAME="Hey, `whoami`!"
echo $NAAAAME
echo "You are at $PWD."
Parameter Operations
#!/bin/bash
# Trim the shortest match from the end.
# ${variable%pattern}
# Trim the longest match from the beginning
# ${variable##pattern}
# Trim the longest match from the end
# ${variable%%pattern}
# Trim the shortest match from the beginning
# ${variable#pattern}
# Return the length of the variable in characters.
# ${#variable}
file=/tmp/my.dir/filename.tar.gz
file_path = ${file%/*}
echo file_path
# -> /tmp/my.dir
file_name = ${file##*/}
echo file_name
# -> filename.tar.gz
file_basename = ${file_name%%.*}
echo file_basename
# -> filename
file_extension = ${file_name#*.}
echo file_extension
# -> tar.gz
echo "The length of your file_path is ${#file_path}"
export API_KEY="1234"
if [ ${#API_KEY} != 3 ]; then
echo "You have entered a wrong API key!"
#return 0
fi
Download Sequence
#!/bin/bash
for i in `seq -f"%03g" 1 104`
do
wget -c "http://koskisuomi.pp.fi/linucast/LinuCast$i.ogg"
done
Cleanup
#!/bin/bash
# Create temporary directory.
scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
# DO ANYTHING!
cp "$scratch/merged.tar.bz2" "$1"
# Define what to do one exit.
function finish {
rm -rf "$scratch"
}
# Run finish on exit.
trap finish EXIT
SVN Update Script
#!/bin/bash
# Change current working directory, while saving the original one.
pushd /srv/www/my-web-project
# Update whole project
svn update
# Echo version.
# | Cut from delimiter :
# | Deleting instances of MS (translate command)
# > Save to revision.txt
svnversion | cut -d: -f | tr -d MS > revision.txt
# Return to original working directory.
popd
Check if executable exists.
# -s = retuns 1 if command is found, 0 if not.
which -s curl && echo lol
function if_cmd_found() { which -s $1; }
if_cmd_found "curl" && echo "Hello!"
Adding optional debug messages to scripts.
# Debug information
function debug() {
if [[ $DEBUG ]]
then
echo ">>> $*"
fi
}
# debug "Doing something."
# export DEBUG=true
You can use Heredoc to work with long messages.
cat << EOF
Usage: myscript <command> <arguments>
Version: 1.0
Available Commands:
install - Install package
uninstall - Uninstall package
update - Update package
list - List packages
EOF
Colored echoes.
# Colored echoes.
NORMAL=$(tput sgr0)
GREEN=$(tput setaf 2; tput bold)
YELLOW=$(tput setaf 3)
RED=$(tput setaf 1)
function red() {
echo -e "$RED$*$NORMAL"
}
function green() {
echo -e "$GREEN$*$NORMAL"
}
function yellow() {
echo -e "$YELLOW$*$NORMAL"
}