或者,一个在外壳程序脚本中进行可靠的文件名处理和其他字符串传递的入门指南。

我编写了一个外壳程序脚本,该脚本在大多数情况下都能正常工作。但是它在某些输入(例如某些文件名)上阻塞。

我遇到了如下问题:


我有一个文件名,其中包含空格hello world,它被当作两个单独的文件helloworld
我的输入行具有两个连续的空格,并且它们在输入中缩小到一个空格。
有时,当输入包含字符\[*?之一时,它们会被替换
实际上是文件名的某些文本。
有撇号'(或双引号") )中的输入,然后事情变得很奇怪。
输入中有一个反斜杠(或:我正在使用Cygwin,并且我的某些文件名具有Windows风格的\分隔符)。

发生了什么以及如何解决?

评论

shellcheck可帮助您提高程序质量。

除了答案中描述的保护性技术外,尽管对于大多数读者来说可能很明显,但我认为值得一提的是,当打算使用命令行工具处理文件时,最好避免在文件中使用奇特的字符。如果可能的话,将名称放在首位。

现在,还有一些工具可以使用正确的引号来重写Shell脚本。

@bli不,这只会使错误花费更长的时间出现。今天隐藏了错误。现在,您不知道以后与代码一起使用的所有文件名。

首先,如果您的参数包含空格,则需要用引号将它们引起来(在命令行上)。但是,您可以获取整个命令行并自己解析。两个空格不会变成一个空格。任何数量的空间都会告诉您的脚本是下一个变量,因此,如果您执行“ echo $ 1 $ 2”之类的操作,那么您的脚本将在两者之间插入一个空格。也可以使用“ find(-exec)”遍历带空格的文件,而不是for循环;您可以更轻松地处理这些空间。

#1 楼

始终在变量替换和命令替换周围使用双引号:"$foo""$(foo)"


如果使用$foo不带引号,则脚本将阻塞包含空格的输入或参数(或命令输出,带有$(foo))或\[*?

在那里,您可以停止阅读。好吧,这里还有更多内容:

read —避免使用read。如果必须使用while IFS= read -r line; do …,请将该read设置为。代替xargs,更喜欢xargsxargs特别对待空格和字符xargs -0。 Zsh用户应该跳过它,并阅读何时需要双引号?代替。如果需要整个细节,请阅读标准或外壳手册。


请注意,以下说明包含一些近似值(声明在大多数情况下是正确的,但可能会受到影响)

为什么需要编写find … | xargs?不带引号会发生什么?

find … -exec …并不意味着“采用变量xargs的值”。这意味着要复杂得多:


首先,获取变量的值。
字段拆分:将值视为空格分隔的字段列表,然后构建结果列表。例如,如果变量包含\"',则此步骤的结果为3元素列表shashdash
生成文件名:将每个字段都视为全局字段(即通配符模式),然后将其替换为与此模式匹配的文件名列表。如果该模式与任何文件都不匹配,则将其保留不变。在我们的示例中,这将导致包含bash的列表,然后是当前目录中的文件列表,最后是ksh。如果当前目录为空,则结果为mkshyash"$foo"

请注意,结果是字符串列表。 Shell语法中有两个上下文:列表上下文和字符串上下文。字段拆分和文件名生成仅在列表上下文中发生,但这是大多数时间。双引号分隔字符串上下文:整个双引号字符串是单个字符串,不能分割。 (例外:$foo扩展到位置参数列表,例如,如果存在三个位置参数,则foo等同于foo * bar ​。请参见$ *和$ @有什么区别?)

相同的情况使用foo*命令替换。附带说明一下,不要使用bar:它的引用规则很奇怪且不可移植,并且所有现代shell都支持foo,除了具有直观的引用规则外,它完全等效。

算术输出替换也经历相同的扩展,但是通常不必担心,因为它仅包含不可扩展的字符(假设bar不包含数字或foo)。

请参阅何时需要双引号?有关可以省略引号的情况的更多详细信息。

除非您想使所有这些棘手问题发生,否则请记住始终在变量和命令替换周围使用双引号。请当心:省略引号不仅会导致错误,还会导致安全漏洞。

如何处理文件名列表?

如果您编写*,并使用空格分隔文件,则此操作不适用于包含空格的文件名。 Unix文件名可以包含bar(始终是目录分隔符)和空字节(您在大多数shell的shell脚本中不能使用)以外的任何字符。

"$@"存在相同的问题。执行此操作时,变量"$@"包含5个字符的字符串"" "" "",这是在您编写$(foo)时扩展了通配符。在您将脚本更改为`foo`之前,此示例将实际起作用。如果`foo`设置为$(foo),则将无法使用。

要处理任何类型的列表(例如文件名),请将其放入数组中。这需要mksh,ksh93,yash或bash(或zsh,它们没有所有这些引用问题);一个普通的POSIX外壳程序(如灰烬或破折号)没有数组变量。

qsh4312078q

Ksh88数组变量具有不同的赋值语法IFS(请参见下面的赋值变量)如果您需要ksh88 / bash可移植性,请在不同的ksh环境中进行操作)。 Bourne / POSIX样式的外壳只有一个数组,即使用-设置的位置参数数组myfiles="file1 file2",该数组对于函数是局部的:以/开头的文件名在相关说明中,请记住,文件名可以以myfiles=*.txt; … process $myfiles(破折号/减号)开头,大多数命令将其解释为表示一个选项。一些命令(例如myfiles*.txt$myfiles)也接受以myfiles="$someprefix*.txt"; … process $myfiles开头的选项。如果您的文件名以可变部分开头,请确保在其前面传递someprefix,如上面的代码段所示。这表明命令已经到达选项的末尾,因此此后的任何内容都是文件名,即使它以final reportset -A myfiles "someprefix"*.txt开头。

或者,您可以确保文件名以"$@"以外的其他字符开头。绝对文件名以set开头,您可以在相对名称的开头添加-。以下代码段将变量-的内容转换为“安全”的方式,以引用相同的文件,确保该文件不以shset开头。
关于该主题的最后说明,请注意,即使在sort之后,某些命令也会将+解释为标准输入或标准输出。如果您需要引用一个名为--的实际文件,或者正在调用这样的程序,并且您不希望它从stdin读取或写入stdout,请确保如上所述重写-。请参阅“ du -sh *”和“ du -sh ./*”之间的区别是什么?进一步的讨论。

如何将命令存储在变量中?

“命令”可以表示三件事:命令名称(作为可执行文件的名称,带有或没有完整路径,或函数名称,内置或别名),带参数的命令名称或一段外壳代码。因此,有不同的方式将它们存储在变量中。

如果有命令名,只需将其存储并照常使用双引号将变量使用即可。 br />
如果您的命令带有参数,则问题与上面的文件名列表相同:这是字符串列表,而不是字符串。您不能仅将参数填充到单个字符串中,并在其之间留有空格,因为如果这样做,您将无法分辨出作为参数一部分的空格与分隔参数的空格之间的区别。如果您的外壳中有数组,则可以使用它们。

myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"


如果您使用的是不包含数组的外壳怎么办?如果您不介意修改位置参数,则仍然可以使用它们。

set -- "$someprefix"*.txt
process -- "$@"


如果您需要存储复杂的shell命令怎么办?重定向,管道等?或者,如果您不想修改位置参数?然后,您可以构建包含命令的字符串,并使用内置的+。文字,因此变量-的值是字符串/。内置的./告诉Shell解析作为参数传递的字符串,就好像它出现在脚本中一样,因此在那一刻将引号和管道进行解析,依此类推。

使用f很棘手。仔细考虑何时解析的内容。特别是,您不能仅将文件名填充到代码中:您需要对其进行引用,就像在源代码文件中一样。没有直接的方法可以做到这一点。如果文件名包含任何外壳程序特殊字符(空格,-+----等),则类似-的字符会中断。 eval仍在code处中断。如果文件名包含'…',则即使code也会中断。有两种解决方案。



在文件名周围添加引号。最简单的方法是在其周围添加单引号,并用/path/to/executable --option --message="hello world" -- /path/to/file1替换单引号。

case "$f" in -* | +*) "f=./$f";; esac



将变量扩展保留在代码内,以便在评估代码时查找它,而不是在构建代码片段时查找它。这比较简单,但仅当变量在执行代码时仍具有相同的值时才起作用,而不是例如如果代码是在循环中构建的。

command_path=""
…
"$command_path" --option --message="hello world"




最后,您真的需要一个包含代码的变量吗?给代码块命名的最自然的方法是定义一个函数。

eval怎么了?

如果没有eval,则code="$code $filename"允许连续行-这是输入的逻辑单行:反斜线也可以避免这些错误)。例如,如果输入是包含三个单词的行,则$;设置为输入的第一个单词,将|设置为第二个单词,将<设置为第三个单词。如果还有更多的单词,则最后一个变量包含设置前面的单词后剩下的所有内容。删除前导和尾随空格。

>设置为空字符串可避免任何修整。请参见为什么经常使用“ IFS =读取时”而不是“ IFS =”。在阅读时。

code="$code \"$filename\""有什么问题吗?

"$\`的输入格式是用空格分隔的字符串,可以选择用单引号或双引号引起来。没有标准的工具会输出这种格式。

code="$code '$filename'"'的输入几乎是行列表,但不完全是-如果行尾有空格,则以下行是续行行。

您可以在适当的地方使用'\''(并且在可用的地方使用:GNU(Linux,Cygwin),BusyBox,BSD,OSX,但不在POSIX中)。这很安全,因为空字节不能出现在大多数数据中,尤其是在文件名中。要生成以空分隔的文件名列表,请使用read(或按如下说明使用-r)。

如何处理read找到的文件?

cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"


read必须是外部命令,不能是Shell函数或别名。如果需要调用Shell处理文件,请显式调用$IFS

set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"


我还有其他问题

浏览此站点上的引号标签,shell或shell脚本。 (单击“了解更多…”以查看一些一般性提示以及一些常见问题的手动选择列表。)如果您已搜索但找不到答案,请提出。

评论


@ John1024这只是GNU功能,因此我坚持使用“没有标准工具”。

–吉尔斯'所以-不再是邪恶的'
2014年5月24日下午5:02

除zsh(甚至在sh仿真中)和mksh外,您还需要在$((... ...))(在某些shell中也为$ [...])周围加引号。

–StéphaneChazelas
2014年5月24日6:39

请注意,xargs -0不是POSIX。除了FreeBSD xargs之外,通常需要xargs -r0而不是xargs -0。

–StéphaneChazelas
2014年5月24日6:41

@StéphaneChazelas我明白了。我认为您可能打算链接到这里,或者至少我应该通读它,但不幸的是,它确实明确指定要在$ IFS上进行字段拆分的数学扩展。叹

–mikeserv
2014-09-14 22:32

另一个不错的功能(仅GNU)是xargs -d“ \ n”,因此您可以运行例如找到PATTERN1 | xargs -d“ \ n” grep PATTERN2,以搜索与PATTERN1匹配且内容与PATTERN2匹配的文件名。如果没有GNU,您可以例如像定位PATTERN1 | perl -pne's / \ n / \ 0 /'| xargs -0 grep PATTERN1

–亚当·卡兹(Adam Katz)
15年1月16日在4:29

#2 楼

尽管Gilles的回答非常好,但我要注意的重点是


始终在变量替换和命令替换处使用双引号:“ $ foo”,“ $(foo) “


当您使用类似Bash的外壳进行单词拆分时,当然,安全建议始终使用引号。但是,不会一直进行单词拆分


§单词拆分

这些命令可以无错误运行

foo=$bar
bar=$(a command)
logfile=$logdir/foo-$(date +%Y%m%d)
PATH=/usr/local/bin:$PATH ./myscript
case $foo in bar) echo bar ;; baz) echo baz ;; esac


我不鼓励用户采取这种行为,但是如果有人坚定地理解
分词发生的时间,那么他们应该能够自己决定何时使用引号。

评论


正如我在回答中提到的那样,请参阅unix.stackexchange.com/questions/68694/…了解详细信息。请注意问题-“为什么我的shell脚本会窒息?”。最常见的问题(来自本网站和其他地方的多年经验)缺少双引号。与“始终使用双引号,但在没有必要的情况下”相比,“始终使用双引号”更容易记住。

–吉尔斯'所以-不再是邪恶的'
2014年5月24日晚上8:25

规则对于初学者来说很难理解。例如,foo = $ bar是可以的,但是export foo = $ bar或env foo = $ var不能(至少在某些shell中)。初学者的建议:除非您知道自己在做什么,并且有充分的理由不这么做,否则请始终引用变量。

–StéphaneChazelas
2014年5月24日17:05

@StevenPenny真的更正确吗?在合理的情况下,引号会破坏脚本吗?在一半情况下必须使用引号,而另一半情况下可以选择使用引号的情况下,应该考虑的建议是“始终使用引号,以防万一”,因为它是真实,简单且风险较小的。众所周知,向初学者教授这样的例外列表是无效的(缺乏上下文,他们不会记住它们)并且适得其反,因为它们会混淆需要/不需要的引号,破坏脚本并激励他们进一步学习。

– Peteris
2014年5月25日晚上8:20

我的$ 0.02就是建议报价都不错。错误地引用不需要的东西是无害的,错误地引用不需要的东西是有害的。因此,对于大多数Shell脚本作者来说,他们永远都不会理解何时精确地进行单词拆分的复杂性,因此,引用所有内容比仅在必要时进行引用要安全得多。

– godlygeek
14年5月25日在17:03

@Peteris和godlygeek:“在合理的情况下,引号会破坏脚本吗?”这取决于您对“合理”的定义。如果脚本设置了criteria =“-type f”,则查找。 $ criteria有效,但是找到。 “ $条件”不是。

– G-Man说“恢复莫妮卡”
2014年8月28日19:06



#3 楼

据我所知,只有两种情况需要对双引号进行扩展,并且这些情况涉及两个特殊的shell参数"$@""$*"-当用双引号引起来时,它们被指定为不同地扩展。在所有其他情况下(可能不包括特定于shell的数组实现),扩展的行为是可配置的-有多种选择。

当然,这并不是说两倍应该避免使用引号-相反,它可能是界定外壳程序必须提供的扩展的最方便,最可靠的方法。但是,我认为,由于已经很好地阐述了替代方案,因此这是讨论外壳扩展值时会发生什么情况的绝佳场所。

外壳,在其内心和灵魂中),是一个命令解释器-它是一个解析器,就像一个大型交互式sed。如果您的shell语句在空格或类似字符上令人窒息,则很可能是因为您尚未完全理解shell的解释过程-尤其是它如何以及为何将输入语句转换为可执行命令的原因。外壳程序的工作是:


接受输入

正确解释并将其拆分为标记化的输入词


input单词是shell语法项,例如$wordecho $words 3 4* 5
单词总是在空格上分割-这只是语法-但只有文字空白字符在其输入文件中提供给shell。



将必要的字段扩展为多个字段


字段由单词扩展产生-它们构成了最终的可执行命令
,除了"$@"$IFS字段拆分,和路径名扩展,输入单词必须始终求值到单个字段。



,然后执行结果命令在大多数情况下,这涉及以某种形式或其他形式传递其解释结果。人们经常说外壳是胶水,如果这是真的,那么它所坚持的是一个或多个进程的参数列表(或字段)。大多数shell不能很好地处理exec字节-如果有的话-这是因为它们已经在上面拆分了。外壳程序要花很多时间,并且必须使用NUL分隔的参数数组来执行此操作,并在exec时将其移交给系统内核。如果要将shell的定界符与其定界数据混合在一起,则shell可能会将其弄糟。像大多数程序一样,其内部数据结构也依赖该定界符。 NUL尤其不能将其拧紧。

这就是exec出现的地方。zsh是一个始终存在的-同样是可设置的-shell参数,它定义shell如何将shell扩展从单词到字段分割-尤其是这些字段应定界的值。 $IFS在除$IFS之外的定界符上分割shell扩展-或者换句话说,shell用内部数据数组中的$IFS替换与NUL值匹配的扩展产生的字节。像这样看时,您可能会开始看到,每个字段拆分的Shell扩展都是一个$IFS分隔的数据数组。

重要的是要了解NUL仅分隔没有用其他方式分隔的扩展-您可以使用$IFS双引号来完成。引用扩展时,可以在其值的开头和至少结尾处定界。在这些情况下,$IFS不适用,因为没有可分隔的字段。实际上,当"设置为空值时,双引号展开与非引号展开具有相同的场分裂行为。

除非引用,否则$IFS本身就是IFS=分隔的外壳扩展。它的默认值为$IFS-当包含在$IFS中时,这三个值都显示特殊的属性。尽管指定了<space><tab><newline>的任何其他值以评估每个扩展出现的单个字段,但指定$IFS的空白-这三个值中的任何一个-扩展为每个扩展序列的单个字段,并且完全消除了前导/尾随序列。这可能是最容易通过示例理解的内容。 br />
默认情况下,外壳程序还会在列表中出现某些未加引号的标记(例如在此处其他地方提到的$IFS)时将其扩展为多个字段。这称为路径名扩展或通配。它是一个非常有用的工具,并且它在以shell的解析顺序进行字段拆分后会发生,因此不受$ IFS的影响-路径名扩展生成的字段在文件名本身的头/尾定界它们的内容包含当前在$IFS中的任何字符。默认情况下,此行为设置为on-但可以很容易地通过其他方式进行配置。至少在以某种方式撤消该设置之前,不会发生路径名扩展-例如,如果当前的shell被另一个新的shell进程替换,或者....

..发出到外壳。双引号-就像$IFS字段拆分一样-使每次扩展都不需要此全局设置。因此:

slashes=///// spaces='     '
IFS=/; printf '<%s>' $slashes$spaces
<><><><><><     >
IFS=' '; printf '<%s>' $slashes$spaces
</////>
IFS=; printf '<%s>' $slashes$spaces
</////     >
unset IFS; printf '<%s>' "$slashes$spaces"
</////     >


...如果当前启用了路径名扩展,则每个参数可能会产生非常不同的结果-因为第一个仅扩展到其字面值(单个星号字符,也就是说完全没有扩展),第二个仅扩展到相同的值如果当前工作目录不包含可能匹配的文件名(并且几乎匹配所有文件名)。但是,如果您这样做:

set -f


...两个参数的结果相同-?*[在这种情况下不会扩展。

评论


我实际上同意@StéphaneChazelas的观点,(大多数情况下)它使事情变得比帮助更令人困惑……但我个人认为它很有帮助,因此我投票赞成。现在,我对IFS的实际工作方式有了一个更好的想法(和一些示例)。我不明白的是为什么将IFS设置为默认值以外的其他方法是一个好主意。

–通配符
16 Jan 9'在4:12



@Wildcard-这是字段定界符。如果变量中有一个值想要扩展到多个字段,则可以在$ IFS上拆分它。 cd / usr / bin;设置-f; IFS = /;用于$ PWD中的path_component;做echo $ path_component;完成打印\ n然后是usr \ n,然后是bin \ n。第一个回显为空,因为/是空字段。 path_components可以包含换行符或空格或其他内容-没关系,因为这些组件是在/而不是默认值上分割的。无论如何,人们总是不停地做。你的壳也这样做

–mikeserv
16年1月9日在6:40



“ $ @”当然是很特殊的,它实际上并没有遵守其他任何规则。我不认为$ *是特殊的,因为未加引号会象您期望的那样对它进行分词,而加引号却变成了您希望的一个斑点。因此,$ *和“ $ *”几乎完全没有用,前者是因为您几乎不需要在参数上拆分新单词,后者是因为您几乎不希望将它们作为一个Blob。

–xpusostomos
8月3日,3:01

#4 楼

我有一个大型视频项目,文件名中带有空格,目录名称中带有空格。尽管find -type f -print0 | xargs -0可以用于多种用途并且可以在不同的外壳上工作,但我发现使用自定义IFS(输入字段分隔符)可以让您在使用bash时更加灵活。下面的代码片段使用bash并将IFS设置为换行符;前提是您的文件名中没有换行符:

(IFS=$'\n'; for i in $(find -type f -print) ; do
    echo ">>>$i<<<"
done)


请注意使用括号分隔IFS的重新定义。我已经读过其他有关如何恢复IFS的文章,但这很容易。

更多,将IFS设置为换行符可以让您事先设置shell变量并轻松打印出来。例如,我可以使用换行符作为分隔符来递增地增大变量V:

V=""
V="./Ralphie's Camcorder/STREAM/00123.MTS,04:58,05:52,-vf yadif"
V="$V"$'\n'"./Ralphie's Camcorder/STREAM/00111.MTS,00:00,59:59,-vf yadif"
V="$V"$'\n'"next item goes here..."


并相应地:

(IFS=$'\n'; for v in $V ; do
    echo ">>>$v<<<"
done)


现在,我可以使用双引号将换行符用echo "$V"“列出” V的设置。 (有关$'\n'的解释,请联系此线程。)

评论


但是那样,您仍然会遇到包含换行符或glob字符的文件名的问题。另请参见:为什么遍历find的输出是错误的做法?如果使用zsh,则可以使用IFS = $'\ 0'并使用-print0(zsh不会在扩展时进行globing,因此glob字符在那里不是问题)。

–StéphaneChazelas
18-2-28在14:10



这适用于包含空格的文件名,但不适用于可能具有敌意的文件名或意外的“无意义”文件名。您可以通过添加set -f轻松解决包含通配符的文件名问题。另一方面,您的方法从根本上失败了,因为文件名包含换行符。当处理除文件名以外的数据时,它还会失败并显示空白项。

–吉尔斯'所以-不再是邪恶的'
18年1月1日在18:52

是的,我的警告是它不适用于文件名中的换行符。但是,我相信我们必须划清界限;-)

–俄罗斯
18年8月8日在18:01

而且我不确定为什么这会引起反对。这是遍历带空格的文件名的一种非常合理的方法。使用-print0需要xargs,并且使用该链有些困难。抱歉,有人不同意我的回答,但这没有理由不赞成。

–俄罗斯
18 Mar 8 '18 at 18:03

#5 楼

使用find directory -print0 | xargs -0的方法应处理所有特殊情况。但是,每个文件/目录需要一个PID,这可能会导致性能问题。

让我描述一下我最近遇到的另一种健壮(和高性能)文件处理方法,如果find输出应作为制表符分隔的CSV数据进行后处理,例如由AWK。在这种处理中,实际上只有文件名中的制表符和换行符是破坏性的:

通过find directory -printf '%P\t///\n'扫描目录。如果路径不包含制表符或换行符,则会导致一条记录包含两个CSV字段:路径本身和包含///的字段。

如果路径中包含制表符,则将有三个字段:pathfragment1,pathfragment2和包含///的字段。

如果包含换行符,将有两条记录:第一条记录将包含路径fragment1,第二条记录将包含路径fragment2和该字段现在包含关键事实是///不能自然地出现在路径中。另外,它是一种防水逃逸或终结器。

可以知道,标签可以安全地转义为///,换行符可以安全转义为find,这又是因为知道///不能
///t///n转换回制表符,并且换行符可以在处理过程中生成某些输出时最终出现。

是的,听起来很复杂,但是提示仅需要两个PID:执行所描述算法的//////t实例。而且速度很快。

这个想法不是我的,我发现它是在用于目录同步的新的(bash)bash脚本(2019)中实现的:Zaloha.sh。
那里有一个文档,实际上描述了该算法。

我无法通过文件名中的特殊字符来中断/阻塞该程序。它甚至可以正确地处理单独命名为newline和tab的目录...

评论


但是要注意,虽然///不会在路径中“自然地”发生(例如,find不会构建包含///的路径),但它可能会出现在用户输入中(查找/// some //// where),并且可以通过将路径与多余的斜杠串联而产生。只要您对输入进行归一化就可以,但是对输入进行归一化本身并不容易。

–吉尔斯'所以-不再是邪恶的'
3月16日14:35

xargs不会为每个输入文件运行一个进程。相反,它会在命令行中堆积尽可能多的文件名,如果输入量很小,则通常会生成单个PID。尝试查找。 -maxdepth 1 -print0 | xargs -0 sh -c'echo“ $$ $ @”“ _进行简单演示。您可以使用xargs -n1或其他要求每个进程分别运行的选项来强制执行该操作;有时,如果xargs的参数很重要,则很难避免每个参数运行一个进程。

–tripleee
3月19日14:03



#6 楼

考虑到上面提到的所有安全隐患,并假设您信任并控制扩展变量,则可以使用eval使用带有空格的多个路径。但是要小心!

 $ FILES='"a b" c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
$ FILES='a\ b c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory