Come sostituire i segnaposto $ {} in un file di testo?


164

Voglio convogliare l'output di un file "template" in MySQL, il file ha variabili come ${dbName}intervallate. Qual è l'utilità della riga di comando per sostituire queste istanze e scaricare l'output sullo standard output?

Risposte:


192

Sed !

Dato template.txt:

Il numero è $ {i}
La parola è $ {parola}

non ci resta che dire:

sed -e "s/\${i}/1/" -e "s/\${word}/dog/" template.txt

Grazie a Jonathan Leffler per la segnalazione di passare più -eargomenti alla stessa sedinvocazione.


13
È possibile combinare questi due comandi sed in uno: sed -e "s / \ $ {i} / 1 /" -e "s / \ $ {word} / dog /"; questo è più efficiente. È possibile riscontrare problemi con alcune versioni di sed in circa 100 di tali operazioni (problema di anni fa - potrebbe non essere ancora vero, ma attenzione a HP-UX).
Jonathan Leffler,

1
Grazie Jonathan, esattamente quello che stavo cercando.
Dana the Sane,

3
Piccolo suggerimento: se "1" o "cane" nell'esempio dato conterrà un simbolo di dollaro, dovresti fuggire con una barra rovesciata (altrimenti non si verifica la sostituzione).
Matthieu,

9
Inoltre non è necessario il cat. Tutto ciò che serve è sed -e "s/\${i}/1/" -e "s/\${word}/dog/" template.text.
HardlyKnowEm

3
Cosa succede se il testo sostitutivo è una password? In questo caso, sedci si aspetta un testo di escape, che è una seccatura.
jpbochi,

179

Aggiornare

Ecco una soluzione di yottatsa su una domanda simile che sostituisce solo variabili come $ VAR o $ {VAR} ed è una breve riga

i=32 word=foo envsubst < template.txt

Naturalmente se io e la parola siamo nel tuo ambiente, allora è giusto

envsubst < template.txt

Sul mio Mac sembra che è stato installato come parte di gettext e da MacGPG2

Vecchia risposta

Ecco un miglioramento alla soluzione di Mogsie su una domanda simile, la mia soluzione non richiede che tu faccia scalare le doppie virgolette, lo fa Mogsie, ma la sua è una fodera!

eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null

Il potere di queste due soluzioni è che si ottengono solo alcuni tipi di espansioni della shell che non si verificano normalmente $ ((...)), `...` e $ (...), sebbene la barra rovesciata sia un carattere di escape qui, ma non devi preoccuparti che l'analisi abbia un bug e fa più righe bene.


6
Sto scoprendo che il nudo envsubstnon funziona se i tuoi dintorni non vengono esportati.
Toddius Zho,

4
@ToddiusZho: Non esiste una variabile d'ambiente che non venga esportata - è proprio l'esportazione che rende una variabile shell una variabile d'ambiente. envsubst, come suggerisce il nome, riconosce solo le variabili di ambiente , non le variabili di shell . Vale anche la pena notare che si envsubsttratta di un'utilità GNU e quindi non preinstallata o disponibile su tutte le piattaforme.
mklement0

2
Forse un altro modo di dire è che envsubst vede solo le proprie variabili di ambiente di processo, quindi le variabili di shell "normali" che potresti aver definito in precedenza (su righe separate) non sono ereditate da processi figlio se non le "esporti". Nel mio esempio di utilizzo di gettext qui sopra, sto modificando l'ambiente ereditato di gettext tramite un meccanismo bash aggiungendo il prefisso al comando che sto per eseguire
plockc

1
Ho una stringa con $ HOME, ho scoperto che $ HOME funziona come shell predefinita, invece $ HOME come mio / home / zw963, ma sembra che non supporti la sostituzione $ (cat / etc / hostname), quindi non completa la mia richiesta.
zw963,

3
Grazie per la "Vecchia risposta", in quanto non solo consente le variabili, ma anche una shell comanda $ (ls -l)
Alek,

46

Usa /bin/sh. Creare uno script di shell piccolo che imposta le variabili, quindi analizzare il modello utilizzando la shell stessa. In questo modo (modifica per gestire correttamente le nuove righe):

File template.txt:

the number is ${i}
the word is ${word}

File script.sh:

#!/bin/sh

