shell - sh 和 ksh 之间不同的管道行为

标签 shell scripting sh ksh pipeline

我已将问题隔离到以下代码片段:

  1. 请注意,当使用 ksh 运行脚本时,空字符串会被分配给 LATEST_FILE_NAME='';但当使用 sh 运行时,脚本会正确地将值分配给变量 $LATEST_FILE_NAME。这反过来会影响 $FILE_LIST_COUNT 的值。
  2. 但由于脚本位于 KornShell (ksh) 中,我不确定是什么原因导致了该问题。
  3. 当我注释掉下面一行中的 tee 命令时,ksh 脚本可以正常工作并正确地将值分配给变量 $LATEST_FILE_NAME
(cd $SOURCE_FILE_PATH; ls *.txt 2>/dev/null) | sort -r > ${SOURCE_FILE_PATH}/${FILE_LIST} | tee -a $LOG_FILE_PATH

请考虑:

<强>1。源代码:script.sh

#!/usr/bin/ksh
set -vx # Enable debugging

SCRIPTLOGSDIR=/some/path/Scripts/TEST/shell_issue
SOURCE_FILE_PATH=/some/path/Scripts/TEST/shell_issue
# Log file
Timestamp=`date +%Y%m%d%H%M`
LOG_FILENAME="TEST_LOGS_${Timestamp}.log"
LOG_FILE_PATH="${SCRIPTLOGSDIR}/${LOG_FILENAME}"
## Temporary files
FILE_LIST=FILE_LIST.temp    #Will store all  extract filenames
FILE_LIST_COUNT=0           # Stores total number of  files

getFileListDetails(){
    rm -f $SOURCE_FILE_PATH/$FILE_LIST 2>&1 | tee -a $LOG_FILE_PATH

    # Get list of all files, Sort in reverse order, and store names of the  files line-wise. If no files are found, error is muted.
    (cd $SOURCE_FILE_PATH; ls *.txt 2>/dev/null) | sort -r > ${SOURCE_FILE_PATH}/${FILE_LIST} | tee -a $LOG_FILE_PATH

    if [[ ! -f $SOURCE_FILE_PATH/$FILE_LIST ]]; then
        echo "FATAL ERROR - Could not create a temp file for  file list.";exit 1;
    fi

    LATEST_FILE_NAME="$(cd $SOURCE_FILE_PATH; head -1 $FILE_LIST)";
    FILE_LIST_COUNT="$(cat $SOURCE_FILE_PATH/$FILE_LIST | wc -l)";

}

getFileListDetails;
exit 0;

<强>2。使用shell时的输出 sh script.sh:

+ getFileListDetails
+ rm -f /some/path/Scripts/TEST/shell_issue/FILE_LIST.temp
+ tee -a /some/path/Scripts/TEST/shell_issue/TEST_LOGS_201304300506.log
+ cd /some/path/Scripts/TEST/shell_issue
+ sort -r
+ tee -a /some/path/Scripts/TEST/shell_issue/TEST_LOGS_201304300506.log
+ ls 1.txt 2.txt 3.txt
+ [[ ! -f /some/path/Scripts/TEST/shell_issue/FILE_LIST.temp ]]
cd $SOURCE_FILE_PATH; head -1 $FILE_LIST
++ cd /some/path/Scripts/TEST/shell_issue
++ head -1 FILE_LIST.temp
+ LATEST_FILE_NAME=3.txt
cat $SOURCE_FILE_PATH/$FILE_LIST | wc -l
++ cat /some/path/Scripts/TEST/shell_issue/FILE_LIST.temp
++ wc -l
+ FILE_LIST_COUNT=3
exit 0;
+ exit 0

<强>3。使用ksh时的输出 ksh script.sh:

