Navigation: Homepage | xmlgawk | Buchkritik | Sitemap

Shell Tipps

Shell Scripting is easy and a lot of fun -- A LOT.

If you are a starter: keep away from

Use everything else -- may the source be with you!

Links:

Motivation

Im Buch "Wicked Cool Shell Scripts" von Dave Tayler habe ich viele "uncoole" Scripte bzw. Fragmente gesehen. Deswegen eine eigene Liste mit Tipps.

Generelles:

Es ist gut, dass in dem Buch die POSIX sh verwendet wird, sie bietet neue Features und bash (ksh) implementieren sehr viel davon -- das macht das Leben leichter!

set PATH

JEDES Shellscript sollte sich den Pfad setzen, um sicherzustellen, dass auch wirklich die Kommandos ausgeführt werden, die das Script erwartet!

 # default PATH on ALL plattforms
 PATH=/usr/bin:/bin
 # reset IFS to " \n\t" (not needed for ksh93, which does this per default)
 IFS=' 
 	'

use $( )

Das Wichtigste ist das bessere Escape Handling für Subprozesse, da man $( ) auch verschachteln kann.

use (( ))

Endlich bequeme Ausdrücke, um zu rechnen:

 # old style
 X=`expr $X + 1`
 # bad
 X=$(( X + 1 ))
 # good
 (( X = X + 1 ))
 # better
 (( X += 1 ))

use [[ ]]

Die Syntax für die neuen Tests ist besser:

 [[ $X != 7 && $Y > 4 ]]
Aber Achtung bei Solaris, da ist /bin/ksh eine ksh88 und die kennt den "==" Operator noch nicht! Bitte nur "=" verwenden, wenn das Script auch unter Solaris ksh88 laufen soll.

use internal string handling

Die POSIX sh bietet einige interne Stringfunktionen, um das Forken von tr, sed, awk einzusparen:

 str=ABC/DEF/xyz
 # remove from the right end (remove tail)
 echo ${str%/xyz}  # returns ABC/DEF
 echo ${str%/*}    # returns ABC/DEF (poor man's dirname)
 echo ${str%%/*}   # returns ABC (greedy)
 # remove from the left end (remove head)
 echo ${str#ABC/}  # returns DEF/xyz
 echo ${str#*/}    # returns DEF/xyz
 echo ${str##*/}   # returns xyz (poor man's basename) (greedy)
 # coole Sache! es werden auch Variablen ausgewertet...
 STRIP=/xyz
 echo ${str%$STRIP} # returns ABC/DEF
 # pick a part offs:len notation
 echo ${str:0:2}   # returns AB
 echo ${str:4:3}   # returns DEF

 # entfernen vom CR aus einem HTTP reply, damit das \r
 # nicht das Logfile 'unbrauchbar' macht...
 CR=$'\r'
 echo ${line%$CR}
 # code sketch
 echo .. | nc ... | while read line; do line=${line%$CR}; ... ; end

Die bash und ksh93 bieten noch einige schöne Funktkionen mehr, die allerdings nicht mehr POSIX konform sind. Dies ist z.B. eine Pattern-Replacement Funktion:

 # an sed like string replacement (see bash(1), ksh(1) for details)
 # ${parameter/pattern/string}  replace first occurence
 # ${parameter//pattern/string} replace all occurences
 echo $PWD             # /c/data/tramms/bin
 echo ${PWD/$HOME/\~}  # ~/bin

use printf

 # bad
 .... | xargs -n 2 | awk '{printf "%s -- %s\n", $1, $2}'
 # good
 .... | xargs -n 2 printf "%s -- %s\n"
Kleiner Tipp: Bei printf kann mit -- das Ende aller Optionen markiert werden, dann stört es auch nicht, wenn Datenparameter oder der Formatstring mit einem '-' beginnen:
 # gibt in den meisten Fällen einen Fehler
 printf "-p >>%s<<\n" "bla"
 # so funktionierts
 printf -- "-p >>%s<<\n" "fasel"

Das echo -n Problem

Dummerweise hat es das ksh print Kommando nicht in den POSIX Standard geschafft. Nun besteht das Problem mit "echo -n" (BSD & Linux) vs "echo ...\c" (POSIX) weiterhin :(

Die Lösung ist der Tipp von oben und eine kleine Shellfunktion:

 # simulate a ksh print function (needs POSIX printf)
 print() {
   case ${1-""} in
     -n) shift; printf "%s"   "$*" ;;
     *)         printf "%s\n" "$*"
   esac
 }

