Shell Scripting
March 10, 2020

Shell scripts | Directories, files and data

Although the best way to get knowledge in a subject is by doing things with it, before we start with full shell scripting we need to get in touch with the command line.

The command line has a good tool repository that allow us to perform any action in the computer without leaving the keyboard.

We are going to be using pure sh. It may happen your computer has another shell as default like bash. Please note that most of the scripts written in sh will work in bash, but not the other way around.

To know which available shells you have, type cat /etc/shells.

Create files and directories

We have to focus in working with text data since it's what the shell understands and because of the Unix philosophy "everything is a file". Previously we saw with mkdir we can create directories to store files, and with touch we can create files to store text data.

Files and directories starting with a dot will be hidden by default.

— Create directories

$ mkdir .scripts

The example above will create a directory named .scripts in our current working directory. We can also target other directories typing the full path.

$ mkdir /other/full/path/myNewDir

— Create files

touch works similar to mkdir; if no full path is typed then it creates the file in our current working directory

$ touch  myscript

You can add an extension .sh to your script but for shell scripts it isn't mandatory. The same way you can create any other kind of file. Just type the desired extension after the name.

You can also create a file by calling vim to open a non-existing file, which will save the opened buffer once we type :w inside vim.

$ vim myscript

Work with files and directories

If we already have existing directories or files that we want to use, we also have the ability to move them freely around the system.

  • locate finds a file if we know its name but we don't remember where it's placed.
$ locate myFile.txt
  • pwd prints the current working directory.
  • ls lists the files inside a directory. Other option is to use printf "%s\n" *. For scripting purposes is better not to use ls.
