ruk·si

Linux
Shells

Updated at 2013-11-03 00:51

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 \
'=&gt; 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"
}

Source