#Set variables
i=1
word="dog"

#Read in template one line at the time, and replace variables (more
#natural (and efficient) way, thanks to Jonathan Leffler).
while read line
do
    eval echo "$line"
done < "./template.txt"

Produzione:

#sh script.sh
the number is 1
the word is dog

2
Perché non solo: mentre leggi la riga; do eval echo "$ line"; done <./template.txt ??? Non è necessario leggere l'intero file in memoria, solo per sputarlo su una riga alla volta tramite un uso intensivo di testa e coda. Ma "eval" è OK, a meno che il modello non contenga caratteri shell come virgolette.
Jonathan Leffler,

16
Questo è molto pericoloso! bashVerrà eseguito tutto il comando nell'input. Se il modello è: "le parole sono; rm -rf $ HOME" perderai i file.
rzymek,

1
@rzymek - ricorda, vuole reindirizzare questo file direttamente al database. A quanto pare, l'input è attendibile.
Gnud,

4
@gnud C'è una differenza tra fidarsi di un file abbastanza per archiviarne il contenuto e fidarsi di esso abbastanza per eseguire tutto ciò che contiene.
Segna il

3
Per notare i vincoli: (a) le virgolette doppie nell'input vengono scartate silenziosamente, (b) il readcomando, come scritto, taglia gli spazi bianchi iniziali e finali da ogni riga e 'mangia' \ caratteri., (C) usa questo solo se hai completamente fidarsi o controllare l'input, poiché le sostituzioni di comandi ( `…` o $(…)) incorporate nell'input consentono l'esecuzione di comandi arbitrari dovuti all'uso di eval. Infine, c'è una piccola possibilità che echoconfonde l'inizio di una riga per una delle sue opzioni della riga di comando.
mklement0

23

Ci stavo ripensando, visto il recente interesse, e penso che lo strumento a cui stavo pensando originariamente fosse m4il macro processore per autotools. Quindi, invece della variabile che ho specificato in origine, useresti:

$echo 'I am a DBNAME' | m4 -DDBNAME="database name"

1
Questa soluzione presenta il minor numero di inconvenienti delle risposte qui. Conoscete qualche modo per sostituire $ {DBNAME} invece di solo DBNAME?
Jack Davidson,

@JackDavidson Vorrei utilizzare envsubstper questo semplice uso di sostituzione / templating variabile, come indicato in altre risposte. m4è un ottimo strumento, ma è un preprocessore completo con molte più funzioni e quindi complessità che potrebbe non essere necessaria se si desidera semplicemente sostituire alcune variabili.
imiric,

13

Template.txt

Variable 1 value: ${var1}
Variable 2 value: ${var2}

data.sh

#!/usr/bin/env bash
declare var1="value 1"
declare var2="value 2"

parser.sh

#!/usr/bin/env bash

# args
declare file_data=$1
declare file_input=$2
declare file_output=$3

source $file_data
eval "echo \"$(< $file_input)\"" > $file_output

./parser.sh data.sh template.txt parsed_file.txt

parsed_file.txt

Variable 1 value: value 1
Variable 2 value: value 2

1
Come è stato notato altrove: utilizzalo solo se ti fidi o controlli completamente dell'input, poiché le sostituzioni di comandi ( `…` o $(…)) incorporate nell'input consentono l'esecuzione di comandi arbitrari dovuti all'uso evale l'esecuzione diretta del codice shell dovuto all'uso source. Inoltre, le doppie virgolette nell'input vengono scartate silenziosamente e echopotrebbero confondere l'inizio di una riga per una delle sue opzioni della riga di comando.
mklement0

