In che modo POSIX può contare il numero di righe in una variabile stringa?


10

So di poterlo fare in Bash:

wc -l <<< "${string_variable}"

Fondamentalmente, tutto quello che ho trovato riguardava l' <<<operatore Bash.

Ma nella shell POSIX, <<<non è definito e non sono riuscito a trovare un approccio alternativo per ore. Sono abbastanza sicuro che ci sia una soluzione semplice a questo, ma sfortunatamente non l'ho trovata finora.

Risposte:


11

La semplice risposta è che wc -l <<< "${string_variable}"è una scorciatoia per ksh / bash / zsh printf "%s\n" "${string_variable}" | wc -l.

In realtà ci sono differenze nel modo in cui <<<una pipe funziona: <<<crea un file temporaneo che viene passato come input al comando, mentre |crea una pipe. In bash e pdksh / mksh (ma non in ksh93 o zsh), il comando sul lato destro della pipe viene eseguito in una subshell. Ma queste differenze non contano in questo caso particolare.

Si noti che in termini di conteggio delle righe, ciò presuppone che la variabile non sia vuota e non termini con una nuova riga. Non terminare con una nuova riga è il caso quando la variabile è il risultato di una sostituzione di comando, quindi nella maggior parte dei casi otterrai il risultato giusto, ma otterrai 1 per la stringa vuota.

Esistono due differenze tra var=$(somecommand); wc -l <<<"$var"e somecommand | wc -l: l'utilizzo di una sostituzione comando e una variabile temporanea rimuove le righe vuote alla fine, dimentica se l'ultima riga di output è terminata in una nuova riga o meno (lo fa sempre se il comando genera un file di testo non vuoto valido) e supera di uno se l'output è vuoto. Se vuoi preservare sia il risultato sia le linee di conteggio, puoi farlo aggiungendo del testo noto e rimuovendolo alla fine:

output=$(somecommand; echo .)
line_count=$(($(printf "%s\n" "$output" | wc -l) - 1))
printf "The exact output is:\n%s" "${output%.}"

1
@Inian Keeping wc -lè esattamente equivalente all'originale: <<<$fooaggiunge una nuova riga al valore di $foo(anche se $fooera vuoto). Spiego nella mia risposta perché questo potrebbe non essere stato ciò che si voleva, ma è ciò che è stato chiesto.
Gilles 'SO- smetti di essere malvagio' il

2

Non conforme ai built-in della shell, utilizzando utility esterne come grepe awkcon opzioni conformi a POSIX,

string_variable="one
two
three
four"

Fare con grepper abbinare l'inizio delle linee

printf '%s' "${string_variable}" | grep -c '^'
4

E con awk

printf '%s' "${string_variable}" | awk 'BEGIN { count=0 } NF { count++ } END { print count }'

Si noti che alcuni degli strumenti GNU, in particolare, GNU grepnon rispettano l' POSIXLY_CORRECT=1opzione per eseguire la versione POSIX dello strumento. Nell'unico grepcomportamento interessato dall'impostazione della variabile sarà la differenza nell'elaborazione dell'ordine dei flag della riga di comando. Dalla documentazione ( grepmanuale GNU ), sembra che

POSIXLY_CORRECT

Se impostato, grep si comporta come richiede POSIX; altrimenti, grepsi comporta più come altri programmi GNU. POSIX richiede che le opzioni che seguono i nomi dei file debbano essere trattate come nomi di file; per impostazione predefinita, tali opzioni sono consentite in primo piano nell'elenco degli operandi e sono trattate come opzioni.

Vedi Come utilizzare POSIXLY_CORRECT in grep?


2
Sicuramente wc -lè ancora praticabile qui?
Michael Homer,

