Wie kann ich "Ausschneiden" auf mehrere Dateien anwenden und dann die Ergebnisse "einfügen"?

8

Ich mache oft Operationen wie

paste <(cut -d, -f1 file1.csv) <(cut -d, -f1 file2.csv)

Das ist sehr mühsam mit mehr als ein paar Dateien.

Kann ich diesen Prozess automatisieren, z. B. durch Globbing? Ich kann die cutErgebnisse mit speichern

typeset -A cut_results
for f in file*.csv; do
    cut_results[$f]="$(cut -d, -f1 $f)"
done

aber ich bin nicht sicher, wie ich von dort aus vorgehen soll.

Shadowtalker
quelle
github.com/thrig/sial.org-scripts/blob/master/misc/stitch verwende ich für so etwas wie diese Aufgabe.
Thrig
Manchmal, wenn die Nr. Von Feldern / Spalten ist bekannt, dass Sie auch alle Dateien einfügen und dann die benötigten Felder ausschneiden können ...
don_crissti
Katze | schneiden | Einfügen?
Bot47
@ MaxRied das macht nicht was will.
Shadowtalker
Dann verstehe ich deine Frage nicht. cat file*.csv | cut -d, -f1 | pasteTun Sie nicht , was Sie zu archivieren versuchen?
Bot47

Antworten:

4

Sie können dies mit Globbing automatisieren, insbesondere mit dem e Glob-Qualifikationsmerkmal plus eval, aber es ist nicht schön und das Zitieren ist schwierig:

eval paste *.csv(e\''REPLY="<(cut -d, -f1 $REPLY)"'\')
  • Der Teil dazwischen \'…\'ist ein Code, der für jede Übereinstimmung des Globs ausgeführt werden muss. Es wird mit der REPLYauf die Übereinstimmung eingestellten Variablen ausgeführt und kann geändert werden.
  • Ich habe den Code in einfache Anführungszeichen gesetzt, damit er nicht erweitert wird, wenn der Glob analysiert wird.
  • Der Code REPLY="<(cut -d, -f1 $REPLY)"generiert die Zeichenfolge, <(cut -d, -f1 file1.csv)wenn die Übereinstimmung stimmt file1.csv. Die doppelten Anführungszeichen sind erforderlich, damit der Teil nach dem Gleichheitszeichen nicht erweitert wird, wenn der eCode ausgeführt wird, abgesehen vom Ersetzen des Werts von REPLY.
  • Da jede Globbed-Datei durch eine Zeichenfolge ersetzt wird,

Es wäre schöner, die Komplexität in einer Funktion zu verbergen. Minimal getestet.

function map {
  emulate -LR zsh
  local cmd pre
  cmd=()
  while [[ $# -ne 0 && $1 != "--" ]]; do
    cmd+=($1)
    shift
  done
  if ((!$#)); then
    echo >&2 "Usage: $0: COMMAND [ARGS...] -- PREPROCESSOR [ARGS...] -- FILES..."
    return 125
  fi
  shift
  while [[ $# -ne 0 && $1 != "--" ]]; do
    pre+="${(q)1} "
    shift
  done
  if ((!$#)); then
    echo >&2 "Usage: $0: COMMAND [ARGS...] -- PREPROCESSOR [ARGS...] -- FILES..."
    return 125
  fi
  shift
  eval "${(@q)cmd}" "<($pre${(@q)^@})"
}

Beispielnutzung (die Syntax erinnert an zargs):

map paste -- cut -d, -f1 -- *.csv
Gilles 'SO - hör auf böse zu sein'
quelle
Toller Tipp zum eQualifier und tolle Idee, ihn in eine generische "Mapping" -Funktion zu packen.
Shadowtalker
Natürlich erstickt dies, wenn Sie --als Argument an eines der Dienstprogramme übergeben müssen, aber ich glaube nicht, dass ich auf diesen
Randfall stoßen werde
Eine Lösung wäre, die findSyntax zu kopieren und ein Semikolon mit Shell-
Escapezeichen zum Beenden
4

Ich denke, Ihre erste Zeile ist ungefähr so ​​gut wie es für einen einfachen Einzeiler geht.

Wenn es eine Reihe von Dateien mit allen unterschiedlichen Namen gibt, können Sie die wiederholte Eingabe mit einem einfachen Verlaufserweiterungs-Cheat etwas reduzieren:

Erster Lauf <(cut -d, -f1

Beachten Sie den nachgestellten Leerzeichen. Beachten Sie auch, dass dieser Befehl eine sekundäre Eingabeaufforderung ausgibt. Drücken Sie einfach Ctrl- C. Der einzige Punkt ist, es dem Verlauf hinzuzufügen.

Nächster Lauf paste !!file1.csv) !!file2.csv)

Das !!wird auf den vollständigen Inhalt des vorherigen Befehlslaufs erweitert, einschließlich des nachgestellten Speicherplatzes. Beachten Sie, dass Sie eine sekundäre Eingabeaufforderung erhalten, wenn Sie die nachfolgenden engen Klammern vergessen. Sie können einfach tippen Ctrl- Cund es in diesem Fall erneut versuchen.

Dies ist ein bisschen hackig, aber gut genug für eine einmalige Verwendung. Wenn Sie viel tun, können Sie eine Bash-Funktion schreiben.

Platzhalter
quelle
3

Versuchen Sie es mit awk

awk '{L[FNR]=L[FNR] $1 "\t"}END{for(i=1;i<=FNR;i++)print L[i]}' *.csv

oder mit sed einfügen

paste *.csv | sed 's/ [^\t]*//g'
Costas
quelle
Ich vergesse immer, dass ich AWK für mehrere Dateien verwenden kann. Dies ist definitiv die beste Antwort für meinen speziellen Anwendungsfall, aber ich akzeptiere die "Karten" -Antwort, weil sie näher an dem liegt, was ich mit dieser Frage im Sinn hatte.
Shadowtalker
1

Ich lerne gerade bashSkripte und dies schien eine ausgezeichnete einfache Aufgabe zu sein, mit der ich üben konnte. Deshalb schrieb ich Folgendes. (Meine andere Antwort gibt den einfachen Hack zur Erweiterung des Verlaufs an, aber dies ist ein vollständiges Skript, und ich hielt es für würdig, eine zusätzliche Antwort zu geben.) Ich glaube, dies ist POSIX-kompatibel und sollte funktionieren #!/bin/sh, aber nicht 100% sicher. EDIT: Eigentlich ist das =~nicht POSIX-kompatibel. Sie können diesen Check-out jedoch durchführen und cutden Fehler zurückgeben lassen.

#!/bin/bash

fieldtocut=1
delimiter=','

usage () {
    cat << EOF
usage: $0 [-f FIELD] [-d DELIMITER] file1..
Cuts field FIELD from each file and pastes it.
Default field is 1, default delimiter is ','
EOF
    exit $1
}

while getopts ':f:d:' opt ; do
    case $opt in
        f)
            if [[ $OPTARG =~ ^[0-9]+$ ]] ; then
                fieldtocut="$OPTARG"
            else
                usage 1
            fi
            ;;
        d)
            delimiter=$OPTARG
            ;;
        *)
            usage 1
            ;;
    esac
done
shift $((OPTIND-1))

[ $# -eq 0 ] && usage 0

pasteargs=''

for file in "$@" ; do
    pasteargs=$(printf '%s' "$pasteargs" '<(cut -d$delimiter -f$fieldtocut ' "$file" ') ')
done

eval paste $pasteargs
Platzhalter
quelle
Ihr Skript schlägt fehl, wenn das Trennzeichen (oder das zu schneidende Feld) ein Shell-Sonderzeichen ist, z. B. ein ;Tabulator, oder wenn die Dateinamen Shell-Sonderzeichen enthalten.
Gilles 'SO - hör auf böse zu sein'
Grrr. Gute Beobachtung. Verlor den Überblick über mein Zitat. Ich habe ein paar andere Möglichkeiten ausprobiert, aber ich war verwirrt mit dem Zitat und machte es am Ende noch weniger praktikabel. Irgendwelche Ratschläge dazu zu Lernzwecken? :)
Wildcard
In zsh können Sie einfach verwenden ${(q)delimiter}. Wenn Sie Code möchten, der auch in Bash funktioniert, ist es schwieriger. Ich denke quoted_single_quote=\'\\\'\'; delimiter="'${delimiter//'/"$quoted_single_quote"}'"funktioniert in ksh93, bash und zsh.
Gilles 'SO - hör auf böse zu sein'
1

Unter der Annahme, dass Ihre Argumente zutreffen "$@", glaube ich so etwas wie:

eval "paste $(printf "<( cut -d, -f1 %q ) " "$@")"

Sollte es tun.

PSkocik
quelle
Sie können auf Befehlszeilenlängenprobleme mit der Art des Ansatzes
stoßen
0

Hier ist eine andere Möglichkeit, die der Antwort von Wildcard sehr ähnlich ist :

files=( file1.csv file2.csv)
eval paste "<( cut -d, -f1 ${^files[@]} )"

Anstelle einer forSchleife wird die ${^ ... }Erweiterung verwendet, die Zsh-spezifisch ist.

Der Grund files, der zuerst zugewiesen werden muss, ist, dass das Globbing immer zuletzt durchgeführt wird. Wenn filesalso automatisch (wie in files=( *.csv )) generiert werden muss, wird so etwas ${^:-( *.csv )}erst erweitert, nachdem alle anderen Erweiterungen aufgetreten sind. Wir wollen, dass es zuerst erweitert wird .

Die ${^ ... }Erweiterung bewirkt, dass sich das resultierende Array wie das Ergebnis der Klammererweiterung verhält. Zuweisen x=(a b)und dann vergleichen echo ${x}ymit echo ${^x}y.

Das Anführungszeichen ist notwendig, um Zsh dazu zu bringen, den umgebenden Text wie eine wörtliche Zeichenfolge zu behandeln. Andernfalls würde die Befehlszeile an den Leerzeichen aufgeteilt, sodass sich unsere ${^ ... }Erweiterung auf reduzieren würde ""${^ ... }"". Das heißt, jedes Element würde nur von einer leeren Zeichenfolge umgeben sein. Das ist,

echo "<( cut -d, -f1 ${^files[@]} )"

und

echo "<( cut -d, -f1 "\
${^files[@]}\
" )"

sind gleichwertig, aber nicht dasselbe wie

echo <( cut -d, -f1 ${^files[@]} )

Das Zitieren bringt jedoch ein neues Problem mit sich: Die Befehlszeile wird analysiert und aufgeteilt, ohne Rücksicht auf die stattfindende Erweiterung. Das heißt, obwohl wir effektiv eingetreten sind

paste <( cut -d, -f1 file1.csv ) <( cut -d, -f1 file2.csv )

wie gewünscht wird dies tatsächlich als analysiert

paste '<( cut -d, -f1 file1.csv )' '<( cut -d, -f1 file2.csv )'

Daher müssen wir evalden korrekt geformten Ausdruck neu analysieren. Vergleichen Sie dies, um dies in Aktion zu sehen

setopt noxtrace
eval paste "<( cut -d, -f1 ${^files[@]} )" 1>/dev/null 2>&1

zu

setopt xtrace
eval paste "<( cut -d, -f1 ${^files[@]} )" 1>/dev/null 2>&1

Ich hoffte , dass eine Kombination von verschachtelten Erweiterungen, die ${ ... :- ... }Erweiterung und dem Parameter Expansion Flags Q, zund / oder swürde ohne zu einer Neubewertung führen eval, aber offenbar nicht der Fall ist. Ich wünschte auch, es gäbe einen Weg, das Globbing zu erzwingen, aber auch das scheint unmöglich.

Shadowtalker
quelle
0

Sie können awkdie Dateien im Gleichschritt durchlaufen und das interessierende Feld aus jeder Datei melden. Fügen Sie diesen Code beispielsweise in eine Datei eincut_files.awk

NR == FNR{printf "%s%s",$1, FS;
for (k=2; k<ARGC; ++k)
    {getline < ARGV[k]; printf "%s%s", $1, k==ARGC-1?"\n":FS}; next};
NR != FNR{for (k=2; k<ARGC; ++k) close(ARGV[k]); exit}

Und dann nenne es so

awk -F',' -f cut_files.awk file1 file2 file3 file4 ....
iruvar
quelle