Shell FAQ
Linuxdays 2016

Lukáš Bařinka

Otázky

  1. Co je špatného na příkazu echo?
  2. Je možné obnovit soubor, pokud existuje už jenom otevřený file descriptor?
  3. Je možné skript zkompilovat?
  4. Jak je možné přizpůsobit bash?
  5. Jak naskriptovat změnu/zadání hesla programům, které ignorují stdin (např. telnet, passwd)?
  1. Jak pracovat s XML nebo JSON daty?
  2. Je možné sledovat události na FS?
  3. Jak definovat a pracovat s polem (jak jej kopírovat)?
  4. Lze zpracovat zvlášť stdout i stderr pomocí roury?
  1. Záleží na rychlosti při použití skriptovacích jazyků?
  2. Jak zpracovávat dlouhé přepínače skriptu?
  3. Lze logovat činnost v terminálu?

Co je špatného na příkazu echo?

  • Není součástí POSIXu ⇒ chování není standardizované
  • Nechová se jako ostatní příkazy

# bash
echo \\\\\\
\\\
echo -e \\\\\\
\\

# ksh
echo \\\\\\
\\
						
  • Nechápe -- jako konec přepínačů
    
    echo -e
    
    echo -e -e
    
    echo -- -e
    -- -e
    
    echo -en '-e\n'
    -e
    								
  • Přidává do výstupu mezery mezi argumenty (zamaskuje chyby)
    
    FILE='my single file'
    echo touch $FILE
    touch my single file
    
    touch $FILE
    ls
    file  my  single
    								

Co použít místo příkazu echo?

printf -- "%s\n" "$var"

printf -v tmp -- "%s " "${var[@]}"
printf -- "%s\n" "${tmp% }"

IFS=" "
printf '%s\n' "$*"

						

Je možné obnovit soubor, pokud existuje už jenom otevřený file descriptor?

Vytvoření a smazání souboru


#!/bin/bash

T=$(mktemp)
echo "$$"
exec >"$T"
rm "$T"

while sleep 5
do
   date | tee /dev/stderr
done
						

ls -l /proc/11748/fd
total 0
lrwx------ 1 ... 0 -> /dev/pts/1
l-wx------ 1 ... 1 -> /tmp/tmp.6LrhtNEhda (deleted)
lrwx------ 1 ... 2 -> /dev/pts/1
lr-x------ 1 ... 255 -> /tmp/holder
						

Obnovení souboru

tail -n +1 -f /proc/_PID_/fd/1 >/tmp/recovery

Je možné skript zkompilovat?

Proč (ne)?

  • Aby nebylo vidět zdroj.
  • Aby běžel rychleji.
  • Aby nebyl závislý na používaných nástrojích.

Obvyklý nástroj (neřeší žádný z důvodů): shc

Příklad skriptu

#!/bin/bash
echo secretpassword | md5sum
						
Kompilace skriptu

shc -f secret.sh

ls -l secret.sh*
-rwxr-xr-x ...   42 ... secret.sh
-rwx--x--x ... 9560 ... secret.sh.x
-rw-r--r-- ... 9630 ... secret.sh.x.c
						
Čas běhu programu

time for ((i=0;i<=1000;i++)); do ./secret.sh ; done >/dev/null

real	0m1.721s
user	0m0.104s
sys	0m0.148s

time for ((i=0;i<=1000;i++)); do ./secret.sh.x ; done >/dev/null

real	0m3.278s
user	0m0.040s
sys	0m0.228s
						
Zdrojový kód

strings /proc/8910/cmdline
./secret.sh.x -c
_spousta_mezer_ 
£!/bin/bash
echo secretpassword | md5sum
./secret.sh.x
						

Jak je možné přizpůsobit bash?

  • Konfigurační skripty:
    ~/.bashrc, ~/.bash_profile, ...
  • Přepínače:
    --login, --init-file, --rcfile, --norc, --posix, ...
  • Proměnné:
    IFS, CDPATH, BASH_ENV, GLOBIGNORE, HIST*, LC_*, PATH, PROMPT_COMMAND, PS1..PS4, TMOUT, ...
  • Nastavení (set -o/+o):
    braceexpand, history, noglob, noclobber, pipefail, posix, verbose, xtrace, vi/emacs, ...
  • Volby (shopt -s/-u):
    cdspell, dotglob, execfail, expand_aliases, extglob, failglob, globstar, globasciiranges, interactive_comments, lastpipe, nocaseglob, nocasematch, nullglob, xpg_echo, ...
  • Akce na příkazové řádce (readline):
    --noediting, $INPUTRC, ~/.inputrc, bind, read -e, ...
  • Programovatelné doplňování:
    bash_completion, $COMP*, complete, ...

Jak naskriptovat změnu/zadání hesla programům, které ignorují stdin (např. telnet, passwd)?


expect - programmed dialogue with interactive programs

What
Expect, an extension to the Tcl scripting language written by Don Libes, is a program to automate interactions with programs that expose a text terminal interface.
Why
It is used to automate control of interactive applications such as telnet, ftp, passwd, fsck, rlogin, tip, ssh, and others.