Oder noch besser: Meide "echo" und nehme "printf".

Strings

Es gibt grundsätzlich drei Arten von Strings:

Unquoted Strings sind die Worte in einem Script:

 echo dies ist meine PID $$
 # echo sieht $#==5 ; $$ wird ausgewertet

Doublequoted Strings fassen mehrere Worte zu einem String zusammen. WICHTIG: Shell Escapes und Variablen werden ausgewertet:

 echo "meine PID ist \"$$\" und mein Name ist $(basename $0)"
 # \x, $$ und $(...) werden _vor_ Uebergabe an echo ausgewertet
 # echo sieht $#==1 $1==meine PID ist "980" und mein Name ist -bash
Anwendung: Strings mit Blanks oder Newlines

Singlequoted Strings erlauben, auf das u.U. massive Escapen von " und $ zu verzichten:

 echo 'meine PID ist \"$$\" und mein Name ist $(basename $0)'
 # keine Auswertungen _vor_ Uebergabe an echo
 # echo sieht $#==1 $1==meine PID ist \"$$\" und mein Name ist $(basename $0)
Ein Singlequoted String darf auch Newlines enthalten:
 echo 'ein mehrzeiliger
 String'
 # das geht :)
Anwendung: Strings mit Newlines und $

Pitfall: Es gibt KEINE Möglichkeit den Singlequote selber zu escapen. Eine Lösung ist:

 echo 'double " '"'"' single' # oder 'double " '\ single'
                              # oder "double \" ' single"
 # $#==1 $1=double " ' single

ANSI-C Strings sind (leider) nicht im POSIX Standard für Shell enthalten, aber bash und ksh93 unterstützen diese Syntax. Damit wird das Definieren von Strings mit Sonderzeichen sehr einfach, da alle ANSI-C Escapesequenzen erkannt werden:

 # CRNL for HTTP Headers
 CRNL=$'\r\n'
 request="GET / HTTP/1.0$CRNL\\
 Host: $HOST$CRNL\\
 $CRNL"
 # use printf to omit echo's trailing newline
 #  $request MUST be quoted to prevent further Shell interpretation
 printf "%s" "$request" | nc $HOST 80

POSIX Shell Arrays

