題 在Bash中獲取沒有擴展名的文件名


我有以下內容 for 循環到單獨 sort 文件夾內的所有文本文件(即為每個文件生成一個已排序的輸出文件)。

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

這幾乎是完美的,除了它目前以下列格式輸出文件:

originalfile.txt_sorted

...而我希望它以下列格式輸出文件:

originalfile_sorted.txt 

這是因為 ${file} 變量包含包含擴展名的文件名。我在Windows上運行Cygwin。我不確定這在真正的Linux環境中會如何表現,但在Windows中,這種擴展的轉移使得Windows資源管理器無法訪問該文件。

如何將文件名與擴展名分開,以便我可以添加 _sorted 兩者之間的後綴,允許我輕鬆區分文件的原始版本和排序版本,同時仍保持Windows的文件擴展名完好無損?

我一直在看什麼 威力 是 可能 解決方案,但對我來說,這些似乎更適合處理更複雜的問題。更重要的是,用我當前的 bash 知識,他們在我的頭上,所以我希望有一個更簡單的解決方案適用於我的謙虛 for 循環,或者有人可以解釋如何將這些解決方案應用到我的情況。


6
2017-09-14 00:03


起源




答案:


您鏈接到的這些解決方案實際上非常好。有些答案可能缺乏解釋,所以讓我們把它整理出來,再補充一些。

你的這一行

for file in *.txt

表示擴展事先已知(注意:符合POSIX的環境區分大小寫, *.txt 不會匹配 FOO.TXT)。在這種情況下

basename -s .txt "$file"

應該返回沒有擴展名的名稱(basename 還會刪除目錄路徑: /directory/path/filename &右箭頭 filename;在你的情況下,這沒關係,因為 $file 不包含這樣的路徑)。要在代碼中使用該工具,您需要一般看起來像這樣的命令替換: $(some_command)。命令替換取輸出 some_command,將其視為一個字符串,並將其放在哪裡 $(…) 是。您的特定重定向將是

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

嵌套引號在這裡是可以的,因為Bash足夠聰明,可以知道其中的引號 $(…) 配對在一起。

這可以改進。注意 basename 是一個單獨的可執行文件,而不是shell內置(在Bash運行中 type basename, 相比於 type cd)。產生任何額外的過程是昂貴的,它需要資源和時間。在循環中產生它通常表現不佳。因此,您應該使用shell提供的任何內容來避免額外的進程。在這種情況下,解決方案是:

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

下面針對更一般的情況解釋語法。


如果您不知道擴展名:

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

語法解釋如下:

  • ${file#*.}  - $file,但最短的字符串匹配 *. 被從前面移除;
  • ${file##*.}  - $file,但最長的字符串匹配 *. 被從前面移除;用它來獲得一個擴展;
  • ${file%.*}  - $file,但最短的字符串匹配 .* 從最後刪除;用它來獲得除擴展之外的所有東西;
  • ${file%%.*}  - $file,但最長的字符串匹配 .* 從最後刪除;

模式匹配是類似於glob的,而不是正則表達式。這意味著 * 是零個或多個字符的通配符, ? 是一個通配符,只有一個字符(我們不需要 ? 在你的情況下)。當你調用時 ls *.txt 要么 for file in *.txt; 你正在使用相同的模式匹配機制。允許使用沒有通配符的模式。我們已經用過 ${file%.txt} 哪裡 .txt 是模式。

例:

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

但要注意:

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

出於這個原因,以下的裝置 威力 有用(但不是,下面說明):

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

它通過識別除擴展之外的所有內容(${file%.*}),然後從整個字符串中刪除它。結果如下:

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

$   # empty output above

請注意 . 這次包括在內。如果,您可能會得到意外的結果 $file 包含文字 * 要么 ?;但是Windows(擴展很重要) 不允許 無論如何,這些字符在文件名中,所以你可能不在乎。然而 […] 要么 {…}如果存在,可能會觸發自己的模式匹配方案並打破解決方案!

您的“改進”重定向將是:

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

不幸的是,它應該支持帶或不帶擴展名的文件名,儘管沒有方括號或大括號。 真可惜。 要修復它,你需要雙引號內部變量。

真正改進的重定向:

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

雙引號使 ${file%.*} 不要充當模式! Bash足夠聰明,可以區分內部和外部引號,因為內部引用嵌入在外部 ${…} 句法。 我認為這是正確的方法

另一個(不完美的)解決方案,讓我們分析它是出於教育原因:

${file/./_sorted.}

它取代了第一個 . 同 _sorted.。如果你最多有一個點,它將工作正常 $file。有類似的語法 ${file//./_sorted.} 取代所有點。據我所知,沒有替代品的替代品 持續 只有點。

仍然是文件的初始解決方案 . 看起來很健壯無擴展的解決方案 $file 是微不足道的: ${file}_sorted。現在我們所需要的只是一種方法來區分這兩種情況。這裡是:

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

當且僅當內容為。時,它返回退出狀態0(true) $file 變量匹配右側模式。該模式表示“至少有一個字符後有一個點”或等效地“有一個點不在開頭”。重點是處理Linux隱藏文件(例如 .bashrc)無延伸,除非有 另一個 在某個地方。

請注意我們需要 [[ 在這裡,不是 [。前者更強大但不幸的是 不便攜;後者是便攜式的,但對我們來說太有限了。

邏輯現在是這樣的:

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

在這之後, $file1 包含所需的名稱,因此您的重定向應該是

… > "./$file1"

整個代碼片段(*.txt 換成了 * 表明我們使用任何延期或不延期):

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

這會嘗試處理目錄(如果有的話);你已經知道了 該怎麼辦 要解決這個問題。


19
2017-09-14 00:20



再一次,一個精彩的答案,謝謝。我對理解所有這一切肯定還有很長的路要走,但是現在我要把它留到一邊,當我有時間的時候,就讀更多關於命令替換的內容。我有一個問題:你提到過 … > "./${file%.txt}_sorted.txt" “避免額外的進程” - 這是因為我們在使用中使用了basename $file 變量之外的變量 for 循環在這裡: basename -s .txt "$file"......還是我誤解了? - Hashim
@Hashim … > "./${file%.txt}_sorted.txt" 是您需要對腳本進行的唯一更改(省略號 … 只是表明你以前擁有的一切 >, 它的 不 你應該在劇本中放置的實際角色;更換 > 和其餘的一致 > "./${file%.txt}_sorted.txt")。它避免了額外的進程,因為現在我們不使用 basename  一點都不;整個魔術都是由貝殼本身完成的 ${file%.txt} 句法。旁注:鞋底 basename -s .txt "$file" 只打印一些東西;如果你認為它改變了變量,你就錯了。 - Kamil Maciorowski
啊,所以正在使用命令替換而不是 basename 而不是與它並排。我知道了。再次感謝你的幫助。 - Hashim
@Hashim不太好。這個片段 > "./$(basename -s .txt "$file")_sorted.txt" 使用命令替換,命令是 basename …。你要么使用這個,要么 > "./${file%.txt}_sorted.txt" 它不使用命令替換。所以它是(命令替換+ basename) XOR 只是花哨的變量擴展 ${file%.txt} 沒有命令替換。 - Kamil Maciorowski
@Hashim或許我不明白你的“而不是 basename“。 - Kamil Maciorowski