Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are shell scripts sensitive to encoding and line endings?

Tags:

bash

shell

sh

I am making a NW.js app on Mac, and want to run the app in dev mode by double-clicking on an icon. First step, I'm trying to make my shell script work.

Using VSCode on Windows (I wanted to gain time), I have created a run-nw file at the root of my project, containing this:

#!/bin/bash  cd "src" npm install  cd .. ./tools/nwjs-sdk-v0.17.3-osx-x64/nwjs.app/Contents/MacOS/nwjs "src" & 

but I get this output:

$ 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)  [email protected] /usr/local/lib/node_modules/npm   : command not found   : No such file or directory   : command not found 

I really don't understand:

  • it seems that it takes empty lines as commands. In my editor (VSCode) I have tried to replace \r\n with \n (in case the \r creates problems) but it changes nothing.
  • it seems that it doesn't find the folders (with or without the dirname instruction), or maybe it doesn't know about the cd command ?
  • it seems that it doesn't understand the install argument to npm
  • the part that really weirds me out, is that it still runs the app (if I did a npm install manually)...

Not able to make it work properly, and suspecting something weird with the file itself, I created a new one directly on the Mac, using vim this time. I entered the exact same instructions, and... now it works without any issue.
A diff on the two files reveals exactly zero difference.

What can be the difference? What can make the first script not work? How can I find out?

Update

Following the accepted answer's recommendations, after the wrong line endings came back, I checked multiple things. It turns out that since I copied my ~/.gitconfig from my Windows machine, I had autocrlf=true, so every time I modified the bash file under Windows, it re-set the line endings to \r\n.
So, in addition to running dos2unix (which you will have to install using Homebrew on a Mac), if you're using Git, check your config.

like image 876
thomasb Avatar asked Sep 16 '16 09:09

thomasb


People also ask

How do you end a line in shell script?

If you want to break up a command so that it fits on more than one line, use a backslash (\) as the last character on the line. Bash will print the continuation prompt, usually a >, to indicate that this is a continuation of the previous line. $ printf "%s\n" "This is a very long printf.

What are shell scripting What are its advantage?

The many advantages include easy program or file selection, quick start, and interactive debugging. A shell script can be used to provide a sequencing and decision-making linkage around existing programs, and for moderately sized scripts the absence of a compilation step is an advantage.

Is shell scripting coding?

A Unix shell is both a command interpreter and a programming language. As a command interpreter, the shell provides the user interface to the rich set of GNU utilities. The programming language features allow these utilities to be combined. Files containing commands can be created, and become commands themselves.

Is bash sensitive to indentation?

Bash indenting is very sensitive to characters. For example a space behind “do” in while/for loops will throw it of. When you have nested loops this is very ugly, and makes it hard to follow the code.

What line endings should be used in a bash script?

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

Why does Bash treat a script file as a comment?

If a script file was saved with such line endings, Bash would only see one long line like so: 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.

What is the difference between Mac line endings and DOS line endings?

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.

How do I include all capital letters in a shell script?

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).


1 Answers

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.

like image 84
Anthony Geoghegan Avatar answered Oct 27 '22 05:10

Anthony Geoghegan