~tudor/rwsh

c72fd2eb1d5f033bd4f4e26053c02c183ddd2512 — Tudor Roman 4 years ago feca411
docs
2 files changed, 492 insertions(+), 26 deletions(-)

M infoeducatie/README.infoeducatie.md
M infoeducatie/README.infoeducatie.pdf
M infoeducatie/README.infoeducatie.md => infoeducatie/README.infoeducatie.md +492 -26
@@ 21,7 21,9 @@ fontsize:

# Introducere - shell-ul

Codul poate fi găsit pe https://git.sr.ht/~tudor/rwsh
Codul poate fi găsit pe https://git.sr.ht/~tudor/rwsh.

Issue tracker: https://todo.sr.ht/~tudor/rwsh.

_Shell-ul_, sau linia de comandă, este interfața textuală a unui sistem de operare. Prin acesta,
utilizatorul poate să execute programe sub formă de _comenzi_, sau să execute


@@ 43,6 45,9 @@ Exemple de astfel de shell-uri sunt GNU `bash` (Bourne Again Shell), `csh`
(C Shell), `ksh` (Korn
Shell), `zsh` (Z Shell), `fish` (Friendly Interactive Shell) etc.

Publicul țintă al acestor programe este format din administratori de sistem,
ingineri software și power useri.

> În blocurile de cod care urmează, textul precedat de simbolul '#' formează
> comentariile.



@@ 96,6 101,13 @@ Laboratoarele Bell.
prelucrează datele linie cu linie. În unele cazuri, aceasta abordare poate fi
ineficientă și pentru procesor, dar și pentru programator.

De asemenea, sintaxa RWSH este una curată, nouă, lipsită de neclaritățile din
limbajele uzuale de shell-scripting, care au apărut din cauza nevoii de a păstra
compatibilitatea cu versiuni mai vechi ale acestora. RWSH nu respectă standardul
POSIX. Acesta poate fi un lucru bun, pentru că permite dezvoltarea unei sintaxe
complet noi, dar și un lucru rău, atunci când compatibilitatea POSIX este
dorită.

## Expresiile regulate structurale (structural regular expressions)

RWSH folosește un sub-limbaj prin care poate fi exprimată structura textului pe


@@ 133,7 145,7 @@ care se află textul cel nou.
`cat document.txt` invocă programul `cat`, care afișează pe ecran conținutul
fișierului `document.txt`.

Comanda, fiind urmată de operatorul pizza (`|>`), conținutul fișierului, în loc
Rezultatul comenzii, fiind urmat de operatorul pizza (`|>`), conținutul fișierului, în loc
să fie afișat pe ecran, va fi pasat comenzilor pizza care urmează.

Prima comandă, `,x/Tudor/ c/Ioan/` are adresa `,`, care se referă la datele de


@@ 143,12 155,12 @@ Comanda `x` executa comanda transmisă ca parametru pentru fiecare subșir care 
potrivește cu expresia regulată dată. Comanda `c/Ioan/` schimbă subșirul cu
textul "Ioan".

Următoarea comanda din șir este `,p`, care afișează textul dat în întregime.
Următoarea comandă din șir este `,p`, care afișează textul dat în întregime.

Pe lângă operația `c`, mai există operațiile `a`, `d` și `i`, care adaugă după
dot, șterg textul din dot și respectiv inserează înaintea dot-ului.
dot, șterge textul din dot și respectiv inserează înaintea dot-ului.

Analogul operației `x` este `y`, care execută o comandă pe subșirurile care nu
Inversul operației `x` este `y`, care execută o comandă pe subșirurile care nu
se potrivesc cu expresia regulată.

O altă abilitate specială este cea de a executa comenzi _pizza_ în paralel,


@@ 192,7 204,6 @@ Comenzile aflate intre acolade sunt cele executate _în paralel_. Efectul
fiecărei comenzi este înregistrat într-un jurnal sub formă de vector. Acestea
sunt interclasate și la final efectele sunt aplicate.

\clearpage
Un alt exemplu este să vedem de câte ori și unde apare cuvântul "linux" în
jurnalul sistemului:



@@ 224,6 235,7 @@ meargă o linie în față și una în spate față de adresa subșirului găsit
pentru a afișa toată linia pe care am găsit subșirul. Altfel, s-ar fi afișat
doar cuvântul "linux".

