随机基准

随机基准

我正在探索让 shell 脚本输入和输出字符串的二维结构的概念,但限制是字符串不包含换行符。它们可以包含空格。

我想要尝试执行此操作的方法是使用未转义的空格作为水平轴的分隔符,换行符是垂直轴的分隔符。

为了进行实验,我使用以下小工具来检查参数:

❯ cat argshow                     
#!/bin/bash                       
                                  
ITERATION=1                       
while (( "$#" )); do              
  echo '$'"${ITERATION}: ${1}"    
  shift                           
  (( ITERATION ++ ))              
done                              

例子:

❯ argshow abc\ def zzz
$1: abc def           
$2: zzz               

根据我正在建立的约定,这是我的第一个测试用例:

echo 'abc\ def ghi\njkl'

这表示一个值(以 JSON 形式表示)

[["abc def", "ghi"], ["jkl"]]

好的。继续前进。

我首先尝试了这个。我在我的 zsh shell 中运行了这个。

❯ echo 'abc\ def ghi\njkl' | while read -r line; do argshow $line; done 
$1: abc\ def ghi                                                        
$1: jkl                                                                 

我对此不太高兴,因为argshow在第一行中收到了一个参数,而它应该是两个。使用-rforread能够恢复反斜杠,这很好(稍后我会担心制表符......)。

我在 bash shell 中运行了相同的命令:

$ echo 'abc\ def ghi\njkl' | while read -r line; do argshow $line; done
$1: abc\
$2: def
$3: ghi\njkl
$ echo $'abc\ def ghi\njkl' | while read -r line; do argshow $line; done
$1: abc\
$2: def
$3: ghi
$1: jkl

这是我的 bash 知识达到极限的地方。这些都不是我所期望的。为什么我必须插入一个eval才能使其按我想要的方式工作?:

$ echo $'abc\ def ghi\njkl' | while read -r line; do eval argshow $line; done
$1: abc def
$2: ghi
$1: jkl

我的印象是,如果我在放置 bash 变量时不使用任何引号,它就会被扩展并拆分为所需的任何内容。毕竟,这就是shellcheck当我在 shell 脚本中使用 shell 变量而不用双引号引用它们时对我大喊大叫的原因。我认为它们总是会被扩展,所以如果变量中碰巧有 2 个空格字符,它就会变成 3 个参数,而不是预期的 1 个参数。

我无法使用此方法来制作我经常使用的脚本。eval进入脚本时会涉及​​到很多邪恶的漏洞。这将是诱人的,因为它允许诸如将参数放入引号中之类的免费功能,可能能够转义换行符,并且由于熟悉而很容易记住语法,但在脚本中使用它太危险了,因为它插入未转义的分号来导致代码执行是很简单的。

也许我真的发现了 shell 脚本的局限性,并且显然需要用真正的编程语言来实现它。这是公平的,但我希望有一些简单的选项可以用来解决这个问题并继续进行这个黑客攻击。

是的,我也知道我可能可以做一些事情,比如传递 bash 数组,但我希望能够将这些数据流转储到简单的文件中,这就是它的全部吸引力,在于它隐式地具有超级基本的功能人类可读的格式。


有关手头任务的更多背景信息...我正在转向一种更加计算机辅助的数据管理方案形式。我的想法是,如果我在相机/无人机等中使用 512GB SD 卡之类的东西。我希望能够将一些较旧的数据保留在存储卡上,以方便起见,也为了工作流程,例如有时我想将一些内容转储到我的 Macbook 上以便更轻松地查看。当将其迁移到我的工作站上的 ZFS 池时,会出现哪些文件已保存的问题,因此我不需要将它们复制回来并浪费空间。有时,如果我过于关注这方面,就会发生无法接受一些应该备份的新内容的情况。在擦除磁盘以便能够继续使用之前检查磁盘的剩余内容(可能几乎已满)始终是一个耗时的过程。

因此,我现在使用的方法是定期构建一个存储池数据库find /pool /pool2 -type f -printf "%M %s %t %p\\n" > ~/Dropbox/find_zfs,然后这个数据库可以很容易地用于不同的事情。导致我产生这个问题的是在编写以下脚本(我将其命名为trawl)的过程中:

#!/bin/bash

# provide a list of dirs and for each of their immediate child file and folder names, trawl through 
# a standard file listing record (this is the FIRST arg) (the kind I ususally use with fzf) to 
# look for any matches. emit human readable output on stderr, and stdout will be a list paths.

# That output will be organized for now as a flat list. The idea here is that the list-of-lists 
# structure of it can trivially be recovered by a wrapping script that knows the input args and can 
# correlate them back out. Inefficient, but simple.

# TODO TODO TODO implement a sanitizer on the input and do a fuzzy check for that to further 
# eliminate manual checking via fzf and suggest matches when they are found

FIRSTARG="$1"
shift

for DIR in "$@"; do
    >&2 echo "CHECKING DIR: $DIR"
    ls -1 "$DIR" | while read -r CHILD; do
        RG_OUT=$(rg -F "$CHILD" "$FIRSTARG")
        if [ $? -eq 0 ]; then
            >&2 echo "$DIR/$CHILD is found with $(echo "$RG_OUT" | wc -l) hits; skipping!"
        else
            >&2 printf "NOT FOUND; adding: "
            echo "$DIR/$CHILD"
        fi
    done
