You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
504 lines
86 KiB
504 lines
86 KiB
import { addBanner, addArticle, addTitle, addHeader, addParagraph, addSubHeader } from '/scripts/article.js'; |
|
import { addInset, addInsetList, addInsetCodeListing, addInsetBulletList } from '/scripts/inset.js'; |
|
import { addImageWithCaption, addButtonGroup } from '/scripts/visuals.js'; |
|
import { addSidebar} from '/scripts/sidebar.js'; |
|
import { addSyntax } from '/scripts/code.js'; |
|
import { menu } from '/scripts/web_dev_buttons.js'; |
|
import { global_menu } from '/scripts/grid_layout1.js'; |
|
import { local_menu } from '/scripts/linux.js'; |
|
|
|
const heading = document.querySelector(".heading"); |
|
const global = document.querySelector(".global_menu"); |
|
const local = document.querySelector(".local_menu"); |
|
const sidebar = document.querySelector(".sidebar"); |
|
const main = document.querySelector(".main_content"); |
|
|
|
heading.append(addTitle("Learn Bash Scripting")); |
|
heading.append(addParagraph("Scott Simpson - LinkedIn Learning - September 2022")); |
|
heading.append(addParagraph("Chapter 2 - PROGRAMMING WITH BASH")); |
|
|
|
main.append(addHeader("Understanding Bash Script Syntax")); |
|
main.append(addParagraph("The concept of a line is very important in Bash and also in Bash scripting. If you think of the process of working at a terminal and typing commands, each command is on a line of its own but that doesn't necessarily mean that a line contains only one command. If we have a number of commands typically run in sequence or that might take some time to complete but we don't want to sit at the keyboard ready to type each new command, we can write a sequence of commands on a single line and these can use a number of separators including pipes and redirects as we have seen.")); |
|
main.append(addParagraph("A couple of other useful separators such as")); |
|
main.append(addSyntax("; - this will execute commands in sequence without any checking and will not always wait for the previous command to finish before executing the next.")); |
|
main.append(addSyntax("&& - similar to the semicolon but will only execute a command if the first command completed successfully.")); |
|
main.append(addParagraph("To demonstrate this, I have used cd / to navigate to the root directory - note that I am signed in under the account philip, not root. This means that I don't have permissions to write to this directory but let's say that I write a command to create a directory called mydir and then a second command to list the contents of that directory. We can do that with a semicolon.")); |
|
main.append(addSyntax("mkdir mydir;ll mydir")); |
|
main.append(addParagraph("As a result, we see a permission error from the first command and an error stating that the directory doesn't exist from the second. We could also do this with ampersands.")); |
|
main.append(addSyntax("mkdir mydir;ll mydir")); |
|
main.append(addParagraph("Again, we can see the permissions error from the first command, but we don't see the second error. This is because the first command failed and therefore the second command was not executed.")); |
|
main.append(addParagraph("For comparison purposes, I then navigated to my home directory and ran both versions of the two commands again (removing the directory afterwards in each case) and this time the only output I got from either is the listing of the contents of mydir, which is of course empty.")); |
|
main.append(addParagraph("In my home directory, the first command executes without an error and so the second command will also execute and this is true regardless of whether I am checking for an error. The output of these commands can be seen in figure 37.")); |
|
main.append(addImageWithCaption("./images/sequence.png", "Figure 37 - Executing two commands on one line.")); |
|
main.append(addParagraph("A sequence of commands like this, whether they are using one of the two separators shown above or a pipe or redirect can be quite complex and can overflow on to the next line in the terminal but they don't have an end of line or newline character, so to the shell, it is still just one line. For that reason, these are oftern referred to as one-liners and they do illustrate an important point in Bash. To a human, the end of a line is the right-hand side of the screen (or left-hand side if the text flows from right to left), but to the shell, an end of line is denoted by an end of line character. Because of this, one line as Bash sees it can look like it occupies several lines in the shell so it is important to be careful to understand what is meant be a line.")); |
|
main.append(addParagraph("One of the main advantages of these one-liners is that they can easily be copied and pasted into the terminal whenever you want to run them and it is quite common for administrators to keep a text file with these one-liners on a machine to make them easier to access.")); |
|
main.append(addParagraph("You can also use one-liners with a Bash alias. Figure 38 shows some examples of these that I use on my own web server.")); |
|
main.append(addImageWithCaption("./images/sequence.png", "Figure 38 - The .bash_aliases file from my web server.")); |
|
main.append(addParagraph("These are not particularly complex, but they help me in every day tasks that I perform on the server and the biggest advantage I get from these is that I can run these commands with my preferred options without having to remember precise details and with a lot less typing! The webcopy alias is probably the most complex and it runs three commands. It starts by writing to the log that the copy process has started, it then copies the files over from where I store them in my home directory when I sync these files from my PC to the HTML directory (the live directory) before writing to the log a message stating that the process is complete. You may have noticed that I separate these commands with &&. This isn't because I am concerned that one of the commands might fail (if it did, it would likely be the copy command). Rather, it just helps to ensure that the final log message isn't written before the process is complete.")); |
|
main.append(addParagraph("The copy_status shows me what processes are running but filters the output so that I only see lines that include cp and I use this so that I can check whether files are being copied before I save over the new files which might lead to corrupted files - this probably isn't a serious issue as long as the files on my Windows machine are intact because any corrupted files would be replaced eventually. The files are also copied every ten minutes with a cron job so but I like to avoid writing files while they are being copied just in case it leads to problems, temporary or otherwise.")); |
|
main.append(addParagraph("The logtail alias is also used to check on the status of files being copied because it is showing me the tail end of the log these messages are written to so I can easily see if the process has started or stopped. The html alias is just a convenient way of navigating to the html folder and the vim alias is just because I tend to type vim rather than vi to invoke vim. Either has exactly the same effect so this is just a preference!")); |
|
main.append(addParagraph("The whole point of this discussion on one-liners is really an introduction to Bash scripting because another way to deal with a complex sequence of commands that you want to be repeatable is to write a shell script. That's the subject of the course so we are now going to start looking at how a Bash script is written.")); |
|
main.append(addParagraph("A Bash script is just a text file. Unlike code written in some languages where you have a specific extension used by the compiler to identify source code, Bash scripts don't really need an extension, but it is common to use an sh extension and like most extensions you will see in Linux, this is just to help a human identify the purpose of the file. There are two ways to run the script. The first is to use the bash command with the name of the script.")); |
|
main.append(addSyntax("bash myscript.sh")); |
|
main.append(addParagraph("Interesting side-note - the script does not have to be executable in order for bash to run it. To demonstrate that, I have created a sample script which I've called myscript.sh. The script outputs some data regarding the bash shell and versions of different applications. I'm running this on my web servers simply because it has more software installed on it.")); |
|
main.append(addParagraph("The file itself (viewed with cat) as well as its output is shown in figure 38.")); |
|
main.append(addImageWithCaption("./images/run_script.png", "Figure 38 - Using the bash command to run a bash script.")); |
|
main.append(addParagraph("Notice that I had obtained a listing showing attributes beforehand so we can see that the owner (me) has read and write permissions and everyone else has read-only permission so the file is not executable by anyone. However, the bash command can still run the file as it is that command that is executing the file so these permissions don't apply.")); |
|
main.append(addParagraph("The other way to run the script is to run as though it were an ordinary command. In other words, just by typing the filename but since the script is most likely not in one of the folders Linux will check for an executable, you will have to tell the shell where to find it. If you are in the directory where the script is stored, you can do that with")); |
|
main.append(addSyntax("./myscript.sh")); |
|
main.append(addParagraph("You can also use the full (or absolute) path.")); |
|
main.append(addSyntax("/home/philip/myscript.sh")); |
|
main.append(addParagraph("Figure 38 shows the results of running these scripts in this way using both the relative and absolute paths but you might notice that the first time I tried to do that, I got a permissions error. This is because although the bash command can execute the command without it needing to be executable, if we execute it as a standalone command, it does have to be executable. For that reason, I changed the permissions on it so that it is executable by the owner - me - and tried again and as you can see, it can now be executed in this way and we see the expected output.")); |
|
main.append(addParagraph("Although I didn't do in this script, it is considered good practice to include a shebang line and sometimes it is required. This tells the shell where to find the program that you want to run the script with and that might not be bash. You might be isong a different scripting language such as Ruby or Python. THe traditional way to write the shebang for a bash script is")); |
|
main.append(addSyntax("#!/bin/bash")); |
|
main.append(addParagraph("This is telling the shell to run the script with the bash command located in the /bin folder so that is an absolute path. It is rare, but not impossible, to find that the bash command is located somewhere else, so it is considered good practice to use the shell's environment to locate it. This will mean that the command will work and run the script wherever it happens to be located and the syntax is")); |
|
main.append(addSyntax("#!/usr/bin/env bash")); |
|
main.append(addParagraph("To test this, I have edited the script with both versions and verified that it still runs as expected, which it does!")); |
|
main.append(addParagraph("Another good reason to include the shebang line is that although most Linux installations these days use bash as the default shell, that is not always the case so adding a shebang that references bash is a way to ensure that it does run with bash.")); |
|
main.append(addParagraph("Let's look at an example of a script file. This is just a simple script that prints out a message, but it demonstrates some basic principles. The code is shown in figure 39.")); |
|
main.append(addImageWithCaption("./images/script.png", "Figure 39 - our demonstration shell script.")); |
|
main.append(addParagraph("This is just ouptutting a couple of lines of text but aside from demonstrating how to do that, the script also demonstrates the use of comments in a shell script. The most important point to take from this is that the commands in this file, that is the two echo commands, could also be run directly from the command prompt. The point is that to a large extent, a script is just a collection of individual commands although you will see that there are some programming structures such as loops that are much more appropriate for a script.")); |
|
main.append(addParagraph("You will notice, as well, that I made the script executable with the chmod command which meant that I could run it like this.")); |
|
main.append(addSyntax("./script.sh")); |
|
main.append(addParagraph("The ./ part of the command is telling the shell that the command is in the current directory. We didn't put the script into a directory that is part of the path environment which means that we do have to tell the shell where to find it and that's why we get an error if we type in only the script name.")); |
|
main.append(addParagraph("Notice as well that I gave the script a .sh extension and you don't need to do that if you make it executable so you will often see that being skipped. Personally, I like to add it because even though the system doesn't need it, it makes it much easier for me to identify this as a shell script but that is really just a personal preference.")); |
|
main.append(addParagraph("Another important point that will not be immediately obvious is that when you run a script like this, it actually runs in its own shell and this is a non-interactive shell which means that it will not have many of the customisations a user may have set for their environment. In most cases, this won't be a problem but it is important to be aware of it and you can, if needed, add commands to the script to set these customisations including set or schopt. In most cases, however, you will probably use these to set configurations to suit the script rather than to match your existing shell configuration.")); |
|
main.append(addParagraph("You might wonder why we would bother with a script and in some cases, the reason will be obvious. If you are performing some task that requires you to type in a lot of commands and you want it to be repeatable, it's probably a good candidate for a script. There are also a couple of other reasons for using a script.")); |
|
main.append(addParagraph("One is that with a script, you can troubleshoot if there is an error such as a type but once you have it working, it will probably always work assuming there are no changes to the parameters. For example, it would be pointless to write a script to delete a directory because we already have a command for that and assuming you don't know in advance what the directory name will be (in other words if you want to be able to delete any specified directory), you would have to pass the directory name as a parameter so use the existing command is probably easier. On the other hand, if you have a set of directories and you want to run commands to delete all of those directories, a script might be useful. You could also add the script to cron but bear in mind that in a sense, this takes away some of the advantage. Essentially, if you add a few simple commands to a script and set this to run in cron, there is a good chance that it would be easier and more straightforward to add those commands to cron.")); |
|
main.append(addParagraph("Nevertheless, for commands that you want to repeat on demand, putting them into a script can be very useful.")); |
|
main.append(addParagraph("The second advantage is that in a script, if you have a command that is tedious to type and prone to typing errors, putting in to a script can be a good way to avoid those problems but again, there may be a better option and in this case, that would be an alias. This means (whether you use a script or an alias), that you only have to type the script or alias name so typing errors are less likely.")); |
|
main.append(addParagraph("Finally, and this is perhaps the least obvious advantage is that if you have some commands in a script, it is easier to share these than it is to share the commands individually and expect another user to put them together!")); |
|
main.append(addHeader("Choosing a1099 Text Editor for Bash Scripting")); |
|
main.append(addParagraph("To a large extent, the choice of a text editor when you are writing a bash script isn’t important but you will most likely find that it really depends on the environment you are working in.")); |
|
main.append(addParagraph("If you are working within a desktop environment, a good IDE like VS Code can be useful but when it comes to writing a bash script, in most or perhaps all cases, a script will be a standalone file. Of course, running it may involve using other files such as data files, but you don’t have the sort of complicated file structure that you will find with many modern applications such as a node or Android application.")); |
|
main.append(addParagraph("The benefits that you generally get from using an IDE include code completing, syntax highlighting and so on although it is also worth noting that a lot of good text editors such as Notepad++ actually offer similar features.")); |
|
main.append(addParagraph("The image below is a screenshot of an IDE on Windows, in this case it is Visual Studio Code, which you can download for free from <a href=' https://code.visualstudio.com/download'> here</a>.")); |
|
main.append(addImageWithCaption("./images/vscode.png", "Editing a bash script in windows using VS Code.")); |
|
main.append(addParagraph("This is the IDE I use and it also has some nice features relevant to bash scripting, particularly the terminal. If, like me, you have a Linux machine such as a Raspberry Pi where you like to do your development work, VS Code will allow you to connect to that machine so you can edit the code from your Windows desktop. Another really useful feature of this is that you can start editing a script by typing a command such as")); |
|
main.append(addSyntax("code script.sh")); |
|
main.append(addParagraph("which will open up the file for you in VS Code.")); |
|
main.append(addParagraph("VS Code is available on pretty much any platform so it can be a good choice because you can pretty much use it on any computer!")); |
|
main.append(addParagraph("If you have to work in a terminal where you don’t have the flexibility to run an IDE, there are several text editors such as vim or nano. Which you use is largely a matter of personal preference or may be restricted to whatever is available. Personally, I find that it takes a lot of practice to get used to. I have used vim quite a lot and I do use it on a daily basis so I tend to prefer it and I really don’t like nano but you’ll find a lot of people prefer it.")); |
|
main.append(addParagraph("The following image shows the same file as in the previous image but this time, it is being edited in vim.")); |
|
main.append(addImageWithCaption("./images/vim.png", "Editing a bash script in vim.")); |
|
main.append(addParagraph("Note that I am assuming that if you are working from a terminal, you are on or are connecting to a Linux machine because you probably wouldn't use a terminal to edit files on a Windows machine. Depending on how that is setup, you may get syntax highlighting but that will not always be the case.")); |
|
main.append(addParagraph("I could sum up here by saying that you can just use whatever text editor you like and that is mostly true for bash scripting where most of your scripts will be short. Two things worth bearing in mind are that the choice of editor will be at least partly down to what's available to you and picking one that you can use on different systems might be useful. More importantly, there may be a number of factors in determining what makes a text editor good, but if you can easily use it to write a text file, that should be the only factor you need to consider.")); |
|
main.append(addParagraph("That being said, that is largely because of the fact that we are talking about bash scripting. For example, I would absolutely not recommend writing a full scale application utilising dozens or hundreds of files using vim.")); |
|
main.append(addParagraph("If you look at the image taken from VS Code above, you may have noticed that the EXPLORER pane on the left side is pretty full. This is showing the files structure for this website and you will see that the script.sh file is there. If I wanted to make a simple change to one of these files, I could in theory do it in vim. I say in theory because these files are on a windows PC but the live files are on a Raspberry Pi and I can certainly make quick changes to the files there with vim.")); |
|
main.append(addParagraph("You need to be careful with this though, particularly if you are editing a different copy of the file. For example, I could open up the file for this page in vim and edit the chapter title for example. Let's say I changed 'Learn Bash Scripting' to 'Learn Vim Scripting', the change would show in the website within about a minute if I edit the file in Visual Studio.")); |
|
main.append(addParagraph("A simple edit like that can easily be done in vim and if I do that, the change shows on the website immediately since I am editing the live files. However, within a minute, this would change back to 'Learn Bash Scripting' because I have WinSCP running to sync the dev copy of the files to the Pi which it will do instantly and once a minute, those files are sync'ed with the live copy.")); |
|
main.append(addParagraph("The point here is that you can run into issues with synchronisation of files if you edit it from different systems or if you have, as I do with my website, a copy of the files for editing and a live copy so this is something to be aware of. It's not as likely to be an issue with a simple bash script but it is something to be aware of.")); |
|
main.append(addParagraph("Actually, when it comes to editing files on different systems, an important consideration is file permissions. For example, if you edit a file on one system and copy if over to another, like with my website, the file will be owned by the user you connected to the system with and that's why I copy the files to a folder on my Pi and from there sync the files to the live folder. That allows me to copy everything across with a standard user account. The sync job runs in the crontab for root so the live files are owned by root.")); |
|
main.append(addParagraph("So these considerations are less significant in the context of bash scripting but they are important to be aware of.")); |
|
main.append(addParagraph("One final point I would make is that if you want to do any serious development, particularly as a career, being able to edit files on different systems and being aware of the issues that can arise is going to be very important for you.")); |
|
main.append(addHeader("Displaying Text with echo")); |
|
main.append(addParagraph("In bash scripting, there are a number of ways of outputting text and one of the most common is echo. It's a really straightforward command in that the syntax is no more complicated than typing echo followed by what you want to display and that can be literal text, variables or a combination of the two.")); |
|
main.append(addParagraph("We have already seen some examples of this in the script.sh file, for instance. In that script, we used echo and string in double quotes but those quotes are optional. We could for example, do the following.")); |
|
main.append(addSyntax("echo hello")); |
|
main.append(addParagraph("If we want to output two strings (or two words), we could do it with")); |
|
main.append(addSyntax("echo hello there")); |
|
main.append(addParagraph("It does seem a little strange to refer to that as two strings but that's not really too important. However, just to make it clearer, if we put one or more of those words in double quotes which is really identifying it as a string, we still get the same output as you can see in the following examples.")); |
|
main.append(addImageWithCaption("./images/echo1.png", "Some examples of using the echo command.")); |
|
main.append(addParagraph("Obviously, these are fairly trivial examples and you can use any of these methods to output text like this but you will almost certainly just use the simplest format in most, if not all, cases so that would be echo followed by the text and although echo doesn't need it, you might want to put the text in quotes.")); |
|
main.append(addParagraph("Actually, just a quick digression here, in these examples, the echo command isn't in a script, these are just being entered at the command line and in most cases, these two scenarios are interchangeable. What I mean by that is that you can insert the command into a script or just run it at the command line and as long as it doesn't depend on other parts of the program to execute correctly, you should see exactly the same output in both cases.")); |
|
main.append(addParagraph("Examples of where you would see a difference or where the command can't be run at the command line would include a command like echo that outputs a variable that is set and perhaps modified in the script. Let's say you have a script which prompts the uses to input their name which you assign to a variable, name, and use in the output. This is shown below.")); |
|
main.append(addImageWithCaption("./images/echo2.png", "Using echo with a variable.")); |
|
main.append(addParagraph("So we have two commands in the script, the first one reads in name and the second outputs a string that includes that variable. If we take the output command and run it at the command line, we see the string is output without a name because the shell doesn't recognise the variable. However, if we run the read command first at the command line, now it does recognise the variable and the output command will give us the same output as we see when running the script.")); |
|
main.append(addParagraph("This can be useful if you are trying to locate an error in your script when you can run the command on its own at the command line.")); |
|
main.append(addParagraph("Again, the quotes are optional so we can write the echo command (either in the script or at the command line) as")); |
|
main.append(addSyntax("echo hello, $name")); |
|
main.append(addParagraph("and the effect is the same. There are some instances where a variable can be used in an echo statement without being initialised beforehand. For example, you can use an echo command to output the return value of a function like this.")); |
|
main.append(addSyntax("echo The kernel is $(uname -r)")); |
|
main.append(addParagraph("The uname command will return some system information and it takes an option, in this case -r for the kernel release, that determines what information is displayed. This will give you output that looks something like this.")); |
|
main.append(addSyntax("The kernel is 6.1.21-v8+")); |
|
main.append(addParagraph("You can get a full list of the available options with")); |
|
main.append(addSyntax("man uname")); |
|
main.append(addParagraph("You can use the cat commands to output the current value of one of the environment variables. You can get a list of these with the command")); |
|
main.append(addSyntax("printenv")); |
|
main.append(addParagraph("and you can output any of the values with the echo command and the variable name. This is shown below.")); |
|
main.append(addImageWithCaption("./images/echo3.png", "The output of printenv and the value of an environment variable displayed with echo.")); |
|
main.append(addParagraph("As we have seen, the decision on whether to use quotes or not can seem to be arbitrary, but there are some occasions when you do need to use them. For example, the command")); |
|
main.append(addSyntax("echo $(uname -r)")); |
|
main.append(addParagraph("Will output the kernel version, but let's say that I don't want to display the command's output, I want to display the command itself as a string. There are a couple of ways to do that and one is to use quotes. Now, $(uname -r) is essentially a variable when we use it in an echo command representing the output from uname so if we put the whole thing in quotes, that won't work. One option would be to output two strings so we can separate the $ from the command so that it is no longer treated as a variable.")); |
|
main.append(addSyntax("echo \"echo $\"\"(uname -r)\"")); |
|
main.append(addParagraph("Actually, the line you see above has a lot of quote marks and because I am generating this with a call to a JavaScript function, those quote marks can cause a lot of confusion. The syntax for the function that generates what I call inset text and this is essentially highlighted text is")); |
|
main.append(addSyntax("main.append(addSyntax(\"\"));")); |
|
main.append(addParagraph("So everything that is being displayed on that line is enclosed in a string passed to the function and then used to generate the HTML element. To get the function to be displayed here, I have put it in a string enclosed in double quotes but since it includes double quotes in the command, the server might well interpret this as two strings since the second set of quotes will terminate the first string prematurely. There are a couple of solutions. One would be to put the string in single quotes so on the backend, the JS code will look like this.")); |
|
main.append(addSyntax("main.append(addSyntax(''));")); |
|
main.append(addParagraph("Alternatively, I can escape characters where I want the character itself to be output rather than being interpreted as a string terminator so the code will look like this.")); |
|
main.append(addSyntax("main.append(addSyntax(\"\\\"\\\"\"));")); |
|
main.append(addParagraph("In that last example, I also included quote marks within the string to show how this works. Obviously, would need to add some text in there to make it clearer so let's say I want to output a quote in double quotes, the command would look like this on the backend")); |
|
main.append(addSyntax("main.append(addSyntax(\"\\\"To be or not to be!\\\"\"));")); |
|
main.append(addParagraph("and the actual output would be")); |
|
main.append(addSyntax("\"To be or not to be!\"")); |
|
main.append(addParagraph("You might be wondering about how this is related to our bash scripting or more specifically, how we use echo either in a script or at the command line. The answer is that these principles also apply when you use the echo command.")); |
|
main.append(addParagraph("We can also mix single and double quotes here but the primary reason for doing that is to allow you to out quote marks so you would usually do that when you have an embedded string which we don't. If we wanted to print a quote out for example, mixing them is more useful so our echo command might look like this")); |
|
main.append(addSyntax("echo '\"To be or not to be!\"'")); |
|
main.append(addParagraph("We can also use escape characters so our command would look like this.")); |
|
main.append(addSyntax("echo \"\\\"To be or not to be\\\"\"")); |
|
main.append(addParagraph("Going back to our example with uname, we can simply escape the $ character so it will be included in the output and will not be interpreted as introducing a variable and the rest of the string would be interpreted as plain text, so that would be")); |
|
main.append(addSyntax("echo \"\$(uname -r)\"")); |
|
main.append(addParagraph("which will give us the output")); |
|
main.append(addSyntax("$(uname -r)")); |
|
main.append(addParagraph("Let's see what happens if we run that same command again with single rather than double quotes, so that is")); |
|
main.append(addSyntax("echo '\$(uname -r)'")); |
|
main.append(addParagraph("You would probably expect the output to be the same, but the output this gives you is actually")); |
|
main.append(addParagraph("\$(uname -r)")); |
|
main.append(addParagraph("The reason for that is that although to some degree, single and double quotes are interchangeable, bash actually treats them slightly differently. When we run a command like this")); |
|
main.append(addSyntax("echo \"$(uname -r)\"")); |
|
main.append(addParagraph("the contents of the string are interpreted. In other words, bash outputs the string but recognises the fact that the string contains a variable and that variable is actually the output of uname so this gives us the command output rather than the command itself. If we run this with single rather than double quotes")); |
|
main.append(addSyntax("echo '$(uname -r)'")); |
|
main.append(addParagraph("The contents of the string are not interpreted, they are just interpreted as being a string literal and so we get the command displayed in the output. This is a really important point because it is easy to see single and double quotes as being interchangeable but this is not always the case. It's also worth remembering because if you do want to generate some output that includes a command, simply using single quotes to wrap the string will be enough but using double quotes will not give you the result you want.")); |
|
main.append(addParagraph("Going back to our example with uname, we can simply escape the $ character so it will be included in the output and will not be interpreted as introducing a variable and the rest of the string would be interpreted as plain text, so that would be")); |
|
main.append(addSyntax("echo \"\$(uname -r)\"")); |
|
main.append(addParagraph("which will give us the output")); |
|
main.append(addSyntax("$(uname -r)")); |
|
main.append(addParagraph("Let's see what happens if we run that same command again with single rather than double quotes, so that is")); |
|
main.append(addSyntax("echo '\$(uname -r)'")); |
|
main.append(addParagraph("You would probably expect the output to be the same, but the output this gives you is actually")); |
|
main.append(addParagraph("\$(uname -r)")); |
|
main.append(addParagraph("The reason for that is that although to some degree, single and double quotes are interchangeable, bash actually treats them slightly differently. When we run a command like this")); |
|
main.append(addSyntax("echo \"$(uname -r)\"")); |
|
main.append(addParagraph("the contents of the string are interpreted. In other words, bash outputs the string but recognises the fact that the string contains a variable and that variable is actually the output of uname so this gives us the command output rather than the command itself. If we run this with single rather than double quotes")); |
|
main.append(addSyntax("echo '$(uname -r)'")); |
|
main.append(addParagraph("The contents of the string are not interpreted, they are just interpreted as being a string literal and so we get the command displayed in the output. This is a really important point because it is easy to see single and double quotes as being interchangeable but this is not always the case. It's also worth remembering because if you do want to generate some output that includes a command, simply using single quotes to wrap the string will be enough but using double quotes will not give you the result you want.")); |
|
main.append(addParagraph("Because text within the single quotes is not interpreted by bash, everything inside the quotes is treated as a literal string, these are sometimes known as strong quotes.")); |
|
main.append(addParagraph("One final thing to be aware of with echo and we saw this in our first script, script.sh which you can see in an earlier section.")); |
|
main.append(addParagraph("As a reminder, the file is also shown below.")); |
|
main.append(addImageWithCaption("./images/vim.png", "The script.sh file.")); |
|
main.append(addParagraph("You might recall that this outputs hello on one line and there on the second line. This is because echo automatically adds a newline character to the end of its output. We can override this by passing it the -n option so if we replace the first echo command in the script with")); |
|
main.append(addSyntax("echo -n \"hello\"")); |
|
main.append(addParagraph("we won't get the newline character so the output would be")); |
|
main.append(addSyntax("hellothere")); |
|
main.append(addParagraph("So both line are going to be output on the same line. You might want to take that into account when writing the script because you would need to insert a space yourself in either of the echo commands or in a separate command. For example, if you choose to add an echo command in between, which is unlikely because it's easier to add a space to one of the existing commands, your script would look like this.")); |
|
main.append(addInsetCodeListing(["#!/usr/bin/env bash", " echo -n \"hello\"", "", " echo -n \" \"", "", " # This is a comment", " echo \"there\""])); |
|
main.append(addParagraph("You would have to remember that when adding the space, you also need the -n option here to prevent echo from adding a newline character which would mean we still get output on two lines. A much neater way to do this and one that is likely to give you an error is to put a space in one of the other commands so your script might look like this")); |
|
main.append(addInsetCodeListing(["#!/usr/bin/env bash", " echo -n \"hello\"", "", " # This is a comment", " echo \"there\""])); |
|
main.append(addHeader("Working with Variables")); |
|
main.append(addParagraph("We mentioned earlier that bash uses the $ sign to denote a variable. Strictly speaking, it introduces a parameter expansion, command substitution or arithmetic expansion.")); |
|
main.append(addParagraph("When you are working with variables, you will generate be using it to denote a parameter expansion and the basic form of that is")); |
|
main.append(addSyntax("${parameter}")); |
|
main.append(addParagraph("Essentially, the parameter name is replaced with the parameter value. When we used echo to output the result of executing the uname command, this was an example of command substitution which is more or less the same thing except that we are swapping the function call with the result of running the function and with arithmetic expansion, we substitute an arithmetic expression with the result of evaluating that expression.")); |
|
main.append(addParagraph("If you are interested, you can get some more info on this in the online bash manual under <a href='https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html'>Shell Parameter Expansion</a>.")); |
|
main.append(addParagraph("Actually using variables is not too different from any other programming language but it also has its own peculiarities. Particularly, when you declare a variable, it is important to ensure you don't embed unnecessary spaces in the command. As an example, a generic variable declaration and initialisation might look something like this.")); |
|
main.append(addSyntax("myvar = 5")); |
|
main.append(addParagraph("This would give you an error in bash because it sees the parameter expression as being a single unit of code and the spaces disrupt that so you need to ensure that there is no space before or after the = sign. If you try to input this at the command line with the spaces, bash will assume that the first part, myvar, is a command so you will likely see a command not recognised error. The correct format for bash is")); |
|
main.append(addSyntax("myvar=5")); |
|
main.append(addParagraph("Bash can be quite flexible when it comes to variable names so you can use upper or lower case or a mixture as well as numeric characters but the name can't start with a number. The environment variables tend to be all upper case so to differentiate your own variables, it is considered good practice to use all lower case but that' is not mandatory.")); |
|
main.append(addParagraph("You will also find that in many cases, bash is also flexible about whether the name or value is enclosed in braces or quotes but this is sometimes required. For example, if the value includes an embedded space, you will need to put that in quotes or escape the space. Usually, using quotes is better because it is a little easier to read.")); |
|
main.append(addParagraph("Let's look at another example.")); |
|
main.append(addSyntax("number=16")); |
|
main.append(addParagraph("We can output the value with echo like this.")); |
|
main.append(addSyntax("echo ${number}")); |
|
main.append(addParagraph("In this case, the braces are not required but you can use them if you like.")); |
|
main.append(addParagraph("The image below shows a slightly more interesting example. This is a script whjere we create 4 different variables, output the initial value, set the value to something else and then output the new value. It also shows the output we get when running the script.")); |
|
main.append(addImageWithCaption("./images/variables.png", "")); |
|
main.append(addParagraph("This demonstrates a number of things related to variables in bash so we will look at each of the variables in turn.")); |
|
main.append(addSyntax("myvar")); |
|
main.append(addParagraph("This is similar to most of the examples we have seen so far. As start by giving it a string a value and after outputting that value with echo, we give it a new value and output that.")); |
|
main.append(addSyntax("myname")); |
|
main.append(addParagraph("This is a little more interesting because it introduces the declare keyword and this serves two purposes. The first is that it lets someone reading the script see that a variable is being declared. This isn't a requirement although I don't yet know if this has any implications for variable scope.")); |
|
main.append(addParagraph("More importantly, it allows you to introduce options so you can create different types of variable. In this case, the -r option was been added and this makes the variable read only. As a result, we get an error when we try to set the variable to a new value.")); |
|
main.append(addParagraph("When we run the echo command to output myname a second time, notice that it still displays the original value.")); |
|
main.append(addSyntax("lowerstring and upperstring")); |
|
main.append(addParagraph("Again, we declare the variable and this time we use -l for lowerstring and -u for upperstring. In both cases, we give the variable an initial value that includes a mixture of upper and lower case letters and after outputting that value, we change it to another value which also has mixed case.")); |
|
main.append(addParagraph("Regardless, the output is lower case only when the variable is declared with the -l option and upper case only when declared with -u. I'm sure that won’t be too much of a surprise.")); |
|
main.append(addParagraph("The main thing to take from this is that the declare command on its own isn't really doing anything, but if you add an option, this allows you define some of the properties of the variable. You can combine options where it makes sense to do that so for example")); |
|
main.append(addSyntax("declare -lr lowerstring")); |
|
main.append(addParagraph("will declare the variable as lowercase and also as read-only. On the other hand")); |
|
main.append(addSyntax("declare -lu lowerstring")); |
|
main.append(addParagraph("which is essentially declaring the variable as both lower and upper case is ignored. There is no man page for declare but you can see a list of the possible options on linuxhimt.com under <a href=' https://linuxhint.com/bash_declare_command/'>Bash Declare Command</a> and the main is by Fahmida Yesmin.")); |
|
main.append(addParagraph("Note that the -r option literally is read-only. Once you have set a value and declared it as read-only, it cannot be changed which is what you would expect. However, you might expect that if you declare a variable as read-only like this")); |
|
main.append(addSyntax("declare -r greeting")); |
|
main.append(addParagraph("you might expect that since it doesn't have a value, it can still be initialised but in fact, you will get an error message telling you the variable is read-only if you try to do that. I guess that if you do something like this")); |
|
main.append(addSyntax("declare -r greeting=\"hello\"")); |
|
main.append(addParagraph("the value is assigned before the read-only property is applied but in either case, once the variable has been declared read-only, you can no longer assign a value to it even if it doesn't have one.")); |
|
main.append(addParagraph("When you declare a variable in a shell, that variable will persist along as the shell is open and will be discarded as soon as you close the shell. If you have had your shell open for some time, you may have trouble remembering what variables you have created and what values have been assigned to them.")); |
|
main.append(addParagraph("Fortunately, with declare you can do a little more than just create a variable. You can use it with a p option but no variable, for example and the result will be that the shell will display a list of all the variables that exist within that shell session together with their values.")); |
|
main.append(addSyntax("declare -p")); |
|
main.append(addParagraph("This will display all the variables recognised by the shell so it does also include the environment variables.")); |
|
main.append(addParagraph("In my experience, you will very rarely see declare being used in a bash script because it has no effect if you are not using an option.")); |
|
main.append(addHeader("Working with Numbers")); |
|
main.append(addParagraph("Bash provides us with a couple of tools for working with numbers and these are arithmetic expansion and arithmetic evaluation.")); |
|
main.append(addParagraph("With arithmetic expansion, we can perform basic arithmetical operations on both literal numbers and variables. Arithmetic evaluation is similar, but is used to make changes to the value of a variable.")); |
|
main.append(addParagraph("Bash support all of the arithmetic operators you might expect to see and these are:")); |
|
main.append(addSyntax("Operator - Operation")); |
|
main.append(addSyntax(" + - Addition")); |
|
main.append(addSyntax(" - - Subtraction")); |
|
main.append(addSyntax(" * - Multiplication")); |
|
main.append(addSyntax(" / - Division")); |
|
main.append(addSyntax(" % - Modulo")); |
|
main.append(addSyntax(" ** - Exponentiation")); |
|
main.append(addParagraph("In bash, we can perform any of these operations with integers but bash does not support these with floats.")); |
|
main.append(addParagraph("Let's look at some examples of using an arithmetic expansion.")); |
|
main.append(addSyntax("echo $((4+4));")); |
|
main.append(addParagraph("This is possibly the simplest form of an arithmetic expansion using only literals and you won't be surprised to see 8 as the output. We can also use variables. Let's start with a variable, a.")); |
|
main.append(addSyntax("let a=4")); |
|
main.append(addParagraph("We''ll create another variable b, giving the same value as a.")); |
|
main.append(addSyntax("let b=a")); |
|
main.append(addParagraph("and we can now put these into our expansion.")); |
|
main.append(addSyntax("echo $((a+b));")); |
|
main.append(addParagraph("This will also give us 8 as the output.")); |
|
main.append(addParagraph("We could write these with arithmetic expressions containing any of the arithmetic operators and these can be combined. For example")); |
|
main.append(addSyntax("echo $(( 3 + 6 – 5 * 5 * 2));")); |
|
main.append(addParagraph("This is probably a little too long to be easily read but it returns -41. In bash, the rules of precedence apply as normal so the multiplications will be done before the addition and subtraction.")); |
|
main.append(addParagraph("We can add parentheses which can change the order in which these operatons are performed but we may also want to add them just to make the whole expression easier to read. For example")); |
|
main.append(addSyntax("echo $(( (3 + 6) – 5 * (5 * 2) ));")); |
|
main.append(addParagraph("is easier to read but in this case , the order of operation is the same so we still see -41 being returned. Note that there are also some spaces added in to separate operations and again, this is for readability.")); |
|
main.append(addParagraph("Let's look at an example where a variable is being modified. To start with, we will create the variable a and give it a value of 3.")); |
|
main.append(addSyntax("let a=3")); |
|
main.append(addParagraph("We will then use an expression to add three to a.")); |
|
main.append(addSyntax("((a=a+3));")); |
|
main.append(addParagraph("which we can also write in shorthand.")); |
|
main.append(addSyntax("((a+=3));")); |
|
main.append(addParagraph("Now, if we output the value of a with a command such as")); |
|
main.append(addSyntax("echo $a")); |
|
main.append(addParagraph("we will see 6 being output. We can also use increment and decrement operators so")); |
|
main.append(addSyntax("((a++));")); |
|
main.append(addParagraph("will increment a. The increment and decrement operators have both pre and post versions although if you don't do anything with the value when you increment it, the effect will be the same. For example, if we run")); |
|
main.append(addSyntax("((++a));")); |
|
main.append(addParagraph("and then output the value of a we will get 8 as the output since a had a value of 6 and has been incremented twice.")); |
|
main.append(addParagraph("To demonstrate the difference, given that a has a value of 8, what would you expect to see when running this command.")); |
|
main.append(addSyntax("echo $((a++));")); |
|
main.append(addParagraph("You might expect to see 9 as the output, but we actually see 8 which might suggest that a wasn't incremented. Let's try running this command and see if it gives you the output you expect.")); |
|
main.append(addSyntax("echo $((++a)")); |
|
main.append(addParagraph("This time, the output is 10 which might seem strange since the last time we output a, we got 8.")); |
|
main.append(addParagraph("The explanation for this is that both commands incremented a as they did when we didn't have the output and the increment in the same command.")); |
|
main.append(addParagraph("The first increment was using the post-increment operator so it did increment a, but it output the value before incrementing it so we got 8 as the output but then a was incremented and therefore had a value of 9.")); |
|
main.append(addParagraph("The second increment operator was the pre-increment operator. It also incremented the value of a, but it does that before outputting it so the value becomes 10 and that's the value we see in the output.")); |
|
main.append(addParagraph("Notice that we never saw 9 in the output because we did not output a after using the post-increment operator. This is something you may need to be careful of because it can lead to some confusion over what the value of the variable is when you use the post increment operator.")); |
|
main.append(addParagraph("It is also worth remembering that in these examples, because we used a command like this")); |
|
main.append(addSyntax("echo $((a++));")); |
|
main.append(addParagraph("Remember this is the post-increment operator. We are doing two things with a, we are displaying it and incrementing it so the order in which these happen is important. If we are only incrementing it, then the order is not important so that's why when we ran")); |
|
main.append(addSyntax("((a++));")); |
|
main.append(addParagraph("and")); |
|
main.append(addSyntax("((++a));")); |
|
main.append(addParagraph("We didn't see any difference in the two. As a matter of interest, you will often see something like i++ or index++ in a for loop but rarely, if ever, ++i or ++index and in this case, it makes absolutely no difference whether you use the pre- or post-increment operator so you can use either without affecting how the loop works. However, the post-increment operator is normally used and this is just a convention, but your loop would probably look weird if you didn't use it. By definition, a convention is not a requirement but not following conventions can be confusing so it is usually best to follow them unless you have a good reason not to!")); |
|
main.append(addParagraph("Notice that we are using double parentheses and sometimes also using a $ sign. The double parentheses is used for arithmetic evaluation and this allows us to modify the value of a variable but it doesn't use the $ sign. If we add the $ sign, this gives us an arithmetic expansion and we cannot modify a variable.")); |
|
main.append(addParagraph("You will also notice that when we enter an arithmetic evaluation at the command line, for example")); |
|
main.append(addSyntax("((a++));")); |
|
main.append(addParagraph("we don't get any output. Obviously, this is because we have modified the value of a but we haven't displayed the new value. We can do that with")); |
|
main.append(addSyntax("echo $a")); |
|
main.append(addParagraph("which will output the current value of a. If we had used a $ with the a in the arithmetic evaluation")); |
|
main.append(addSyntax("(($a++));")); |
|
main.append(addParagraph("we would get an error like this.")); |
|
main.append(addSyntax("-bash: ((: 14++: syntax error: operand expected (error token is "+")")); |
|
main.append(addParagraph("Hopefully, you will be able to make sense of this but what has happened? I should point out that the current value of a is 14 so the $a has been expanded, replacing a with the actual value. In essence, the command is equivalent to")); |
|
main.append(addSyntax("((14++));")); |
|
main.append(addParagraph("14 is a literal number, it can't have any value other than 14 and it doesn't make any sense to try to increment it so it gives an error saying this operand is not what it expects to see.")); |
|
main.append(addParagraph("We've already seen something called the combination assignment and this is a shorthand method of changing the value of a variable. For example, consider the following")); |
|
main.append(addSyntax("((a=+2));")); |
|
main.append(addParagraph("This is equivalent to")); |
|
main.append(addSyntax("((a=a+2));")); |
|
main.append(addParagraph("This can be a convenient shorthand but it also helps us to avoid I confusing situation. For example, if we run this as")); |
|
main.append(addSyntax("a=a+2")); |
|
main.append(addParagraph("It looks like that would do the same thing. However, if we then output the value of a, we get this")); |
|
main.append(addSyntax("a+2")); |
|
main.append(addParagraph("You will also say other scenarios where we're seeing something similar and this is because bash is treating this as a string.")); |
|
main.append(addParagraph("Actually, the problem here is that we are not using the correct syntax for an arithmetic evaluation. We are missing the parenthesis.")); |
|
main.append(addParagraph("Personally, I don't see that using the assignment shorthand helps here, but using the correct syntax to let bash know that we are using.")); |
|
main.append(addParagraph("A better way to do that, however, is to use the declare variable which allows us to tell bash what the type of a variable is. To do this, we need to add an option to declare so, for instance, if we want to declare a as an integer, we can do it like this")); |
|
main.append(addSyntax("declare -i a=4")); |
|
main.append(addParagraph("For more info on declare, there is a page on linuxhint.com entitled <a href='https://linuxhint.com/bash_declare_command/'>Bash Declare Command</a> which was written by Fahmida Yesmin.")); |
|
main.append(addParagraph("Using the declare command means that bash will no longer use the variable a as a string. In other words, it recognises that a is an integer an so it treats it as an integer. That means we cannot do something like")); |
|
main.append(addSyntax("a=a+2")); |
|
main.append(addParagraph("and assuming a has a value of four, if we now outputs the value of a we will get 6. So, bash recognises that is an integer and we no longer get this weird behaviour where bash suddenly starts treating a as a string which can be very confusing, especially for beginners.")); |
|
main.append(addParagraph("If you are using numbers a lot in your scripts, you will eventually find that you need to use floating point numbers. In bash, the only division operator we have is for integer division. For that reason, if we want to do floating point division (or any floating point arithmetic) we need to use something other than bash.")); |
|
main.append(addParagraph("One example if a tool we can use to do floating point arithmetic is bc – basic calculator. To see how that works, we will create two integer variables with declare statements. We will use c for the first and give it a value of 1.")); |
|
main.append(addSyntax("declare -i c=1")); |
|
main.append(addParagraph("We will call the second d and give it a value of 3.")); |
|
main.append(addSyntax("declare -i d=3")); |
|
main.append(addParagraph("We will then set the value of e to the result we get dividing c by d and pipe that calculation to bc. One way to do that would be like this.")); |
|
main.append(addSyntax("e=$(echo \"scale=3; $c/$d\" | bc)")); |
|
main.append(addParagraph("To understand what is happening here, the key is the order in which things happen. Firstly, we have a string literal which is")); |
|
main.append(addSyntax("\"scale=3; $c/$d\")")); |
|
main.append(addParagraph("Since it is a string literal, it just returns itself. The echo statement outputs this and ordinarily, it would send its output to the screen, but in this case it is being piped in to another command, bc.")); |
|
main.append(addParagraph("The bc command takes the input which is")); |
|
main.append(addSyntax("scale=3; $c/$")); |
|
main.append(addParagraph("and performs the division operation. The value for scale determines the number of places to which the division operation returns so it should give us an approximation of one third. Finally, the result of that operation is assigned to the variable e which has not been declared as an integer and if we output e")); |
|
main.append(addSyntax("echo $e")); |
|
main.append(addParagraph("we get")); |
|
main.append(addSyntax(".333")); |
|
main.append(addParagraph("One important point about the output here is that although bc has given us the correct result for that calculation, it actually outputs this as a string. For example, if we want to display the output with a 0 in front of the decimal point, we could output it like this")); |
|
main.append(addSyntax("echo 0$e")); |
|
main.append(addParagraph("which uses string concatenation to put the 0 at the front of the output.")); |
|
main.append(addParagraph("RAMDOM is a useful built-in variable in bash whose value corresponds to a random integer between 0 and 32,767. For example, if we run the command")); |
|
main.append(addSyntax("echo $RANDOM")); |
|
main.append(addParagraph("There are a couple of ways in which this value can be manipulated to make it more useful. For example, if we want a random number greater than 0 but we are not too concerned about the upper limit, we can add 1 to the response so")); |
|
main.append(addSyntax("echo $RANDOM+1")); |
|
main.append(addParagraph("In most cases, we will be concerned about the upper limit. For example, a typical use-case might be where we want to simulate the role of a die so we are looking for a value between 1 and 6.")); |
|
main.append(addParagraph("Consider the following expression.")); |
|
main.append(addSyntax("a=x%6")); |
|
main.append(addParagraph("This will give us the remainder after dividing x by 6. This should be a value between 0 and 5 provided that the value of x is at least 5. In this case, RANDOM is perfectly adequate for this purpose so we might use the expression")); |
|
main.append(addSyntax("a=RANDOM%6")); |
|
main.append(addParagraph("This will give us a random value and since we know this is a number between 0 and 32767, %5 will give us a value between 0 and 5 and this is essentially a random number between 0 and 5. If we then add 1 to that value, this becomes a random number between 1 and 6.")); |
|
main.append(addParagraph("I don't want to digress too far into how computers in general or how bash in particular generates a random number but you probably know that it is not generally random. However, as long as you don't know in advance what number RANDOM will throw up, you can treat it as a random number. The code to generate that number is")); |
|
main.append(addSyntax("a=RANDOM%6+1")); |
|
main.append(addParagraph("It is critical for this to work properly that if you take a general expression")); |
|
main.append(addSyntax("a=x%y+1")); |
|
main.append(addParagraph("the output should be a random number between 1 and y but only if x either has a value or is a random value that is at least 5.")); |
|
main.append(addParagraph("Consider this scenario. We generate a random number between 1 and 3 and then use that as the x value in this expression with 6 as the y value so")); |
|
main.append(addSyntax("let a=RANDOM%3+1;let b=a%6;echo $b")); |
|
main.append(addParagraph("It might be worth taking a moment to think about what the output of this would be and you can find that out by running it several times. The output will be a random number between 1 and 3 and the value will always be the value we got from the first expression. Since the maximum value of that expression is 3, which is less than 6, running integer division on this with 6, in other words")); |
|
main.append(addSyntax("a/6")); |
|
main.append(addParagraph("will always return 0. The remainder will therefore be whatever number we get from the first expression so it will always be somewhere between 1 and 3. Let's look at another example which shows how we can weight the die in favour of smaller numbers.")); |
|
main.append(addSyntax("let a=RANDOM%7;let b=a%6+1;echo $b")); |
|
main.append(addParagraph("This will work and in fact the issue with it is a little more subtle. We start by generating a random number between 1 and 7 and then use that with %6 to get a number between 1 and 6. The problem is that we have 7 possible inputs and 6 possible outputs which means that there are 2 values (which are 1 and 7), either of which will ultimately simulate rolling a 1 but there is only one possible input for each of the other outputs which means that 1 is twice as likely to appear as any other number.")); |
|
main.append(addParagraph("If you test your code which is generating these numbers, this can be quite hard to spot because although 1 is twice as likely to appear as any other number, since it is a random process, it may appear less frequently than other numbers. The larger the number of tests, the more likely you are to spot that but it is something to be aware of.")); |
|
main.append(addParagraph("You might wonder why we don't just use one expression here and the answer is that normally we would so we might just use an expression like")); |
|
main.append(addSyntax("let a=RANDOM%6+1;echo $a")); |
|
main.append(addParagraph("In this case, there are 32768 possible values for RAMDOM and 6 possible outputs. Now, if you divide that number by 6, you will get something like 5461.333 which essentially means that there are 5461 possible inputs for each output but for numbers 1 and 2, there are 5462 which means that the 'die' marginally favours these two numbers.")); |
|
main.append(addParagraph("For all numbers to be equally likely, the number of must be exactly divisible by 6.")); |
|
main.append(addParagraph("The difference here is negligible so it is unlikely to be an issue and this is because the numbers of inputs is so much larger than the number of outputs. If it is lower, the effect will be more pronounced and it was in order to demonstrate that that I was generating a random number and using it to generate the output.")); |
|
main.append(addParagraph("Let's just finish this digression with another example.")); |
|
main.append(addSyntax("let a=RANDOM%3;let b=a%2+1;echo $b")); |
|
main.append(addParagraph("This will give is a random value of a that is between 0 and 2 so it has three possible inputs, each of which we will assume is just as likely as any other. We then run")); |
|
main.append(addSyntax("b=a%2+1")); |
|
main.append(addParagraph("which, based on what we get as input, will generate a number between 1 and 2, basically simulating a coin toss. Let's think about what outcome we will get for each of the possible inputs.")); |
|
main.append(addParagraph("Where a=1, the second expression becomes 1%2 which is 1, the remainder. Let's assume 1 is equivalent to heads.")); |
|
main.append(addParagraph("Where a=2, the second expression becomes 2%2 which gives us 0 as the remainder and we will call that tails.")); |
|
main.append(addParagraph("Where a=3, , the second expression becomes 3%2 which gives us 1 as the remainder and that is tails again.")); |
|
main.append(addParagraph("So we have three possible inputs where for any one of these, the probability of getting that number is 1 in 3. Since either 0 or 2 is equivalent to heads, that means the likelihood of heads being the ultimate outcome is 2 out of 3. In other words, heads is twice as likely to appear as tails.")); |
|
main.append(addParagraph("In this case, the only way for this to be completely fair is if the number of outputs is divisible by 2.")); |
|
main.append(addParagraph("In more general terms, for all outcomes to be equally likely, the number of possible inputs must be divisible by the number of possible outputs. However, if that is not the case, you want the number of inputs to be much greater than the number of outputs. In other words, the higher the value of")); |
|
main.append(addSyntax("inputs/outputs")); |
|
main.append(addParagraph("the less statistically significant the difference will be.")); |
|
main.append(addParagraph("Let's now consider what we might think of as the opposite problem. What if")); |
|
main.append(addSyntax("inputs/outputs")); |
|
main.append(addParagraph("is less than 1. For instance, let’s say that we have")); |
|
main.append(addSyntax("let a=RANDOM;let b=a%65535+1;echo $b")); |
|
main.append(addSyntax("inputs/outputs")); |
|
main.append(addParagraph("gives us about 0.5 in this case so we have twice as many inputs as outputs and it means that")); |
|
main.append(addSyntax("b=a%65535")); |
|
main.append(addParagraph("will always give you a as the answer since b/a is always going to be less than one, therefore a will always be the remainder. This is quite fair in that no possible outcome is likelier than any other but if you expect this to generate a random number between 0 and 65534, you will find that it can't do that because the maximum value of that remainder is 32767.")); |
|
main.append(addParagraph("In other words, this is exactly the same as")); |
|
main.append(addSyntax("let a=RANDOM;let b=a+1;echo $b")); |
|
main.append(addParagraph("which will generate a random number between 1 and 32768.")); |
|
main.append(addParagraph("For most needs, random is fine and will work well enough if not perfectly but in general, bash is not the best tool for performing any kind of arithmetic and sometimes you will need some help from another tool such as bc. As a matter of interest, you can also use AWK for performing floating point arithmetic so just be aware of these limitations.")); |
|
main.append(addHeader("Comparing Values with Test")); |
|
main.append(addParagraph("Bash has a built-in called test which allows us to test for a wide variety of conditions such as whether a file exists, whether it is a directory and so on. You can find out more about it by checking out the man page or with the help command, ie")); |
|
main.append(addSyntax("help test")); |
|
main.append(addParagraph("When a bash script is finished running, it will return a value to indicate success – with a 0 - or failure – with a 1. Similarly test will return a 0 for true and a 1 for false. Consider the following test.")); |
|
main.append(addSyntax("[ -d ~ ]")); |
|
main.append(addParagraph("This is testing whether a file is a directory, that's the -d part and the file that is being tested is the user's home directory.")); |
|
main.append(addParagraph("This isn't intended to be used as something that you run at the command line to check a file so it doesn't give you any feedback. However, it can be used in logical constructs within a script. For example, you might use it as the condition in a loop or as the condition for an if statement.")); |
|
main.append(addParagraph("If you have any experience in any other programming language, you will most likely find it strange that 0 represents true or success and 1 (strictly speaking, any value other than 0) represents false or failure but this is because of the fact that this is an exit status rather than a binary value. You will see 0 used as an exit status for a successfully completed program in other languages, most notably C or C++ so it may help to remember that this is an exit status.")); |
|
main.append(addParagraph("The environment variable, $, stores the most recent exit status so after running the above test, since the home directory is a directory, something like")); |
|
main.append(addSyntax("echo $?")); |
|
main.append(addParagraph("should output a 0 which it does. For comparison, I also ran the test")); |
|
main.append(addSyntax("[ -d codespaces_ws.png ]")); |
|
main.append(addParagraph("which is, as you can see, an image file so this should return false and outputting the value of $? this time gives a 1.")); |
|
main.append(addParagraph("If you really want to run a test like this and see the result at the command line, we can put the test and the output on the same line for convenience so")); |
|
main.append(addSyntax("[ -d /bin/bash ]; echo $?")); |
|
main.append(addParagraph("will output a 1 since bash /bin/bash is not a directory but if we amend this to")); |
|
main.append(addSyntax("[ d /bin ]; echo $?")); |
|
main.append(addParagraph("this is a directory so we will see a 0 as the output. Just a quick sidebar, it may be tempting to say that these tests will tell you whether /bin for example is a file or a directory and there is a similar test")); |
|
main.append(addSyntax("[ -f /bin/bash ]; echo $?")); |
|
main.append(addParagraph("which will return a 0 because /bin/bash is a regular file. It is nevertheless worth remembering that Linux treats everything as a file and that includes directories. In spite of that")); |
|
main.append(addSyntax("[ -d /bin ]; echo $?")); |
|
main.append(addParagraph("will return a 1 because this is a directory. While it is technically true to say that it is a file, if you look up the -f option on the man page, you will see that it is testing for a regular file so you can read that as something that is not a directory.")); |
|
main.append(addParagraph("There are a lot of conditions you can test for and it would be worth spending some time checking these out in the list on the test man page.")); |
|
main.append(addParagraph("We can also run tests for things like whether two strings are identical. For example, the test")); |
|
main.append(addSyntax("[ \"cat\" = \"dog\" ]; echo $?")); |
|
main.append(addParagraph("will output a 1 whereas")); |
|
main.append(addSyntax("[ \"cat\" = \"cat\" ]; echo $?")); |
|
main.append(addParagraph("will output a 0.")); |
|
main.append(addParagraph("We can use regular comparison operators (such as the greater than or less than symbols) but these are reserved for strings so if we run a test like")); |
|
main.append(addSyntax("[ 5 < 9 ]; echo $?")); |
|
main.append(addParagraph("this will return a 1 since the test failed. That is something you probably wouldn't have expected but we also see this error which may make the result clearer")); |
|
main.append(addSyntax("-bash: 9: No such file or directory")); |
|
main.append(addParagraph("So the test failed because bash wasn't expecting an integer value there. We can perform a similar test with integers using -lt (or -gt etc) so")); |
|
main.append(addSyntax("[ 5 -lt 9 ]; echo $?")); |
|
main.append(addParagraph("will give us a 0 in the output because 5 is less than 9. If we run this test")); |
|
main.append(addSyntax("[ 5 -lt 4 ]; echo $?")); |
|
main.append(addParagraph("we will see a 1 being output because 5 is not less than 9. We can also negate an expression with an exclamation mark so")); |
|
main.append(addSyntax("[ ! 5 -lt 4 ]; echo $?")); |
|
main.append(addParagraph("will output a 0 indicating success. This is because 5 is not less than 4 so that returns a 1 for false but the negation operator converts that to a 0 for success so we might read this is as the test returned true because 5 is NOT less than 4.")); |
|
main.append(addParagraph("One final point to note here is that you will no doubt have already noticed that bash can be quite sensitive to spaces between operators and operands and sometime it will require them, sometime it won't. When running tests like these, we do need to put spaces between the brackets, the operators and the operands otherwise the tests won't work.")); |
|
main.append(addHeader("Comparing Values with Extended Test")); |
|
main.append(addParagraph("Extended test adds a few more features to test, but is otherwise not too different. For example, we previously ran this test.")); |
|
main.append(addSyntax("'[ 4 -lt 3 ]")); |
|
main.append(addParagraph("This returns a 1 because, of course, 4 is not less than three. To run this as an extended test, we use double brackets but otherwise it is the same, it is still important to insert spaces between the brackets and the expression and between the operator and the operands.")); |
|
main.append(addSyntax("'[[ 4 -lt 3 ])")); |
|
main.append(addParagraph("With extended test, we can also combine expressions within a test. For example")); |
|
main.append(addSyntax("[[ -d ~ && -a /bin/bash ]]; echo $?")); |
|
main.append(addParagraph("Will test whether the home directory is a directory and whether bash is a regular file in the bin directory.")); |
|
main.append(addParagraph("Note the use of the AND operator (&&) which means that both of these things must be true to get a zero response, which is what we do get. If we change this to")); |
|
main.append(addSyntax("[[ -d ~ && -d /bin/bash ]]; echo $?")); |
|
main.append(addParagraph("we will get a 1 response because the home directory is a directory but /bin/bash is not. If we replace && with ||, in other words we are using OR rather than AND")); |
|
main.append(addSyntax("[[ -d ~ || -d /bin/bash ]]; echo $?")); |
|
main.append(addParagraph("only one of these tests needs to return 0 in order to get a 0 response so as before, the first expression returns 0, the second returns 1 and overall we get a 0 response.")); |
|
main.append(addHeader("Comparing Values with Extended Test")); |
|
main.append(addParagraph("Extended test adds a few more features to test, but is otherwise not too different. For example, we previously ran this test.")); |
|
main.append(addSyntax("'[ 4 -lt 3 ]")); |
|
main.append(addParagraph("This returns a 1 because, of course, 4 is not less than three. To run this as an extended test, we use double brackets but otherwise it is the same, it is still important to insert spaces between the brackets and the expression and between the operator and the operands.")); |
|
main.append(addSyntax("'[[ 4 -lt 3 ])")); |
|
main.append(addParagraph("With extended test, we can also combine expressions within a test. For example")); |
|
main.append(addSyntax("[[ -d ~ && -a /bin/bash ]]; echo $?")); |
|
main.append(addParagraph("Will test whether the home directory is a directory and whether bash is a regular file in the bin directory.")); |
|
main.append(addParagraph("Note the use of the AND operator (&&) which means that both of these things must be true to get a zero response, which is what we do get. If we change this to")); |
|
main.append(addSyntax("[[ -d ~ && -d /bin/bash ]]; echo $?")); |
|
main.append(addParagraph("we will get a 1 response because the home directory is a directory but /bin/bash is not. If we replace && with ||, in other words we are using OR rather than AND")); |
|
main.append(addSyntax("[[ -d ~ || -d /bin/bash ]]; echo $?")); |
|
main.append(addParagraph("only one of these tests needs to return 0 in order to get a 0 response so as before, the first expression returns 0, the second returns 1 and overall we get a 0 response.")); |
|
main.append(addParagraph("There is a feature related to using the && operator in general which seems to apply to some degree in bash and that is short-circuiting. Consider this example.")); |
|
main.append(addSyntax("[[ -d /bin/bash ]] && echo /bin/bash is a directory")); |
|
main.append(addParagraph("In general terms, and will only return a true value if both arguments are true. If the first argument is false, it doesn't matter what value the second argument returns so bash doesn't need to evaluate it.")); |
|
main.append(addParagraph("In this example, we have an extended test followed by the AND operator an echo statement. If the first value is true, bash will look to see if there is a second argument that returns true. It finds an echo statement and it will run it. If the first value is false, bash won't look for anything else. In this example, the extended test returns a false so bash doesn't look at what is after the AND operator, it doesn't check to see if there is a false value or a true value. In practice, this means that, in this example, the echo statement only runs if /bin/bash is a directory. It's not, so we don't see any output.")); |
|
main.append(addParagraph("Essentially, this means that you can run a command based on the result of the test. As a matter of interest, recall that the value we are describing as true or false is actually an exit status so the echo command in that sense does return a true value if it does run so if you do something like")); |
|
main.append(addSyntax("[[ -d /bin ]] && echo /bin is a directory")); |
|
main.append(addParagraph("and then output $?, this will give you a 0 because both the test and the command returned an exit status of 0 so overall, the result of running both is 0.")); |
|
main.append(addParagraph("You can also do this with two commands rather than an extended test and a command so")); |
|
main.append(addSyntax("ls && echo \"listed the directory\"")); |
|
main.append(addParagraph("will give you the output")); |
|
main.append(addSyntax("listed the directory")); |
|
main.append(addParagraph("In bash, there are also a couple of built-ins that will generate a true or false result and these are true and false. For instance, ")); |
|
main.append(addSyntax("true && echo \"success!\"")); |
|
main.append(addParagraph("will echo success whereas")); |
|
main.append(addSyntax("false && echo \"success!\"")); |
|
main.append(addParagraph("will not give you any output.")); |
|
main.append(addParagraph("Extended test allows you match with a regular expression. The general syntax of that is")); |
|
main.append(addSyntax("[[ string =~ regex ]]")); |
|
main.append(addParagraph("For example")); |
|
main.append(addSyntax("[[ \"cat\" =~ c.* ]]; echo $?")); |
|
main.append(addParagraph("is basically testing to see if the word cat starts with a c so this will obviously output 0 since there is a match. On the other hand")); |
|
main.append(addSyntax("[[ \"bat\" =~ c.* ]]; echo $?")); |
|
main.append(addParagraph("will return a 1 because bat does not start with a c.")); |
|
main.append(addParagraph("Regular expressions cab be quite a complex subject so you might want to look at courses covering that particular subject. For example, a couple of courses on LinkedIn Learning are <a href='https://www.linkedin.com/learning/learning-regular-expressions-15586553'>Learning Regular Expressions</a> by Kevin Skoglund (published in October 2022) and <a href=' https://www.linkedin.com/learning/bash-patterns-and-regular-expressions'>Bash Patterns and Regular Expressions</a> by Grant McWilliams (published in June 2019).")); |
|
main.append(addParagraph("If you do any bash scripting, you will be doing tests like these all the time. As a general rule, it's probably better to use extended tests rather than regular tests due to the additional features you get with it. However, if a script is going to be executed on a number of different systems, some of them may not have extended tests, so sticking to regular tests gives you greater portability.")); |
|
main.append(addHeader("Formatting and Styling Text Ouput")); |
|
main.append(addParagraph("There are several ways to produce nicely formatted output in and one of the simplest is to simply output text using the echo command with the option -e which will cause bash to correctly interpret escape sequences.")); |
|
main.append(addParagraph("As an example, if we run the command")); |
|
main.append(addSyntax("echo -e \"Name\t\tNumber;\"; echo -e \"Scott\t\t123\"")); |
|
main.append(addParagraph("the output we get is shown below.")); |
|
main.append(addImageWithCaption("./images/ech4.png", "Output when using echo with the -e option.")); |
|
main.append(addParagraph("The \t sequences are interpreted as tab characters and this gives us output that is neatly arranged in columns. Note that we could have done this with a single echo character. Remember that by default, echo will generate a new line character when outputting text and because of that, using two echo statements gives us the output on separate lines. We could have done this with a new line escape sequence like this.")); |
|
main.append(addSyntax("echo -e \"Name\t\tNumber\nScott\t\t123\"")); |
|
main.append(addParagraph("This produces exactly the same output. In a script, you would probably use two echo statements on separate lines so that each line of the script corresponds to a line of output which would make the script easier to read/debug and is generally tidier.")); |
|
main.append(addParagraph("There are a few other escape sequences you can use to do various things. One which can be quite useful is \a which is the 'bell' sequence. It doesn't work on all systems but does work on my Raspberry Pi and you can use it just like any other sequence. For example, the command")); |
|
main.append(addSyntax("echo -e \"\\a\"; sleep 3; echo hello; echo -e \"\\a\";")); |
|
main.append(addParagraph("This will sound the bell (although on my Pi it doesn't really sounds like a bell), wait for three seconds and then output the word hello and ring the bell again!")); |
|
main.append(addParagraph("The shell itself uses a set of special characters for things like the position of the cursor, the colours used and so on and these can also be used with echo and the -e option. Before we go in to that in any detail, look at the next image.")); |
|
main.append(addImageWithCaption("./images/escape.png", "The escape.sh script and its output.")); |
|
main.append(addParagraph("This demonstrates a couple of interesting point and I will cover things that you can infer from looking at the code before we go into a proper description of it. For a start, the first line (not including the shebang line")); |
|
main.append(addSyntax("echo -e \"\\033[33;44mColor Text\\033[0m\"")); |
|
main.append(addParagraph("This is showing the general syntax of this type of escape sequence. Each sequence starts with \\033 so that probably is indicating that this is a bash environment variable.")); |
|
main.append(addParagraph("We then have [33;44m and we can see from the output that there is a change to the text color. On this line, the text colour looks like it is yellow and the background colour is blue so the two numbers clearly represent the colours and I would guess that the first is the text colour and the second is the background colour. We can confirm that quite easily by changing what we are guessing is the background colour on the second line to match the first and when we rerun the script, they do have the same background colour.")); |
|
main.append(addParagraph("Notice that we have \\033[0m at the end of most lines and in the case of the last line, also in the middle. This seems to be a kind of reset to default sequence. You can see that in the last line where we set the text colour to red and we also have an underline style. We then reset this and set the text colour to red again but without the underline.")); |
|
main.append(addParagraph("Notice as well that there are two lines without any styling that look like this.")); |
|
main.append(addSyntax("echo \"some text that shouldn't have styling\"")); |
|
main.append(addParagraph("The first of these was displayed with the same styling as the previous text and the second was displayed without any styling. I*n both cases, if you look at the previous line you will see that there is either no \\033[0m sequence at the end and there we see that the line has the same styling as the previous line or this sequence is at the end and the line doesn't have any styling so this does look like a 'switch-off' command for the styling.")); |
|
main.append(addParagraph("A couple of other things to mention. In the last line, we had almost the same styling twice but in the first instance this is with an underline added and there is also an additional number, 4 in this case, which we can assume represents the underline.")); |
|
main.append(addParagraph("There is also an m at the end of each sequence which I guess indicates that the styling is finished so that's a terminator. There doesn't seem to be anything explicit indicating that this is the foreground colour, this is a text style and so on which suggests this is implicit and I think there are two possibilities here. The first is that this is determined by the order which we can test by changing the last line to")); |
|
main.append(addSyntax("echo -e \"\\033[40;31;4mERROR:\\033[0m\\033[31;40m Something went wrong.\\033[0m\"")); |
|
main.append(addParagraph("By changing the order, we may have changed the styling but if we run the script again, we still have red underlined text so clearly the order is not important.")); |
|
main.append(addParagraph("A simpler explanation is that we have different numbers for text styles such as underline and colours with likely different numbers for foreground and background colours. If we switch the values round on the first couple of lines, we again see this has no effect so that confirms that the position doesn't affect whether a style affects the colour or the background colour.")); |
|
main.append(addParagraph("This was a bit of a digression in some ways given that the video will, I assume, now go and explain these things but I wanted to try figuring it out for myself and I think this is a good practice when trying to work out what a piece of code does. I will only add anything from the video if there is anything I got wrong in this analysis or if there are any other additional points of interest.")); |
|
main.append(addParagraph("Just to clarify, the \\033 is an indicator that what follows are text formatting instructions. One thing I missed is the opening square brackets which is the opposite of the m. This is indicates the start of the formatting instructions. You can find a list of these in the online advanced bash manual which you can access or download from <a href='' https://tldp.org/LDP/abs/abs-guide.pdf>here<a/> or I have made a copy available on this site which you can get from <a href='/downloads/abs-guide.pdf'>here</a>. This is not the best reference because you would need to search through it to find the codes you want but overall it is a good reference. For completeness, you can also access and download the bash manual from <a href=' https://www.gnu.org/software/bash/manual/bash.pdf'>here</a> or you can download it from this site <a href='/downloads/bash.pdf'>here</a>.")); |
|
main.append(addParagraph("Both of these do mention the escape sequences but there is a goof reference on ioflood.com entitled <a href=' https://ioflood.com/blog/bash-color/#Bash_Color_Basics_Coloring_Text_with_Escape_Sequences'>Bash Colors | Color Codes and Syntax Cheat Sheet</a>. Thanks to Gabriel Ramuglia for providing that. Gabriled is the founder and owner of ioflood.com and I recommend checking it out.")); |
|
main.append(addParagraph("I want to mention another site here which is <a href=' https://devhints.io/'>Rico's Cheatsheets</a> which gives you access to a large number of cheat sheets which are not downloadable but is a useful reference and it covers topics such as bash, vin and other topics relevant to either scripting or the command line. It does also cover other subjects such as MySQL or React.")); |
|
main.append(addParagraph("I can't find a comprehensive list of these sequences but it is worth bearing in mind that different shells and even different versions of a particular shell may use different sequences. The best option if you do need to know a particular sequence (eg, how do I set the background colour to red?) is probably to find a good reference!")); |
|
main.append(addParagraph("The following image is taken from the course video and is a list of the most common foreground and background colours.")); |
|
main.append(addImageWithCaption("./images/colours.png", "Common terminal colors and styles.")); |
|
|
|
addSidebar("linux"); |