Programování v shellu
Zpracování příkazové řádky

Lukáš Bařinka

Náplň cvičení

Cílem cvičení je vyzkoušení praktických důsledků zpracování (interpretace) příkazové řádky.

  • Detekce znaků rušících speciální význam (Quoting)
  • Odstranění komentářů (Comments)
  • Rozdělení řádky (Lists) na jednotlivé příkazy (Pipelines)
  • Expanze speciálních znaků (Expansion)
  • Rozdělení na slova (Word splitting)
  • Náhrada jmen souborů (Pathname expansion)
  • Přesměrování (Redirection)
  • Hledání (spuštění) příkazů

Detekce znaků rušících speciální význam (Quoting)

  • Mnoho znaků a řetězců má v shellu speciální význam
  • Pro jeho odstranění slouží znaky \ ' "
  • Metaznaky oddělují slova: ␣ ⇥ ↵ | & ; ( ) < >
  • Oddělovače slov
    
    									~printf␣'|%s|\n'␣␣␣a␣␣bc␣↵
    								
    
    									int main (int argc, char * argv[]) { ...
    								
  • Znaky: \ ' "
    
    									~printf '%s je %s\n' \\ 'zpetne lomitko' \' apostrof \" uvozovka
    									~printf '%s\n' "text 'v' uvozovkach"
    									~printf '%s\n' "text \"v\" uvozovkach"
    									~printf '%s\n' 'text "v" apostrofech'
    									~printf '%s\n' 'text \'v\'apostrofech'
    									'
    								
  • Rozdělení dlouhé řádky
    
    									~printf '.%s.\n' text\
    									na\
    									nekolik\
    									radek
    								
  • Speciální znaky
    
    									~printf '=%s=\n' $'a\tb\nc'
    									~printf '=%s=\n' $'\'\"'
    									~printf '%s\n' $'\U1f607' $'\U1f608'
    								
  • Znak z klávesnice (^V)
    
    									~printf '%s\n' '<CTRL-V><ESC>cx<CTRL-V><TAB>y'
    								

Odstranění komentářů (Comments)

  • Slovo začínající znakem # uvozuje komentář
  • Komentář je až do konce řádku
  • Komentář pro neprovedení řádku
    
    									~cd /
    									/#touch file
    									/cd
    									~touch file
    								
  • Znak # uprostřed/na začátku slova
    
    									~printf '%s\n' a #b c
    									~printf '%s\n' a#b c
    								
  • Quoting #
    
    									~printf '%s\n' 'a #b c' "#"
    								
  • Komentář na víceřádkovém vstupu
    
    									~printf '%s\n' a \
    									#b \
    								

Rozdělení řádky (Lists) na kolony (Pipelines) a jednotlivé příkazy (Simple Commands)

  • Jednoduchý příkaz je ukončen typicky ↵ ; | &
  • Kolona příkazů (pipeline) jsou příkazy oddělené |
  • Pokud jeden z příkazů kolony skončí, ukončí se i ostatní
  • Oddělovačem v seznamu příkazů (list) jsou ; & || &&

							~set -x

							~cd /tmp; ls | wc -c; cd
							+ cd /tmp
							+ wc -c
							+ ls
							1234
							+ cd
						

							~cd /tmp & ls | wc -c & cd
							[1] 5654
							+ cd /tmp
							[2] 5656
							+ wc -c
							+ ls
							+ cd
							[1]-  Done             cd /tmp
							~65432

							~set +x

							~cd ..;cd ..;cd ..;cd ..;pwd|wc -c;cd
							2
						

							~cd foo || mkdir foo || echo "Adresar 'foo' nelze vytvorit"
							bash: cd: foo: No such file or directory
							~ls -d foo
							foo
							~rmdir foo && echo "Adresar 'foo' smazan"
							Adresar 'foo' smazan
							~rmdir foo && echo "Adresar 'foo' smazan"
							rmdir: failed to remove 'foo': No such file or directory
							~touch foo
							~cd foo || mkdir foo || echo "Adresar 'foo' nelze vytvorit"
							bash: cd: foo: Not a directory
							mkdir: cannot create directory 'foo': File exists
							Adresar 'foo' nelze vytvorit
							~rm foo
						

Expanze speciálních znaků (Expansion)

  • Expanze se děje nad jednotlivými slovy řádky
  • Některé expanze mění počet slov na řádce
  • Speciální znaky a konstrukce zodpovědné za expanzi:
    {,} ~ $ ${} $() $(())
  • Pokud není "$...", výsledek podléhá dalšímu zpracování
  • Brace Expansion
    
    									~mkdir files; cd files
    									~/filestouch {a..z}.{txt,jpg} {001..020}; ls
    									~/filesmv ~/files/x.{jpg,jpeg}; ls
    								
  • Tilde Expansion
    
    									~/filesprintf '%s = %s\n' '~' ~ '~+' ~+ '~-' ~- '~root' ~root
    									~/filesA=~+:~-; B=~+,~-; C='~'; declare -p A B C
    								
  • Parameter Expansion
    
    									~/filesmin=1; max=10; printf '%s\n' {$min..$max}
    									~/filest='Text na nekolik slov'
    									~/filesprintf '%s.\n' "$t"
    									~/filesprintf '%s.\n' "$ticek"
    									~/filesprintf '%s.\n' "${t}icek"
    								
  • Command Substitution
    
    									~/filesdate +%T
    									~/filescas=$(date +%T)
    									~/filesdeclare -p cas
    									~/filesu=$(getent passwd "$USER"); declare -p u
    									~/filesu=$(getent passwd); declare -p u
    								
  • Arithmetic Expansion
    
    									~/filesx=2 y=3
    									~/filesprintf '%03d\n' $((x+y*x))
    									~/filesz=$((x+y)); declare -p x y z
    									~/files((z=x+y)); declare -p x y z
    									~/files((z++)); declare -p x y z
    								

Rozdělení na slova (Word splitting)

  • Pokud řetězec pro expanzi není v " ",
    výsledek se rozdělí na slova
  • Znaky pro dělení slov jsou v proměnné IFS
  • Standardní oddělovače jsou ␣ ⇥ ↵
  • (Ne)použití " " kolem expandovaného slova
    
    									~/filest='Text na nekolik slov'
    									~/filesprintf '%s.\n' $t
    									~/filesprintf '%s.\n' "$t"
    								
  • Parametry skriptu $* a $@ s/bez " "
    
    									~/filesset -- 'a b' c 'd e f'
    									~/filesprintf '.%s.\n' $#
    									~/filesprintf '.%s.\n' $*
    									~/filesprintf '.%s.\n' $@
    									~/filesprintf '.%s.\n' "$*"
    									~/filesprintf '.%s.\n' "$@"
    								
  • Přenastavení IFS
    
    									~/filesset | grep ^IFS=
    									~/filesoldIFS=$IFS
    									~/filesa='␣␣␣a:b::c␣␣␣'
    									~/filesprintf '%s|\n' $a
    									~/filesIFS=:
    									~/filesprintf '%s|\n' $a
    									~/filesIFS=
    									~/filesprintf '%s|\n' $a
    									~/filesunset IFS
    									~/filesprintf '%s|\n' $a
    									~/filesIFS=$oldIFS
    								

Náhrada jmen souborů
(Pathname expansion)

  • Seznam odpovídajících jmen souborů je abecedně seřazený
  • Při prázdném seznamu jmen zůstane původní slovo
  • Jména souborů začínající . se standardně neuvažují
  • Znaky pro expanzi jmen souborů jsou ? * []
  • Použití ? * [ ]
    
    									~/filesls           ~/filesls [ps0]*
    									~/filesls 01?       ~/filesls [p-s]*
    									~/filesls ???       ~/filesls [^p-s]*
    									~/filesls 0?0       ~/filesls *[02468]
    									~/filesls *         ~/filesls *[^02468]
    									~/filesls *.txt     ~/filescd
    									~/filesls *j*       ~ls -ld */
    									~/filesls a*        ~ls -ld */*
    
    									~/filesPATTERN=*
    									~/filesls $PATTERN
    									~/filesls "$PATTERN"
    								
  • Expanze neexistující položky
    
    									~/filesls f*o?o[0-9]
    								
  • Expanze začínající .
    
    									~/filestouch .a ..b ...c
    									~/filesls -ld .*
    									~/filesls -ld .[^.]*
    									~/filesls -ld .[^.]* ..?*
    								
  • Vypnutí expanze jmen souborů
    
    									~/filesset -f
    									~/filesls -ld *
    									~/filesls -ld .*
    									~/filesls -ld .[^.]*
    									~/filesls -ld .[^.]* ..?*
    									~/filesset +f
    								

Přesměrování (Redirection)

  • Přesměrování se provádí před spuštěním příkazu
  • Přesměrování platí pouze pro spuštěný příkaz
  • Přesměrování se vyhodnocuje zleva doprava
  • Přesměrování je možné psát kamkoliv, obvykle na konec
  • Přesměrování výstupu jednoho příkazu
    
    									~ls > list; wc -l list
    									~ls >> list; wc -l list
    									~date >list; wc -l list
    									~echo 1; echo 2 > list; echo 3
    									~echo 1; echo 2> list; echo 3
    								
  • Přesměrování výstupu a chybového výstupu
    
    									~ls / foo bar | wc -l
    									~ls / foo bar >list | wc -l
    									~ls / foo bar >list 2>/dev/null | wc -l
    									~rm list
    									~ls / foo bar >/dev/null 2>/dev/null | wc -l
    									~ls -l list
    									~ls / foo bar >/dev/null 2>/dev/null | wc -l >/dev/null
    								
  • Přesměrování vstupu ze souboru/procesu
    
    									~wc -l /etc/passwd
    									~cat /etc/passwd | wc -l
    									~wc -l </etc/passwd
    								
  • Přesměrování Here-Document
    
    									~wc <<KONEC
    									jedna dva
    									tri
    									KONEC
    								
  • Přesměrování Here-Strings
    
    									~printf '%s\n' "$PATH" | wc -c
    									~wc -c <<<"$PATH"
    								
  • Jméno souboru pro přesměrování může být v proměnné
    
    									~N=/dev/null
    									~ls / foo bar
    									~ls / foo bar >"$N"
    									~ls / foo bar >"$N" 2>"$N"
    								
  • Samotné přesměrování nemůže být v proměnné
    
    									~N='> /dev/null'
    									~ls -d
    									~ls -d $N
    									~ls -d "$N"
    								
  • Nejdříve se provede přesměrování, pak příkaz
    
    									~ls -l list > list
    									~less list
    								
  • Nejdříve se provede globbing, pak přesměrování
    
    									~ls > l?st
    									~ls > files/00?
    								
  • Příkaz může být prázdný, pouze otevře soubor pro zápis
    
    									~> list 
    									~ls -l list
    								

Ukládání do souboru a do proměnné

  • Uložení výstupu do souboru
    
    									~date >x
    									~less x
    								
  • Uložení výstupu do souboru se jménem v proměnné
    
    									~x=soubor
    									~date >"$x"
    									~less "$x"
    								
  • Uložení výstupu do proměnné
    
    									~x=$( date )
    									~declare -p x
    								
  • Načtení obsahu souboru do proměnné
    
    									~unset x
    									~date >x
    									~x=$( <x )
    
    
    
    									~declare -p x
    								

Spouštění příkazu
(Command Execution)

  • Pokud název příkazu neobsahuje /,
    spouští se 1) funkce, 2) vestavěný nebo 3) externí příkaz
  • Pokud název příkazu obsahuje /,
    spouští se soubor ze zadané cesty
  • Externí příkaz se hledá v adresářích podle proměnné PATH
  • Jméno příkazu (pokud není "quoted") podléhá aliasům
  • Spouštění příkazu jménem (bez cesty)
    
    									fray1:~declare -p PATH
    									declare -x PATH="/opt/local/bin:/opt/java/jdk/bin:/usr/bin:..."
    									fray1:~oldIFS=$IFS IFS=:
    									fray1:~find $PATH -name grep 2>/dev/null
    									/usr/bin/grep
    									fray1:~IFS=$oldIFS
    									fray1:~find /usr -name grep 2>/dev/null
    									/usr/gnu/bin/grep
    									/usr/bin/grep
    									/usr/xpg4/bin/grep
    									fray1:~type grep
    									grep is /usr/bin/grep
    								
  • Spouštění příkazu jménem (s/bez cesty)
    
    									fray1:~grep --version
    									grep: illegal option -- version
    									Usage: grep [-c|-l|-q] -bhinsvw pattern file . . .
    									fray1:~/usr/xpg4/bin/grep --version
    									/usr/xpg4/bin/grep: illegal option -- version
    									Usage:  grep [-c|-l|-q] [-bhinsvwx] pattern_list [file ...]
    									        grep [-c|-l|-q] [-bhinsvwx] [-e pattern_list]... [-f pattern_file]... [file...]
    									...
    									fray1:~/usr/gnu/bin/grep --version
    									/usr/gnu/bin/grep (GNU grep) 2.14
    									Copyright (C) 2012 Free Software Foundation, Inc.
    									...
    								
  • Spouštění skriptu pomocí interpretu
    
    									~printf '%s\n' '#!/bin/bash' date 'sleep 3' pwd >skript
    									~bash skript
    									~bash <skript
    									~printf '%s\n' '#!/bin/bash' date 'sleep 3' pwd | bash
    
    									~chmod +x skript
    									~./skript
    								
  • Spouštění s nastavením proměnné
    
    									~declare -p PAGER
    									~PAGER='wc -l' man ls
    									~declare -p PAGER
    									~export PAGER='wc -l'; man ls
    									~declare -p PAGER
    
    									~A=5; bash -c 'declare -p A'; declare -p A
    									~A=7 bash -c 'declare -p A'; declare -p A
    								
  • Expanze aliasu pokud není "quoted" (nefunguje ve skriptu!)
    
    									~alias
    									...
    									~alias ls
    									alias ls='ls --color=auto'
    									~unalias ls
    									~alias ll='ls -l'
    									~ll
    									~'ll'; \ll
    									bash: ll: command not found
    									bash: ll: command not found
    									~alias lc='ls | wc -l'
    									~lc
    									123
    								
  • Spouštění vestavěného/externího příkazu
    
    									fray:~cd /bin
    									fray:/binpwd
    									fray:/binman pwd
    									fray:/binpwd --help
    									fray:/binhelp pwd
    									fray:/bin/bin/pwd --help
    									fray:/bin/bin/pwd
    								

Pro shell je podstatné, jak vypadá řádka před náhradami,
nikoli po nich. To umožňuje předvídatelnost chování.


								~cmd='date'                           ~cmd='echo $((1+2))'
								~$cmd                                 ~$cmd
								...                                     $((1+2))
								~cmd='date >d'                        ~cmd='x=5'
								~$cmd                                 ~$cmd
								date: invalid date '>d'                 bash: x=5: command not found
								~cmd='date | wc -l'                   ~alias ll='ls -l'
								~$cmd                                 ~cmd='ll'
								date: invalid option -- 'l'             ~$cmd
								Try 'date --help' for more information. bash: ll: command not found
						

								~type date
								date is /bin/date
								~type ls
								ls is aliased to `ls --color=auto'
								~type quote
								quote is a function
								quote ()
								{
									local quoted=${1//\'/\'\\\'\'};
									printf "'%s'" "$quoted"
								}
								~type cd
								cd is a shell builtin
								~type for
								for is a shell keyword