@MichaelHomer: da quello che ho osservato, ha wc -lbisogno di un flusso delimitato da una nuova riga (con un finale '\ n` alla fine per contare correttamente). Non si può usare un semplice FIFO con cui usare printf, ad esempio printf '%s' "${string_variable}" | wc -lpotrebbe non funzionare come previsto, ma a <<<causa del trascinamento \naggiunto dall'eruzione
Inian,

1
Era quello che printf '%s\n'stava facendo, prima che tu lo tirassi fuori ...
Michael Homer,

1

La stringa qui <<<è praticamente una versione di una riga del documento qui <<. Il primo non è una funzionalità standard, ma il secondo lo è. Puoi usare <<anche in questo caso. Questi dovrebbero essere equivalenti:

wc -l <<< "$somevar"

wc -l << EOF
$somevar
EOF

Tuttavia, nota che entrambi aggiungono una nuova riga in più alla fine di $somevar, ad esempio questa stampa 6, anche se la variabile ha solo cinque righe:

s=$'foo\n\n\nbar\n\n'
wc -l <<< "$s"

Con printf, puoi decidere se vuoi la nuova riga aggiuntiva o meno:

printf "%s\n" "$s" | wc -l         # 6
printf "%s"   "$s" | wc -l         # 5

Tuttavia, tieni presente che wcconta solo le righe complete (o il numero di caratteri di nuova riga nella stringa). grep -c ^dovrebbe anche contare il frammento di riga finale.

s='foo'
printf "%s" "$s" | wc -l           # 0 !

printf "%s" "$s" | grep -c ^       # 1

(Naturalmente puoi anche contare le linee interamente nella shell usando l' ${var%...}espansione per rimuoverle una alla volta in un ciclo ...)


0

Nei casi sorprendentemente frequenti in cui ciò che è effettivamente necessario fare è in qualche modo elaborare tutte le linee non vuote all'interno di una variabile (incluso il conteggio), è possibile impostare IFS su una nuova riga e quindi utilizzare il meccanismo di suddivisione delle parole della shell per interrompere le linee non vuote a parte.

Ad esempio, ecco una piccola funzione di shell che somma le linee non vuote all'interno di tutti gli argomenti forniti:

lines() (
IFS='
'
set -f #disable pathname expansion
set -- $*
echo $#
)

Le parentesi, anziché le parentesi graffe, vengono utilizzate qui per formare il comando composto per il corpo della funzione. Questo fa sì che la funzione venga eseguita in una subshell in modo da non inquinare la variabile IFS del mondo esterno e l'impostazione di espansione del percorso su ogni chiamata.

Se vuoi iterare su righe non vuote puoi farlo in modo simile:

IFS='
'
set -f
for line in $lines
do
    printf '[%s]\n' $line
done

Manipolare IFS in questo modo è una tecnica spesso trascurata, utile anche per fare cose come l'analisi di nomi di percorsi che potrebbero contenere spazi dall'input colonnare delimitato da tabulazioni. Tuttavia, è necessario essere consapevoli del fatto che la rimozione deliberata del carattere spazio solitamente incluso nell'impostazione predefinita di IFS di spazio-tab-newline può finire per disabilitare la divisione delle parole in luoghi in cui normalmente ci si aspetterebbe di vederlo.

Ad esempio, se stai usando le variabili per creare una riga di comando complicata per qualcosa del genere ffmpeg, potresti voler includere -vf scale=$scalesolo quando la variabile scaleè impostata su qualcosa di non vuoto. Normalmente puoi farlo con ${scale:+-vf scale=$scale}ma se IFS non include il suo solito carattere di spazio al momento dell'espansione di questo parametro, lo spazio tra -vfe scale=non verrà utilizzato come separatore di parole e ffmpegverrà passato tutto -vf scale=$scalecome un singolo argomento, che non capirà.

Per rimediare, faresti sia necessario assicurarsi IFS è stato set più normalmente prima di fare l' ${scale}espansione, o fare due espansioni: ${scale:+-vf} ${scale:+scale=$scale}. La suddivisione delle parole che la shell esegue nel processo di analisi iniziale delle righe di comando, al contrario della divisione che esegue durante la fase di espansione dell'elaborazione di tali righe di comando, non dipende dall'IFS.

Qualcos'altro che potrebbe valere la pena se stai per fare questo tipo di cose sarebbe la creazione di due variabili di shell globali per contenere solo una scheda e solo una nuova riga:

t=' '
n='
'

In questo modo puoi semplicemente includere $te $nin espansioni in cui hai bisogno di schede e newline, piuttosto che sporcare tutto il tuo codice con spazi bianchi tra virgolette. Se preferisci evitare gli spazi bianchi citati del tutto in una shell POSIX che non ha altri meccanismi per farlo, printfpuò aiutarti anche se hai bisogno di un po 'di armeggiare per aggirare la rimozione di nuove righe finali nelle espansioni di comandi:

nt=$(printf '\n\t')
n=${nt%?}
t=${nt#?}

A volte l'impostazione di IFS come se fosse una variabile d'ambiente per comando funziona bene. Ad esempio, ecco un ciclo che legge un percorso che può contenere spazi e un fattore di ridimensionamento da ciascuna riga di un file di input delimitato da tabulazioni:

while IFS=$t read -r path scale
do
    ffmpeg -i "$path" ${scale:+-vf scale=$scale} "${path%.*}.out.mkv"
done <recode-queue.txt

In questo caso il readbuiltin vede IFS impostato su solo una scheda, quindi non dividerà anche la linea di input che legge sugli spazi. Ma IFS=$t set -- $lines non funziona: la shell si espande $linesman mano che costruisce gli setargomenti del builtin prima di eseguire il comando, quindi l'impostazione temporanea di IFS in un modo che si applica solo durante l'esecuzione del builtin stesso arriva troppo tardi. Questo è il motivo per cui i frammenti di codice che ho fornito hanno soprattutto impostato IFS in un passaggio separato e perché devono affrontare il problema di preservarlo.

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.