done

我还制作了一个递归版本,以更详尽地检查每个文件,而不仅仅是给定目录的第一级子文件。

正如您所看到的,这样做的想法是我基本上可以实现半自动文件备份重复数据删除。

当然,在有人提到它之前,我可能只是为 ZFS 本身启用重复数据删除功能,但人们普遍认为,它只值得在非常狭窄的用例中施加的各种性能成本,其中(主要是)批量介质备份不是其中之一。

您可能会问,这如何适应,我想要一个“同步点”,这就是trawl这里实现的,二维结构的两个轴是:(X)输入目录中的所有单独子文件,以及(Y ) 单独的输入目录。

我最终确实回避了二维要求,正如您在我的脚本注释中看到的那样,因为事实证明它并不是那么重要,而且我可以在事后将其逆转。事实上,如果我确实想保存它,那么发出 JSON 是最明智的。

有了这个,工作流程现在是

  • 插入磁盘(如果适用)
  • trawl ~/Dropbox/find_zfs srcdir/ srcdir2/
  • 目视检查输出以确认其有意义
  • 再次运行它,但通过管道输入mkdir -p /pool/backup-$(date +%F); while read -r LINE; do mv "$LINE" /pool/backup-$(date +%F); done

这有点有趣,因为我确实有一个真正的打字稿程序,我编写了这个程序来为我完成同样的事情,并且它确实具有相同的功能,但实现交互式和灵活的工作流程并不那么自然。

答案1

如果您想在一个变量中包含多个单词,请使用数组,而不是标量变量,如果您想read\其视为转义运算符(用于字段分隔符和行继续),请省略-r,这就是read默认情况下不执行的操作。

所以,在 ksh / zsh / yash 中:

read -A fields; showargs "${fields[@]}"

或者在bash中:

read -a fields; showargs "${fields[@]}"

(注意字符read分割$IFS,默认情况下恰好包含空格、制表符和换行符(以及 zsh 中的 nul))。

$IFS在 zsh 中,您可以像在其他类似 Bourne 的 shell 中一样,通过使用 (助记符:看起来像剪刀) 对字符进行$=line$line$=引号的标量变量扩展拆分。或者$=~line对于其他 shell 执行的完整 split+glob,并且 shellcheck 会警告您。或者${(s[ ])line}明确地分割空间而不是$IFS当前包含的内容。但在这里,这不会帮助您处理反斜杠。

要处理任意 shell 语法引用,而不仅仅是反斜杠(进行 shell 语法标记化和引号删除)而不求助于eval,请参阅@thrig 的好答案关于zsh 的zQ参数扩展标志。不过我要补充一点,如果你想保留空元素,你需要:

$ line='foo\ bar "" blah'
$ fields=( "${(Q@)${(z)line}}" )
$ typeset fields
fields=( 'foo bar' '' blah )

或者Z[n]代替z换行符也被视为令牌分隔符而不是命令分隔符令牌。

无论如何,这都是在进行完整的 shell 语法(此处为 zsh 语法)标记化,而不仅限于引号处理。例如,a$(b c)<y>y<<<"a b"一行将被标记为fields=( 'a$(b c)' '<' y '>' y '<<<' 'a b' ),而不是fields=( 'a$(b' 'c)<y>y<<<a b' )您希望的那样。

对于具有多维数组和数据(反)序列化 1 支持的 shell,请参阅 ksh93:

$ printf '%s\n' $'( (\'a b\' a) (\'b\\c\n\' d) )' | 
> ksh -c 'read -C a; typeset -p a; printf "<%s>\n" "${a[1][0]}"'
typeset -a a=(('a b' a) ($'b\\c\n' d) )
<b\c
>

虽然我上次测试过它(很多年前),但它有很多错误,而且很可能仍然如此。eval如果不能保证输入是有效的序列化流,我不能保证它会比使用更安全。

哦!实际上不是:

$ printf '%s\n' '( [`uname>&2`]=1 )' | ksh -c 'read -C a'
Linux

另请注意,JSON 字符串可以包含 NUL,但只有 zsh 的变量中可以包含 NUL。使用适当的编程语言而不是 shell 来处理复杂的数据结构对我来说更有意义。


1 根据记录,AT&T 在开发团队解散之前发布了 ksh93 的最后一个 beta 版本,ksh2020(现已废弃)所基于的 ksh93 也对 JSON(反)序列化提供了一些实验性支持,尽管它有很多错误(并且因此从 ksh2020 中删除)。它还具有 CSV 输入/输出支持。

答案2

read将所有附加列分配给给定的姓氏,因此所有列最终都在您的line变量中。在ZSH中:

% echo 'abc\ def ghi' | while read -r line; do print -Rl $line; done
abc\ def ghi
% echo 'abc\ def ghi' | while read -r line and; do print -Rl $line $and; done
abc\
def ghi
% echo 'abc\ def ghi' | while read -r line and another; do print -Rl $line $and $another; done
abc\
def
ghi

