Bash argumentumok és titkaik

Bash terminál képe a pixabay.com-ról

A Bash egy népszerű Linux shell, amit ugyan kényelmi szempontból sokan leváltanak a termináljukban egy másfajta alapértelmezett shellre, például Z shellre, de a szkriptek továbbra is Bash-hez készülnek. Van egy-két sajátossága a Bash-nek, de az olyan alap funkció, mint az argumentumok átadásának működése néha úgy tűnik, kevés figyelmet kap, hiszen más shellekben is hasonlóan működik, nincsen benne semmi különleges. De vajon valóban így van? Ki mered jelenteni, hogy mindent tudsz róluk? Ha igen, a cikk talán nem neked készült. Régóta írok Bash szkripteket is, de mégis volt, aminek én sem voltam teljesen tudatában. Olvasd tovább a cikket és teszteld le a saját tudásodat is.

Tartalomjegyzék

Argumentumok átadása

[Tartalom]

Mielőtt az argumentumok eléréséről beszélnénk, tudni kell őket definiálni. A legegyszerűbb, amikor egy szkript futtatásakor a fájlnak adod át az argumentumokat.

./myscript.sh arg1 arg2

De mi van, ha szóközt is szeretnél az argumentumban? A fenti példánál arg1 és arg2 két külön argumentumot jelent. Ha viszont idézőjelbe teszed, akkor egynek számít.

./myscript.sh "arg1 arg2" arg3

A fenti kód valójában csak két argumentumot ad át a shell szkriptnek. Azt viszont fontos tudni, hogy az átadott argumentum értéke nem fogja tartalmazni az idézőjeleket. Hogy ez miért fontos, arra hamarosan kitérek. Van viszont más módja is a szóközök átadásának. A backslash karakter használata.

./myscript.sh arg1\ arg2 arg3

Így nem használtam idézőjelet, de továbbra is két argumentumot adtam át, mivel a backslash karakterrel jeleztem a parancsértelmezőnek, hogy a szóközt ne vegye határoló karakternek, azaz ne értelmezze. Ez persze működne az idézőjelre is, így ha szükséges, azt is átadhatod az argumentum részeként:

./myscript.sh \"arg1\ arg2\" arg3

Egy összetettebb paraméternél ez nem túl átlátható egy szkriptben, de olyankor használhatod a printf utasítást, ami képes elvégezni helyetted az átalakításokat. A printf önmagában egyszerű. A fenti backslash-es alakot az alábbi módon lehet kiírni a terminálba:

printf '%q\n' '"arg1 arg2"'

Fontos viszont, hogy az így kapott eredmény egy egyszerű string lesz, ami tartalmazza a backslash-t, ám az nem lesz hatással az utána levő szóköz értelmezésére. Tehát akár egy változóba teszed az értéket, akár közvetlenül átadod egy subshellel, továbbra is idézőjelbe kell tenni.

./myscript.sh "$(printf '%q\n' '"arg1 arg2"')" arg3

A $( )-be írt kódban az idézőjel nem fogja lezárni a körülötte levő idézőjeleket, bár elképzelhető, hogy némelyik IDE számára problémát jelent a szerkezet felismerése és helytelenül színezi majd a forráskódot, vagy szintaktikai hibának véli. Nem az. A megfelelő idézőjelek használata viszont fontos, hiszen aposztrófok között nem lettek volna értelmzeve a bash utasítások.

A függvények argumentumai ugyanígy működnek. Ott persze elég csak a függvény nevét írni a "./" nélkül

function myfunction() {
   # ...
}
myfunction "$(printf '%q\n' '"arg1 arg2"')" arg3

Argumentum változók

[Tartalom]

Jöjjön akkor egy lista és rövid magyarázat, milyen változókkal lehet elérni az argumentumokat függetlenül attól, hogy függvényről van szó vagy egy szkriptfájlnak átadott argumentumról.

  • $1: Az első átadott argumentum

  • $2: A második átadott argumentum. Természetesen lehetne tovább is sorolni, de elég annyit tudni, hogy a sorszámmal lehet hivatkozni az argumentumra.

  • $*: Az összes átadott argumentum egy stringben. Itt lesz fontos a tény, hogy az idézőjelek nem kerülnek alapértelmezetten átadásra az argumentumok körül. Ha ugyanis a csillag változóval lekéred az argumentumokat, idézőjelek nélküli listát kapsz, amiből nem fog kiderülni, hogy valójában melyik szóköz jelenti az argumentumok határát és melyik egy argumentum része. Tulajdonképpen egy join műveletről van szó szóközökkel.

  • $@: Az összes argumentum tömbje. Ha egy szimpla echo utasítással kiírod a terminálban a változó értékét, nem látsz különbséget a $*hoz képest, ám egy ciklusban nagyon is fontos jellemzője, hogy nem a kiíráskor megjelent szóközök mentén lesznek az elemek iterálva, hanem úgy, ahogy az argumentumokat átadtad.