[wikipedia.org]


LC_ALL=C passwd <<EOF # GNU/Linux
> abc123
> xyz987
> xyz987
> EOF
Changing password for barinkl.
(current) UNIX password: Enter new UNIX password:
Retype new UNIX password: passwd: password updated
successfully
						

LC_ALL=C passwd <<EOF # Solaris 11
> abc123
> xyz987
> xyz987
> EOF
passwd: Changing password for barinkl
Enter existing login password: 
						

#!/usr/bin/expect -f
# wrapper to make passwd(1) be non-interactive
# username is passed as 1st arg, passwd as 2nd

set password [lindex $argv 0]
set newpassword [lindex $argv 1]

spawn passwd
expect "assword:"
send "$password\r"
expect "assword:"
send "$newpassword\r"
expect "assword:"
send "$newpassword\r"
expect eof
						

./po abc123 xyz987
spawn passwd
Enter existing login password: 
New Password: 
Re-enter new Password: 
passwd: password successfully changed for barinkl
						

expect -c '
spawn telnet www.google.com 80
send "GET / HTTP/1.0\n\n"
expect eof
'
						

#!/usr/bin/expect -f
spawn telnet www.google.com 80
send "GET / HTTP/1.0\n\n"
expect eof
						

Syntaxe


send "string"
expect {
	pattern { action }
	pattern { action }
	...
}
send "..."
expect { ... }
...
						

{ } lze vynechat, v případě jediného nebo žádného vzoru/akce

Typy vzorů

Vzor je řetězec, který může obsahovat proměnné ($var)

  • "String"
  • -re "RE" { ^, $, ., [ ], [^ ], *, ( | ) }
  • -gl "GlobPattern" { ^, $, ?, [ ], * }
  • -ex "ExactString"
  • eof
  • timeout

Další informace

Jak pracovat s XML nebo JSON daty?

  • Naivní přístup
    • Textový přístup (textové nástroje)
    • Obvykle řádkově orientované
    • Bez hlubší znalosti struktury (kontextu)
    • V mnoha případech funkční
  • Specializované nástroje

XML


<employees>
   <employee><firstName>John</firstName><lastName>Doe</lastName></employee>
   <employee><firstName>Anna</firstName><lastName>Smith</lastName></employee>
   <employee><firstName>Peter</firstName><lastName>Jones</lastName></employee>
</employees>
						

xmlstarlet sel -t -v '/employees/employee[1]/firstName' -n <xml
John

xmlstarlet sel -t -m '/employees/employee[1]/*' -v 'name()' -n <xml
firstName
lastName

xmlstarlet sel -t -v '/employees/employee/lastName' -n <xml
Doe
Smith
Jones
						

JSON


{"employees":[
   {"firstName":"John", "lastName":"Doe"},
   {"firstName":"Anna", "lastName":"Smith"},
   {"firstName":"Peter", "lastName":"Jones"}
]}
						

jq '.employees[1].firstName' <json
"Anna"

jq '.employees[0] | keys' <json
[ "firstName", "lastName" ]

jq '.employees[].lastName' <json
"Doe"
"Smith"
"Jones"

						

Je možné sledovat události na FS?

inotify - monitoring filesystem events

  • Sleduje události pro soubor/adresář
  • Nezaručí 100% sledování (netFS, mmap(2), (rekurze) race-conditions, ...)
  • V zásadě použitelný

Syntaxe

inotifywait - wait for changes to files using inotify

-m, --monitor
-r, --recursive
-t seconds, --timeout seconds
-e event, --event event
access, modify, attrib, close_write, close_nowrite, close, open, create, delete, ...
--format fmt
%w (sledovaný soubor), %f (jméno souboru pro sledovaný adresář), %e (událost), %T (čas)

               inotifywait -mr .
               Setting up watches.  Beware: since -r
               was given, this may take a while!
               Watches established.
touch file     ./ CREATE file
               ./ OPEN file
               ./ ATTRIB file
               ./ CLOSE_WRITE,CLOSE file
date > file    ./ MODIFY file
               ./ OPEN file
               ./ MODIFY file
               ./ CLOSE_WRITE,CLOSE file
						

inotifywait -mr \
   --format %w%f \
   --event close_write \
   --quiet \
   . \
| (
   IFS=$'\n'
   while read
   do
      ls -i "$REPLY"
   done
)
2228283 ./file
2228256 ./file2
2228299 ./file3
						

Jak definovat a pracovat s polem (jak jej kopírovat)?

Deklarace pole a jeho vypsání

arr=(one two three)

declare -p arr
declare -a arr='([0]="one" [1]="two" [2]="three")'

( IFS=:; echo ${arr[*]} )
one two three

( IFS=:; echo "${arr[*]}" )
one:two:three
						
Zkopírování jednoho pole do druhého

arr=(abc def [7]=ghi)

declare -p arr
declare -a arr='([0]="abc" [1]="def" [7]="ghi")'

