Shell 脚本对编码和行结尾敏感吗?

我正在 MacOS 上制作一个 NW.js 应用程序,并希望在开发模式下运行该应用程序 通过双击图标。 在第一步中,我尝试使我的 shell 脚本工作。

使用 Windows 上的 VS Code (我想争取时间) ,我在项目的根目录下创建了一个 run-nw文件,其中包含:

#!/bin/bash


cd "src"
npm install


cd ..
./tools/nwjs-sdk-v0.17.3-osx-x64/nwjs.app/Contents/MacOS/nwjs "src" &

但我得到了这个输出:

$ sh ./run-nw


: command not found
: No such file or directory
: command not found
: No such file or directory


Usage: npm <command>


where <command> is one of:  (snip commands list)


(snip npm help)


npm@3.10.3 /usr/local/lib/node_modules/npm
: command not found
: No such file or directory
: command not found

有些事我不明白。

  • 它似乎将空行作为命令。 在我的编辑器(VS 代码)中,我试图用 \n代替 \r\n (以防 \r产生问题)但它不会改变任何东西。
  • 好像找不到文件夹了 (使用或不使用 dirname指令) , 或者它不知道 cd命令?
  • 它似乎不理解 installnpm的参数。
  • 真正让我感到奇怪的是它还能运行这个应用程序 (如果我手动做 npm install) ..。

不能让它正常工作,并怀疑一些奇怪的东西 文件本身,我直接在 Mac 上创建了一个新的,这次使用的是 vim。 我输入了完全相同的指令,而且... 现在它没有任何工作 问题。
两个文件的 diff显示完全没有差别。

有什么区别吗? 是什么导致第一个脚本不起作用? 我怎样才能找到答案?

更新

按照接受的答案的建议,后错误的行 结局出来了,我查了很多东西。 事实证明,因为我从 Windows 中复制了我的 ~/.gitconfig 机器,我有 autocrlf=true,所以每次我修改 bash 文件,它将行尾重新设置为 \r\n
因此,除了运行 dos2unix(您将不得不 安装) ,如果你正在使用 Git,请检查你的 .gitconfig文件。

52836 次浏览

Yes. Bash scripts are sensitive to line-endings, both in the script itself and in data it processes. They should have Unix-style line-endings, i.e., each line is terminated with a Line Feed character (decimal 10, hex 0A in ASCII).

DOS/Windows line endings in the script

With Windows or DOS-style line endings , each line is terminated with a Carriage Return followed by a Line Feed character. You can see this otherwise invisible character in the output of cat -v yourfile:

$ cat -v yourfile
#!/bin/bash^M
^M
cd "src"^M
npm install^M
^M
cd ..^M
./tools/nwjs-sdk-v0.17.3-osx-x64/nwjs.app/Contents/MacOS/nwjs "src" &^M

In this case, the carriage return (^M in caret notation or \r in C escape notation) is not treated as whitespace. Bash interprets the first line after the shebang (consisting of a single carriage return character) as the name of a command/program to run.

  • Since there is no command named ^M, it prints : command not found
  • Since there is no directory named "src"^M (or src^M), it prints : No such file or directory
  • It passes install^M instead of install as an argument to npm which causes npm to complain.

DOS/Windows line endings in input data

Like above, if you have an input file with carriage returns:

hello^M
world^M

then it will look completely normal in editors and when writing it to screen, but tools may produce strange results. For example, grep will fail to find lines that are obviously there:

$ grep 'hello$' file.txt || grep -x "hello" file.txt
(no match because the line actually ends in ^M)

Appended text will instead overwrite the line because the carriage returns moves the cursor to the start of the line:

$ sed -e 's/$/!/' file.txt
!ello
!orld

String comparison will seem to fail, even though strings appear to be the same when writing to screen:

$ a="hello"; read b < file.txt
$ if [[ "$a" = "$b" ]]
then echo "Variables are equal."
else echo "Sorry, $a is not equal to $b"
fi


Sorry, hello is not equal to hello

Solutions

The solution is to convert the file to use Unix-style line endings. There are a number of ways this can be accomplished:

  1. This can be done using the dos2unix program:

    dos2unix filename
    
  2. Open the file in a capable text editor (Sublime, Notepad++, not Notepad) and configure it to save files with Unix line endings, e.g., with Vim, run the following command before (re)saving:

    :set fileformat=unix
    
  3. If you have a version of the sed utility that supports the -i or --in-place option, e.g., GNU sed, you could run the following command to strip trailing carriage returns:

    sed -i 's/\r$//' filename
    

    With other versions of sed, you could use output redirection to write to a new file. Be sure to use a different filename for the redirection target (it can be renamed later).

    sed 's/\r$//' filename > filename.unix
    
  4. Similarly, the tr translation filter can be used to delete unwanted characters from its input:

    tr -d '\r' <filename >filename.unix
    

Cygwin Bash

With the Bash port for Cygwin, there’s a custom igncr option that can be set to ignore the Carriage Return in line endings (presumably because many of its users use native Windows programs to edit their text files). This can be enabled for the current shell by running set -o igncr.

Setting this option applies only to the current shell process so it can be useful when sourcing files with extraneous carriage returns. If you regularly encounter shell scripts with DOS line endings and want this option to be set permanently, you could set an environment variable called SHELLOPTS (all capital letters) to include igncr. This environment variable is used by Bash to set shell options when it starts (before reading any startup files).

Useful utilities

The file utility is useful for quickly seeing which line endings are used in a text file. Here’s what it prints for for each file type:

  • Unix line endings: Bourne-Again shell script, ASCII text executable
  • Mac line endings: Bourne-Again shell script, ASCII text executable, with CR line terminators
  • DOS line endings: Bourne-Again shell script, ASCII text executable, with CRLF line terminators