Arrays are nice, when you need lists. POSIX Shell aarays allow only integers as indices -- sorry no associative arrays.

 # declare it
 typeset urilist
 # initialize array with positional parameters $1 $2 ...
 set -A urilist "$@"
 # append to end of array
 urilist[${#urilist[*]}]="an appended array element"
 # print num of elem # all indices : all values
 print ${#urilist[*]} "#" ${!urilist[*]} : ${urilist[*]}
 # iterate through array (handle blanks correct)
 for i in "${urilist[@]}"
 do
   printf ">>>%s<<<\n" "$i"
 done

dont use 'more'

Normalerweise macht man keine interaktiven Shellscripte, wenn doch dann $PAGER nehmen, wenn gesetzt

 # pipe a file through $PAGER (default more)
 .... | ${PAGER:-more}
 

Vermeide temporäre Dateien

Das Anlegen temporärer Dateien für die Laufzeit eines Scripts ist heikel:

The magic of the *

JEDER hat schon mal "mv *.c *.c.bak" versucht und ist kläglich gescheitert. Warum?

Weil die Shell * interpretiert BEVOR sie das Programm "mv" aufruft! Die Shell expandiert das (Glob)-Pattern, in obigen Beispiel zu:

 mv x1.c x2.c x3.c *.c.bak
Die Annahme ist, dass die drei Dateien (x1.c x2.c x3.c) im aktuellen Verzeichnis liegen. Da *.c.bak auf keine Datei matched, wird das Muster unverändert an 'mv' übergeben.

Es ist Aufgabe des 'mv' Programms (und damit auch von eigenen Shellscripts) mit der variablen Anzahl Parameter fertig zu werden.

Somit muss man ein Shellscript schreiben, dass die Aufgabe des "Massen-Moves" übernimmt, oder man nimmt ein fertiges Tool.

Beispielscript:

 for i in *.c
 do
   mv $i ${i}.bak
 done
 # kurz
 for i in *.c ; do mv $i ${i}.bak ; done

Aber wie geht "mv *.c.bak *.c"? Nun, das ".bak" am Ende des Namens abhacken, das macht "${...%.bak}":

 for i in *.c ; do mv $i ${i%.bak} ; done

Ergo: Alles nicht so schlimm, das mentale Modell muss stimmen -- dann ists ganz einfach :)

Use scripts in a script

Das wirklich schöne an Shell Programmierung ist, dass man sehr einfach andere Script- oder Spezialsprachen einbauen ('embed') kann.

 $ ls -l | awk '{print $3}'

Oder mittlere Zeilenzahl von C-Files in einem Directory:

 wc -l *.c | awk '
  { sum += $1; lines++ }
  END { print int(sum/lines) }'

Ein weitere Option ist, die Anzahl abgespaltener Prozesse pro Script zu reduzieren, statt

 .... | \ 
 egrep '[[:digit:]]+\.' | \ 
 awk '{print $2}' | \ 
 cut -d\? -f1
dass drei Prozesse benötigt, geht auch:
 .... | \ 
 awk '/[[:digit:]]+\./ {sub(/\?.*$/, "", $2); print $2}'
Das ist nur ein Subprozess (awk), der die ganze Arbeit macht, aber weniger Resourcen belegt.
 
Das Beispiel zeigt auch sehr schön, wie gut man den Unix Werkzeugkasten kombinieren kann. Es muss nicht das Universaltool geben, sondern die Kombination ist mehr als die Summe aller Einzelteile.

Testing

Auch Shellscripte sollen getestet werden. Hier ein paar Testcases für das neue Millenium. Funktioniert Dein Script bei:

Das sind zum Teil ganz schön harte Nüsse ... ;-) Wer das nicht glaubt, soll doch hier http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html nachlesen!

Quizfrage: Welche zwei Zeichen sind als einzige in Unix Filenamen verboten?

bash

BASH Programming http://www.linuxgazette.com/issue5[2345]/okopnik.html

Ein komplettes Bash Buch zum Download: http://www.network-theory.co.uk/bash/manual/

Eine Liste mit Pitfalls für Anfänger: http://wooledge.org/mywiki/BashPitfalls

Autologout: export TMOUT=x where x is in SECONDS

Umstellen des Ausgabeformats des eingebauten time Commands der bash:

        * Default  TIMEFORMAT=$'\nreal\t%3lR\nuser\t%3lU\nsys\t%3lS'
        * POSIX.2  TIMEFORMAT=$'real %2R\nuser %2U\nsys %2S'
        * BSD      TIMEFORMAT=$'\t%1R real\t%1U user\t%1S sys'
        * System V TIMEFORMAT=$'\nreal\t%1R\nuser\t%1U\nsys\t%1S'
        * ksh      TIMEFORMAT=$'\nreal\t%2lR\nuser\t%2lU\sys\t%2lS'
        * note the difference between 1 and l
        * %P CPU percentage (%U + %S) / %R
usage:
 TIMEFORMAT=$'\t%3R real\t%3U user\t%3S sys\t%P %%'
 time ls -a
 # use POSIX format
 time -p ls -a
 # Auswertung
 (time ls -a >/dev/null) 2>&1 | gawk '{print $1, $(NF-1)}'

ksh93 lseek in the shell

von DGK 2005-06-15:

 The new redirection operators, <# and ># are used to seek.
 For example,
    <# (( EOF-36))
 will seek to 36 bytes before the end-of-file.  You can apply this
 along with any redirection so that
    cat < file <# ((80))
 will cat the file starting from offset 80.
 The value of $(n<#) is the current offset on file descriptor <n>.
  
 The current syntax can be extended to add other seek options like
 SEEK_HOLE, by using,
    <# (( HOLE))
 to seek to the next hole.

Korn Shell 93

Das Buch zur Korn Shell "The New Korn Shell" von Bolsky und Korn (s.a. TheNewKornshell )

Die ksh93 (http://www.kornshell.com ) hat einige schöne neue Features:

Hier ein Beispiel für einen Mini http Client:

 #!/bin/ksh93
 # open socket
 exec 3<> /dev/tcp/127.0.0.1/80
 # send header
 print -u3 "GET / HTTP/1.0"
 # terminate header
 print -u3 ""
 # read reply (timeout 2.5 seconds)
 while read -u3 -t 2.5 line
 do
   # dont expand \\ notation
   print -r $line
 done
 # close socket
 exec 3>&-

Kleine (aber feine) Unterschiede bash vs ksh93

trap

pipes and read

function

POSIX shell

Auch mit einer POSIX shell kann man sehr viel erreichen. Die POSIX shell /bin/sh ist der kleinste gemeinsame Nenner moderner Unixe.

Empfehlen kann ich hier:

man-pages

Doku soll ja nicht vegessen werden. man-Pages sind immer noch das Beste, was ich diesbezüglich kenne. Hier eine kleine Hilfestellung: http://home.alltel.net/kollar/effman.html

Guru Section to ksh93

Alarms

The alarm built-in hasn't been documented. It was written as an example of how object based builtins could be written in ksh93.

If I remember how it works, you can create an alarm object x that expires in a given time, say now + 30 seconds, with alarm x +30 and you can define the function function x.alarm { print alarm has gone off }

The alarm method for the x object will be triggered after 30 seconds whether the shell is waiing for input or waiting for a program to complete.

alarm allows fractional seconds and the option -r allows repeating alarms. Use unset x to remove an alarm before it triggers.

TCP connect mit Timeout

Hier die lange Variante mit Koprozess:

 # connect tcp socket to FD 3
 #  usage: connect IP PORT [TIMEOUT]
 #    TIMEOUT in seconds (fracts allowed) default 20.0
 #    return codes:
 #      0 OK (use FD3 for socket)
 #      1 conn refused
 #      2 timeout
 #      3 INT TERM catched
 #      4 should not happen (race condition), but socket is usable
 function connect
 {
   typeset ip=$1 port=$2 secs=${3-20}
   typeset watchdog= rc=
 
   # ALRM is signaled by timeout, catch it and deliver to caller
   trap 'trap - ALRM INT TERM; return 2' ALRM
 
   # we need to kill this watchdog process if the main proceeds
   sleep $secs && kill -ALRM $$ & watchdog=$!
 
   # terminate the wtachtdog process in case of Ctrl-C (INT) or
   # kill (TERM) is sent to this process
   trap "kill $watchdog && return 3 || return 4" INT TERM
 
   # use ksh93 alias and do not terminate in case of error
   { redirect 3<>/dev/tcp/$ip/$port; } 2>/dev/null
   rc=$?
 
   # terminate the watchdog process
   kill $watchdog || return 4
 
   # function exit restores default ALRM, INT and TERM handlers
   return $rc
 }
 
 # connect 10.0.1.1 80 5
 # print -u3 $HTTPREQUEST

Hier der Knaller mit alarm:

 #
 # connect tcp socket to FD
 #  usage: connect FD IP PORT [TIMEOUT]
 #    TIMEOUT in seconds (fracts allowed) default 20.0
 #    return codes:
 #      0 OK (use FD for socket)
 #      1 conn refused or timeout
 function connect
 {
    # set alarm to happen now + secs (pseudo var declaration)
    # watchdog is automatically deleted on function exit
    alarm watchdog +${4-20}
    function watchdog.alarm { return 1; }
 
    # use ksh93 redirect alias and discard error messages
    { eval "redirect ${1}<>/dev/tcp/${2}/${3}"; } 2>/dev/null
 }

Structured Variables

Das funktioniert leider nicht so, wie man es naiverweise erwarten könnte.

 xxx=(s1=str1 s2=str2)
 typeset -p - # alle settings
 print $xxx # ( s1=s1 s2=s2 )
 eval yyy=$xxx # deepcopy(?)

 # print var 'xxx'
 typeset -p - | foreach '[[ $2 == xxx* ]] && print -r $*'

 # rekursiver aufbau
 a=()
 a.sub1=111
 a.sub2=...
 # a+=(s1=2 s2=m) geht nicht

 # strange behaviour....
 $ typeset -A a
 $ a[0]=aa
 $ a[1]=bb
 $ print ${#a[*]}
 2
 $ typeset -p - | grep ' a'
 typeset a[1]=bb
 $ print ${a[0]}
 aa
 $ typeset -p - | grep ' a'
 typeset a[0]=aa
 typeset a[1]=bb

AWK für Arme

Die KSH93 Shellfunction 'foreach' implementiert ein AWK ähnliches Konstrukt, ohne einen Subprozess abzuspalten:

 #
 # usage: foreach [-c:--call] "commands ...."
 # example: find .... -print | foreach ....
 #
 function foreach
 {
   typeset opt=
   typeset func=0
   while getopts "c{C:call}" opt
   do
     case $opt in
       c|C) func=1;;
       ?) return 1;;
     esac
   done
   shift $((OPTIND - 1))
   ## print -u2 $OPTIND $*
   typeset command="$*"
   typeset line=
   # noglob: do not expand shell metachars
   set -o noglob
   while read -r line
   do
     if [[ $func == 1 ]]
     then
       $command $line
     else
       set -f -- $line
       eval $command
     fi
   done
   return 0
 }

Zum Nachlesen http://blog.fpmurphy.com/tag/ksh93

IRC client in Shell

Gefunden unter: http://lists.canonical.org/pipermail/kragen-hacks/2008-February/000480.html

GNU Make Debugging

GNU Make passt zwar nicht direkt zur Shell, aber Debugging von Makefiles kann schwierig werden. Hier eine schöne Seite dazu: http://ddj.com/article/printableArticle.jhtml?articleID=197003338&dept_url=/dept/linux/ Der Clou ist, die Shell zu wechseln, dann sieht man, was make wirklich macht:

 OLD_SHELL := $(SHELL)
 SHELL = $(warning [$@ ($^) ($?)])$(OLD_SHELL) -x

Jason's Tipps

Last day of month test

 lastdayp() { test `date +%d` -eq `cal | tail -2 | head -1 | awk '{print $NF}'`;}
Usage:
  $ lastdayp && echo last day of month || echo not last day of month
  $ not last day of month

Age in years

 ageinyears() { expr \( `date +%s` - `date -u +%s -d $1` \) / 31556926; }
Usage:
  $ echo `ageinyears 19680214T0630`
  $ 39

Note: Have to give birth date in UTC, but will work anywhere regardless of where the server or client is.

AWK Tipps

Shift away the first 3 columns:

 # shift away some leading values
 BEGIN { shift=3 }
 {
   nf=0
   for (x=shift+1; x<=NF; x++)
     $(++nf) = $x
   NF=nf
   print
 } 

  Date of creation:
     01-Jul-2004
  last modified $Date: 2012/01/23 20:25:31 $