Sostituzione comandi: divisione su newline ma non spazio


30

So di poter risolvere questo problema in diversi modi, ma mi chiedo se c'è un modo per farlo usando solo i built-in bash e, in caso contrario, qual è il modo più efficace per farlo.

Ho un file con contenuti come

AAA
B C DDD
FOO BAR

con ciò intendo solo che ha diverse righe e ogni riga può avere o meno spazi. Voglio eseguire un comando come

cmd AAA "B C DDD" "FOO BAR"

Se uso cmd $(< file)ottengo

cmd AAA B C DDD FOO BAR

e se uso cmd "$(< file)"ottengo

cmd "AAA B C DDD FOO BAR"

Come faccio a trattare ogni riga esattamente con un parametro?


Risposte:


26

portabile:

set -f              # turn off globbing
IFS='
'                   # split at newlines only
cmd $(cat <file)
unset IFS
set +f

O usando una subshell per rendere IFSlocali le modifiche e all'opzione:

( set -f; IFS='
'; exec cmd $(cat <file) )

La shell esegue la divisione dei campi e la generazione del nome file sul risultato di una variabile o di una sostituzione di comando che non è racchiusa tra virgolette. Quindi è necessario disattivare la generazione del nome file set -fe configurare la divisione dei campi con IFSper rendere separati solo i campi da riga.

Non c'è molto da guadagnare con i costrutti bash o ksh. È possibile rendere IFSlocale una funzione, ma non set -f.

In bash o ksh93, è possibile memorizzare i campi in un array, se è necessario passarli a più comandi. È necessario controllare l'espansione al momento della creazione dell'array. Quindi si "${a[@]}"espande negli elementi dell'array, uno per parola.

set -f; IFS=$'\n'
a=($(cat <file))
set +f; unset IFS
cmd "${a[@]}"

10

Puoi farlo con un array temporaneo.

Impostare:

$ cat input
AAA
A B C
DE F
$ cat t.sh
#! /bin/bash
echo "$1"
echo "$2"
echo "$3"

Riempi l'array:

$ IFS=$'\n'; set -f; foo=($(<input))

Usa l'array:

$ for a in "${foo[@]}" ; do echo "--" "$a" "--" ; done
-- AAA --
-- A B C --
-- DE F --

$ ./t.sh "${foo[@]}"
AAA
A B C
DE F

Non riesco a capire un modo per farlo senza quella variabile temporanea - a meno che la IFSmodifica non sia importante per cmd, nel qual caso:

$ IFS=$'\n'; set -f; cmd $(<input) 

dovrebbe farlo.


IFSmi confonde sempre. IFS=$'\n' cmd $(<input)non funziona IFS=$'\n'; cmd $(<input); unset IFSfunziona. Perché? Immagino che userò(IFS=$'\n'; cmd $(<input))
Old Pro

6
@OldPro IFS=$'\n' cmd $(<input)non funziona perché si trova solo IFSnell'ambiente di cmd. $(<input)viene espanso per formare il comando, prima che IFSvenga eseguita l'assegnazione a .
Gilles 'SO- smetti di essere malvagio'

8

Sembra che il modo canonico per farlo bashsia qualcosa di simile

unset args
while IFS= read -r line; do 
    args+=("$line") 
done < file

cmd "${args[@]}"

o, se la tua versione di bash ha mapfile:

mapfile -t args < filename
cmd "${args[@]}"

L'unica differenza che posso trovare tra il mapfile e il ciclo while-read rispetto al one-liner

(set -f; IFS=$'\n'; cmd $(<file))

è che il primo convertirà una riga vuota in un argomento vuoto, mentre il one-liner ignorerà una riga vuota. In questo caso, il comportamento con una sola linea è quello che preferirei comunque, quindi doppio vantaggio in quanto compatto.

Vorrei usare IFS=$'\n' cmd $(<file)ma non funziona, perché $(<file)viene interpretato per formare la riga di comando prima che abbia IFS=$'\n'effetto.

Anche se non funziona nel mio caso, ora ho imparato che molti strumenti supportano le linee di terminazione null (\000)invece di newline (\n)ciò che rende molto più facile questo quando si tratta, per esempio, di nomi di file, che sono fonti comuni di queste situazioni :

find / -name '*.config' -print0 | xargs -0 md5

fornisce un elenco di nomi di file completi come argomenti a md5 senza alterazioni, interpolazioni o altro. Questo porta alla soluzione non integrata

tr "\n" "\000" <file | xargs -0 cmd

sebbene anche questo ignori le linee vuote, sebbene catturi le linee che hanno solo spazi bianchi.


Usare cmd $(<file)valori senza virgolette (usare l'abilità di bash per dividere le parole) è sempre una scommessa rischiosa. Se una riga è *, verrà espansa dalla shell in un elenco di file.

3

È possibile utilizzare il bash incorporato mapfileper leggere il file in un array

mapfile -t foo < filename
cmd "${foo[@]}"

o, non testato, xargspotrebbe farlo

xargs cmd < filename

Dalla documentazione di mapfile: "mapfile non è una funzionalità di shell comune o portatile". E infatti non è supportato sul mio sistema. xargsnon aiuta neanche.
Old Pro,

Avresti bisogno xargs -doxargs -L
James Youngman

@James, no, non ho -dun'opzione ed xargs -L 1esegue il comando una volta per riga, ma divide ancora args su spazi bianchi.
Old Pro,

1
@OldPro, beh, hai chiesto "un modo per farlo usando solo i bash incorporati" invece di "una funzionalità di shell comune o portatile". Se la tua versione di bash è troppo vecchia, puoi aggiornarla?
Glenn Jackman,

mapfileè molto utile per me, poiché afferra le righe vuote come elementi dell'array, cosa che il IFSmetodo non fa. IFSconsidera le nuove righe contigue come un unico delimitatore ... Grazie per averlo presentato, poiché non ero a conoscenza del comando (sebbene, in base ai dati di input dell'OP e alla riga di comando prevista, sembra che in realtà voglia ignorare le righe vuote).
Peter

0
old=$IFS
IFS='  #newline
'
array=`cat Submissions` #input the text in this variable
for ...  #use parts of variable in the for loop
... 
done
IFS=$old

Il modo migliore che ho trovato. Funziona e basta.


E perché funziona se si imposta IFSsullo spazio, ma la domanda è non dividere lo spazio?
RalfFriedl,

0

File

Il loop più semplice (portatile) per dividere un file su newline è:

#!/bin/sh
while read -r line; do            # get one line (\n) at a time.
    set -- "$@" "$line"           # store in the list of positional arguments.
done <infile                      # read from a file called infile.
printf '<%s>' "$@" ; echo         # print the results.

Che stamperà:

$ ./script
<AAA><A B C><DE F>

Sì, con IFS predefinito = spacetabnewline.

Perché funziona

  • IFS verrà utilizzato dalla shell per dividere l'input in diverse variabili. Poiché esiste solo una variabile, la shell non esegue alcuna divisione. Quindi, nessun cambiamento IFSnecessario.
  • Sì, gli spazi / tab iniziali e finali vengono rimossi, ma in questo caso non sembra essere un problema.
  • No, non viene eseguito alcun globbing poiché nessuna espansione non viene quotata . Quindi, non è set -fnecessario.
  • L'unico array utilizzato (o necessario) sono i parametri posizionali simili ad array.
  • L' -ropzione (non elaborata) consente di evitare la maggior parte della barra rovesciata.

Ciò non funzionerà se è necessaria la divisione e / o il globbing. In tali casi è necessaria una struttura più complessa.

Se hai bisogno (ancora portatile) di:

  • Evitare la rimozione di spazi / tabulazioni iniziali e finali, usare: IFS= read -r line
  • Linea di divisione a Vars su qualche personaggio, uso: IFS=':' read -r a b c.

Dividi il file su qualche altro personaggio (non portatile, funziona con ksh, bash, zsh):

IFS=':' read -d '+' -r a b c

Espansione

Naturalmente, il titolo della tua domanda riguarda la divisione dell'esecuzione di un comando su newline evitando la divisione su spazi.

L'unico modo per ottenere la divisione dalla shell è lasciare un'espansione senza virgolette:

echo $(< file)

Ciò è controllato dal valore di IFS e, su espansioni non quotate, viene applicato anche il globbing. Per realizzare quel lavoro, hai bisogno di:

  • Impostare IFS per nuova linea unica , per ottenere la scissione sulla nuova linea solo.
  • Deseleziona l'opzione di shell globbing set +f:

    set + f IFS = '' cmd $ (<file)

Ovviamente, questo cambia il valore di IFS e del globbing per il resto della sceneggiatura.

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.