web-dev-qa-db-fra.com

Obtenir le nom du fichier sans extension dans Bash

J'ai la boucle for suivante pour sort individuellement tous les fichiers texte d'un dossier (c'est-à-dire la production d'un fichier de sortie trié pour chacun).

for file in *.txt; 
do
   printf 'Processing %s\n' "$file"
   LC_ALL=C sort -u "$file" > "./${file}_sorted"  
done

C’est presque parfait, sauf qu’il produit actuellement des fichiers au format:

originalfile.txt_sorted

... alors que j'aimerais qu'il produise des fichiers au format:

originalfile_sorted.txt 

En effet, la variable ${file} contient le nom du fichier, y compris son extension. J'exécute Cygwin sur Windows. Je ne sais pas comment cela se comporterait dans un véritable environnement Linux, mais sous Windows, ce décalage de l'extension rend le fichier inaccessible par l'explorateur Windows.

Comment puis-je séparer le nom de fichier de l'extension pour pouvoir ajouter le suffixe _sorted entre les deux, ce qui me permet de différencier facilement les versions d'origine et les versions triées des fichiers tout en conservant intactes les extensions de fichier Windows?

J'ai étudié ce que pourrait être possible solutions, mais ceux-ci me semblent plus aptes à traiter des problèmes plus complexes. Plus important encore, avec ma connaissance bash actuelle, ils me dépassent la tête. J'espère donc qu'il existe une solution plus simple qui s'applique à mon humble boucle for ou que quelqu'un puisse expliquer comment appliquer ces solutions à ma situation. .

6
Hashim

Ces solutions que vous associez sont en fait plutôt bonnes. Certaines réponses peuvent manquer d'explication, alors allons-y, ajoutons peut-être d'autres.

Votre ligne

for file in *.txt

indique que l'extension est connue à l'avance (remarque: les environnements compatibles POSIX sont sensibles à la casse, *.txt ne correspondra pas à FOO.TXT). Dans ce cas

basename -s .txt "$file"

devrait renvoyer le nom sans l'extension (basename supprime également le chemin du répertoire: /directory/path/filenamefilename; dans votre cas, cela n'a pas d'importance, car $file ne contient pas un tel chemin). Pour utiliser l'outil dans votre code, vous avez besoin d'une substitution de commande qui se présente généralement comme suit: $(some_command). La substitution de commande prend la sortie de some_command, la traite comme une chaîne et la place là où se trouve $(…). Votre redirection particulière sera

… > "./$(basename -s .txt "$file")_sorted.txt"
#      ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the output of basename will replace this

Les citations imbriquées sont acceptables ici car Bash est suffisamment intelligent pour savoir que les citations contenues dans $(…) sont appariées.

Cela peut être amélioré. Remarque basename est un exécutable distinct, et non un shell intégré (dans Bash, exécutez type basename, comparez à type cd). Générer n'importe quel processus supplémentaire est coûteux, cela prend des ressources et du temps. Le frapper dans une boucle donne généralement des résultats médiocres. Par conséquent, vous devez utiliser tout ce que Shell vous propose pour éviter des processus supplémentaires. Dans ce cas, la solution est:

… > "./${file%.txt}_sorted.txt"

La syntaxe est expliquée ci-dessous pour un cas plus général.


Si vous ne connaissez pas l'extension:

… > "./${file%.*}_sorted.${file##*.}"