Argumentumok listájának megjelenítése

[Tartalom]

Az előzőleg megtanultak alapján a $*-gal is meg lehetne jeleníteni az argumentumokat, de mivel a sem idézőjeleket nem fogják tartalmazni az értékek, sem a backslasheket, valójában nem derülne ki, hány argumentum lett átadva. Marad tehát a $@.

for arg in "$@"; do
  echo "$arg"
done

A fenti kód azért működik, mert a $@ egy tömb, az in és a do kulcsszó közötti rész pedig tekinthető a for ciklus argumentumának. Mivel az argumentum tömb volt, ráadásul idézőjelben, a kifejezés argumentumok listájára bontódik ki, mintha azokat egyenként, manuálisan, idézőjelpárok között soroltam volna fel. Összesen három argumentum esetén a fenti kód ekvivalens az alábbival:

for arg in "$1" "$2" "$3" ; do
  echo "$arg"
done

A fenti megoldás függvényekben is működik, azaz egy tömböt egy függvénynek átadva valójában az adott függvény több argumentumot kap, nem pedig egy tömböt.

Bash

myfunction "$@"

Itt is fontos az idézőjel. Ellenkező esetben a függvény, illetve a for ciklus nem a tömb elemeit kapná argumentumaiként, hanem a $* eredményéhez hasonlóan szimplán az elemek összefűzött listáját. Az idézőjel azt is jelzi ugyanis, hogy a benne levő tartalmat úgy kell megtartani, ahogy van. Beleértve a sortöréseket és szóközöket is. A tömb esetén ez a gyakorlatban azt jelenti, hogy idézőjel nélkül elveszti tömb jellegét, mint argumentum.

Ehhez hasonló más programnyelvekben is létezik. PHP-ben a három pont jelzi átadáskor az argumentum neve előtt, hogy a tömböt több argumentumra kell kibontani.

myfunction(...$argv)

Argumentumlista másolása változóba

[Tartalom]

Bár a függvénynek idézőjelben átadható volt az argumentumlista, Ha azt tömbként szeretnéd másolni egy másik változóba, jelölnöd kell, hogy az új változó is egy tömb. Tömböt pedig így lehetne deklarálni:

args=("arg1" "arg2" "arg3")

A zárójel jelzi a tömböt, abban viszont az idézőjelek közöt levő argumentumlista éppúgy működik, mint a for ciklusnál vagy a függvénynek átadott paraméternél. Azaz több tömbelemre bontódik ki.

args=("$@")

Innentől viszont a hivatkozás megváltozik. Mostantól az "$args" változónév fog hasonlóan viselkedni, mint eddig a "$" karakter, viszont mostantól a más nyelvekből is ismert szögletes zárójelben kell a tömbindexeket megadni, ráadásul a dollár utáni teljes kifejezést még egy kapcsos zárójelbe is be kell tenni. Ezt amúgy bármilyen változóval meg lehet tenni, nem csak tömbökkel. Sőt, néha szükséges is.

Lehet számokkal hivatkozni az elemekre sorrendjük szerint

echo "${args[1]}"

Lehet összefűzött karakterláncként lekérni az elemek listáját:

echo "${args[*]}"

És lehet az összes elemet kérni tömbként:

for arg in "${args[@]}"; do
  echo "$arg"
done

Ha pusztán a változó nevét írod index nélkül, csak a tömb első értékére tudsz hivatkozni. Ez hasonló A C nyelveni pointerekhez, ahol maga a változó csak az első elemre mutat.

echo "$args"

Argumentumlista átadása egy új Bash processnek

[Tartalom]

Megesik, hogy magát a Bash shellt szeretnéd közvetlenül futtatni egy átadott paranccsal. Ez lehet akár azért, mert a szkript nem Bash-ben lett írva, de egy rövid kódrészletet mégis Bash-ben szükséges futtatni, vagy bármi más okból más paramétezéssel szeretnél egy shellt indítani. Ekkor viszont a futtatandó utasítások argumentumként lesznek átadva egy stringben. Ha ilyenkor szeretnél egy tömböt argumentumként átadni az utasításoknak, az idézőjelezés fokozottanszámít. A parancsot legjobb lenne aposztrófok közé tenni, hogy a speciális karaktereket ne értelmezze a bash, hanem átadja a stringnek. Az argumentumokat viszont stringben csak úgy tudnád átadni, ha azokban is minden speciális karaktert levédesz. Ez valahogy így nézne ki:

escapedArgs=("$(for i in "$@"; do printf '"%q"\n' "$i"; done)")
bash -c "for i in \"$escapedArgs\"; do echo \"\$i\"; done"

Ennél viszont sokkal egyszerűbb már a bash-nek is argumentumként átadni a $@ változót:

bash -c 'for i in "$@"; do printf "%s\n" "$i"; done' -- "$@"

