Obter nome de arquivo sem extensão no Bash

6

Eu tenho o seguinte for loop para individualmente sort todos os arquivos de texto dentro de uma pasta (ou seja, produzindo um arquivo de saída classificado para cada um).

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

Isso é quase perfeito, exceto que atualmente gera arquivos no formato de:

originalfile.txt_sorted

... enquanto gostaria que saísse arquivos no formato de:

originalfile_sorted.txt 

Isso é porque o ${file} variável contém o nome do arquivo, incluindo a extensão. Estou executando o Cygwin no topo do Windows. Não tenho certeza de como isso se comportaria em um ambiente Linux verdadeiro, mas no Windows, esse deslocamento da extensão torna o arquivo inacessível pelo Windows Explorer.

Como posso separar o nome do arquivo da extensão para que eu possa adicionar o _sorted sufixo entre os dois, permitindo-me facilmente diferenciar as versões originais e ordenadas dos arquivos, mantendo as extensões de arquivo do Windows intactas?

Eu estive olhando o que poderia estar possível soluções, mas para mim estas parecem mais equipadas para lidar com problemas mais complicados. Mais importante, com a minha atual bash conhecimento, eles passam por cima da minha cabeça, então estou esperando que haja uma solução mais simples que se aplique ao meu humilde for loop, ou então alguém pode explicar como aplicar essas soluções à minha situação.

Hashim
fonte

Respostas:

19

Estas soluções que você liga são de fato muito boas. Algumas respostas podem não ter explicação, então vamos resolver, adicionar mais algumas talvez.

Esta sua linha

for file in *.txt

indica que a extensão é conhecida de antemão (nota: os ambientes compatíveis com POSIX diferenciam maiúsculas e minúsculas, *.txt não corresponderá FOO.TXT ). Nesse caso

basename -s .txt "$file"

deve retornar o nome sem a extensão ( basename também remove o caminho do diretório: /directory/path/filename & amp; rightarrow; filename; no seu caso, não importa porque $file não contém esse caminho). Para usar a ferramenta em seu código, você precisa de uma substituição de comando que se pareça com isso em geral: $(some_command). A substituição de comandos leva a saída de some_command, trata-o como uma string e coloca-o onde $(…) é. Seu redirecionamento particular será

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

Citações aninhadas são OK aqui porque Bash é inteligente o suficiente para saber as citações dentro $(…) estão emparelhados juntos.

Isso pode ser melhorado. Nota basename é um executável separado, não um shell embutido (no Bash run type basename, comparado a type cd ). Gerar qualquer processo extra é custoso, requer recursos e tempo. Desovar em um loop geralmente tem um desempenho ruim. Portanto, você deve usar o que a shell lhe oferece para evitar processos extras. Neste caso, a solução é:

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

A sintaxe é explicada abaixo para um caso mais geral.


Caso você não saiba a extensão:

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