+ getFileListDetails
+ tee -a /some/path/Scripts/TEST/shell_issue/TEST_LOGS_201304300507.log
+ rm -f /some/path/Scripts/TEST/shell_issue/FILE_LIST.temp
+ 2>& 1
+ tee -a /some/path/Scripts/TEST/shell_issue/TEST_LOGS_201304300507.log
+ sort -r
+ 1> /some/path/Scripts/TEST/shell_issue/FILE_LIST.temp
+ cd /some/path/Scripts/TEST/shell_issue
+ ls 1.txt 2.txt 3.txt
+ 2> /dev/null
+ [[ ! -f /some/path/Scripts/TEST/shell_issue/FILE_LIST.temp ]]
+ cd /some/path/Scripts/TEST/shell_issue
+ head -1 FILE_LIST.temp
+ LATEST_FILE_NAME=''
+ wc -l
+ cat /some/path/Scripts/TEST/shell_issue/FILE_LIST.temp
+ FILE_LIST_COUNT=0
exit 0;+ exit 0

最佳答案

好吧,开始了……这是一个棘手而微妙的问题。答案在于管道是如何实现的。 POSIX指出

If the pipeline is not in the background (see Asynchronous Lists), the shell shall wait for the last command specified in the pipeline to complete, and may also wait for all commands to complete.)

注意关键字may。许多 shell 以所有命令都需要完成的方式实现这一点,例如请参阅联机帮助页:

The shell waits for all commands in the pipeline to terminate before returning a value.

请注意 中的措辞联机帮助页:

Each command, except possibly the last, is run as a separate process; the shell waits for the last command to terminate.

在您的示例中,最后一个命令是 tee命令。由于tee没有输入因为你重定向stdout${SOURCE_FILE_PATH}/${FILE_LIST}在之前的命令中,它立即退出。过于简单化地说,tee比之前的重定向更快,这意味着当您读取文件时,文件可能尚未完成写入。您可以通过添加 sleep 来测试这一点(这不是修复!)在整个命令的末尾:

$ ksh -c 'ls /tmp/* | sort -r > /tmp/foo.txt | tee /tmp/bar.txt; echo "[$(head -n 1 /tmp/foo.txt)]"'
[]

$ ksh -c 'ls /tmp/* | sort -r > /tmp/foo.txt | tee /tmp/bar.txt; sleep 0.1; echo "[$(head -n 1 /tmp/foo.txt)]"'
[/tmp/sess_vo93c7h7jp2a49tvmo7lbn6r63]

$ bash -c 'ls /tmp/* | sort -r > /tmp/foo.txt | tee /tmp/bar.txt; echo "[$(head -n 1 /tmp/foo.txt)]"'
[/tmp/sess_vo93c7h7jp2a49tvmo7lbn6r63]

话虽如此,这里还有一些其他需要考虑的事情:

  1. 始终引用您的变量,尤其是在处理文件时,以避免通配、分词(如果您的路径包含空格)等问题:

    do_something "${this_is_my_file}"

  2. head -1已弃用,请使用 head -n 1

  3. 如果一行中只有一个命令,则结束分号 ;是多余的...跳过它

  4. LATEST_FILE_NAME="$(cd $SOURCE_FILE_PATH; head -1 $FILE_LIST)"

    不需要cd首先进入目录,只需将整个路径指定为 head 的参数:

    LATEST_FILE_NAME="$(head -n 1 "${SOURCE_FILE_PATH}/${FILE_LIST}")"

  5. FILE_LIST_COUNT="$(cat $SOURCE_FILE_PATH/$FILE_LIST | wc -l)"

    这称为Useless Use Of Cat因为cat不需要 - wc可以处理文件。您可能使用它是因为 wc -l myfile 的输出包括文件名,但您可以使用例如FILE_LIST_COUNT="$(wc -l < "${SOURCE_FILE_PATH}/${FILE_LIST}")"相反。

此外,您还需要阅读 Why you shouldn't parse the output of ls(1)How can I get the newest (or oldest) file from a directory? .

关于shell - sh 和 ksh 之间不同的管道行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16069339/

相关文章:

android shell命令获取结果

linux - Bash 脚本获取记录文件的时间戳并将其从另一个日志文件的时间戳中减去

perl - 为什么 prinseqlite perl 循环中没有写入输出文件?

bash -/bin/sh VS/bin/bash 中的 Unicode 字符

linux - Grep 查找行号后的下一个空行

python - Python 中的 Tab 键不缩进

mysql - 从 shell 将多个 .sql 转储文件导入 mysql 数据库

linux - 当没有输入参数时,如何使用 getopts 来避免运行脚本?

python - 让 bash 脚本将多个程序作为单独的进程执行

linux - 从最后一行开始读取文件