Modo elegante per costruire una pipeline in base al valore restituito e non al codice di uscita?


8

Quando il codice di stato è inutile, c'è comunque modo di costruire una pipeline basata sull'output di stdout?

Preferirei che la risposta non riguardasse il caso d'uso ma la domanda nell'ambito della shell scripting. Quello che sto cercando di fare è trovare il pacchetto più specifico disponibile nel repository indovinando il nome in base ai codici di paese e lingua.

Prendi ad esempio questo,

  • $PACKAGE1=hunspell-en-zz
  • $PACKAGE2=hunspell-en

La prima ipotesi è più appropriata ma potrebbe non esistere. In questo caso, voglio return hunspell-en( $PACKAGE2) perché la prima opzione hunspell-en-zz( $PACKAGE1) non esiste.

pipeline di apt-cache

Il comando apt-cacherestituisce esito positivo (che è definito dalla shell come codice di uscita zero) ogni volta che il comando è in grado di essere eseguito (dai documenti di apt-cache)

apt-cache restituisce zero durante il normale funzionamento, 100 decimale in caso di errore.

Ciò rende più difficile l'utilizzo del comando in una pipeline. Normalmente, mi aspetto che l'equivalente nella ricerca di pacchetti di un 404 provochi un errore (come accadrebbe con curlo wget). Voglio cercare per vedere se esiste un pacchetto, e in caso contrario ricorrere ad un altro pacchetto se esiste .

Questo non restituisce nulla, poiché il primo comando restituisce successo (quindi rhs nel ||non viene mai eseguito)

apt-cache search hunspell-en-zz || apt-cache search hunspell-en

apt-cache search con due argomenti

Questo non restituisce nulla, poiché apt-cachegli argomenti di AND,

apt-cache search hunspell-en-zz hunspell-en

Dai documenti di apt-cache

Argomenti separati possono essere usati per specificare più schemi di ricerca che sono uniti.

Quindi, poiché uno di questi argomenti chiaramente non esiste, questo non restituisce nulla.

La domanda

Qual è il linguaggio della shell per gestire convenzioni come quelle trovate in apt-cachecui il codice di ritorno è inutile per l'attività? E il successo è determinato solo dalla presenza di output su STDOUT?

Simile a

  • make find fail quando non è stato trovato nulla

    entrambi derivano dallo stesso problema. La risposta scelta menziona find -zquale soluzione tristemente non è applicabile qui ed è specifica del caso d'uso. Non vi è alcuna menzione di un linguaggio o costruzione di una pipeline senza utilizzare la terminazione null (non un'opzione attiva apt-cache)


Sei sicuro che hunspell-enesista? Ad ogni modo, puoi usare apt-cache policye grep per ^$PACKAGENAME:.
AlexP,

@AlexP questi sono solo esempi che hunspell-en non esiste perché fanno pacchetti con nomi di paesi, hunspell-aresiste e non ci sono pacchetti di nomi di paesi . Devo trovare il pacchetto più accurato per un determinato paese e lingua.
Evan Carroll,

2
findè proprio come apt-cachesotto questo aspetto: inutile codice di ritorno, il successo si basa sull'output.
Muru,

