If you are a starter: keep away from
Use everything else -- may the source be with you!
Links:
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!
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=' '
Das Wichtigste ist das bessere Escape Handling für Subprozesse, da man $( ) auch verschachteln kann.
Endlich bequeme Ausdrücke, um zu rechnen:
# old style X=`expr $X + 1` # bad X=$(( X + 1 )) # good (( X = X + 1 )) # better (( X += 1 ))
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.
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
# 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"
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".
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 -bashAnwendung: 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
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
Normalerweise macht man keine interaktiven Shellscripte, wenn doch dann $PAGER nehmen, wenn gesetzt
# pipe a file through $PAGER (default more)
.... | ${PAGER:-more}
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.bakDie 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 :)
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.
Auch Shellscripte sollen getestet werden. Hier ein paar Testcases für das neue Millenium. Funktioniert Dein Script bei:
Quizfrage: Welche zwei Zeichen sind als einzige in Unix Filenamen verboten?
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)}'
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.
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>&-
trap
pipes and read
function
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:
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
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.
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
}
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
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
Gefunden unter: http://lists.canonical.org/pipermail/kragen-hacks/2008-February/000480.html
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
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.
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 $