Comment sélectionner plusieurs lignes à partir d'un fichier ou d'un tuyau dans un script?
j'aimerais avoir un script, appelé lines.sh
que je peux tuyau de données pour sélectionner une série de lignes.
Par exemple, si j'avais le fichier suivant:
.txt
a
b
c
d
alors je pourrais courir:
cat test.txt | lines 2,4
et il produirait
b
d
j'utilise zsh, mais préférerais une solution bash si possible.
7 réponses
vous pouvez utiliser ce awk:
awk -v s='2,4' 'BEGIN{split(s, a, ","); for (i in a) b[a[i]]} NR in b' file
two
four
Via un script séparé lines.sh
:
#!/bin/bash
awk -v s="" 'BEGIN{split(s, a, ","); for (i in a) b[a[i]]} NR in b' ""
puis donner les permissions d'exécution:
chmod +x lines.sh
et appelez-le comme:
./lines.sh '2,4' 'test.txt'
Essayer sed
:
sed -n '2p; 4p' inputFile
-n
indique sed
pour supprimer la sortie, mais pour les lignes 2
et 4
, la commande p
(imprimer) est utilisée pour imprimer ces lignes.
vous pouvez également utiliser des gammes, par exemple:
sed -n '2,4p' inputFile
deux versions pures Bash. Puisque vous êtes à la recherche de solutions générales et réutilisables, vous pourriez aussi bien mettre un peu d'effort dans cela. (Voir aussi la dernière section).
Version 1
ce script slurps l'ensemble du stdin dans un tableau (en utilisant mapfile
, donc il est assez efficace) et ensuite imprime les lignes spécifiées sur ses arguments. Les plages sont valides, par exemple,
1-4 # for lines 1, 2, 3 and 4
3- # for everything from line 3 till the end of the file
vous pouvez les séparer par des espaces ou virgule. Les lignes sont imprimées exactement dans l'ordre les arguments sont donnés:
lines 1 1,2,4,1-3,4- 1
va Imprimer la ligne 1 deux fois, puis la ligne 2, puis la ligne 4, puis les lignes 1, 2 et 3, puis tout de la ligne 4 jusqu'à la fin, et enfin, la ligne 1 à nouveau.
Ici, vous allez:
#!/bin/bash
lines=()
# Slurp stdin in array
mapfile -O1 -t lines
# Arguments:
IFS=', ' read -ra args <<< "$*"
for arg in "${args[@]}"; do
if [[ $arg = +([[:digit:]]) ]]; then
arg=$arg-$arg
fi
if [[ $arg =~ ([[:digit:]]+)-([[:digit:]]*) ]]; then
((from=10#${BASH_REMATCH[1]}))
((to=10#${BASH_REMATCH[2]:-$((${#lines[@]}))}))
((from==0)) && from=1
((to>=${#lines[@]})) && to=${#lines[@]}
((from<=to)) || printf >&2 'Argument %d-%d: lines not in increasing order' "$from" "$to"
for((i=from;i<=to;++i)); do
printf '%s\n' "${lines[i]}"
done
else
printf >&2 "Error in argument \`%s'.\n" "$arg"
fi
done
- Pro: c'est vraiment cool.
- Con: doit lire le flux entier dans la mémoire. Ne convient pas pour les flux infinis.
Version 2
cette version aborde le problème précédent des flux infinis. Mais vous perdrez la capacité de répéter et de réorganiser les lignes.
même chose, les fourchettes sont permises:
lines 1 1,4-6 9-
affichera les lignes 1, 4, 5, 6, 9 et tout jusqu'à la fin. Si l'ensemble des lignes est limité, quitte dès que la dernière ligne est lue.
#!/bin/bash
lines=()
tillend=0
maxline=0
# Process arguments
IFS=', ' read -ra args <<< "$@"
for arg in "${args[@]}"; do
if [[ $arg = +([[:digit:]]) ]]; then
arg=$arg-$arg
fi
if [[ $arg =~ ([[:digit:]]+)-([[:digit:]]*) ]]; then
((from=10#${BASH_REMATCH[1]}))
((from==0)) && from=1
((tillend && from>=tillend)) && continue
if [[ -z ${BASH_REMATCH[2]} ]]; then
tillend=$from
continue
fi
((to=10#${BASH_REMATCH[2]}))
if ((from>to)); then
printf >&2 "Invalid lines order: %s\n" "$arg"
exit 1
fi
((maxline<to)) && maxline=$to
for ((i=from;i<=to;++i)); do
lines[i]=1
done
else
printf >&2 "Invalid argument \`%s'\n" "$arg"
exit 1
fi
done
# If nothing to read, exit
((tillend==0 && ${#lines[@]}==0)) && exit
# Now read stdin
linenb=0
while IFS= read -r line; do
((++linenb))
((tillend==0 && maxline && linenb>maxline)) && exit
if [[ ${lines[linenb]} ]] || ((tillend && linenb>=tillend)); then
printf '%s\n' "$line"
fi
done
- Pro: C'est vraiment cool et de ne pas lire l'intégralité du flux dans la mémoire.
- Con: ne peut pas répéter ou réorganiser les lignes comme la Version 1. La vitesse n'est pas le point le plus fort.
autres réflexions
si vous voulez vraiment un script général génial qui fait ce que la Version 1 et la Version 2 fait, et plus encore, vous devriez certainement envisager d'utiliser une autre langue, par exemple, Perl: vous gagnerez beaucoup (en particulier la vitesse)! vous serez en mesure d'avoir de belles des options qui feront beaucoup de choses plus cool. Cela vaut peut-être la peine à long terme, car vous voulez un script général et réutilisable. Vous pourriez même finir par avoir un script qui lit les e-mails!
Disclaimer. Je n'ai pas vérifié ces scripts... méfiez-vous donc des bugs!
solution rapide pour votre ami. Entrée:
.txt
a
b
c
d
e
f
g
h
i
j
test.sh
lines (){
sed -n "$( echo "$@" | sed 's/[0-9]\+/&p;/g')"
}
cat 1.txt | lines 1 5 10
Ou si vous voulez avoir votre lines
comme script:
lines.sh
IFS=',' read -a lines <<< ""; sed -n "$( echo "${lines[@]}" | sed 's/[0-9]\+/&p;/g')" ""
./lines.sh 1,5,10 test.txt
sortie dans les deux cas:
a
e
j
S'il s'agit d'une opération ponctuelle et qu'il n'y a pas beaucoup de lignes à choisir, vous pouvez utiliser pick pour les sélectionner manuellement:
cat test.txt | pick | ...
un écran interactif s'ouvrira vous permettant de sélectionner ce que vous voulez.
bien, à condition que:
- votre fichier est assez petit
- vous n'avez pas de point-virgule (ou un autre personnage de votre choix) dans le fichier
- cela ne vous dérange pas d'utiliser plusieurs pipes
, vous pouvez utiliser quelque chose comme:
cat test.txt |tr "\n" ";"|cut -d';' -f2,4|tr ";" "\n"
où-f2, 4 indique les lignes que vous voulez extraire
essayez ceci:
file=
for var in "$@" //var is all line numbers
do
sed -n "${var}p" $file
done
j'ai créé un script avec 1 paramètre de fichier, et un nombre illimité de paramètres pour les numéros de ligne. Vous l'appelleriez ainsi:
lines txt 2 3 4...etc