# both parameters should list the same
$ ls .scripts/
$ printf "%s\n" .scripts/*

  • mv can move a file from a destination to another, and it can rename files too.
# move the file to another location
$ mv /original/path/file /destination/path/

# change the file's name
$ mv /original/path/old_file_name /original/path/new_file_name

  • cp copies a file into another location. Using the -r flag it can copy directories too.
# copy a file into another location
$ cp /original/path/file /destination/path/

# copy a directory into another location
$ cp -r original/path/dir/ /destination/path/dir/

  • rm removes a file. Using the -r flag it makes the deletion recursive which is useful to remove directories.
# this removes the file if it exists
$ rm .scritps/test.sh

# this removes the file, and ignores it if the marked file doesn't exist
$ rm -f .scripts/test.sh

# this recursively removes a directory, ignoring nonexistent files
$ rm -rf .scripts/

Work with files' data

Once we know how to create and manipulate directories and files it's time to work with data.

The easiest way to write data in a file is by actually typing it, or copy-pasting via a text editor like vim. But there are situations in where we may need to write a log from an action that happens in the machine, or where we don't have the ability to open a text editor to manually type anything (plus it's supposed we're looking to automate tasks and remove some manual interaction).

This is where redirection and the following commands come into place.

  • less prints a file content on the standard output letting the user to scroll trough it page by page.
  • cat concatenates files and prints them on the standard output. As an easy example, try typing cat followed by a path to a text file you have inside your computer. You should see the text file's content printed in the terminal instance. It can also do more things (we'll get back to it in a few lines).
  • tee redirects output to multiple files, copies standard input to standard output and also to any files given as arguments.
  • grep searches input files for a given pattern and displays the relevant lines.
# find the word 'hello' inside the file demo.sh
$ grep hello  .scripts/demo.sh

Most programs have three common jobs: They get input, process that input with the given instructions and translate the processed data into an output result (that can happen to be an error too). Internally, the shell references input, output, error to 0, 1, 2 respectively.

I/O redirection allow us to change where Input/Output comes from since in a normal case scenario, our input is the keyboard and our output is the screen.

Let's introduce append >> and truncate > as our main output redirection operators.

In output redirection, shell will take the standard output of the command and write it to a file instead of displaying it on a screen. It's the shell (sh, bash, etc) the one creating the file and not the redirected (cat, printf, ls, etc) command.

Append [>>] works as follows:

  • Create the specified file if that file doesn't exist.
  • Write at the end of the file.
$ printf "%s\n" "Appending the next info:" > myFile.txt
$ printf "%s\n" * >> myFile.txt
$
$ cat myFile.txt
  Appending the next info:
  Documents/
  Downloads/
  myFile.txt

Note that your list may differ when using the ls command since it'll list your current directories and files.

— Truncate [>] works the following way:

  • Create the specified file if that file doesn't exist.
  • Remove the file's content.
  • Write content to the file.
$ printf "%s\n" "Writing a line" > myFile.txt
$ printf "%s\n" "Truncating a second line" > myFile.txt
$
$ cat myFile.txt
  Truncating a second line

This way we can decide whether we store all the information or only the latest one and make it available for other functions or programs.

If we want to log the output error of a program we need to explicitly specify the redirection operand to do so by adding the internal descriptor, which in the case of stderr is 2.

# this will produce an error since cat cannot display directories
$ cat .scripts/ 2> cat-error.txt

It may happen that we need both stdout and stderr redirected to one file. The way we can tell shell to do that is by redirecting our stderr to stdout the same time we're redirecting our stdout from the program.

# this way we redirect stderr to stdout and print it in the terminal
$ cat .scripts/ 2>&1 > cat-out.txt 

# this way we redirect stderr to stdout and write it into the file
$ cat .scripts/ > cat-out.txt 2>&1

Using the cat command with redirection can copy files into another files, or append a file's content into another:

$ cat myFile > log.txt

will copy myFile's content inside the log.txt file, erasing everything inside log.txt first, while

$ cat myFile >> log.txt

will add myFile's content at the end of the log.txt file.

The cat command can also allow us to write everything we type into the terminal to a file using >> append or > truncate.

$ cat >myFile.txt will let you write the text on terminal which will be saved in a file named file.

$ cat >>myFile.txtwill do the same, except it will append the text to the end of the file.

Note that to end writing into the file we need to press CTRL+D which sends an "end-of-file" character or in order to automate things, we can add an escape string.

# standard EOF workflow:
$ cat > myFile.txt << EOF
everything typed here will be written.
EOF

# custom escape string:
$ cat <<"END" >myFile.txt
  all things here will be written.
  END

Here we can see input redirection being managed by << , indicating the shell to take the text after << as the end of the input.

When writing shell scripts we may face a situation where we need indentation while executing an action before EOF. In this case we need to change << to <<- and use tabs (not spaces) to indent.

# indenting while using command line tools
$ cat > myFile.ext <<- EOF
  this text can be indented.
EOF

# indenting inside a shell script
if [ cond ]; then
  cat > myFile.txt <<- EOF
  we're
  indenting.
  EOF
fi

Instead of truncating, using < will redirect data into a command as its input.

# store content of a directory into a file.
printf "%s\n" .scripts/* > storedList.txt

# pass the created file as grep's input
grep ".sh" < storedList.txt

Working with commands' data

We've been looking at ways to get our output written into a file and our input redirected to take the content from a file. What about letting a command take the output of another command as its input?

Pipes [represented with a vertical bar | ] are the answer to that question.

As an example running top we can display all the ongoing processes and hardware demand of our computer, but the list is huge and maybe we're only interested in the Cpu(s) usage. We know that grep can take an input pattern and print relevant lines from a file, so using a pipe will allow us to combine both commands and get what we're looking for.

$ top | grep Cpu(s)

We can use more than one pipe in a command instruction.

Maybe we want to log all computer's usage data while only printing in the screen the Cpu(s) usage. We can use tee to pass top into a logfile and to grep too.

$ top | tee top-log.txt | grep Cpu(s)

Similar to file redirection, we can pipe stderr instead of stdout. To do so we have to add an & symbol after the pipe's vertical bar |&.

Commands can be queued and executed only if some previous ones have met some special condition. Let's take a look at how: imagine two commands, X and Y.

# & Runs X and then runs Y  in an asynchronous way.
$ X & Y

# ; Runs X and afterwards runs Y in a synchronous way.
$ X ; Y

# && Runs Y only if X is successful in synchronous and.
$ X && Y

# || Runs Y only if X is not successful.
$ X || Y

Almost every commands make use of standard input / output and pass the errors through standard error. Redirection is really handy when solving problems that require to filter a lot of data in order to get the byte we're looking for.