A sintaxe explicada:

  • ${file#*.} - $file, mas a string mais curta correspondente *. é removido da frente;
  • ${file##*.} - $file, mas a string mais longa correspondente *. é removido da frente; use-o para obter apenas uma extensão;
  • ${file%.*} - $file, mas a string mais curta correspondente .* é removido do final; use-o para obter tudo menos a extensão;
  • ${file%%.*} - $file, mas com a correspondência de cadeia mais longa .* é removido do final;

Correspondência de padrões é glob-like, não regex. Isso significa * é um curinga para zero ou mais caracteres, ? é um curinga para exatamente um caractere (não precisamos ? no seu caso embora). Quando você invoca ls *.txt ou for file in *.txt; você está usando o mesmo mecanismo de correspondência de padrões. Um padrão sem curingas é permitido. Nós já usamos ${file%.txt} Onde .txt é o padrão.

Exemplo:

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

Mas cuidado:

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

Por esta razão, a seguinte engenhoca poderia ser útil (mas não é, explicação abaixo):

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

Ele funciona identificando tudo menos a extensão ( ${file%.*} ), em seguida, remove isso da cadeia inteira. Os resultados são assim:

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

$   # empty output above

Note o . está incluído neste momento. Você pode obter resultados inesperados se $file literalmente contido * ou ?; mas o Windows (onde as extensões são importantes) não permite esses caracteres em nomes de arquivos de qualquer maneira, então você pode não se importar. Contudo […] ou {…}, se presente, pode desencadear seu próprio esquema de correspondência de padrões e quebrar a solução!

Seu redirecionamento "melhorado" seria:

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

Deve suportar nomes de arquivos com ou sem extensão, embora não com colchetes ou chaves, infelizmente. Uma pena. Para corrigi-lo, você precisa duplicar a variável interna.

Redirecionamento realmente melhorado:

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

Cotação dupla faz ${file%.*} não atuar como um padrão! Bash é inteligente o suficiente para distinguir citações internas e externas porque as internas estão embutidas no exterior ${…} sintaxe. Eu acho que esse é o jeito certo .

Outra solução (imperfeita), vamos analisá-lo por razões educacionais:

${file/./_sorted.}

Substitui o primeiro . com _sorted.. Ele funcionará bem se você tiver no máximo um ponto $file. Existe uma sintaxe semelhante ${file//./_sorted.} que substitui todos os pontos. Tanto quanto eu sei, não há variante para substituir o último ponto apenas.

Ainda a solução inicial para arquivos com . parece robusto. A solução para sem extensão $file é trivial: ${file}_sorted. Agora tudo o que precisamos é uma maneira de distinguir os dois casos. Aqui está:

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

Retorna o status de saída 0 (verdadeiro) se e somente se o conteúdo de $file variável corresponde ao padrão do lado direito. O padrão diz "há um ponto depois de pelo menos um caractere" ou "há um ponto que não está no começo". O objetivo é tratar os arquivos ocultos do Linux (por exemplo, .bashrc ) como sem extensão, a menos que haja outro pontilhe em algum lugar.

Note que precisamos [[ aqui não [. O primeiro é mais poderoso, mas infelizmente não portável ; o último é portátil, mas muito limitado para nós.

A lógica agora é assim:

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

Depois disto, $file1 contém o nome desejado, portanto seu redirecionamento deve ser

… > "./$file1"

E todo o trecho de código ( *.txt substituído por * para indicar que trabalhamos com qualquer extensão ou sem extensão):

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

Isso tentaria processar diretórios (se houver) também; você já sabe o que fazer para fixar isso.

Kamil Maciorowski
fonte
Mais uma vez, uma resposta brilhante, obrigada. Eu definitivamente estou longe de entender tudo isso, mas por enquanto eu vou deixar isso para um lado e apenas ler mais sobre a substituição de comandos quando eu tiver tempo. Uma pergunta que tenho: você mencionou que … > "./${file%.txt}_sorted.txt" "evita processos extras" - é porque estamos usando basename no $file variável fora do for loop aqui: basename -s .txt "$file"... ou eu entendi mal?
Hashim
@Hashim … > "./${file%.txt}_sorted.txt" é a única mudança que você precisa fazer no seu script (reticências apenas indica tudo o que você tem antes >, Está não um personagem real que você deve colocar no seu script; substituir > e o resto da linha com > "./${file%.txt}_sorted.txt" ). Evita processos extras porque agora não usamos basename em absoluto ; toda a magia é feita pela própria casca graças a ${file%.txt} sintaxe. Nota lateral: sola basename -s .txt "$file" apenas imprime algo; se você acha que altera a variável, você está errado.
Kamil Maciorowski
Ah, então a substituição de comando está sendo usada em vez de basename em vez de ao lado dele. Entendo. Obrigado novamente por sua ajuda.
Hashim
1
@Hashim Não é bem assim. Este fragmento > "./$(basename -s .txt "$file")_sorted.txt" usa a substituição de comando, o comando é basename …. Você usa isso ou > "./${file%.txt}_sorted.txt" que não usa substituição de comando. Então é (substituição de comando + basename ) xor apenas expansão variável fantasia ${file%.txt} sem substituição de comando.
Kamil Maciorowski
@Hashim Ou talvez eu não entendi o seu "em vez de basename ".
Kamil Maciorowski