Miért ne használjunk basht

Például ezért: http://nion.modprobe.de/blog/archives/531-Altering-a-variable-outside-t…

Az még rendben van, hogy pipelineon subshellt nyit minden commandhoz (POSIX standard megengedi), ezért direktben nem olvashatom ki a whileon belüli változókat. De a szép, shell-független workaround természetesen csak bashban (és dashban, de ez most mindegy) nem működik.

Egyébként a problémám: van egy függvényem, ami visszaad n sort, és egy tömböt kellene építeni a sorokból. A sorokon belül lehet whitespace, ezeknek a tömbelembe kell kerülnie.

Lehetséges megoldás lenne:


# példa függvény
fuggveny()
{
	echo "nincsspace"
	echo "van space"
}

# feltöltjük t-t
fuggveny | { while read i; do
	t=("${t[@]}" "$i")
done };

# kiolvassuk t-t
for i in "${t[@]}"; do
	echo "elem: $i"
done

# elvárt kimenet:
#
# elem: nincsspace
# elem: van space

Ahogy a fenti linken is olvasható, van bash-only workaround, kénytelen leszek azt használni, de ez így undorító (mivel a shebang-ot is át kell írni #!/bin/bash-ra, #!/bin/sh-val nem megy). Ha teheted inkább ne használj basht.

Hozzászólások

ilyenre személy szerint nem használnék bash-t. lassú és kényelmetlen. akkor már ruby, perl vagy python.

Initrd-be megy, elvileg persze tetszőleges shellt rakhatok bele, de gyakorlatilag más scriptekkel is együtt kell működnöm, ezért bash már adott. Mivel figyelnem kell a tárhelyre is (gyakorlatilag minden kilobyte számít), ezért más shellt nem akarok belerakni.

Meg egyébként is, utálom azokat a workaroundokat amik csak azért kellenek mert amivel dolgozok az szar.

--
Don't be an Ubuntard!

Úgy van, hogy a futó rendszerre bash van telepítve (függőségek miatt bash mindenképpen települ), és helyspórolás miatt nincs más shell telepítve. Az initrd-et egy script rakja össze, ami a futó rendszer programjait használja. Így csak bash maradt. Nem, nem fogom megcsinálni, hogy az initrd összerakó scriptbe belegányoljam, hogy más shellt rakjon bele.

--
Don't be an Ubuntard!

Viszont lzma-val tömörítve a teljes initrd-em (benne: bash, mount, sleep, és egy statikus bináris + a libek és a /init) kicsivel kevesebb, mint 2MB. Busybox telepítése minimum 200kb. Ha kiszórom bash-t, mount-ot és sleep-et, azzal nyerek nyersen 1MB-ot a libjeikkel együtt, ez betömörítve nem tudom mennyire jön ki, de szerintem ez úgy pont 200kb környékén van. Szóval gyakorlatilag ugyanott volnék, csak akkor a /init helyett az initrd összerakó scriptembe kellene belegányolni, azt viszont nem szeretném mert abban jelenleg nincs csúnya hack, initrd-ben meg már gyakorlatilag mindegy.

--
Don't be an Ubuntard!

Tökmindegy, a lényeg hogy inkább gányolok a /init-ben mint az initrd összelapátoló scriptben, mivel a /init sokkal kevésbé kell, hogy általános legyen, mint az initrd összelapátoló script. Pontoabban gyakorlatilag egyiknek sem kell általánosnak lennie, de én magam felé fel szoktam állítani olyan követelményeket amikre a gyakorlatban nincs szükség, az ilyenek miatt. Arról már nem is beszélve, hogy a fenti bash bugot egyszer talán még javítják, és akkor mennyivel jobb lesz kiszedni azt az egy kommentet és kikommentezni egy másik kódrészletet, mint egy egész scriptet visszaírni. :)

--
Don't be an Ubuntard!

Gondold végig, hogy mit csinál: minden egyes alkalommal átmásolja a tömb teljes tartalmát, majd a végére odabiggyeszt még egy elemet.

szerk.: Illusztrálom:

$ time for i in $(seq 1 1000) ;do t=("${t[@]}" "$i") ;done