The GNU version of the cat utility has a -v, --show-nonprinting option that displays non-printing characters.

The dos2unix utility is specifically written for converting text files between Unix, Mac and DOS line endings.

Useful links

Wikipedia has an excellent article covering the many different ways of marking the end of a line of text, the history of such encodings and how newlines are treated in different operating systems, programming languages and Internet protocols (e.g., FTP).

Files with classic Mac OS line endings

With Classic Mac OS (pre-OS X), each line was terminated with a Carriage Return (decimal 13, hex 0D in ASCII). If a script file was saved with such line endings, Bash would only see one long line like so:

#!/bin/bash^M^Mcd "src"^Mnpm install^M^Mcd ..^M./tools/nwjs-sdk-v0.17.3-osx-x64/nwjs.app/Contents/MacOS/nwjs "src" &^M

Since this single long line begins with an octothorpe (#), Bash treats the line (and the whole file) as a single comment.

Note: In 2001, Apple launched Mac OS X which was based on the BSD-derived NeXTSTEP operating system. As a result, OS X also uses Unix-style LF-only line endings and since then, text files terminated with a CR have become extremely rare. Nevertheless, I think it’s worthwhile to show how Bash would attempt to interpret such files.

One more way to get rid of the unwanted CR ('\r') character is to run the tr command, for example:

$ tr -d '\r' < dosScript.py > nixScript.py

On JetBrains products (PyCharm, PHPStorm, IDEA, etc.), you'll need to click on CRLF/LF to toggle between the two types of line separators (\r\n and \n).

enter image description here enter image description here

The simplest way on MAC / Linux - create a file using 'touch' command, open this file with VI or VIM editor, paste your code and save. This would automatically remove the windows characters.

Coming from a duplicate, if the problem is that you have files whose names contain ^M at the end, you can rename them with

for f in *$'\r'; do
mv "$f" "${f%$'\r'}"
done

You properly want to fix whatever caused these files to have broken names in the first place (probably a script which created them should be dos2unixed and then rerun?) but sometimes this is not feasible.

The $'\r' syntax is Bash-specific; if you have a different shell, maybe you need to use some other notation. Perhaps see also Difference between sh and bash

I was trying to startup my docker container from Windows and got this:

Bash script and /bin/bash^M: bad interpreter: No such file or directory

I was using git bash and the problem was about the git config, then I just did the steps below and it worked. It will configure Git to not convert line endings on checkout:

  1. git config --global core.autocrlf input
  2. delete your local repository
  3. clone it again.

Many thanks to Jason Harmon in this link: https://forums.docker.com/t/error-while-running-docker-code-in-powershell/34059/6

Before that, I tried this, that didn't works:

  1. dos2unix scriptname.sh
  2. sed -i -e 's/\r$//' scriptname.sh
  3. sed -i -e 's/^M$//' scriptname.sh

I ran into this issue when I use git with WSL. git has a feature where it changes the line-ending of files according to the OS you are using, on Windows it make sure the line endings are \r\n which is not compatible with Linux which uses only \n.

You can resolve this problem by adding a file name .gitattributes to your git root directory and add lines as following:

config/* text eol=lf
run.sh text eol=lf

In this example all files inside config directory will have only line-feed line ending and run.sh file as well.

If you are using a text editor like BBEdit you can do it at the status bar. There is a selection where you can switch.

Change the CRLF to LF using BBEdit

For the sake of completeness, I'll point out another solution which can solve this problem permanently without the need to run dos2unix all the time:

sudo ln -s /bin/bash `printf 'bash\r'`

Since VS Code is being used, we can see CRLF or LF in the bottom right depending on what's being used and if we click on it we can change between them (LF is being used in below example):

Screenshot of shortcut UI

We can also use the "Change End of Line Sequence" command from the command pallet. Whatever's easier to remember since they're functionally the same.

If you're using the read command to read from a file (or pipe) that is (or might be) in DOS/Windows format, you can take advantage of the fact that read will trim whitespace from the beginning and ends of lines. If you tell it that carriage returns are whitespace (by adding them to the IFS variable), it'll trim them from the ends of lines.

In bash (or zsh or ksh), that means you'd replace this standard idiom:

IFS= read -r somevar    # This will not trim CR

with this:

IFS=$'\r' read -r somevar    # This *will* trim CR

(Note: the -r option isn't related to this, it's just usually a good idea to avoid mangling backslashes.)

If you're not using the IFS= prefix (e.g. because you want to split the data into fields), then you'd replace this:

read -r field1 field2 ...    # This will not trim CR

with this:

IFS=$' \t\n\r' read -r field1 field2 ...    # This *will* trim CR

If you're using a shell that doesn't support the $'...' quoting mode (e.g. dash, the default /bin/sh on some Linux distros), or your script even might be run with such a shell, then you need to get a little more complex:

cr="$(printf '\r')"
IFS="$cr" read -r somevar    # Read trimming *only* CR
IFS="$IFS$cr" read -r field1 field2 ...    # Read trimming CR and whitespace, and splitting fields

Note that normally, when you change IFS, you should put it back to normal as soon as possible to avoid weird side effects; but in all these cases, it's a prefix to the read command, so it only affects that one command and doesn't have to be reset afterward.

For IntelliJ users, here is the solution for writing Linux script.
Use LF - Unix and masOS (\n)

enter image description here

Scripts may call each other. An even better magic solution is to convert all scripts in the folder/subfolders:

find . -name "*.sh" -exec sed -i -e 's/\r$//' {} +

You can use dos2unix too but many servers do not have it installed by default.

For Notepad++ users, this can be solved by:

enter image description here