Jak POSIX-ly policzyć liczbę wierszy w zmiennej typu string?


10

Wiem, że mogę to zrobić w Bash:

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

Zasadniczo wszystko, co znalazłem, dotyczyło <<<operatora Bash.

Ale w powłoce POSIX <<<jest niezdefiniowany i od wielu godzin nie jestem w stanie znaleźć alternatywnego podejścia. Jestem pewien, że istnieje proste rozwiązanie tego problemu, ale niestety nie znalazłem do tej pory.

Odpowiedzi:


11

Prosta odpowiedź brzmi: wc -l <<< "${string_variable}"skrót dla ksh / bash / zsh printf "%s\n" "${string_variable}" | wc -l.

W rzeczywistości istnieją różnice w sposobie <<<i działaniu potoku: <<<tworzy plik tymczasowy, który jest przekazywany jako dane wejściowe do polecenia, podczas gdy |tworzy potok. W bash i pdksh / mksh (ale nie w ksh93 lub zsh) polecenie po prawej stronie potoku działa w podpowłoce. Ale różnice te nie mają znaczenia w tym konkretnym przypadku.

Zauważ, że pod względem liczenia linii zakłada się, że zmienna nie jest pusta i nie kończy się na nowej linii. Nie kończy się znakiem nowej linii, gdy zmienna jest wynikiem podstawienia polecenia, więc w większości przypadków otrzymasz poprawny wynik, ale dostaniesz 1 za pusty ciąg.

Istnieją dwie różnice między var=$(somecommand); wc -l <<<"$var"i somecommand | wc -l: użycie podstawienia polecenia i zmiennej tymczasowej usuwa puste linie na końcu, zapomina, czy ostatni wiersz wyniku zakończył się nową linią, czy nie (zawsze tak jest, jeśli polecenie wyświetla poprawny niepusty plik tekstowy) i przeliczy się o jeden, jeśli wynik jest pusty. Jeśli chcesz zachować wynik i policzyć wiersze, możesz to zrobić, dodając znany tekst i usuwając go na końcu:

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

1
@Inian Keeping wc -ljest dokładnie równoważne z oryginałem: <<<$foododaje nowy wiersz do wartości $foo(nawet jeśli $foobył pusty). W mojej odpowiedzi wyjaśniam, dlaczego być może nie tego chciałem, ale o to pytano.
Gilles 'SO - przestań być zły'

2

Niespełniającego shell Zabudowy, za pomocą narzędzi zewnętrznych jak grepi awkz opcji zgodnych z POSIX,

string_variable="one
two
three
four"

Robiąc z, grepaby dopasować początek linii

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

I z awk

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

Zauważ, że niektóre narzędzia GNU, szczególnie GNU grepnie respektuje POSIXLY_CORRECT=1opcji uruchomienia wersji POSIX narzędzia. W grepjedynej zachowanie wpływa ustawienie zmiennej będzie różnica w przetwarzaniu rzędu flagi linii poleceń. Z dokumentacji ( greppodręcznik GNU ) wynika, że ​​tak

POSIXLY_CORRECT

Jeśli jest ustawiony, grep zachowuje się tak, jak wymaga POSIX; inaczej grepzachowuje się bardziej jak inne programy GNU. POSIX wymaga, aby opcje następujące po nazwach plików były traktowane jak nazwy plików; domyślnie takie opcje są permutowane na początku listy argumentów i są traktowane jako opcje.

Zobacz Jak używać POSIXLY_CORRECT w grep?


2
Z pewnością wc -ljest tu nadal opłacalne?
Michael Homer,

@MichaelHomer: Z tego, co zaobserwowałem, wc -lpotrzebny jest prawidłowy strumień rozdzielany znakiem nowej linii (posiadający znak „\ n” na końcu, aby poprawnie liczyć). Nie można użyć prostego FIFO do użycia printf, np. printf '%s' "${string_variable}" | wc -lMoże nie działać zgodnie z oczekiwaniami, ale działałoby <<<to z powodu ciągnięcia \ndołączonego przez tustring
Inian

1
Tak się printf '%s\n'działo, zanim go wyjąłeś ...
Michael Homer,

1

Ciąg tutaj <<<jest w zasadzie wersją dokumentu w jednym wierszu <<. Ta pierwsza nie jest standardową funkcją, ale druga jest. Możesz także użyć <<w tym przypadku. Powinny być równoważne:

wc -l <<< "$somevar"

wc -l << EOF
$somevar
EOF

Należy jednak pamiętać, że oba dodają dodatkową linię na końcu $somevar, np. Drukuje 6, mimo że zmienna ma tylko pięć linii:

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

Dzięki printfmożesz zdecydować, czy chcesz dodać nowy wiersz, czy nie:

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