for index in "${!arr[@]}"; do
   new[$index]=${arr[$index]}
done

declare -p arr new
declare -a arr='([0]="abc" [1]="def" [7]="ghi")'
declare -a new='([0]="abc" [1]="def" [7]="ghi")'
						
Zkopírování jednoho pole do druhého

arr=(abc def [7]=ghi)
declare -p arr

declare -a arr='([0]="abc" [1]="def" [7]="ghi")'

declare -p arr | sed 's/ arr=/ new=/'
declare -a new='([0]="abc" [1]="def" [7]="ghi")'

eval $(declare -p arr | sed 's/ arr=/ new=/')

declare -p arr new
declare -a arr='([0]="abc" [1]="def" [7]="ghi")'
declare -a new='([0]="abc" [1]="def" [7]="ghi")'
						
Zkopírování jednoho pole do druhého

arr=(abc def [7]=ghi)

declare -p arr
declare -a arr='([0]="abc" [1]="def" [7]="ghi")'

new=("${arr[@]}")

declare -p arr new
declare -a arr='([0]="abc" [1]="def" [7]="ghi")'
declare -a new='([0]="abc" [1]="def" [2]="ghi")'
						

Lze zpracovat zvlášť stdout i stderr pomocí roury?

přesměrování stdout i stderr pomocí zděděné roury

touch a b c d
(
 ls a b foo c d xxx 2>&1 1>&3 \
 | grep -cH '.' >&2
) 3>&1 | wc -l
(standard input):2
4
					

Záleží na rychlosti při použití skriptovacích jazyků?

0.1 s
wc -L words
0.25 s
awk 'length>max {max=length} END {print max}' words
1.5 s
awk '{print length}' | sort -n | tail -1
3 s
tr -c '\n' . | sort -u | tail -1 | wc -c
75 min

len=$(wc -l < words); i=1
while [ "$i" -le "$len" ]; do
	head -n "$i" words | tail -1 | wc -c
	i=$(expr "$i" + 1)
done | sort -n | tail -1
						

Built-in příkazy a aritmetické prostředí


loop1() { i=0; while [ $i -lt 1000 ]; do
	echo $i; ((i++)); done; }
loop2() { i=0; while [ $i -lt 1000 ]; do
	echo $i; i=`expr $i + 1`; done; }

time loop1
user: 0.140s | system: 0.008s | real: 0.151s

enable -n echo; time loop1
user: 2.756s | system: 2.344s | real: 3.529s

enable -n echo [ ; time loop1
user: 5.404s | system: 2.344s | real: 6.640s

enable -n echo [ ; time loop2
user: 7.568s | system: 4.596s | real: 10.678s
						

Jak zpracovávat dlouhé přepínače skriptu?


if ! LINE=$(
   getopt -n "$0" \
      -o lm:o:: \
      -l long,mandatory:,optional:: \
      -- "$@"
)
then
   usage >&2
   exit 2
fi

eval set -- "$LINE"
						

while [ $# -gt 0 ]
do
   case $1 in
      -l|--long) LONG=1; shift;;
      -m|--mandatory) MANDATORY=$2; shift 2;;
      -o|--optional) OPTIONAL=$2; shift 2;;
      --) shift; break;;
      -*) echo "$0: Unrecognised option '$1'" >&2
          usage >&2; exit 2;;
       *) break;;
   esac
done
						

Lze logovat činnost v terminálu?

Nastavení (automatického) nahrávání v ~/.bashrc


[ $SHLVL -eq 1 ] && {
   file=/tmp/typecript.$$@$(date +%s)
   script --timing="$file.time" "$file"
   exit
}
						

Přehrání v terminálu (ideálně ve stejném)


scriptreplay \
   -t /tmp/typescript.PID@TIMESTAMP.time \
   /tmp/typescript.PID@TIMESTAMP
						

Diskuse

Děkuji za pozornost

Dotazník a jeho zpracování

  • Google docs (dotazník)
  • Export do CSV
  • Metrika odpovědí:
    
    #!/bin/sed -f
     
    s@Kvůli této otázce stojí za to přijít.@1@g
    s@Zajímavé téma.@2@g
    s@Nevím/neutrální odpověď.@3@g
    s@Nezajímavé téma.@4@g
    s@Toto téma by mě odradilo od účasti.@5@g
    							
  • Metrika otázek: sum(odpovědí)/count(odpovědí)

csvtool -u TAB col 3-32 "$1" \
| ./t2i \
| awk -F'\t' '
   NR==1 { for (i=1;i<=NF;i++) t[i]=$i }
   NR>1  { for (i=1;i<=NF;i++) 
              if ($i) {v[i]+=$i;c[i]++} }
   END   { for (i=1;i<=30;i++) 
              if (c[i]) print v[i]/c[i], t[i] } 
' \
| sort -n

csvtool col 33 "$1" \
| csvtool -u ^ transpose - \
| sed -r '
   s/\^+/^/g
   s/\^/\n\n==========\n\n/g
'