real	0m1.262s
user	0m1.250s
sys	0m0.000s
$ unset t
$ time for i in $(seq 1 1000) ;do t[${#t[@]}]="$i" ;done

real	0m0.010s
user	0m0.010s
sys	0m0.000s

Azért a shell nem egy optimalizáló fordító, hogy akár arra is odafigyeljen, hogy az értékadás bal és jobb oldala ugyanazon változókra történő hivatkozást tartalmaz-e, vagy sem. Ráadásul a példában szereplő esetben kéne még egy kis intelligencia is ahhoz, hogy felfogja, hogy itt neki volna mit gyorsítania. Persze amekkora a bash, akár ez is belférhetne. Mellesleg sz megoldása nemcsak hogy gyorsabb, de legalább a POSIX-szintaxissal is egybevág (ellentétben az eredeti "t=( a b c )" formával, ami helyett pl ksh-ban "set -A t a b c" formában kéne írni).

OT: Úgy tűnik gyors doksinézés után, hogy igazad van. Totál megzavart, hogy a különböző kereskedelmi Jujnikszok POSIX-kompatibilisnek kikiáltott shell-verziói kb mind a Korn-shellen alapszanak, és általában nem dobták ki a Korn-shell-féle tömbkezelési szintaxist. (Azt amúgy nem látom, hogy ha tényleg nincs tömbkezelés, akkor hogyan akarhat bárki is tömbváltozók segítségével hordozható kódot írni. Az viszont gyanúsan fennáll, hogy az "a[N]=M" forma *több* shellben működik, mint a legelőször emlegetett a=(x y z). Ugyanis megy bash-ban is, *ksh*-ban is (ami minimum ksh88, ksh93, és pdksh). A zsh-t nem tudom, melyiket szereti.)

+1 ez a bugfeature mar nehanyszor az en eletemet is megkeseritette.
---
Internet Memetikai Tanszék

Most komolyan nem tudod beallitani az IFS-t hogy a szokoz ne legyen benne ?

A problémára természetesen létezik tökéletesen hordozható megoldás. Például egy összegzés:


gen_ints()
{
  seq 1 100
}

use()
{
  printf '%d\n' "$1"
}

gen_ints \
| (
    SUM=0

    while read CUR; do
      SUM=$((SUM + CUR))
    done

    use "$SUM"
  )

Gyerekből a szülő környezetét nem lehet megváltoztatni. Nem is érdemes megpróbálni; számolja ki a gyerek, amit kell, aztán használja is. Semmi értelme visszatérni a parent shell-be. Ha nem szeretjük a mély indentációt, csináljunk függvényeket legkívül, azok kötelezően másolódnak a subshell env-be is.

http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.htm…

A bash nagyszerűen megfelel interaktív munkára, illetve ha olyan szkriptet írunk, amelynek nem kell hordozhatónak lennie. Ha hordozható szkriptet írunk, akkor a POSIX vonatkozó fejezetét böngészgetve dolgozunk, és dash-sal, vagy valamely más, kifejezetten POSIX-kompatibilitásra kihegyezett shell-lel teszteljük, és a felhasználótól is megköveteljük a dokumentációban, hogy olyannal futtassa.

Azt mutasd meg, hogy a fenti use-os megoldással hogyan tudsz szóközhelyesen(!) tömböt kipasszolni. Ugyanis sajnos amint stringgé konvertálódik a változó, a separator whitespace és az elemen belüli whitespace között megszűnik a különbség.

Egyébként arra én is gondoltam, hogy a subshellen belül folytatom a végrehajtást, csak az a baj, hogy eleve egy ciklusban futna a fenti kód (egymásba ágyazás), azaz a végén n subshellem lenne, ami szerintem nem túl szerencsés.

A dasht hoztad alternatívának, ahogy a bejegyzésben írtam, a bashon kívül a dashban sem megy a "shellfüggetlen" workaround. (Ash port netbsd-ből, azóta debian fejleszti, nem is vártam mást.)

--
Don't be an Ubuntard!

"Azt mutasd meg, hogy a fenti use-os megoldással hogyan tudsz szóközhelyesen(!) tömböt kipasszolni. Ugyanis sajnos amint stringgé konvertálódik a változó, a separator whitespace és az elemen belüli whitespace között megszűnik a különbség."

Idézőjeleket mindenhova.

use ()
{
        for i in "$@"; do
                echo "elem: $i"
        done
}

t=("nincsspace" "van space")
use "${t[@]}"

Úgy értem, oké hogy kiprinteled, de építs belőle tömböt.

Egyébként pont ugyanígy működik a saját fv-em is, van egy tömb bemenete és kiprinteli bizonyos szabályok szerint a tömb bizonyos elemeit, egy sorba 1 elemet. Tehát ha én a tömbépítő whileból csinálok egy ilyen use-t akkor pont ott vagyok ahol voltam.

--
Don't be an Ubuntard!

Mégegyszer: van egy függvény, kiköp n sort stdoutra. Ebből kell tömböt építeni, egy sor -> egy elem. Kigondolt, de nem jó megoldás:


fuggveny|while read i; do
	t[${#t[@]}="$i"
done

Erre írta lacos, hogy a whileból hívjak meg egy fv-t ami megint csak printel. Akkor most mennyivel is vagyok előrébb?

--
Don't be an Ubuntard!

hogyan tudsz szóközhelyesen(!) tömböt kipasszolni

Nem passzolom ki. A függvényt legelőször kint definiáljuk, de az bemásolódik a subshell exec env-be is, futni pedig ott fut.


use_1()
{
  local K

  K=${#MYARR[@]}
  while test $K -gt 0; do
    K=$((K-1))
    printf '%u: \"%s\"\n' $K "${MYARR[K]}"
  done
}

use_2()
{
  local K

  K=0
  while test 0 -lt $#; do
    printf '%u: \"%s\"\n' $((K++)) "$1"
    shift
  done
}

for ((K=0; K<10; K++)); do
  printf '%d %d\n' $((2*K)) $((2*K+1))
done \
| (
    declare -a MYARR

    while read X Y; do
      MYARR[ ${#MYARR[@]} ]="$X $Y"
    done

    use_1
    use_2 "${MYARR[@]}"
  )

escape()
{
  local TMP

  while test 0 -lt $#; do
    TMP=\'${1//\'/\'\\\'\'}\'
    printf '%s ' "$TMP"
    shift
  done
}

f1()
{
  escape "a'b&"
  escape "c d e"     'f"g\h'
  escape i$'\n'j
}

assign_to()
{
  local ARRAY="$1"
  local ESC_LIST="$2"

  eval "$ARRAY=($ESC_LIST)"
}

assign_to Z "$(f1)"

for ((K=0; K < ${#Z[@]}; K++)); do
  printf '%u: >>%s<<\n' $K "${Z[K]}"
done

Ahogy más is rámutatott már: onnantól, hogy tömböket használunk, nincs értelme hordozhatóságról beszélni. A jelenleg érvényes POSIX szabvány Shell Command Language fejezete nem tartalmazza azt a szót, hogy "array". A POSIX nélkül számomra meg érdektelen (általánosabban: hivatalos szabvány nélkül valamelyest értelmezhetetlen) a hordozhatóság kérdése. (Igen, ebből az is következik, hogy a GNU autotools-nak nem vagyok nagy barátja.) A kérdésben bash szerepelt, a script bash script.

eval-lal természetesen megoldhatók array támogatás nélkül is az egészekkel indexelt tömbök: eval "MYARR_${IDX}=$ESC_VAL" :) A cserélős behelyettesítést kiválthatjuk sed-del, az "array initializer list"-et meg azzal, hogy az escape függvény közvetlenül a fenti értékadás-sztringeket generálja (pontosvesszővel lezárva), amelyeket összefűzünk, majd egyetlen script-ként futtatunk végül az eval-ban. Átírjam? :)

De köszönöm amúgy :)

Na végül sikerült megoldani a problémát subshellek és rekurzív függvényhívás nélkül, az által, hogy a függvényt inlineoltam a kódba. A tömböket nem úsztam meg (pedig az lett volna az igazán elegáns megoldás) és a futásidő se túl kedvező (O(k*n^2) ahol k egy file sorainak száma, n pedig egy könyvtárban levő fileok száma), de legalább működik, és a tömböket leszámítva shellfüggetlen. :)

Persze ha a bash nem viselkedik máshogy mint kellene, akkor már 2 napja kész lennék.

--
Don't be an Ubuntard!

Előfeltétel: csak shell builtinek használata, azaz nem hívhatunk meg semmilyen külső programot, coreutilsból se.

Van egy könyvtár, benne egy nagy rakás csomag. A csomagkezelő paraméterein át kell adni ezeket a csomagokat, pl. install a.pkg b.pkg c.pkg d.pkg e.pkg f.pkg. Erre ugye az a triviális megoldás, hogy install *.pkg. Csakhogy nem mindegy, milyen sorrendben adjuk át ezeket a csomagokat az installnak, mert van pár csomag, amiknek előbb kell teleülnie mint a többinek. Azaz pl. install e.pkg b.pkg a.pkg c.pkg d.pkg f.pkg (e.pkg és b.pkg került előre).

Azt találtam ki, hogy létrehozok egy filet, amiben felsorolom az először telepíteni kívánt csomagokat. Azaz a konkrét példában e.pkg és d.pkg. File olvasás megy pl. read-del, ahhoz nem kell külső command. Könyvtár listázásához se kell külső command, wildcarddal és for ciklussal megoldható a dolog. Most már csak az a probléma, hogy azokat a fileokat, amiket már hozzáadtunk az install command line-jához, azokat a könyvtár listázás során ne adjuk át még egyszer (ezt ugyanis a csomagkezelő nem díjazza). Ezért kellett tömb, hogy végig tudjunk iterálni a már hozzáadott csomagokon, így ellenőrizve, hogy a soron következő csomagot hozzáadhatjuk-e a command line-hoz.

A pontos megvalósításban persze egy picit máshogy oldottam meg, mert eleve csak a tömböt építem fel, és csak a legvégén adom át install "${t[@]}" formában.

Persze ha hozzávesszük, hogy elvileg a csomagnevekben nincs space, akkor valamivel nagyobb a mozgásterünk, de úgy voltam vele, hogy ha szép megoldás kell akkor erre is fel kell készülni.

--
Don't be an Ubuntard!

Ezért kellett tömb, hogy végig tudjunk iterálni a már hozzáadott csomagokon, így ellenőrizve, hogy a soron következő csomagot hozzáadhatjuk-e a command line-hoz.

Ezert nem kell tomb, erre vannak a listak. A tombok akkor jok, ha kulon akarsz hivatkozni egy elemre cikluson kivul, ha mar ugyis mindig vegig kell raja menni teljesen felesleges.


IFS=:

keep="e.pkg:ur csomag.pkg:b.pkg"

for pkg in *.pkg; do
	for i in $keep; do
		if [ "$pkg" = "$i" ]; then
			echo "skip: $pkg"
			continue 2
		fi
	done
	echo $pkg
done

Mar is eleg erdekes, ha space van a csomagnevben nemhogy ":" es tarsai. :D A lenyeg, hogy egy valoszinutlen karaktert kell valasztanod ami lehet %-tol a \n-ig akarmi.

A legvégén hogy adod oda a listát az install-nak?

Ha ragaszkodsz a legvegehez, akkor epithetsz belole egy uj listat, de szerintem ez felesleges. Mert nem jo, ha echo helyett egybol installt tolsz?

Nem először, de a busybox-ot nézted már...? busybox ls *.pkg | busybox grep -Ev "$(busybox cat nemkellmar)"

A nemkellmar fájlban felsorolod a csomagokat "|"-pal elválasztva, és kész a lista, hogy mit kell még.
Nagyjából:


install $(busybox tr "|" " " < nemkellmar) $(busybox ls *.pkg | busybox grep -Ev "$(busybox cat nemkellmar)")

igaz, itt a szóközöket tartalmazó csomagnevek gondot okozhatnak, de azt már rád bízom :-P

Bár a téma végérvényesen elsüllyedt, belefutottam újra a problémába, ezúttal talán jobban érzékelhető lesz, mennyire súlyos is valójában.

Adott egy textfile, amiben tetszőleges tartalmú soraink lehetnek (tehát nem kimondottan filenevek), és megkülönböztetünk speciális sorokat, amik a többi sor tartalmának feldolgozását befolyásolják. Például:


#title
Cím ide

#author
Szerző is van

#lehet komment is

#summary
Egy rövid összefoglaló
Lehet több sor is

Ez egy klasszikus állapotgépes feladat, átültetve shellre valahogy így oldanám meg:


#!/bin/sh

. /usr/lib/dsw_fuggvenyek	#do something width függvények

filename="$1"	#az nem opció, hogy beleirányítsuk a file tartalmát a scriptbe

state="none"

cat "$filename"|while read line; do
	if [ "$(echo $line|cut -c 1)" = "#" ]; then
		case "$line" in
			"#title")
				state="title"
			;;
			"#author")
				state="author"
			;;
			"#summary")
				state="summary"
			;;
		esac
	else
		case "$state" in
			"title")
				dsw_title "$line"
				state="none"	#title csak egysoros lehet
			;;
			"author")
				dsw_author "$line"
				state="none"	#author is csak egysoros lehet
			;;
			"summary")
				dsw_summary "$line"
			;;
		esac
	fi
done

Persze ez így nem fog működni, mert a state változót hiába módosítjuk a pipeon "belül", az "kívül" változatlan marad. Megoldás? Nekem nincs rá ötletem.

--
Don't be an Ubuntard!

Mire is kell a megoldás? A state változót sehol sem használod a while cikluson kívül. Egyébként a fenti script a fenti inputtal működik dash-sel és bash-sel is; legalábbis ha a dsw_* függvények simán echo "* $@"-t csinálnak, akkor azt írja ki, amit szerintem ki kellene írnia.

És hogy a mai optimalizálós tipp is meglegyen:


	if [ "$(echo $line|cut -c 1)" = "#" ]; then

helyett


	if [ "${line#"#"}" != "$line" ]; then

Hát ha probléma a pipe-on "belüliség/kívüliség", akkor felejtsd el a pipe-ot:


cat "$1" | while read line ; do
...
done 

helyett, ahogy kitaláltad, használd ezt:


while read line ; do
...
done < "$1" 

formát, és készen is vagy. Bár lehet, hogy a "beleirányítani nem lehet" kitételedet valahogyan nem értem (mivel nem mondhatnám, hgy túlaludtam magam, lehet hogy nem értem a problémát, akkor bocs).