\clearpage
Exemplu mai complex: afișarea liniilor care conțin cuvântul "linux", dar fără
timpul evenimentelor (textul dintre paranteze pătrate de la începutul fiecărei
linii):


@@ 238,7 250,8 @@ dmesg |> ,x/^\[.*\] /d |> ,x/linux/ {
Rezultatul va fi pasat comenzii `lolcat` pentru a fi afișat în culorile
curcubeului.

\clearpage
Detalii legate de comenzile disponibile pot fi găsite in [anexă](#anexa).

## Avantajele abordării RWSH

Integrarea uneltelor de prelucrare a textului în cadrul shell-ului este


@@ 246,7 259,7 @@ inevitabilă. Uneltele convenționale, precum `grep`, `sed`, `cut`, `tr` etc. su
folosite în aproape orice _shell script_ din cauza funcțiilor elementare pe care
le prestează. Majoritatea shell-urilor moderne prezintă unele astfel de
funcționalități tocmai pentru că sunt indispensabile și sunt prea lungi de
scris. Uitați care este diferența dintre eliminarea sufixului numelui unui
scris. Priviți care este diferența dintre eliminarea sufixului numelui unui
fișier în mod tradițional vs. cu ajutorul sintaxei speciale din cel mai popular
shell, GNU _bash_:



@@ 286,8 299,7 @@ eficientă și mai citibilă.
# Funcționalitatea de limbaj de programare

Pentru moment, în limita timpului disponibil, am reușit să implementez
variabilele, șirurile de caractere, operațiile matematice, blocurile de cod și blocurile decizionale
(`if`).
variabilele, șirurile de caractere, operațiile matematice, blocurile de cod, blocurile decizionale (`if`), buclele (`while`) și două blocuri speciale pentru pattern matching: `switch` și `match`.

## Variabilele



@@ 295,33 307,171 @@ Valorile sunt atribuite variabilelor cu comanda `let`. Variabilele sunt
declarate automat la atribuire.

```bash
let nume Tudor
let nume = Tudor
echo "Salut, $nume!" # Va afisa "Salut, Tudor!"

let nume Andrei
let nume = Andrei
echo "Salut, $nume!" # Va afisa "Salut, Andrei!"
```

Pentru a folosi o variabilă, numele ei va fi precedat de simbolul `$`.

Pentru a șterge variabila, se va folosi comanda `unset`: `unset nume`.
Pentru a șterge variabila, se va folosi `let` cu flag-ul `-e` (erase): `let -e
name`.

**Notă**: există o variabilă specială, numită `?`. Ea ține minte "exit code"-ul
ultimei comenzi executate. Comanda precedentă se consideră executată cu succes
dacă `?` va fi egal cu 0.

Variabilele pot avea un _scope_ asemănător limbajelor moderne de programare,
spre deosebire de POSIX shell. Când o variabilă este creată, ea va fi atribuită
blocului de cod în care se află. Atunci când atribuim o valoare nouă unei
variabile în interiorul unui bloc inferior de cod, variabila va rămâne în blocul
superior. Putem crea o nouă variabilă cu același nume în blocul inferior de cod
cu flag-ul `-l` (local).

**Exemple:**

Păstrarea scope-ului:

```bash
let name = Tudor
echo $name # Tudor
{
	echo $name # Tudor
	let name = Ioan
	echo $name # Ioan
}
echo $name # Ioan
```

Crearea unui nou scope local:

```bash
let name = Tudor
echo $name # Tudor
{
	echo $name # Tudor
	let -l name = Ioan
	echo $name # Ioan
}
echo $name # Tudor
```

Atribuirea unui scope inferior atunci când variabila nu există:

```bash
echo $name # nimic
{
	let name = Tudor
	echo $name # Tudor
}
echo $name # nimic
```
\clearpage

### Vectorii (array)

Toate variabilele sunt stocate drept vectori. Când declarăm o variabilă simplă,
se declară de fapt un vector cu un element.

Putem declara un vector cu mai multe elemente:

```bash
let fructe = [ mere rosii prune ]
echo mie îmi place să mănânc $fructe[2]
```

Dacă folosim un vector fără un index, fiecare element va fi tratat ca un
argument separat (array expansion).

```bash
echo $fructe # echo primeste trei parametrii
```

Putem folosi asta pentru a crea vectori pe baza altora:

```bash
let numere = [ 1 2 3 ]
let numere_speciale = [ 5 $numere 8 9 4 ] # elementele vor fi 5 1 2 3 8 9 4
```

Expresiile glob, precum `*.mp4`, sunt tratate și ele ca argumente diferite:

```bash
let list_of_movies = [ *.mp4 ]
let list_of_music = [ *.ogg *.flac ]
```

Dacă folosim vectorul fără un index, dar în interiorul unui șir de caractere cu
ghilimele, elementele lui vor fi tratate ca un singur parametru, cu spații între
ele:

```bash
echo "$fructe" # echo primeste un parametru
```

Dacă numele vectorului are sufixul `PATH` (exemple: `PATH`, `MANPATH`, `LD_LOAD_PATH` etc.), în scrierea lui ca șir de caractere, elementele nu vor fi
delimitate de spații, ci de doua puncte (":"):

```bash
echo "$PATH" # afișează /bin:/usr/bin:/usr/local/bin etc
echo $PATH   # afișează /bin /usr/bin /usr/local/bin,
             # dar fiecare path va fi perceput ca un
		     # argument separat de catre echo
```

Variabilele de mediu (environment variable) care au sufixul "PATH", precum cele
enumerate mai sus, vor fi automat convertite în vectori.
\clearpage

### Comanda `let`

Pe lângă atribuirea de valori și crearea variabilelor în scope-uri noi, `let`
știe și să creeze variabile de mediu (environment variables):

```bash
let -x variabila_de_mediu = "o valoare" # -x vine de la eXport

# echivalentul bash:
export variabila_de_mediu="o valoare"
```

Ștergerea variabilelor:

```bash
let -e variabila # sterge variabila normala
let -xe variabila # sterge variabila de mediu
```

Pe lângă `=`, `let` are și alți operatori pentru diverse scurtături:

```bash
let i += numar # echivalent cu let i = $(calc $i + numar)
# exista si -=, *=, /=, %=

let array ++= element # adauga "element" in array
# echivalent cu let array = [ $array element ]

let array ::= element # adauga "element" la inceput in array
# echivalent cu let array = [ element $array ]

let array ++= [ element1 element2 ] # adauga mai multe elemente
let array ::= [ element1 element2 ] # adauga mai multe elemente la inceput
```

## Șirurile de caractere

Parametrii comenzilor date sunt exprimați ca șiruri de caractere separate prin
spațiu. Pentru a putea folosi șiruri de caractere cu caractere speciale și
spații. Pentru a putea folosi șiruri de caractere cu caractere speciale și
spații, acestea vor fi înconjurate de ghilimele (`"`) sau apostrofuri (`'`).

Apostrofurile diferă de ghilimele prin faptul că în șirurile de caractere cu
apostrofuri, cuvintele precedate de `$` nu vor fi tratate ca variabile.

\clearpage
```bash
let nume Tudor
let nume = Tudor
echo Salut, $nume! # Va afisa "Salut, Tudor!"
                   # Comanda echo primeste doi parametri: "Salut," si "Tudor!"



@@ 337,7 487,7 @@ echo 'Salut, $nume!' # Va afisa "Salut, $nume!"
regulile fiecăruia:

```bash
let nume Tudor
let nume = Tudor

echo Salut", $nume"'!' # Va afisa tot "Salut, Tudor!"
                       # Comanda echo primeste un singur parametru


@@ 351,21 501,47 @@ echo Este ora $(date +%H:%M) # Va afisa "Este ora 11:27"
                             # Comanda echo primeste 3 parametri
```

**Important**: valorile variabilelor / elementelor vectorilor, indiferent de cate spații conțin, vor fi
tratate întotdeauna ca un singur parametru. RWSH nu necesită "quoting"-ul
variabilelor.

```bash
# bash:
filename="video haios.mp4"
rm $filename
# rm: cannot remove 'video': No such file or directory
# rm: cannot remove 'haios.mp4': No such file or directory

# rwsh:
let filename = "video haios.mp4"
rm $filename # merge fara nicio problema, cum ne-am aștepta
rm $(echo video haios.mp4) # merge și așa
```

\clearpage
În cazul în care vreți ca o variabilă sa fie "extinsă" ca în bash, aveți două
opțiuni:

1. Declarați variabila ca vector: `let filename = [ video haios.mp4 ]`
2. Folosiți comanda `eval`: `eval rm $filename`

**Notă**: dacă un șir de caractere simplu (fără ghilimele sau apostrofuri)
conține la început o cale de fișier care începe cu caracterul `~`, tilda va fi
înlocuită de calea către directorul utilizatorului. Exemplu:

```bash
ls ~/src # Afiseaza conținutul folder-ului /home/tudor/src
ls ~altuser/dir # Afișează conținutul folder-ului /home/altuser/dir
```

\clearpage
## Blocurile de cod și blocurile `if`

Sintaxa pentru un bloc `if` este `if (condiție) comandă_de_executat`.
## Blocurile de cod și blocurile `if` și `while`

Sintaxa pentru blocurile `if` și `while` este `if (condiție) comandă_de_executat`, respectiv `while (condiție) comandă_de_executat`.

Condiția este o comandă. Dacă "exit code"-ul comenzii din condiție este 0,
condiția este validă, iar comanda se va executa.
condiția este validă, iar comanda se va executa. În cazul lui `while`, comanda
se va executa cât timp condiția se evaluează cu codul 0.

Dacă vrem să executăm mai multe comenzi, vom folosi blocul de cod, scris între
acolade:


@@ 388,19 564,122 @@ else nu_avem_incotro

Cum condiția este o comandă, putem sa folosim pipe-uri, operatori pizza, etc.

Condiția poate fi negată cu operatorul `!`: `if (! condiție) fa_ceva`.

Mai pot fi folosiți și operatorii logici `||` și `&&`, chiar și în afara
condiției, ca in bash.

\clearpage
## Operațiile matematice

Operațiile matematice se fac cu comanda `calc`. Putem stoca rezultatul într-o
variabila astfel:

```bash
let a 2
let b 3
let c $(calc $a + $b)
let a = 2
let b = 3
let c = $(calc $a + $b)

echo "Rezultatul este $c" # Va afisa "Rezultatul este 5"
```

Incrementarea și decrementarea variabilelor se poate face direct cu `let`:

```bash
let i = 0
let n = 10
while ([ $i -lt $n ]) {
	fa_ceva
	let i += 1
}
```

## Pattern matching: `switch` si `match`

Blocul `switch` arată în felul următor:

```
switch $valoare
	/pattern_1/ fa_ceva
	/pattern_2/ fa_altceva
	...
	/pattern_n/ nu_stiu_ce_sa_mai_fac
	// fallthrough
end
```

`switch` evaluează pattern-urile (care sunt regex-uri) în ordine și excută
comanda asociată primului regex care se potrivește cu valoarea dată.

**Notă**: `switch` ar putea fi îmbunătățit cu condiții care nu țin doar de
natura șirului de caractere. Momentan nu există nicio modalitate de a folosi
`switch` pe intervale numerice, de exemplu.

\clearpage
**Exemplu**:

```
switch $status
	/\[ERROR\] (.*)/ echo s-a petrecut o eroare: $1
	/\[WARNING\] (.*)/ echo avertizare: $1
end
```

Putem folosi `switch` și ca să împărțim un text în mai multe câmpuri:

```
switch $status
	/\[(?P<type>.*)\] (.*)/ echo got event type \"$type\" with message: $2
end
```

`match` este similar comenzii SRE `x`: citește de la `stdin` și pentru fiecare
subșir care se potrivește cu un regex, execută comanda asociată. Dacă un subșir
se potrivește cu mai multe regex-uri, se execută comenzile asociate tuturor
regex-urilor cu care se potrivește. `match` este echivalent-ul pattern-urilor
din `awk`. Totuși, `awk` execută comenzile pe întreaga linie care se potrivește
cu regex-ul, în timp ce RWSH ia strict textul potrivit.

**Exemplu**: afișează textul dintre ghilimele

```bash
echo 'un text cu "ghilimele" in el. si '"'apostrofuri'" |
	awk '/".*"/ { print "ghilimele", $1 }'"
		 /'.*'/"' { print "apostrofuri", $1 }'
```

Va afișa "ghilimele un apostrofuri un", deoarece `print $1` afișează primul cuvânt de pe linia cu
ghilimele ("un"), nu textul dintre ghilimele.

Următoarea este varianta corectă:

```bash
echo 'un text cu "ghilimele" in el. si '"'apostrofuri'" | awk '{
	for (i = 1; i <= NF; i++) {
		if (match($i, /".*"/)) {
			print "ghilimele", $i
		} else if (match($i, '"/'.*'/"')) {
			print "apostrofuri", $i
		}
	}
}'
```

Codul acesta nici măcar nu mai folosește pattern-urile din `awk`.

Varianta RWSH este mai simplă:

```bash
echo 'un text cu "ghilimele" in el. si '"'apostrofuri'" |
	match
		/"(.*)"/ echo ghilimele $1
		/'(.*)'/ echo apostrofuri $1
	end
```

Această variantă nu doar că este mai ușoară de înțeles, ba chiar extrage textul
dintre ghilimele / apostrofuri.

\clearpage
# Detalii tehnice



@@ 410,14 689,201 @@ MacOS, FreeBSD etc.
Programul este scris în limbajul de programare Rust, un limbaj similar cu C++
care pune accent pe corectitudinea programului și a modelului de memorie. Cum
shell-ul este un program cheie în orice sistem de calcul, acesta nu trebuie să
aibă erori de memorie sau probleme de securitate (a se vedea: [Shellshock][1])

Pentru a asigura siguranța codului și sănătatea minții, folosesc teste automate
aibă erori de memorie sau probleme de securitate (a se vedea: [Shellshock][1]).
Rust de asemenea vine cu sintaxă și librărie standard modernă, făcând experiența
de programare mai apropiată de un limbaj de programare "ușor", precum Python sau
Go.

Pentru a asigura siguranța codului și sănătatea minții, folosesc \
**teste
automate**
pentru a detecta bug-uri în cod. Acestea se execută cu comanda `cargo test` din
Rust și cu script-ul `run_examples.sh` din folder-ul `examples`.

[1]: https://en.wikipedia.org/wiki/Shellshock_(software_bug)

Aceste teste sunt executate automat la fiecare `git push` într-un sistem tip CI
(continous integration) la adresa [https://builds.sr.ht/~tudor/rwsh][builds]. La fiecare
push, serviciul compilează codul, verifică ca fiecare fișier să conțină antetul
pentru licența GPL și execută testele. Dacă testele merg bine, codul este împins
automat pe GitHub. Dacă testele merg bine, codul este împins automat pe GitHub.

[builds]: https://builds.sr.ht/~tudor/rwsh

Librăriile folosite includ `nix` pentru funcțiile de bibliotecă pentru sistemul
de operare, `regex` pentru expresiile regulate _simple_, și `calculate` pentru
funcția de calculator.

# To do

Am implementat funcțiile cele mai importante importante pentru a obține o soluție consistentă pentru prezentare.
Funcționalități care vor fi implementate:

* Buclă cu iterator (`for`)
* Funcții
* Variabile hash map
* Execuția comenzilor din SRE
* Job control (foarte complex)
* [Ticket-ul #4](https://todo.sr.ht/~tudor/rwsh/4) (pe
	`https://todo.sr.ht/~tudor/rwsh`)

\clearpage

## Arhitectura aplicației

Codul este împărțit în mai multe module de tip "crate", specifice limbajului de
programare Rust. Codul executabilului este în modulul executabil, care se
folosește de modulul "lib". Modulul "lib" este mai departe împarțit în:

Modul | Descriere
------|----------
`builtin` | Conține codul fiecărui program "builtin", precum `calc`, `let`, `eval`, `exit` etc.
`parser` | Se ocupă de transformarea textului în arbore de sintaxă.
`shell` | Conține cod specific funcționalității de shell, precum execuția codului, stocarea variabilelor.
`sre` | Codul din spatele expresiilor SRE.
`task` | Fiecare activitate pe care o duce shell-ul, fie ea execuția comenzilor, procesarea șirurilor de caractere, execuția blocurilor de cod, blocurilor `if`, `while`, `switch`, `match`, pipe-uri etc.
`tests` | Cod comun testelor.
`util` | Conține codul responsabil cu citirea propriu-zisă a liniilor de cod de la tastatură sau din fișier. Conține o interfață abstractă pentru asta.

## Ghid de instalare

* Instalați Rust folosind [`rustup`](https://rustup.rs/).
* Clonați repo-ul: `git clone https://git.sr.ht/~tudor/rwsh && cd rwsh`
* Compilați: `cargo build --release`
* Executați: `cargo run --release`

\clearpage
# Anexă - comenzi disponibile {#anexa}

## `p`

Afișează conținutul de la adresa _dot_. Nu acceptă parametri.

**Exemplu:** Afișează tot conținutul.

```
|> ,p
```

## `a`, `c`, `i` și `d`

Adaugă după, înlocuiește, inserează înaintea sau șterge conținutul _dot_.

**Exemple:**

```
|> 2a/O nouă linie\n/ # adaugă o nouă linie între liniile 2 și 3

|> /To Do/ c/Done/ # înlocuiește statutul unei notițe

|> 2i/O nouă linie\n/ # adaugă o nouă linie între liniile 1 și 2

echo "text de șters" |> /de șters/d |> ,p # afișează "text"
```

## `g` și `v`

Execută o comandă pe textul de la adresa _dot_ dacă respectivul text se
potrivește cu un regex.

`v` este opusul comenzii `g`: Execută comanda dacă textul **nu** se potrivește.

**Exemplu:**

Dacă primul cuvânt de la poziția _dot_ este "Tudor", afișează poziția.

Comenzile `g` și `v` sunt folosite în general împreună cu `x` și `y`, nu pe cont propriu.

```
|> /\b.+\b/ g/Tudor/ =

```

Înlocuiește toate aparițiile cuvântului "vi" cu "emacs". Dacă un cuvânt conține "vi", el nu va fi alterat. Alternativ, se poate folosi metacaracterul `\b` în regex.

```
|> ,x/vi/ v/.../ c/Emacs/
# echivalent cu
|> ,x/\bvi\b/ c/Emacs/
```

## `x` și `y`

Execută o comandă pentru fiecare subșir care se potrivește cu un regex în cadrul
textului de la adresa _dot_.

Comanda `y` este opusul comenzii `x`: execută comanda pe textul situat **între**
subșirurile care se potrivesc cu regex-ul, în cadrul textului de la adresa _dot_.

**Exemplu:**

Înlocuiește toate aparițiile lui "Tudor" cu "Ioan".

```
|> ,x/Tudor/ c/Ioan/
```

Înlocuiește toți identificatorii numiți `n` într-un cod sursă, cu `num`, fără să
atingă șirurile de caractere, aflate între ghilimele sau apostrofuri.

```
|> ,y/".*"|'.*'/ x/\bn\b/ c/num/
```

## `=`

Afișează poziția adresei _dot_ în caractere de la începutul fișierului.

**Exemplu:**

Va afișa `#8,#13`.

```
echo "eu sunt Tudor" |> /Tudor/=
```

\clearpage
## Adrese

Extras din manualul editorului `sam(1)`:

## Addresses

An address identifies a substring in a file.  In the following, 'character n' means the null string after the n-th
character in the file, with 1 the first character in the file. 'Line n' means the n-th match, starting at the beginning of the file, of the regular expression `.*\n?`.
All files always have a current substring, called dot, that is the default address.

### Simple Addresses

`#n` The empty string after character n; `#0` is the beginning of the file.

`n` Line n; 0 is the beginning of the file.

`/regexp/`; `?regexp?` The substring that matches the regular expression,
found by looking toward the end (/) or beginning (?)
of the file, and if necessary continuing the search
from the other end to the starting point of the search.
The matched substring may straddle the starting point.
When entering a pattern containing a literal question
mark for a backward search, the question mark should be
specified as a member of a class.

`0` The string before the first full line.

`$` The null string at the end of the file.

`.`    Dot.

### Compound Addresses

In the following, `a1` and `a2` are addresses.

`a1+a2` The address `a2` evaluated starting at the end of `a1`.
`a1-a2` The address `a2` evaluated looking in the reverse direction starting at the beginning of `a1`.
`a1,a2` The substring from the beginning of `a1` to the end of `a2`. If `a1` is missing, `0` is substituted. If `a2` is
missing, `$` is substituted.
`a1;a2` Like `a1`,`a2`, but with `a2` evaluated at the end of, and
dot set to, `a1`.

The operators `+` and `-` are high precedence, while `,` and `;` are
low precedence.

M infoeducatie/README.infoeducatie.pdf => infoeducatie/README.infoeducatie.pdf +0 -0