sed - supprimer la toute dernière occurrence d'une chaîne (une virgule) dans un fichier?


15

J'ai un très gros fichier csv. Comment supprimer le dernier ,avec sed (ou similaire)?

...
[11911,0,"BUILDER","2014-10-15","BUILDER",0,0],
[11912,0,"BUILDER","2014-10-15","BUILDER",0,0],
[11913,0,"BUILDER","2014-10-15","BUILDER",0,0],
]

Sortie désirée

...
[11911,0,"BUILDER","2014-10-15","BUILDER",0,0],
[11912,0,"BUILDER","2014-10-15","BUILDER",0,0],
[11913,0,"BUILDER","2014-10-15","BUILDER",0,0]
]

La commande sed suivante supprimera la dernière occurrence par ligne, mais je veux par fichier.

sed -e 's/,$//' foo.csv

Cela ne fonctionne pas non plus

sed '$s/,//' foo.csv

La virgule est-elle toujours sur l'avant-dernière ligne?
John1024

Oui, l'avant-dernière ligne
spuder

Réponses:


12

En utilisant awk

Si la virgule est toujours à la fin de l'avant-dernière ligne:

$ awk 'NR>2{print a;} {a=b; b=$0} END{sub(/,$/, "", a); print a;print b;}'  input
[11911,0,"BUILDER","2014-10-15","BUILDER",0,0],
[11912,0,"BUILDER","2014-10-15","BUILDER",0,0],
[11913,0,"BUILDER","2014-10-15","BUILDER",0,0]
]

Utilisation de awketbash

$ awk -v "line=$(($(wc -l <input)-1))" 'NR==line{sub(/,$/, "")} 1'  input
[11911,0,"BUILDER","2014-10-15","BUILDER",0,0],
[11912,0,"BUILDER","2014-10-15","BUILDER",0,0],
[11913,0,"BUILDER","2014-10-15","BUILDER",0,0]
]

En utilisant sed

$ sed 'x;${s/,$//;p;x;};1d'  input
[11911,0,"BUILDER","2014-10-15","BUILDER",0,0],
[11912,0,"BUILDER","2014-10-15","BUILDER",0,0],
[11913,0,"BUILDER","2014-10-15","BUILDER",0,0]
]

Pour OSX et autres plates-formes BSD, essayez:

sed -e x -e '$ {s/,$//;p;x;}' -e 1d  input

En utilisant bash

while IFS=  read -r line
do
    [ "$a" ] && printf "%s\n" "$a"
    a=$b
    b=$line
done <input
printf "%s\n" "${a%,}"
printf "%s\n" "$b"

Peut-être que c'est parce que je suis sur un Mac, mais la commande sed donne une erreursed: 1: "x;${s/,$//;p;x}; 2,$ p": extra characters at the end of x command
spuder

@spuder Oui, OSX a BSD sedet il est souvent différent de manière subtile. Je n'ai pas accès à OSX pour tester cela, mais veuillez essayersed -n -e x -e '${s/,$//;p;x;}' -e '2,$ p' input
John1024

Oui, ce second a travaillé sur Mac
spuder

4

Vous pouvez simplement essayer la commande Perl one-liner ci-dessous.

perl -00pe 's/,(?!.*,)//s' file

Explication:

  • , Correspond à une virgule.
  • (?!.*,)La recherche d'anticipation négative affirme qu'il n'y aurait pas de virgule après cette virgule correspondante. Cela correspondrait donc à la dernière virgule.
  • sEt le plus important est le smodificateur DOTALL qui fait que le point correspond également aux caractères de nouvelle ligne.

2
Vous pouvez également faire: perl -0777 -pi -e 's/(.*),(.*?)/\1\2/s'. Cela fonctionne parce que le premier .*est gourmand, tandis que le second ne l'est pas.
Oleg Vaskevich

4
lcomma() { sed '
    $x;$G;/\(.*\),/!H;//!{$!d
};  $!x;$s//\1/;s/^\n//'
}

Cela ne devrait supprimer que la dernière occurrence d'un ,dans n'importe quel fichier d'entrée - et il imprimera toujours ceux dans lesquels un ,ne se produit pas. Fondamentalement, il met en mémoire tampon des séquences de lignes qui ne contiennent pas de virgule.

Quand il rencontre une virgule, il échange la mémoire tampon de ligne actuelle avec la mémoire tampon de maintien et de cette manière imprime simultanément toutes les lignes qui se sont produites depuis la dernière virgule et libère sa mémoire tampon de conservation.

Je creusais simplement mon fichier historique et j'ai trouvé ceci:

lmatch(){ set "USAGE:\
        lmatch /BRE [-(((s|-sub) BRE)|(r|-ref)) REPL [-(f|-flag) FLAG]*]*
"       "${1%"${1#?}"}" "$@"
        eval "${ZSH_VERSION:+emulate sh}"; eval '
        sed "   1x;     \\$3$2!{1!H;\$!d
                };      \\$3$2{x;1!p;\$!d;x
                };      \\$3$2!x;\\$3$2!b'"
        $(      unset h;i=3 p=:-:shfr e='\033[' m=$(($#+1)) f=OPTERR
                [ -t 2 ] && f=$e\2K$e'1;41;17m}\r${h-'$f$e\0m
                f='\${$m?"\"${h-'$f':\t\${$i$e\n}\$1\""}\\c' e=} _o=
                o(){    IFS=\ ;getopts  $p a "$1"       &&
                        [ -n "${a#[?:]}" ]              &&
                        o=${a#-}${OPTARG-${1#-?}}       ||
                        ! eval "o=$f;o=\${o%%*\{$m\}*}"
        };      a(){    case ${a#[!-]}$o in (?|-*) a=;;esac; o=
                        set $* "${3-$2$}{$((i+=!${#a}))${a:+#-?}}"\
                                ${3+$2 "{$((i+=1))$e"} $2
                        IFS=$;  _o=${_o%"${3+$_o} "*}$*\
        };      while   eval "o \"\${$((i+=(OPTIND=1)))}\""
                do      case            ${o#[!$a]}      in
                        (s*|ub)         a s 2 ''        ;;
                        (r*|ef)         a s 2           ;;
                        (f*|lag)        a               ;;
                        (h*|elp)        h= o; break     ;;
                esac;   done;   set -f; printf  "\t%b\n\t" $o $_o
)\"";}

C'est en fait assez bon. Oui, il utilise eval, mais il ne lui passe jamais rien au-delà d'une référence numérique à ses arguments. Il construit des sedscripts arbitraires pour gérer une dernière correspondance. Je vais te montrer:

printf "%d\" %d' %d\" %d'\n" $(seq 5 5 200) |                               
    tee /dev/fd/2 |                                                         
    lmatch  d^.0     \  #all re's delimit w/ d now                           
        -r '&&&&'    \  #-r or --ref like: '...s//$ref/...'      
        --sub \' sq  \  #-s or --sub like: '...s/$arg1/$arg2/...'
        --flag 4     \  #-f or --flag appended to last -r or -s
        -s\" \\dq    \  #short opts can be '-s $arg1 $arg2' or '-r$arg1'
        -fg             #tacked on so: '...s/"/dq/g...'                     

Cela imprime ce qui suit à stderr. Ceci est une copie de lmatchl'entrée de:

5" 10' 15" 20'
25" 30' 35" 40'
45" 50' 55" 60'
65" 70' 75" 80'
85" 90' 95" 100'
105" 110' 115" 120'
125" 130' 135" 140'
145" 150' 155" 160'
165" 170' 175" 180'
185" 190' 195" 200'

Le evalsous-shell ed de la fonction parcourt une fois tous ses arguments. En les parcourant, il itère un compteur de manière appropriée en fonction du contexte de chaque commutateur et ignore autant d'arguments pour la prochaine itération. À partir de là, cela fait l'une des quelques choses par argument:

  • Pour chaque option, l'analyseur d'options s'ajoute $aà $o. $aest attribué en fonction de la valeur $iqui est incrémentée par le nombre d'arguments pour chaque arg traité. $ase voit attribuer l'une des deux valeurs suivantes:
    • a=$((i+=1)) - ceci est attribué si une option courte n'a pas d'argument ajouté ou si l'option était longue.
    • a=$i#-?- ceci est attribué si l'option est courte et que son argument lui est ajouté.
    • a=\${$a}${1:+$d\${$(($1))\}}- Indépendamment de l'affectation initiale, $ala valeur de est toujours entourée d'accolades et - dans un -scas - $iest parfois incrémentée de plus et un champ supplémentaire délimité est ajouté.

Le résultat est qu'on evalne passe jamais une chaîne contenant des inconnues. Chacun des arguments de ligne de commande est référencé par son numéro d'argument numérique - même le délimiteur qui est extrait du premier caractère du premier argument et est la seule fois où vous devez utiliser le caractère non échappé. Fondamentalement, la fonction est un générateur de macros - elle n'interprète jamais les valeurs des arguments d'une manière spéciale car elle sedpeut (et le fera, bien sûr) facilement gérer cela lorsqu'elle analyse le script. Au lieu de cela, il organise simplement judicieusement ses arguments dans un script réalisable.

Voici une sortie de débogage de la fonction au travail:

... sed "   1x;\\$2$1!{1!H;\$!d
        };      \\$2$1{x;1!p;\$!d;x
        };      \\$2$1!x;\\$2$1!b
        s$1$1${4}$1
        s$1${6}$1${7}$1${9}
        s$1${10#-?}$1${11}$1${12#-?}
        "
++ sed '        1x;\d^.0d!{1!H;$!d
        };      \d^.0d{x;1!p;$!d;x
        };      \d^.0d!x;\d^.0d!b
        sdd&&&&d
        sd'\''dsqd4
        sd"d\dqdg
        '

Et lmatchpeut donc être utilisé pour appliquer facilement des expressions rationnelles aux données après la dernière correspondance dans un fichier. Le résultat de la commande que j'ai exécutée ci-dessus est:

5" 10' 15" 20'
25" 30' 35" 40'
45" 50' 55" 60'
65" 70' 75" 80'
85" 90' 95" 100'
101010105dq 110' 115dq 120'
125dq 130' 135dq 140sq
145dq 150' 155dq 160'
165dq 170' 175dq 180'
185dq 190' 195dq 200'

... qui, étant donné le sous-ensemble de l'entrée de fichier qui suit la dernière mise en /^.0/correspondance, applique les substitutions suivantes:

  • sdd&&&&d- se remplace $matchpar lui-même 4 fois.
  • sd'dsqd4 - le quatrième guillemet simple suivant le début de la ligne depuis le dernier match.
  • sd"d\dqd2 - idem, mais pour les guillemets doubles et globalement.

Et donc, pour montrer comment on pourrait utiliser lmatchpour supprimer la dernière virgule d'un fichier:

printf "%d, %d %d, %d\n" $(seq 5 5 100) |
lmatch '/\(.*\),' -r\\1

PRODUCTION:

5, 10 15, 20
25, 30 35, 40
45, 50 55, 60
65, 70 75, 80
85, 90 95 100

1
@don_crissti - c'est beaucoup mieux maintenant - j'ai abandonné l' -moption et l' ai rendue obligatoire, je suis passée à plusieurs arguments pour re et repl pour -set j'ai également implémenté une gestion correcte du délimiteur. Je pense que c'est pare-balles. J'ai utilisé avec succès un espace et une seule citation comme délimiteur,
mikeserv

2

Si la virgule n'est peut-être pas sur l'avant-dernière ligne

Utilisation de awket tac:

tac foo.csv | awk '/,$/ && !handled { sub(/,$/, ""); handled++ } {print}' | tac

La awkcommande est simple pour effectuer la substitution la première fois que le motif est vu.  tacinverse l'ordre des lignes du fichier, la awkcommande finit donc par supprimer la dernière virgule.

On m'a dit que

tac foo.csv | awk '/,$/ && !handled { sub(/,$/, ""); handled++ } {print}' > tmp && tac tmp

peut être plus efficace.


2

Si vous pouvez utiliser tac:

tac file | perl -pe '$_=reverse;!$done && s/,// && $done++;$_=reverse'|tac

1

voir /programming/12390134/remove-comma-from-last-line

Cela fonctionne pour moi:

$cat input.txt
{"name": "secondary_ua","type":"STRING"},
{"name": "request_ip","type":"STRING"},
{"name": "cb","type":"STRING"},
$ sed '$s/,$//' < input.txt >output.txt
$cat output.txt
{"name": "secondary_ua","type":"STRING"},
{"name": "request_ip","type":"STRING"},
{"name": "cb","type":"STRING"}

Ma meilleure façon est de supprimer la dernière ligne et après avoir supprimé la virgule, ajoutez à nouveau le] char


1

Essayez avec ci vi- dessous :

  vi "+:$-1s/\(,\)\(\_s*]\)/\2/e" "+:x" file

Explication:

  • $-1 sélectionner l'avant-dernière ligne

  • s remplacer

  • \(,\)\(\_s*]\)trouver une virgule suivie ]et séparée par des espaces ou une nouvelle ligne
  • \2remplacer par \(\_s*]\)ex. espaces ou nouvelle ligne suivi de]

-1

Essayez avec la sedcommande ci-dessous .

sed -i '$s/,$//' foo.csv

1
Cela supprimera la virgule de fin de chaque ligne, ce n'est pas ce que OP souhaite.
Archemar

@Archemar Non, il supprimera uniquement sur la dernière ligne mais cela ne fonctionnera pas pour les données d'OP qui ne sont pas dans la dernière ligne
αғsнιη
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.