diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..9e9a20da6d4f52c03ffd139211dc72dd3961247a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +Bash.html diff --git a/Bash.md b/Bash.md new file mode 100644 index 0000000000000000000000000000000000000000..ce766255f1eac39646c2e0cdccec769b1556b42f --- /dev/null +++ b/Bash.md @@ -0,0 +1,994 @@ +--- +marp: true +title: Introduction to Bash Scripting +author: P.Y. Barriat +description: Introduction to Bash Scripting +backgroundImage: url('assets/back.png') +_backgroundImage: url('assets/garde.png') +footer: 09/11/2023 | Introduction to Bash Scripting +_footer: "" +paginate: true +_paginate: false +--- + +Introduction to Bash Scripting<!--fit--> +=== + +https://gogs.elic.ucl.ac.be/pbarriat/learning-bash + + + +##### October 17, 2023 + +###### CISM/CÉCI Training Sessions + + +--- + +# Linux command line + +A Linux terminal is where you enter Linux commands + +It's called the **C**ommand **L**ine **U**ser **I**nterface + +**CLUI** is one of the many strengths of Linux : + +- allows to be independent of distros (or UNIX systems like OSX) +- allows to easily work at distance (SSH) +- allows to join together simple (and less simple) commands to do complex things and automate **= scripting** + +In Linux, process automation relies heavily on scripting. This involves creating a file containing a series of commands that can be executed together + +--- + +# Linux Shell + +A **shell** is a program that takes commands from the keyboard and gives them to the operating system to perform + +The main function is to interpret your commands **= language** + +Shells have some built-in commands + +A shell also supports programming constructs, allowing complex commands to be built from smaller parts **= scripts** + +Scripts can be saved as files to become new commands +> many commands on a typical Linux system are scripts + +--- + +# Bash + +The **Bash** shell is one of several shells available for Linux + +It is the default command interpreter on most GNU/Linux systems. The name is an acronym for the "**B**ourne-**A**gain **SH**ell" + +### Bash Scripting Demo + +```bash +#!/bin/bash + +# declare STRING variable +STRING="Hello World" + +# print variable on a screen +echo $STRING +``` + +--- + + + +--- + +# Shell syntax rules + +Shells use 3 **"standard I/O streams"** + +- `stdin` is the standard input stream, which provides input to commands +- `stdout` is the standard output stream, which displays output from commands +- `stderr` is the standard error stream, which displays error output from commands + +Shell has several **meta-characters** and **control operators** + +> `|`, `&`, `>`, `;` , etc. + +--- + +# Bash environment + +In a Bash shell many things constitute your environment + +- the form of your prompt +- your home directory and your working directory +- the name of your shell +- functions that you have defined +- etc. + +Environment includes many variables that may have been set **by bash** or **by you** +> Access the value of a variable by prefixing its name with `$` + +--- + +# Environment variables + +| Variables | | +| ------------ | --------------- | +| `USER` | the name of the logged-in user | +| `HOME` | the user's home directory (similar to `~` ) | +| `PWD` | the current working directory | +| `SHELL` | the name of the shell | +| `UID` | the numeric user id of the logged-in user | + +> You can use special files to control bash variables : `$HOME/.bashrc` + +--- + +# Bash Scripting basics + +By naming convention, bash scripts end with `.sh` +> however, bash scripts can run perfectly fine without any extension + +A good practice is to define a `shebang` : first line of the script, `shebang` is simply an absolute path to the shell interpreter (see `echo $SHELL` result) +> combination of `bash #` and `bang !` + +### Comments start with `#` + +On a line, any characters after `#` will be ignored -- *with the exception of* `#!` + +```bash +echo "A comment will follow." # Comment here. +# ^ Note whitespace before # +``` + +--- + +### There is no standard indentation + +- Pick a standard in your team that you can all work to +- Use something your editor makes easy (**Vim** uses `Tab`) + +### Command separators + +Commands can be combined using **meta-characters** and **control operators** + +```bash +# cmd1; cmd2 +$ cd myfolder; ls # no matter cd to myfolder successfully, run ls + +# cmd1 && cmd2 +$ cd myfolder && ls # run ls only after cd to myfolder + +# cmd1 || cmd2 +$ cd myfolder || ls # if failed cd to myfolder, `ls` will run +``` + +--- + +# Permissions and execution + +- Bash script is nothing else just a **text file** containing instructions to be executed sequentially + > by default in Linux, a new text file is **-rw-r--r--** (or 644) +- You can run the script `hello_world.sh` using + * `sh hello_world.sh` + * `bash hello_world.sh` + * `chmod u+x run_all.sh` then `./hello_world.sh` + > after the `chmod`, you file is **-rwxr--r--** (or 744) + +--- + +# Variables and data types in Bash + +Variables let you store data : **numeric values** or **character(s)** + +You can use variables to read, access, and manipulate data throughout your script + +**There are no data types in Bash** + +Set the variable values in the following ways : + +- assign directly : `greeting="Welcome"` or `a=4` +- access the variable value using `$`: `echo $greeting` +- assign based on variable: `b=$a` + + > !!! no space before or after `=` in the assignation !!! + > `myvar=Hello World` :boom: `-bash: World: command not found` + +--- + +### Quotes for character(s) `" '` + +Double will do variable substitution, single will not + +### Command Substitution + +```bash +#!/bin/bash +# Save the output of a command into a variable +myvar=$( ls ) +``` + +### Export variables + +```bash +#!/bin/bash +var1=blah +# Make the variable `var1` available to child processes +export var1 +./script2.sh +``` + +--- + +## Variable naming conventions + +* Variable names **should start** with a letter or an underscore + +* Variable names can contain letters, numbers, and underscores + +* Variable names are **case-sensitive** + +* Variable names **should not** contain spaces or **special characters** + +* Use descriptive names that reflect the purpose of the variable + +* Avoid using **reserved keywords**, such as `if`, `then`, `else`, `fi`, and so on... + +* **Never** name your private variables using only **UPPERCASE** characters + +--- + +| Special Variables | | +| ------------ | --------------- | +| `$0` | the name of the script | +| `$1` - `$9` | the first 9 arguments | +| `$#` | how many arguments were passed | +| `$@` | all the arguments supplied | +| `$$` | the process ID of the current script | +| `$?` | the exit status of the most recently run process | +| `$RANDOM` | returns a random number | +| `$LINENO` | returns the current line number | +| `$SECONDS` | the number of seconds since the script was started | + +--- + +# Arithmetic + +| Operator | Operation | +| ------------ | --------------- | +| `+` `-` `\*` `/` | addition, subtraction, multiply, divide | +| `var++` | increase the variable var by 1 | +| `var--` | decrease the variable var by 1 | +| `%` | modulus (Return the remainder after division) | + +Several ways to go about arithmetic in Bash scripting : + `let`, `expr` or using **double parentheses** + +Return the length of a variable : `${#var}` + +--- + +- `let` make a variable equal to an expression + +```bash +#!/bin/bash +let a=5+4 +echo $a # 9 +let a++ +let "a = 4 * $a" +``` + +- `expr` : print out the result of the expression + +```bash +#!/bin/bash +expr 5 + 4 # 9 +expr 5+4 # boom +expr "5 + 4" # boom +a=$( expr 10 - 3 ) +``` + +- double parentheses : return the result of the expression + > it is the preferred method + +--- + +```bash +#!/bin/bash + +a=$(( 4 * 5 )) +a=$(( 4 + 5 )) +a=$((3+5)) + +b=$(( a + 3 )) +echo $b # 11 + +b=$(( $a + 4 )) +echo $b # 12 + +(( b++ )) +(( b += 3 )) +echo $b # 16 +``` + +--- + +# Conditional statements + +```bash +#!/bin/bash +echo "Please enter a number: " +read num + +if [ $num -gt 5 ] && [ $num -le 7 ] +then + echo "$num is 6 or 7" +elif [ $num -lt 0 ] || [ $num -eq 0 ]; then + echo "$num is negative or zero" +else + echo "$num is positive (but not 6, 7 or zero)" +fi +``` + +> read the **standard input stream** with the `read` command + +--- + +### `test` command + +```bash +test -s /proc/cpuinfo +echo $? +``` + +| Operator | Description | +| ------------ | --------------- | +| `-d FILE` | FILE exists and is a directory | +| `-e FILE` | FILE exists | +| `-r FILE` | FILE exists and the read permission is granted | +| `-s FILE` | FILE exists and it's size is greater than zero (ie. it is not empty)| +| `-w FILE` | FILE exists and the write permission is granted | +| `-x FILE` | FILE exists and the execute permission is granted | + +--- + +| Operator | Description | +| ------------ | --------------- | +| `! EXPRESSION` | The EXPRESSION is false| +| `-n STRING` | The length of STRING is greater than zero | +| `-z STRING` | The lengh of STRING is zero (ie it is empty) | +| `STR1 = STR2` | STRING1 is equal to STRING2 | +| `STR1 != STR2` | STRING1 is not equal to STRING2 | +| `INT1 -eq INT2` | INTEGER1 is numerically equal to INTEGER2 (or `==`) | +| `INT1 -gt INT2` | INTEGER1 is numerically greater than INTEGER2 | +| `INT1 -lt INT2` | INTEGER1 is numerically less than INTEGER2 | +| `INT1 -ne INT2` | INTEGER1 is numerically not equal to INTEGER2 | + +--- + +### Conditional: light variation + +Check an expression in the `if` statement ? +Use the double brackets just like we did for variables : + +```bash +#!/bin/bash +echo "Please enter a number: " +read num + +if (( $num % 2 == 0 )) +then + echo "$num is an even number !" +fi +``` + +--- + +### Case Statements + +```bash +#!/bin/bash +space_free=$( /usr/bin/df -h | awk '{ print $5 }' | sort -n | tail -n 1 | sed 's/%//' ) +case $space_free in + [1-5]*) + echo "Plenty of disk space available" + ;; + [6-7]*) + echo "There could be a problem in the near future" + ;; + 8*) + echo "Maybe we should look at clearing out old files" + ;; + 9*) + echo "We could have a serious problem on our hands soon" + ;; + *) + echo "Something is not quite right here" + ;; +esac +``` + +--- + +# Arrays + +### Indexed arrays + +```bash +#!/bin/bash +# Declare an array with 4 elements +my_array=( 'Debian Linux' 'Redhat Linux' Ubuntu OpenSUSE ) +# get number of elements in the array +my_array_length=${#my_array[@]} + +# Declare an empty array +my_array=( ) +my_array[0]=56.45 +my_array[1]=568 +echo Number of elements: ${#my_array[@]} +# echo array's content +echo ${my_array[@]} +``` + +--- + +### Associative arrays = Dictionaries (Bash 4.0 or higher) + +By default, a bash array is an indexed array : need to use the `declare` command + +```bash +#!/bin/bash +declare -A acronyms +acronyms[ACK]=Acknowledgement +acronyms[EOF]="End of Frame" +echo ${acronyms[ACK]} +if [ ${acronyms[EOF]+_} ]; then echo "Found"; else echo "Not found"; fi +``` + +```bash +#!/bin/bash +declare -A countries=( [ALB]=Albania [BHR]=Bahrain [CMR]=Cameroon [DNK]=Denmark [EGY]=Egypt ) +echo ${countries[@]} +echo ${!countries[@]} + +countries+=( [FJI]=Fiji ) +echo ${countries[@]} +unset countries[BHR] +echo ${countries[@]} +``` + +--- + +### About `declare` + +```bash +declare --help +``` + +Examples : + +- declare a variable without a value +- force a variable to be an integer only +- declare a variable as a **parameter** (read-only) +- force a character(s) variable to store all uppercase or lowercase letters +- display the attributes and values of all variables +- etc. + +--- + +# Loops + +Useful for automating repetitive tasks + +Basic loop structures in Bash scripting : + +* `while` : perform a set of commands while a test is true +* `until` : perform a set of commands until a test is true +* `for` : perform a set of commands for each item in a list + +* controlling loops + + * `break` : exit the currently running loop + * `continue` : stop this iteration of the loop and begin the next iteration + +* last loop mechanism : `select` allows you to create a simple menu system + +--- + +### Examples + +```bash +#!/bin/bash +# Basic while loop +counter=0 +while [ $counter -lt 3 ]; do + let counter+=1 + echo $counter +done +``` +```bash +#!/bin/bash +# Basic until loop +counter=1 +until [ $counter -gt 10 ]; do + echo $counter + ((counter++)) +done +``` + +--- + +```bash +# range +for i in {1..5} +``` +```bash +# list of strings +words='Hello great world' +for word in $words +``` +```bash +# range with steps for loop +for value in {10..0..2} +``` +```bash +# set of files +for file in $path/*.f90 +``` +```bash +# command result +for i in $( cat file.txt ) +``` + + + +--- + +```bash +#!/bin/bash +# How to Read a File Line By Line + +input="/path/to/txt/file" +while IFS= read -r line +do + echo "$line" +done < "$input" +``` + +The internal field separator (`IFS`) is set to the empty string to preserve whitespace issues + +> by default `read` removes all leading and trailing whitespace characters such as spaces and tabs + +The `-r` option is used not to allow backslashes to escape any characters + +--- + +```bash +#!/bin/bash +# How to iterate over keys or values of an Array + +declare -A fruits +fruits[south]="Banana" +fruits[north]="Orange" +fruits[west]="Passion Fruit" +fruits[east]="Pineapple" + +for key in "${!fruits[@]}" +do + echo "Key is '$key' => Value is '${fruits[$key]}'" +done +``` + +--- + +# Arguments - Positional Parameters + +How to pass command-line arguments to a bash script ? + +Try a simple example called `test_arg.sh` : + +```bash +#!/bin/bash +echo $1 $2 $4 +echo $0 +echo $# +echo $* +``` + +```bash +bash test_arg.sh a b c d e +``` + +```bash +a b d +test_arg.sh +5 +a b c d e +``` + +--- + +### Flags + +```bash +#!/bin/bash +while getopts u:a:f: flag +do + case "${flag}" in + u) username=${OPTARG};; + a) age=${OPTARG};; + f) fullname=${OPTARG};; + esac +done +echo "Username: $username"; +echo "Age: $age"; +echo "Full Name: $fullname"; +``` + +```bash +bash test_arg.sh -f 'John Smith' -a 25 -u john +``` + +--- + +# Redirections + +Use the meta-character `>` in order to control the output streams `stdout` and `stderr` for a command or a bash script + +### From bash script + +```bash +#!/bin/bash +#STDOUT to STDERR +echo "Redirect this STDOUT to STDERR" 1>&2 +#STDERR to STDOUT +cat $1 2>&1 +``` + +### Output streams to file(s) + +```bash +./my_script.sh > STDOUT.log 2> STDERR.err +``` + +--- + +# Return codes + +Linux command returns a status when it terminates normally or abnormally + +* every Linux command has an exit status +* the exit status is an integer number +* a command which exits with a **0** status has **succeeded** +* a **non-zero** (1-255) exit status indicates **failure** + +How do I display the exit status of shell command ? + +```bash +date +echo $? +``` + +> [List of common exit codes for GNU/Linux](https://slg.ddnss.de/list-of-common-exit-codes-for-gnu-linux/) + +--- + +How to store the exit status of the command in a shell variable ? + +```bash +#!/bin/bash +date +status=$? +echo "The date command exit status : ${status}" +``` + +How to use the `&&` and `||` operators with **exit codes** + +```bash +command && echo "success" +command || echo "failed" +command && echo "success" || echo "failed" +``` + +```bash +_files="$@" +[[ "$_files" == "" ]] && { echo "Usage: $0 file1.png file2.png"; exit 1; } +``` + +--- + +# Functions + +* "small script within a script" that you may call multiple times +* great way to reuse code +* a function is most reuseable when it performs a single task +* good to put ancillary tasks within functions : logically separate from main + +```bash +#!/bin/bash +hello_world () { + echo 'hello, world' +} +hello_world +``` + +> defining a function doesn’t execute it + +Functions must be declared **before** they are used + +--- + +## Variables Scope + +```bash +#!/bin/bash +# Define bash global variable +# This variable is global and can be used anywhere in this bash script +var="global variable" + +function my_function { +# Define my_function local variable +# This variable is local to my_function only +echo $var +local var="local variable" +echo $var +} + +echo $var +my_function +# Note the bash global variable did not change +# "local" is my_function reserved word +echo $var +``` + +--- + +## Return Values + +Bash functions don’t allow you to return a value when called + +After completion, the return value is the status of the last statement (so 0-255) + +Can be specified by using `return` : + +```bash +#!/bin/bash + +my_function () { + echo "some result" + return 55 +} +my_function +echo $? +``` + +--- + +Return an arbitrary value from a function : assign the result of the function + +```bash +#!/bin/bash +my_function () { + func_result="some result" +} +my_function +echo $func_result +``` + +Better way is to send the value to `stdout` using `echo` + +```bash +#!/bin/bash +my_function () { + local func_result="some result" + echo "$func_result" +} +func_result="$(my_function)" +echo $func_result +``` + +--- + +## Passing Arguments + +In the same way than a bash script: see above (`$1`, `$*`, etc) + +```bash +#!/bin/bash +print_something () { + echo Hello $1 +} +print_something Mars +``` + +## Be careful in case of "overriding commands" + +```bash +#!/bin/bash +ls () { + command ls -lh +} +ls +``` + +--- + +# Shell vs Environment Variables + +Consider the script `test.sh` below : + +```bash +#!/bin/bash +echo $var1 +echo $var2 +``` + +Then run this script : + +```bash +var1=23 +export var2=12 +bash test.sh +``` + +--- + +# Subshells + +* subshell is a "child shell" spawned by the main shell ("parent shell") +* subshell is separate instance of the command process, run as a new process +* unlike calling a shell script, subshells inherit the same variables as the original process +* a subshell allow you to execute commands within a separate shell environment = *Subshell Sandboxing* + > useful to set temporary variables or change directories without affecting the parent shell's environment +* subshells can be used for parallel processing + +--- + +### Syntax + +A command list embedded between parentheses runs as a subshell : + +```bash +#!/bin/bash +( command1 ; command2 ; command3 ) +``` + +Or : + +```bash +#!/bin/bash +bash -c "command1; command2; command3" +``` + +> Reminder : variables in a subshell are not visible outside the block of code in the subshell + +--- + +### Sourcing Bash scripts + +```bash +#!/bin/bash +COUNTRY="Belgium" +greeting() { + echo "You're in $1" +} +greeting $COUNTRY +``` + +```bash +COUNTRY="France" +./myScript.sh # or bash or exec +echo $COUNTRY +greeting $COUNTRY +``` + +```bash +source myScript.sh +echo $COUNTRY +greeting $COUNTRY +``` + +--- + +Differences between **Sourcing** and **Executing** a script + +- source a script = execution in the current shell + > variables and functions are valid in the current shell after sourcing + +- execute a script = execution in a new shell (in a subshell of the current shell) + > all new variables and functions created by the script will only live in the subshell + +Source a script using `source` or `.` + +```bash +source myScript.sh +. myScript.sh +``` + +> official one is `.` Bash defined `source` as an alias to the `.` + +--- + +### Running parallel processes in subshells + +Processes may execute in parallel within different subshells +> permits breaking a complex task into subcomponents processed concurrently + +Exemple : `job.sh` + +```bash +#!/bin/bash +i=0 +while [ $i -lt 10 ]; do + echo "${i}: job $1" + i=$[$i+1] + sleep 0.2 +done +``` + +2 ways to use it : +sequential processing (`manager_seq.sh`) or parallel processing (`manager_par.sh`) + +--- + +```bash +#!/bin/bash +# manager_seq.sh +echo "start" +./job.sh 1 +./job.sh 2 +echo "done" +``` + +```bash +#!/bin/bash +# manager_par.sh +echo "start" +./job.sh 1 & +./job.sh 2 & +wait # Don't execute the next command until subshells finish. +echo "done" +``` + +```bash +time ./manager_seq.sh +time ./manager_par.sh +``` + +--- + +# Debug + +Tips and techniques for debugging and troubleshooting Bash scripts + +### use `set -x` + +enables debugging mode : print each command that it executes to the terminal, preceded by a `+` + +### check the exit code + +```bash +#!/bin/bash +if [ $? -ne 0 ]; then + echo "Error occurred" +fi +``` + +--- + +### use `echo` + +"classical" but useful technique : insert `echo` throughout your code + +```bash +#!/bin/bash +echo "Value of variable x is: $x" +``` + +### use `set -e` + +this option will cause Bash to exit with an error if any command in the script fails + +--- + +Thank you for your attention<!--fit--> +=== \ No newline at end of file diff --git a/Bash.pdf b/Bash.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1a3a40fa6eaddbe2c3a723798cf05d99f46ec9db Binary files /dev/null and b/Bash.pdf differ diff --git a/README.md b/README.md index 8cb2f6237711d13c79efe2df6f3e91d5e4735b29..355ab2d35bc6c6036210dfa334c0558e3e5f2015 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # learning-bash - This is the repository for the training Markdown and applications \ No newline at end of file +This is the repository for the training Markdown and applications + +You can find the slides [here](./Bash.pdf). + +Presentation is [here](https://www.elic.ucl.ac.be/users/pbarriat/slides/Bash.html) \ No newline at end of file diff --git a/assets/back.png b/assets/back.png new file mode 100644 index 0000000000000000000000000000000000000000..0674657248db1242420acd99ec7d510b8f3236cd Binary files /dev/null and b/assets/back.png differ diff --git a/assets/bash_logo.png b/assets/bash_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..00c486b8e77a4e2a28d49b2f75875683e800a9d9 Binary files /dev/null and b/assets/bash_logo.png differ diff --git a/assets/garde.png b/assets/garde.png new file mode 100644 index 0000000000000000000000000000000000000000..7c82359c3ac6126ffeed79a081ab2c75c00e19dc Binary files /dev/null and b/assets/garde.png differ diff --git a/assets/marp.config.js b/assets/marp.config.js new file mode 100644 index 0000000000000000000000000000000000000000..c5a35bca5b0623dc640ce779365a6d00bf5dab64 --- /dev/null +++ b/assets/marp.config.js @@ -0,0 +1,5 @@ +const marpKrokiPlugin = require('./kroki-plugin') + +module.exports = { + engine: ({ marp }) => marp.use(marpKrokiPlugin) +} diff --git a/assets/scripting.png b/assets/scripting.png new file mode 100644 index 0000000000000000000000000000000000000000..8323da3c1fad863b829610ca54fca13c9fd37f20 Binary files /dev/null and b/assets/scripting.png differ diff --git a/assets/select_example.png b/assets/select_example.png new file mode 100644 index 0000000000000000000000000000000000000000..44a10f34bbe84b6b023403419345a17c3d618c79 Binary files /dev/null and b/assets/select_example.png differ diff --git a/assets/tum.css b/assets/tum.css new file mode 100644 index 0000000000000000000000000000000000000000..08d289362d7399b163db43fbc0e37f9b4a6c9397 --- /dev/null +++ b/assets/tum.css @@ -0,0 +1,55 @@ +/* @theme tum */ + +@import 'default'; + +section { + /*background-color: #fff; + color: #000; + background-image: url('images/TUM_Logo_blau_rgb_s.svg'); + background-repeat: no-repeat; + background-position: right 40px top 40px; + background-size: 8%;*/ +} + +section.lead { + /*background-image: url('images/TUM_Uhrenturm.png'); + background-position: right; + background-size: 45%;*/ +} + +section h1, +section h2 { + color: #1f315c; +} +section a { + color: #5fb2e6; +} +section footer, +section::after { + color: #9cb7d4; +} + +section.invert { + background-color: #003359; + color: #fff; + /*background-image: url('images/TUM_Logo_weiss_rgb_s.svg');*/ +} + +section.lead.invert { + /*background-image: url('images/TUM_Uhrenturm_w.png');*/ +} + +section.invert h1, +section.invert footer, +section.invert::after { + color: #fff; +} + +section.invert a { + color: #e37222; +} + +/* Add "Page" prefix and total page number */ +section::after { + content: attr(data-marpit-pagination) ' / ' attr(data-marpit-pagination-total); +} diff --git a/compile.sh b/compile.sh new file mode 100755 index 0000000000000000000000000000000000000000..f0e736e2c1bf94712fd5d34ba7b5b4fd150319e3 --- /dev/null +++ b/compile.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# +# PY Barriat, September 2023 +# +# Download and install marp (MarkDown slides extension) from here: +# https://github.com/marp-team/marp-cli/releases +# + +marp --allow-local-files --theme ./assets/tum.css Bash.md -o Bash.pdf +marp --template bespoke --bespoke.progress --allow-local-files --theme ./assets/tum.css Bash.md -o Bash.html