Ale wtedy, Należy pamiętać, że wctylko liczy kompletne linie (lub liczbę znaków nowej linii w ciągu). grep -c ^powinien także liczyć ostatni fragment linii.

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

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

(Oczywiście można również policzyć linie całkowicie w powłoce, używając ${var%...}rozszerzenia do usuwania ich pojedynczo w pętli ...)


0

W tych zaskakująco częstych przypadkach, w których to, co naprawdę musisz zrobić, jest przetworzenie wszystkich niepustych linii w zmiennej w pewien sposób (w tym ich zliczanie), możesz ustawić IFS tylko na nową linię, a następnie użyć mechanizmu dzielenia słów powłoki, aby przerwać niepuste linie od siebie.

Na przykład, oto mała funkcja powłoki, która sumuje niepuste linie we wszystkich dostarczonych argumentach:

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

Nawiasy, a nie nawiasy klamrowe, są tutaj używane do utworzenia polecenia złożonego dla treści funkcji. Powoduje to, że funkcja jest wykonywana w podpowłoce, dzięki czemu nie zanieczyszcza zewnętrznych zmiennych IFS i ustawienia rozwijania nazw ścieżek przy każdym wywołaniu.

Jeśli chcesz iterować po niepustych liniach, możesz to zrobić podobnie:

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

Manipulowanie IFS w ten sposób jest często pomijaną techniką, przydatną również do robienia takich rzeczy, jak parsowanie nazw ścieżek, które mogą zawierać spacje z kolumnowych danych rozdzielanych tabulatorami. Należy jednak pamiętać, że celowe usunięcie znaku spacji zwykle zawartego w domyślnym ustawieniu IFS spacji-tab-nowej linii może spowodować wyłączenie podziału słów w miejscach, w których normalnie można się tego spodziewać.

Na przykład, jeśli używasz zmiennych do zbudowania skomplikowanego wiersza poleceń dla czegoś podobnego ffmpeg, możesz chcieć uwzględnić -vf scale=$scaletylko wtedy, gdy zmienna scalejest ustawiona na coś niepustego. Zwykle można to osiągnąć za pomocą, ${scale:+-vf scale=$scale}ale jeśli IFS nie zawiera zwykłego znaku spacji w czasie, gdy rozszerzenie parametru jest wykonywane, odstęp między -vfi scale=nie będzie używany jako separator słów i ffmpegzostanie przekazany -vf scale=$scalejako pojedynczy argument, którego nie zrozumie.

Aby ustalić, że ci, że albo trzeba się upewnić, IFS postawiono bardziej zwykle przed wykonaniem ${scale}ekspansji, czy dwa rozszerzenia: ${scale:+-vf} ${scale:+scale=$scale}. Słowo dzielenie, które powłoka wykonuje podczas wstępnego analizowania wierszy poleceń, w przeciwieństwie do dzielenia, które wykonuje podczas fazy ekspansji przetwarzania tych wierszy poleceń, nie zależy od IFS.

Coś innego, co może być warte twojego czasu, jeśli masz zamiar to zrobić, to utworzenie dwóch globalnych zmiennych powłoki, które będą zawierać tylko tabulator i tylko nową linię:

t=' '
n='
'

W ten sposób możesz po prostu włączać $ti $nrozszerzać, gdzie potrzebujesz tabulatorów i znaków nowej linii, zamiast zaśmiecać cały kod cytowanym białym znakiem. Jeśli wolisz całkowicie unikać cytowanych białych znaków w powłoce POSIX, która nie ma do tego żadnego innego mechanizmu, printfmożesz pomóc, choć potrzebujesz trochę błahostki, aby obejść usuwanie końcowych znaków nowej linii w rozszerzeniach poleceń:

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

Czasami ustawienie IFS tak, jakby była zmienną środowiskową na polecenie, działa dobrze. Na przykład, oto pętla, która odczytuje nazwę ścieżki, która może zawierać spacje i współczynnik skalowania z każdej linii pliku wejściowego rozdzielanego tabulatorami:

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

W tym przypadku readwbudowany widzi IFS ustawiony tylko na tabulator, więc nie podzieli linii wejściowej, którą odczytuje również na spacje. Ale IFS=$t set -- $lines nie działa: powłoka rozwija się, $linesgdy buduje setargumenty wbudowane przed wykonaniem polecenia, więc tymczasowe ustawienie IFS w sposób, który ma zastosowanie tylko podczas wykonywania samego wbudowanego, przychodzi za późno. Właśnie dlatego fragmenty kodu, które podałem, przede wszystkim ustawiają IFS w osobnym kroku i dlatego muszą zająć się kwestią jego zachowania.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.