A dupla kötőjel $@ előtt fontos. Ez jelzi, hogy minden, ami utána jön, az átadott utasításban érhető el argumentumként, nem pedig a "bash" program új paramétereinek tekintendő.

Példák argumentumlista továbbadására

[Tartalom]

Az eddig tanultakra egy komplett példa az alább idézett szkript. A szkript elején a teszteléskor átadott argumentumokat láthatod kommentben, ahogy minden példánál a kimenet is megtalálható megjegyzésben.

#!/bin/bash

# Run args.sh with the following arguments:
#
# ./args.sh "arg1 arg2" arg3

function main() {
  echo 1: "$1"
  echo 2: "$2"
}

# Copy the argument list
args=("$@")

echo "List Arguments: "
echo

for i in "$@"; do
  echo "$i"
done

echo
echo

# Pass all arguments to a function

## Right

echo 'Command: main "$@"'
main "$@"
echo
# Output:
#
# 1: arg1 arg2
# 2: arg3

echo 'Command: main "${args[@]}"'
main "${args[@]}"
echo
# Output:
#
# 1: arg1 arg2
# 2: arg3

## Wrong

echo 'Command: main "$*"'
main "$*"
echo
# Output:
#
# 1: arg1 arg2 arg3
# 2:

echo 'Command: main $@'
main $@
echo
# Output:
#
# 1: arg1
# 2: arg2

echo 'Command: main $*'
main $*
echo
# Output:
#
# 1: arg1
# 2: arg2

echo 'Command: main "$args"'
main "$args"
echo
# Output:
#
# 1: arg1 arg2
# 2:

echo 'Pass argument list to a new bash process:'
echo

# Hard way
escapedArgs=("$(for i in "$@"; do printf '"%q"\n' "$i"; done)")
bash -c "for i in \"$escapedArgs\"; do echo \"\$i\"; done"
echo
# Output:
#
# 1: arg1 arg2
# 2: arg3

# Easy way
bash -c 'for i in "$@"; do printf "%s\n" "$i"; done' -- "$@"
echo
# Output:
#
# 1: arg1 arg2
# 2: arg3 "$@"; do printf '"%q"\n' "$i"; done)")
bash -c "for i in \"$escapedArgs\"; do echo \"\$i\"; done"
echo

# Easy way
bash -c 'for i in "$@"; do printf "%s\n" "$i"; done' -- "$@"
echo

Összefoglalás

[Tartalom]

Könnyű lehet elveszni a különböző változók jelentésében, de a cikkből megtanulhattad, hogy bár nem kötelező, mindig javasolt idézőjelek közé tenni a változókat is, hogy biztosan a várt eredményt kaphasd. Különbség van viszont a "hagyományos" idézőjel és az aposztróf között. Utóbbi éppen akkor hasznos, amikor karakterre pontosan a kódszerkesztőben begépelt eredményt szeretnéd viszontlátni a kimeneten. Ha viszont szeretél ebbe szövegbe mégis becsempészni változót, némileg komplikálja a dolgot, de a printf ilyenkor segíthet akár backslash karaktereket szeretnél a kódba tenni, akár a változó értékét átadni.

Legtöbbször érdemes a kukac karakterrel hivatkozni az argumentumok listájára, ami az argumentumok tömbjét tartalmazza. Ezt a tömböt másolni is lehet új változóba, amennyiben a keerek zárójelpárral jelzed az új változó tömb típusát.

Szkript és függvény argumentuma között nincs sok különbség. A szkriptek viszont gyakrabban rendelkezhetnek összetettebb paraméterezéssel, ahogy úgynevezett flag-eket vagy opciókat lehet átadni, ahol az argumentum nevet is kap. Ezekben az esetekben viszont rendszerint a getopt vagy getopts parancsokkkal gyűjtik be az értékeket, de továbbra is lehetnek név nélküli argumentumok. Sok esetben ilyenkor a dupla kötőjellel lehet jelezni direkt módon, hogy utána ezek az argumentumok következnek, ahogy azt a bash programnál is mutattam.

Érdemes még a shift utasítást is észben tartani, amivel az argumentumokat jobbról balra el lehet tolni, és ezzel a bal oldalon levőket törölni az argumentum listából, miután azokra már nincs szükség.

Ha szeretnél továbbra is ilyen egyszerűbb témákról olvasni a Docker konténerek, Ansible és hasonló haladóbb témák mellett, jelezd kommentben vagy dobj egy like-ot facebookon. Ha úgy gondolod, másnak is hasznosak lehetnek a cikkben leírtak, ne habozz megosztani a linket velük. Végül pedig, ahogy arra mindig felhívom a figyelmet, bármilyen hibát, tévedést, pontatlanságot jelezz kérlek hozzászólásban. Harcoljunk együtt a félrevezető cikkek ellen!

Megosztás/Mentés