![在 bash 中建立我自己的 cp 函數](https://rvso.com/image/112012/%E5%9C%A8%20bash%20%E4%B8%AD%E5%BB%BA%E7%AB%8B%E6%88%91%E8%87%AA%E5%B7%B1%E7%9A%84%20cp%20%E5%87%BD%E6%95%B8.png)
cp
對於一項作業,我被要求巧妙地編寫一個 bash 函數,該函數具有與函數(複製)相同的基本功能。它只需將一個文件複製到另一個文件,因此無需將多個文件複製到新目錄。
由於我是 bash 語言的新手,我無法理解為什麼我的程式無法運作。原始函數要求覆蓋文件(如果它已經存在),所以我嘗試實現它。它失敗。
該文件似乎在多行上失敗,但最重要的是在檢查要複製到的文件是否已存在的情況下([-e "$2"]
)。即便如此,它仍然顯示如果滿足該條件(檔案名稱...)則應該觸發的訊息。
有人可以幫我修復這個文件,可能為我對語言的基本理解提供一些有用的見解嗎?代碼如下。
#!/bin/sh
echo "file variable: $2"
if [-e file]&> /dev/null
then
echo "The file name already exists, want to overwrite? (yes/no)"
read | tr "[A-Z]" "[a-z]"
if [$REPLY -eq "yes"] ; then
rm "$2"
echo $2 > "$2"
exit 0
else
exit 1
fi
else
cat $1 | $2
exit 0
fi
答案1
如果目標文件已經存在,該cp
實用程式將很樂意覆寫該文件,而不提示使用者。
cp
實現基本功能而不使用的函數cp
將是
cp () {
cat "$1" >"$2"
}
如果您想在覆蓋目標之前提示使用者(請注意,如果函數是由非互動式 shell 呼叫的,則可能不需要這樣做):
cp () {
if [ -e "$2" ]; then
printf '"%s" exists, overwrite (y/n): ' "$2" >&2
read
case "$REPLY" in
n*|N*) return ;;
esac
fi
cat "$1" >"$2"
}
診斷訊息應發送至標準錯誤流。這就是我所做的printf ... >&2
。
請注意,我們實際上並不需要rm
目標文件,因為重定向會截斷它。要是我們做過首先想要rm
它,然後你必須檢查它是否是一個目錄,如果是,則將目標檔案放在該目錄中,就像這樣cp
。這是這樣做的,但仍然沒有明確的rm
:
cp () {
target="$2"
if [ -d "$target" ]; then
target="$target/$1"
fi
if [ -d "$target" ]; then
printf '"%s": is a directory\n' "$target" >&2
return 1
fi
if [ -e "$target" ]; then
printf '"%s" exists, overwrite (y/n): ' "$target" >&2
read
case "$REPLY" in
n*|N*) return ;;
esac
fi
cat "$1" >"$target"
}
您可能還想確保源確實存在,這是cp
做do (cat
也這樣做,所以當然可以完全省略它,但這樣做會建立一個空的目標檔案):
cp () {
if [ ! -f "$1" ]; then
printf '"%s": no such file\n' "$1" >&2
return 1
fi
target="$2"
if [ -d "$target" ]; then
target="$target/$1"
fi
if [ -d "$target" ]; then
printf '"%s": is a directory\n' "$target" >&2
return 1
fi
if [ -e "$target" ]; then
printf '"%s" exists, overwrite (y/n): ' "$target" >&2
read
case "$REPLY" in
n*|N*) return ;;
esac
fi
cat "$1" >"$target"
}
該函數不使用“bashisms”,並且應該在所有sh
類似的 shell 中工作。
透過更多的調整來支援多個原始檔案和一個-i
在覆蓋現有檔案時啟動互動式提示的標誌:
cp () {
local interactive=0
# Handle the optional -i flag
case "$1" in
-i) interactive=1
shift ;;
esac
# All command line arguments (not -i)
local -a argv=( "$@" )
# The target is at the end of argv, pull it off from there
local target="${argv[-1]}"
unset argv[-1]
# Get the source file names
local -a sources=( "${argv[@]}" )
for source in "${sources[@]}"; do
# Skip source files that do not exist
if [ ! -f "$source" ]; then
printf '"%s": no such file\n' "$source" >&2
continue
fi
local _target="$target"
if [ -d "$_target" ]; then
# Target is a directory, put file inside
_target="$_target/$source"
elif (( ${#sources[@]} > 1 )); then
# More than one source, target needs to be a directory
printf '"%s": not a directory\n' "$target" >&2
return 1
fi
if [ -d "$_target" ]; then
# Target can not be overwritten, is directory
printf '"%s": is a directory\n' "$_target" >&2
continue
fi
if [ "$source" -ef "$_target" ]; then
printf '"%s" and "%s" are the same file\n' "$source" "$_target" >&2
continue
fi
if [ -e "$_target" ] && (( interactive )); then
# Prompt user for overwriting target file
printf '"%s" exists, overwrite (y/n): ' "$_target" >&2
read
case "$REPLY" in
n*|N*) continue ;;
esac
fi
cat -- "$source" >"$_target"
done
}
您的程式碼中的間距錯誤if [ ... ]
(需要在 之前、之後[
和之前留出空間]
)。您也不應該嘗試將測試重定向到,/dev/null
因為測試本身沒有輸出。此外,第一個測試應該使用位置參數$2
,而不是字串file
。
像我一樣使用case ... esac
,您可以避免使用小寫/大寫來自用戶的回應tr
。在 中bash
,如果您無論如何都想這樣做,一種更便宜的方法是使用REPLY="${REPLY^^}"
(for uppercasing) 或REPLY="${REPLY,,}"
(for lowercasing)。
如果使用者對您的程式碼說“是”,則該函數會將目標檔案的檔案名稱放入目標檔案中。這不是原始檔案的副本。它應該會落到函數的實際複製位。
複製位是您使用管道實現的。管道用於將資料從一個命令的輸出傳遞到另一個命令的輸入。這不是我們需要在這裡做的事情。只需調用cat
來源檔案並將其輸出重定向到目標檔案即可。
你之前打電話也有同樣的問題tr
。 read
將設定變數的值,但不產生輸出,因此透過管道傳輸read
任何內容都是無意義的。
除非用戶說“不”,否則不需要明確退出(或者該函數遇到一些錯誤情況,如我的程式碼位元中所示,但因為它是我使用的函數return
而不是exit
)。
另外,你說的是“函數”,但你的實作是一個腳本。
看看https://www.shellcheck.net/,它是識別 shell 腳本中有問題的部分的好工具。
使用cat
只是一複製文件內容的方法。其他方式包括
dd if="$1" of="$2" 2>/dev/null
- 使用任何類似過濾器的實用程序,可以使資料僅通過,例如
sed "" "$1" >"2"
或awk '1' "$1" >"$2"
等tr '.' '.' <"$1" >"$2"
。 - ETC。
棘手的一點是讓函數將元資料(所有權和權限)從來源複製到目標。
cp
另一件需要注意的事情是,如果目標是/dev/tty
(非常規文件)之類的東西,我編寫的函數的行為將完全不同。