1
Sì, sono d'accordo che entrambi derivano dallo stesso problema. La risposta scelta menziona le menzioni -zche purtroppo non è una soluzione qui, quindi il problema specifico del caso d'uso non è applicabile. E non si fa menzione di un linguaggio o della costruzione di una pipeline senza usare la terminazione nulla (non un'opzione attiva apt-cache)
Evan Carroll,

1
@EvanCarroll la terminazione nulla è del tutto facoltativa. L'ho usato solo perché è il modo più sicuro per gestire i nomi dei file, quindi ci si aspetterebbe finddi essere usato -print0e così grep -z. Poiché apt-cache non fornisce output con terminazione null, non è necessario -z.
Muru,

Risposte:


5

Creare una funzione che accetta un comando e restituisce true se ha un output.

r() { local x=$("$@"); [ -n "$x" ] && echo "$x"; }

( ( r echo -n ) || echo 'nada' ) | cat      # Prints 'nada'
( ( r echo -n foo ) || echo 'nada' ) | cat  # Prints 'foo'

Quindi per questo caso d'uso funzionerà così,

r apt-cache search hunspell-en-zz || r apt-cache search hunspell-en

Si noti che r printf '\n\n\n'restituirebbe false. Con shell diverse da zsh, r printf '\0\0\0'restituirebbe anche false. Lo stesso farebbe r printf '\0a\0b\0c'con alcune conchiglie.
Stéphane Chazelas il

3

Per quanto ne so, non esiste un modo standard per affrontare quei casi in cui il successo di un comando è determinato dalla presenza di output. Tuttavia, puoi scrivere soluzioni alternative.

Ad esempio, è possibile salvare l'output del comando in una variabile e quindi verificare se quella variabile è vuota o meno:

output="$(command)"

if [[ -n "${output}" ]]; then
  # Code to execute if command succeded
else
  # Code to execute if command failed
fi

Penso che questo risponda alla domanda in modo generale, ma se parliamo di apt-cache searchalcune soluzioni mi vengono in mente.

Ho uno script che semplifica la gestione dei pacchetti. Alcune delle sue funzioni sono queste:

search() {
  local 'package' 'packages'
  packages="$( apt-cache search '.*' | cut -d ' ' -f '1' | sort )"
  for package; do
    grep -F -i -e "${package}" <<< "${packages}"
  done
}


search_all() {
  local 'package'
  for package; do
    apt-cache search "${package}" | sort
  done
}


search_description() {
  local 'package' 'packages'
  packages="$( apt-cache search '.*' | sort )"
  for package; do
    grep -F -i -e "${package}" <<< "${packages}"
  done
}


search_names_only() {
  local 'package'
  for package; do
    apt-cache search --names-only "${package}" | sort
  done
}

Questi ti permettono di fare più ricerche in un singolo comando. Per esempio:

$ search hunspell-en-zz hunspell-en
hunspell-en-au
hunspell-en-ca
hunspell-en-gb
hunspell-en-med
hunspell-en-us
hunspell-en-za

Ogni funzione cerca nel database in modo diverso, quindi i risultati possono variare a seconda della funzione che si utilizza:

$ search gnome | wc -l
538
$ search_all gnome | wc -l
1322
$ search_description gnome | wc -l
822
$ search_names_only gnome | wc -l
550

2

Non lo definirei elegante ma penso che potrebbe fare il lavoro:

search_packages () {
    local packages=($@)
    local results=()
    for package in "${packages[@]}"; do
        results=($(apt-cache -n search "$package"))
        if [[ "${#results[@]}" -eq 0 ]]; then
            echo "$package not found."
        elif [[ "${#results[@]}" -eq 1 ]]; then
            do stuff with "$package"
        else
            echo "Warning! Found multiple packages for ${package}:"
            printf '\t-> %s\n' "${results[@]}"
        fi
    done
}

Sfortunatamente non ho una macchina debian su cui testare. Ho incluso l' -nopzione "solo nomi" apt-cacheper provare a limitare i risultati della ricerca in quanto sembra che tu sia per lo più sicuro di ciò che stai cercando.

Può essere eseguito come:

$ search_packages hunspell-en-zz hunspell-en
$ my_packages=('hunspell-en-zz' 'hunspell-en')
$ search_packages "${my_packages[@]}"

1
Questo è esattamente quello che stavo pensando di fare, tuttavia stavo cercando qualcosa di un po 'più elegante, quindi vediamo se qualcuno ha qualcos'altro intelligente (come una soluzione più astratta lontano dal caso d'uso) se non segnerò come scelto.
Evan Carroll,

1
Idealmente, apt-cache restituirebbe qualcosa di meno stupido.
Evan Carroll,

1
@EvanCarroll, hai provato a scherzare con l' -qopzione quiet? La pagina man non è molto dettagliata, ma forse cambia i valori di ritorno?
jesse_b,

1
restituisce ancora 0. = (
Evan Carroll il

2

Muru ha chiarito che nei commenti grepverrà restituito lo stato 1 se non vi sono input. Quindi puoi aggiungere grep .nello stream e se non ci sono input per abbinare il pattern ., cambierà il codice di stato:

( ( echo -n | grep . ) || echo 'nada' ) | cat      # prints 'nada'
( ( echo -n foo | grep . ) || echo 'nada' ) | cat  # prints 'foo'

Nel caso d'uso che assomiglia a questo. Nel seguito, non c'è, -pl-plquindi cade indietro e ritornahunspell-pl

apt-cache search hunspell-pl-pl | grep . || apt-cache search hunspell-pl

O,

apt-cache search hunspell-en-US | grep . || apt-cache search hunspell-en

C'è un -en-UScosì che ritorna hunspell-en-us.

Guarda anche,


grep .restituisce vero se l'input contiene almeno una riga (completamente delimitata con alcune implementazioni) che contiene almeno un carattere (ben formato con la maggior parte delle implementazioni) e rimuoverà altrimenti le righe vuote. grep '^'funzionerebbe meglio nel verificare che ci sia un po 'di output, anche se con qualche implementazione potrebbe comunque restituire false se l'input è una riga non delimitata (e potrebbe rimuovere quella riga, o con altre implementazioni, restituire true ma aggiungere la nuova riga mancante). Alcune implementazioni grep soffocano anche sul personaggio NUL.
Stéphane Chazelas il

2

È possibile definire un:

has_output() {
  LC_ALL=C awk '1;END{exit!NR}'
}

E poi:

if cmd | has_output; then
  echo cmd did produce some output
fi

Alcune awkimplementazioni potrebbero soffocare sui caratteri NUL nell'input.

Al contrario grep '^', quanto sopra sarebbe garantito per funzionare su un input che non termina con un carattere di nuova riga, ma aggiungerebbe la nuova riga mancante.

Per evitarlo ed essere portatile con i sistemi in cui awksoffoca su NUL, è possibile utilizzare perlinvece:

has_output() {
  perl -pe '}{exit!$.'
}

Con perl, potresti anche definire una variante che gestisce i file arbitrari in modo più elegante:

has_output() {
  PERLIO=:unix perl -pe 'BEGIN{$/=\65536} END{exit!$.}'
}

Ciò limita l'utilizzo della memoria (come per i file che non hanno caratteri di nuova riga come i file sparsi di grandi dimensioni).

Puoi anche creare varianti come:

has_at_least_one_non_empty_line() {
  LC_ALL=C awk '$0 != "" {n++};1; END{exit!n}'
}

o:

has_at_least_one_non_blank_line() {
  awk 'NF {n++};1; END{exit!n}'
}

(attenzione alla definizione di spazi vuoti varia tra le awkimplementazioni, alcune in cui è limitata a spazio e tabulazione, altre in cui include anche caratteri di spaziatura verticale ASCII come CR o FF, altre in cui considera gli spazi bianchi delle impostazioni locali)

Idealmente, su Linux, ti consigliamo di utilizzare la splice()chiamata di sistema per massimizzare le prestazioni. Non so di un comando che esporrebbe, ma si può sempre utilizzare python's ctypes:

has_output() {
  python -c 'if 1:
    from ctypes import *
    import sys
    l = CDLL("libc.so.6")
    ret = 1
    while l.splice(0,0,1,0,65536,0) > 0:
      ret = 0
    sys.exit(ret)'
}

(nota che lo has_outputstdin o lo stdout (o entrambi) devono essere una pipe per splice()funzionare).


0

Suggerirei di usare funzioni incorporate di base della shell:

ck_command() { [ -n $("$@") ] ; }

Ecco il caso di test più semplice:

ck_command echo 1 ; echo $?

ck_command echo ; echo $?

Quindi potresti facilmente usarlo con il ||costrutto a cui sei abituato:

ck_command command_1 || ck_command command_2

Questa semplice funzione funzionerà come vorresti con il tuo apt_cachecomportamento qualunque sia il numero di argomenti.


Tranne questo perde STDOUT nel processo, ck_command echo 'asdf' | catnon produce nulla.
Evan Carroll,

2
→ EvanCarroll: questo non era nel tuo § "La domanda". Per ottenere anche questa conservazione dell'output, guarda la risposta molto elegante e semplice di @roaima: unix.stackexchange.com/a/413344/31707 .
dan,
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.