因此,您实际上需要插值(eval最坏的情况是)line来处理反斜杠和文字\n字符串。在 ZSH 中,(z)“像 zsh 命令行一样分割单词”非常接近:

% echo 'abc\ def ghi' | while read -r line; do cols=(${(z)line}); printf ">%s<\n" $cols[@]; done
>abc\ def<
>ghi<

也不echo是真正可移植的,可能应该在可能的情况下替换为printfshell 特定的内置函数,例如printZSH 中的:

% printf "abc\ def ghi\njkl" | while read -r line; do cols=(${(z)line}); printf ">%s<\n" $cols[@]; done
>abc\ def<
>ghi<

哎呀,我们jkl去哪儿了?无声的数据丢失可不是什么好玩的事。

% printf "abc\ def ghi\njkl\n" | while read -r line; do cols=(${(z)line}); printf ">%s<\n" $cols[@]; done
>abc\ def<
>ghi<
>jkl<

就这样……但是仍然存在这种\情况,也许我们可以取消引用它?还是在ZSH:

% printf "abc\ def ghi\njkl\n" | while read -r line; do cols=(${(z)line}); print -l ${(Q)cols}; done
abc def
ghi
jkl

请注意,printf正在提供换行符;如果\n出现文字则有也可以插入这些选项,这是更多的工作。在大多数其他 shell 中,您将执行 an 操作,eval因为它们可能没有合适的参数标志来执行 ZSH 的操作。因此,eval如果您想继续在该项目中使用 shell,那么要么到处都是,希望什么都不执行,要么是脚本的两个不同版本。也许(z)在一些测试中,我不能 100% 确定 ZSH 总是会按照您的输入格式执行您想要的操作。

(我确实为从 shell 切换设置了一个非常低的阈值,特别是在while使用类似棘手的东西时,或者脚本长度超过大约 20 行时。)

随机基准

对于源自 . 的 265625 行输入文件,Perl 比 ZSH 快 1282% /etc/passwd

#!/usr/bin/env perl
#our @array; # I'll let the shell folks puzzle out multi-dimensional arrays
while (readline) {
    chomp;
    #push @array, [ map { s/\\ / /gr } split /(?<!\\) / ];
    print join(' ', map { s/\\ / /gr } split /(?<!\\) /), "\n";
}

#!/usr/bin/env zsh
# do you like silent data loss? if so, remove the || check
while IFS= read line || [ -n "$line" ]; do
  fields=( "${(Q@)${(z)line}}" )
  typeset -p fields
done

答案3

碰巧,shell 在处理拆分(不带引号的变量扩展)或读取时可能会非常浅。

当您的代码是时,您会引入这个问题执行 argshow $line

使用bash:

$ set -- 1 2 3 4
$ echo ${@@A}
set -- '1' '2' '3' '4'

$ var='1 2 3 4'
$ set -- $var
$ echo ${@@A}
set -- '1' '2' '3' '4'         # that seems to work.

$ var='1 2\ 3 4'; set -- $var
$ echo ${@@A}
set -- '1' '2\' '3' '4'        # oops, why is that `\` ignored ? 

\忽略,因为没有规则给出\特殊语法(当从变量扩展时)。

\是的,在解析命令行时(仍然)有一个特殊规则:

$ set -- 1 2\ 3 4
$ echo ${@@A}
set -- '1' '2 3' '4'

但这不适用于变量扩展。

$'...'对于 read,我们需要单独的行,但这可以使用 printf 或ANSI-C 转义格式来完成。

$ printf '%s]n' a b c d
a
b
c
d

$ i=1; printf '%s\n' a b c d | while read -r line; do printf '%s %s \n' $((i++)) $line; done
1 a 
2 b 
3 c 
4 d 

$ var='a b c d'
$ i=1; printf '%s\n' $var | while read -r line; do printf '%s %s \n' $((i++)) $line; done
1 a
2 b
3 c
4 d           # it seems to work.

$ var='a b\ c d'
$ i=1; printf '%s\n' $var | while read -r line; do printf '%s %s \n' $((i++)) $line; done
1 a 
2 b\
3 c
4 d  

这就是为什么有很多关于“引用变量扩展”的帖子。处理数组和处理字符串不一样。

$ var=( a b\ c d)
$ printf '%s\n' "${var[@]}"
a
b c
d

# Or even
$ set -- "${var[@]}"
$ printf '%s\n' "$@"
a
b c
d

简而言之:变量扩展被分割为仅有的IFS 分隔符。通常减少为一种类型的分隔符(空格)。

$ ( IFS=''; var='a b\ c d'; printf '<%s> ' $var; echo )
<a b\ c d> 

$ ( IFS=' '; var='a b\ c d'; printf '<%s> ' $var; echo )
<a> <b\> <c> <d>

这并不包括\在命令行上使用的带引号的空格 ( ) 的效果。

shellzsh甚至更浅,它根本不分割变量扩展(除非要求)。

% var='a b\ c d'
% printf '<%s>\n' $var
<a b\ c d>

% printf '<%s>\n' $=var
a
b\
c
d

\问题还是一样。

相关内容