La syntaxe expliquée:

  • ${file#*.} - $file, mais la chaîne la plus courte correspondant à *. est supprimée de l'avant;
  • ${file##*.} - $file, mais la plus longue chaîne correspondant à *. est supprimée de l'avant; utilisez-le pour obtenir juste une extension;
  • ${file%.*} - $file, mais la chaîne la plus courte correspondant à .* est supprimée de la fin; utilisez-le pour obtenir tout sauf l'extension;
  • ${file%%.*} - $file, mais avec la plus longue chaîne correspondant à .* est supprimé de la fin;

La correspondance de modèle ressemble à un glob, pas à une regex. Cela signifie que * est un caractère générique pour zéro ou plusieurs caractères, ? est un caractère générique pour exactement un caractère (nous n'avons pas besoin de ? dans votre cas cependant). Lorsque vous appelez ls *.txt ou for file in *.txt;, vous utilisez le même mécanisme de correspondance de modèle. Un modèle sans caractères génériques est autorisé. Nous avons déjà utilisé ${file%.txt}, où .txt est le motif.

Exemple:

$ file=name.name2.name3.ext
$ echo "${file#*.}"
name2.name3.ext
$ echo "${file##*.}"
ext
$ echo "${file%.*}"
name.name2.name3
$ echo "${file%%.*}"
name

Mais méfiez-vous:

$ file=extensionless
$ echo "${file#*.}"
extensionless
$ echo "${file##*.}"
extensionless
$ echo "${file%.*}"
extensionless
$ echo "${file%%.*}"
extensionless

Pour cette raison, l’engin suivant pourraitêtre utile (mais ce n’est pas le cas, explication ci-dessous):

${file#${file%.*}}

Cela fonctionne en identifiant tout sauf l'extension (${file%.*}), puis supprime cela de la chaîne entière. Les résultats sont comme ça:

$ file=name.name2.name3.ext
$ echo "${file#${file%.*}}"
.ext
$ file=extensionless
$ echo "${file#${file%.*}}"

$   # empty output above

Notez que le . est inclus cette fois. Vous pourriez obtenir des résultats inattendus si $file contenait le littéral * ou ?; mais Windows (où les extensions sont importantes) n'autorise de toute façon pas ces caractères dans les noms de fichiers, vous pouvez donc vous en soucier. Cependant, […] ou {…}, s'il est présent, peut déclencher son propre schéma de correspondance de modèle et casser la solution!

Votre redirection "améliorée" serait:

… > "./${file%.*}_sorted${file#${file%.*}}"

Il devrait supporter les noms de fichiers avec ou sans extension, mais pas avec des crochets ou des accolades, malheureusement. Tout à fait dommage. Pour résoudre ce problème, vous devez doubler la variable interne.

Redirection vraiment améliorée:

… > "./${file%.*}_sorted${file#"${file%.*}"}"

Les doubles guillemets font que ${file%.*} ne se comporte pas comme un modèle! Bash est assez intelligent pour distinguer les guillemets intérieurs et extérieurs car ceux-ci sont incorporés à la syntaxe ${…} extérieure. Je pense que c'est la bonne façon .

Une autre solution (imparfaite), analysons-la pour des raisons pédagogiques:

${file/./_sorted.}

Il remplace le premier . par _sorted.. Cela fonctionnera bien si vous avez au plus un point dans $file. Il existe une syntaxe similaire ${file//./_sorted.} qui remplace tous les points. Autant que je sache, il n'y a pas de variante pour remplacer le dernierpoint uniquement.

Néanmoins, la solution initiale pour les fichiers avec . semble robuste. La solution pour $file sans extension est simple: ${file}_sorted. Maintenant, tout ce dont nous avons besoin est un moyen de différencier les deux cas. C'est ici:

[[ "$file" == *?.* ]]

Elle renvoie le statut de sortie 0 (true) si et seulement si le contenu de la variable $file correspond au modèle de droite. Le motif indique "il y a un point après au moins un caractère" ou, de manière équivalente, "il y a un point qui n'est pas au début". Le but est de traiter les fichiers cachés de Linux (par exemple, .bashrc) comme sans extension, sauf s’il existe un autredot quelque part.

Notez que nous avons besoin de [[ ici, pas [. Le premier est plus puissant mais malheureusement pas portable ; ce dernier est portable mais trop limité pour nous.

La logique va maintenant comme ceci:

[[ "$file" == *?.* ]] && file1="./${file%.*}_sorted.${file##*.}" || file1="${file}_sorted"

Après ceci, $file1 contient le nom désiré, votre redirection devrait donc être

… > "./$file1"

Et l'extrait de code complet (*.txt remplacé par * pour indiquer que nous travaillons avec n'importe quelle extension ou aucune extension):

for file in *; 
do
   printf 'Processing %s\n' "$file"
   [[ "$file" == *?.* ]] && file1="./${file%.*}_sorted.${file##*.}" || file1="${file}_sorted"
   LC_ALL=C sort -u "$file" > "./$file1"  
done

Cela essaierait de traiter les répertoires (le cas échéant) également; vous savez déjà ce qu'il faut faire pour le réparer.

19
Kamil Maciorowski