Sfortunatamente, questo rimuove tutte le doppie virgolette (") dal file dei risultati. C'è un modo per fare lo stesso senza rimuovere le doppie virgolette?
Ivaylo Slavov

Ho trovato quello che cercavo qui: stackoverflow.com/a/11050943/795158 ; Ho usato envsubst. La differenza è che i vari devono essere esportati, il che andava bene per me.
Ivaylo Slavov

se il file di testo contiene "` "o". " , la sostituzione fallirà.
shuiqiang,

12

Ecco una robusta funzione Bash che, nonostante l'utilizzo eval, dovrebbe essere sicura da usare.

Tutti ${varName}i riferimenti alle variabili nel testo di input vengono espansi in base alle variabili della shell chiamante.

Nient'altro viene espanso: né riferimenti a variabili i cui nomi non sono racchiusi in {...}(come $varName), né sostituzioni di comandi ( $(...)e sintassi legacy `...`), né sostituzioni aritmetiche ( $((...))e sintassi legacy $[...]).

Per trattare un $come un letterale, \-scapparlo; per esempio:\${HOME}

Si noti che l'input è accettato solo tramite stdin .

Esempio:

$ expandVarsStrict <<<'$HOME is "${HOME}"; `date` and \$(ls)' # only ${HOME} is expanded
$HOME is "/Users/jdoe"; `date` and $(ls)

Codice sorgente funzione:

expandVarsStrict(){
  local line lineEscaped
  while IFS= read -r line || [[ -n $line ]]; do  # the `||` clause ensures that the last line is read even if it doesn't end with \n
    # Escape ALL chars. that could trigger an expansion..
    IFS= read -r -d '' lineEscaped < <(printf %s "$line" | tr '`([$' '\1\2\3\4')
    # ... then selectively reenable ${ references
    lineEscaped=${lineEscaped//$'\4'{/\${}
    # Finally, escape embedded double quotes to preserve them.
    lineEscaped=${lineEscaped//\"/\\\"}
    eval "printf '%s\n' \"$lineEscaped\"" | tr '\1\2\3\4' '`([$'
  done
}

La funzione assume che nessuna 0x1, 0x2, 0x3, e 0x4caratteri di controllo sono presenti in ingresso, perché tali caratteri. sono usati internamente - poiché la funzione elabora il testo , questo dovrebbe essere un presupposto sicuro.


2
Questa è una delle migliori risposte qui. Anche con l'uso evalè abbastanza sicuro da usare.
Anubhava,

1
Questa soluzione funziona con file JSON! (scappando "correttamente!)
WBAR,

2
Un aspetto positivo di questa soluzione è che ti consente di fornire i valori predefiniti per le variabili mancanti ${FOO:-bar}o di generare qualcosa solo se è impostato - ${HOME+Home is ${HOME}}. Sospetto che con una piccola estensione potrebbe anche restituire codici di uscita per variabili mancanti, ${FOO?Foo is missing}ma al momento tldp.org/LDP/abs/html/parameter-substitution.html ha un elenco di questi se ciò aiuta
Stuart Moore

11

Creare rendertemplate.sh:

#!/usr/bin/env bash

eval "echo \"$(cat $1)\""

E template.tmpl:

Hello, ${WORLD}
Goodbye, ${CHEESE}

Rendering del modello:

$ export WORLD=Foo
$ CHEESE=Bar ./rendertemplate.sh template.tmpl 
Hello, Foo
Goodbye, Bar

2
Questo elimina le stringhe tra virgolette doppie
vrtx54234

Ho provato: eval "echo $ (cat $ 1)" - senza virgolette, e ha funzionato per me.
access_granted

2
Dal punto di vista della sicurezza, questa è una cattiva notizia. Se il tuo modello contiene $(rm -rf ~), lo stai eseguendo come codice.
Charles Duffy,

eval "echo \"$(cat $1)\"" Funziona alla grande !
dev devv

10

ecco la mia soluzione con perl basata sull'ex risposta, sostituisce le variabili d'ambiente:

perl -p -e 's/\$\{(\w+)\}/(exists $ENV{$1}?$ENV{$1}:"missing variable $1")/eg' < infile > outfile

2
Questo è fantastico Non sempre avere il perl, ma quando lo fai, questo è semplice e diretto.
Aaron McMillin,

5

Se sei aperto all'utilizzo di Perl , questo sarebbe il mio suggerimento. Anche se probabilmente ci sono alcuni esperti di sed e / o AWK che probabilmente sanno come farlo molto più facilmente. Se hai una mappatura più complessa con più di un semplice dbName per i tuoi sostituti potresti estenderlo abbastanza facilmente, ma potresti anche metterlo in uno script Perl standard a quel punto.

perl -p -e 's/\$\{dbName\}/testdb/s' yourfile | mysql

Un breve script Perl per fare qualcosa di leggermente più complicato (gestire più chiavi):

#!/usr/bin/env perl
my %replace = ( 'dbName' => 'testdb', 'somethingElse' => 'fooBar' );
undef $/;
my $buf = <STDIN>;
$buf =~ s/\$\{$_\}/$replace{$_}/g for keys %replace;
print $buf;

Se si nomina lo script sopra come sostituire-script, potrebbe essere utilizzato come segue:

replace-script < yourfile | mysql

1
Funziona con variabili singole, ma come posso includere "o" per gli altri?
Dana the Sane,

2
Ci sono molti modi in cui puoi farlo con perl, tutti a seconda di quanto tu abbia voluto farlo complicato e / o sicuro. Esempi più complicati sono disponibili qui: perlmonks.org/?node_id=718936
Beau Simensen,

3
L'uso di perl è molto più pulito rispetto al tentativo di utilizzare la shell. Dedica del tempo a farlo funzionare piuttosto che provare alcune delle altre soluzioni basate su shell menzionate.
jdigital,

1
Di recente ha dovuto affrontare un problema simile. Alla fine sono andato con Perl (tutto sembrava promettente per un po ', ma era troppo difficile da controllare).
sfugge il

5

Ecco un modo per fare in modo che la shell esegua la sostituzione, come se il contenuto del file fosse invece digitato tra virgolette doppie.

Utilizzando l'esempio di template.txt con i contenuti:

The number is ${i}
The word is ${word}

La riga seguente farà interpolare la shell del contenuto di template.txt e scriverà il risultato su standard out.

i='1' word='dog' sh -c 'echo "'"$(cat template.txt)"'"'

Spiegazione:

  • i e word vengono passati come variabili di ambiente ignorate nell'esecuzione di sh.
  • sh esegue il contenuto della stringa che viene passata.
  • Le stringhe scritte una accanto all'altra diventano una stringa, quella stringa è:
    • ' echo "' + " $(cat template.txt)" + ' "'
  • Poiché la sostituzione è tra "", $(cat template.txt)" diventa l'output dicat template.txt .
  • Quindi il comando eseguito da sh -c diventa:
    • echo "The number is ${i}\nThe word is ${word}",
    • dove ie wordsono le variabili di ambiente specificate.

Dal punto di vista della sicurezza, questa è una cattiva notizia. Se il tuo modello contiene, diciamo, '$(rm -rf ~)'$(rm -rf ~)le virgolette letterali nel file modello corrisponderanno a quelle che hai aggiunto prima della sua espansione.
Charles Duffy,

Non credo che le virgolette all'interno del modello corrispondano alle virgolette fuori dal modello, credo che la shell stia risolvendo il modello e la stringa in-terminal in modo indipendente (rimuovendo efficacemente le virgolette), quindi concatenandole. Una versione del test che non cancella la tua home directory è '$(echo a)'$(echo a). Produce 'a'a. La cosa principale che sta accadendo è che viene valutato il primo echo aall'interno di ', il che potrebbe non essere quello che ti aspetti dal momento che è in ', ma è lo stesso comportamento di includere 'in una "stringa tra virgolette.
Apriori,

Pertanto, ciò non è sicuro nel senso che consente all'autore del modello di eseguire il proprio codice. Tuttavia, il modo in cui vengono valutate le virgolette non influisce sulla sicurezza. Espandere qualsiasi cosa è una "stringa quotata (incluso $(...)) è il punto.
Apriori,

È questo il punto? Vedo solo che chiedono ${varname}, non altre, espansioni a rischio di sicurezza più elevato.
Charles Duffy,

... Detto questo, devo essere diverso (ri: le citazioni in-template e out-template sono in grado di corrispondere). Quando inserisci una virgoletta singola nella stringa, ti dividi in una stringa a virgoletta singola echo ", seguita da una stringa a virgolette doppie con i contetn letterali di template.txt, seguita da un'altra stringa letterale ", tutte concatenate in un singolo argomento passato a sh -c. Hai ragione sul fatto che 'non è possibile abbinare (poiché è stato consumato dalla shell esterna anziché passata a quella interna), ma "certamente può, quindi un modello contenente Gotcha"; rm -rf ~; echo "potrebbe essere eseguito.
Charles Duffy,

4

file.tpl:

The following bash function should only replace ${var1} syntax and ignore 
other shell special chars such as `backticks` or $var2 or "double quotes". 
If I have missed anything - let me know.

script.sh:

template(){
    # usage: template file.tpl
    while read -r line ; do
            line=${line//\"/\\\"}
            line=${line//\`/\\\`}
            line=${line//\$/\\\$}
            line=${line//\\\${/\${}
            eval "echo \"$line\""; 
    done < ${1}
}

var1="*replaced*"
var2="*not replaced*"

template file.tpl > result.txt

2
Ciò non è sicuro poiché eseguirà sostituzioni di comandi nel modello se hanno una barra rovesciata principale, ad es.\$(date)
Peter Dolberg,

1
A parte il punto valido di Peter: ti suggerisco di usare while IFS= read -r line; docome readcomando, altrimenti spogli gli spazi bianchi iniziali e finali da ogni riga di input. Inoltre, echopotrebbe confondere l'inizio di una riga per una delle sue opzioni della riga di comando, quindi è meglio usarlo printf '%s\n'. Infine, è più sicuro fare una doppia citazione ${1}.
mklement0

4

Suggerirei di usare qualcosa come Sigil : https://github.com/gliderlabs/sigil

È compilato su un singolo binario, quindi è estremamente facile da installare sui sistemi.

Quindi puoi fare un semplice one-liner come il seguente:

cat my-file.conf.template | sigil -p $(env) > my-file.conf

Questo è molto più sicuro evale semplice dell'uso di regex osed


2
Bella risposta! È un sistema di templating adeguato e molto più facile da lavorare rispetto alle altre risposte.
Erfan,

A proposito, meglio evitare cate usare <my-file.conf.templateinvece in modo da dare sigilun vero handle di file invece di un FIFO.
Charles Duffy,

2

Ho trovato questa discussione mentre mi chiedevo la stessa cosa. Mi ha ispirato a questo (attento con i backtick)

$ echo $MYTEST
pass!
$ cat FILE
hello $MYTEST world
$ eval echo `cat FILE`
hello pass! world

4
Una scorciatoia per bash $(cat file)è$(< file)
glenn jackman,

3
Apparentemente questo metodo fa confusione con le interruzioni di riga, ovvero il mio file è stato ripetuto in una riga.
Arthur Corenzan,

@ArthurCorenzan: In effetti, le interruzioni di riga vengono sostituite con spazi. Per risolvere il problema, dovresti usarlo eval echo "\"$(cat FILE)\""ma potrebbe non essere all'altezza se le doppie virgolette nell'input vengono scartate.
mklement0

Come è stato notato altrove: utilizzalo solo se ti fidi o controlli completamente dell'input, poiché le sostituzioni di comandi ( `…` o $(…)) incorporate nell'input consentono l'esecuzione di comandi arbitrari dovuti all'uso eval.
mklement0

2

Molte scelte qui, ma ho pensato che avrei gettato il mio sul mucchio. È basato su perl, ha come target solo le variabili del modulo $ {...}, accetta il file da elaborare come argomento e genera il file convertito su stdout:

use Env;
Env::import();

while(<>) { $_ =~ s/(\${\w+})/$1/eeg; $text .= $_; }

print "$text";

Ovviamente non sono davvero una persona perversa, quindi potrebbe esserci facilmente un difetto fatale (funziona per me però).


1
Funziona bene. È possibile eliminare la Env::import();riga: l'importazione è implicita da use. Inoltre, suggerisco di non creare prima l'intero output in memoria: basta usare print;invece che $text .= $_;all'interno del loop e rilasciare il printcomando post-loop .
mklement0

1

Può essere fatto in bash se hai il controllo del formato del file di configurazione. Devi solo procurarti (".") Il file di configurazione piuttosto che eseguirne la subshell. Ciò garantisce che le variabili vengano create nel contesto della shell corrente (e continuino ad esistere) piuttosto che nella subshell (dove la variabile scompare quando la subshell esce).

$ cat config.data
    export parm_jdbc=jdbc:db2://box7.co.uk:5000/INSTA
    export parm_user=pax
    export parm_pwd=never_you_mind

$ cat go.bash
    . config.data
    echo "JDBC string is " $parm_jdbc
    echo "Username is    " $parm_user
    echo "Password is    " $parm_pwd

$ bash go.bash
    JDBC string is  jdbc:db2://box7.co.uk:5000/INSTA
    Username is     pax
    Password is     never_you_mind

Se il tuo file di configurazione non può essere uno script di shell, puoi semplicemente 'compilarlo' prima di eseguirlo (la compilazione dipende dal tuo formato di input).

$ cat config.data
    parm_jdbc=jdbc:db2://box7.co.uk:5000/INSTA # JDBC URL
    parm_user=pax                              # user name
    parm_pwd=never_you_mind                    # password

$ cat go.bash
    cat config.data
        | sed 's/#.*$//'
        | sed 's/[ \t]*$//'
        | sed 's/^[ \t]*//'
        | grep -v '^$'
        | sed 's/^/export '
        >config.data-compiled
    . config.data-compiled
    echo "JDBC string is " $parm_jdbc
    echo "Username is    " $parm_user
    echo "Password is    " $parm_pwd

$ bash go.bash
    JDBC string is  jdbc:db2://box7.co.uk:5000/INSTA
    Username is     pax
    Password is     never_you_mind

Nel tuo caso specifico, potresti usare qualcosa come:

$ cat config.data
    export p_p1=val1
    export p_p2=val2
$ cat go.bash
    . ./config.data
    echo "select * from dbtable where p1 = '$p_p1' and p2 like '$p_p2%' order by p1"
$ bash go.bash
    select * from dbtable where p1 = 'val1' and p2 like 'val2%' order by p1

Quindi convoglia l'output di go.bash in MySQL e voilà, speriamo che non distruggerai il tuo database :-).


1
Non è necessario esportare le variabili dal file config.data; è sufficiente impostarli. Inoltre, non sembra che tu stia leggendo il file modello in nessun momento. O, forse, il file modello è modificato e contiene le operazioni 'echo' ... o mi sto perdendo qualcosa?
Jonathan Leffler,

1
Un buon punto sulle esportazioni, lo faccio di default in modo che siano disponibili per le sottotitoli e non causi danni poiché muoiono quando escono. Il file 'template' è lo script stesso con le sue dichiarazioni echo. Non è necessario introdurre un terzo file: è fondamentalmente un'operazione di tipo mailmerge.
paxdiablo,

1
Lo "script stesso con le sue dichiarazioni di eco" non è un modello: è uno script. Pensa alla differenza di leggibilità (e manutenibilità) tra <xml type = "$ TYPE"> e echo '<xml type = "' $ TYPE '">'
Pierre-Olivier Vares

1
@Pierre, non ci sono dichiarazioni di eco nel mio script di configurazione, sono solo esportazioni, e ho mostrato come puoi evitarlo anche con un minimo di pre-elaborazione. Se stai parlando dell'istruzione echo nei miei altri script (come go.bash), hai la parte sbagliata dello stick - non fanno parte della soluzione, sono solo un modo per mostrare che le variabili vengono impostato correttamente.
paxdiablo,

1
@paxdiablo: sembra che tu abbia dimenticato la domanda: << Voglio reindirizzare l'output di un file "template" in MySQL >>. Quindi l'uso di un modello È la domanda, non è "la parte sbagliata del bastone". Esportazione di variabili e li eco in un altro script solo non risponde alla domanda a tutti
Pierre-Olivier Vares

0

Modifica sul posto perl di file potenzialmente multipli, con backup.

  perl -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : ""/eg' \
    -i.orig \
    -p config/test/*

0

Avrai bisogno di qualcosa di più robusto dei suggerimenti attuali perché mentre funzionano per il tuo (per ora) caso d'uso limitato, non saranno sufficienti per situazioni più complesse.

Hai bisogno di un renderer migliore. Hai bisogno del miglior renderer. Hai bisogno di The Renderest!

Dato template.txt:

Ciao, {{person}}!

Correre:

$ person = Bob ./render template.txt

E vedrai l'output

Ciao Bob!

Scrivilo su un file reindirizzando stdout su un file:

$ person = Bob ./render template.txt> rendering.txt

E se ti capita di eseguire il rendering di uno script con $ {} variabili che non vuoi interpolare, The Renderest ti copre senza che tu debba fare altro!

Continua e ottieni la tua copia su https://github.com/relaxdiego/renderest

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.