I recently wanted to migrate from a Bash 3 shell script to Python 3. This is nothing but a brain dump comparing a few bits and bobs.

I’m pretty much doing what’s expedient, in a way that makes both the Bash script and the Python script look very similar. I am sure there is a lot of room to optimize the Python code, either through the standard library or third party modules.

ℹ️ Note

What I show below is by no means the best or even the correct way, as I don’t know Python.

Hash bang

```bash #!/bin/bash ```
```python #!/usr/bin/env python ```

Console output

- Multiline `echo` could also take the form of `echo -e "Line 1\nLine2"`. - In the second example, the single quote is required to avoid `!` history expansion, or just `set +o histexpand` to turn it off. - Reading and returning environment variables is possible when sourcing the script, .e.g `SET_ENV=1 . script.sh`.
- In Python, `"` and `'` are interchangeable - use whichever that the string does not contain, e.g. `"don't"` vs `'"do"'` - Use Pyhton f-strings, and never go back to `print('Hello ' + a + '!')`! Available since [Python 3.6 December 2016](https://docs.python.org/3/whatsnew/3.6.html). - An error is raised if the environment variable in `os.environ[var]` is not defined. Use `.get()` with a default value instead. - Finally, setting an environment variable back in the calling shell is **not possible!**
echo "Line 1
Line 2"

a=“world” echo “Hello $a”’!'

echo $PWD echo $SET_ENV export NEW_ENV=0

print("""Line 1
Line 2""")

a='world'
print(f'Hello {a}!')

import os
print(os.environ['PWD'])
print(os.environ.get('SET_ENV', 'default'))

Coloured console output

- Echo to stdout with ANSI escape codes using `echo -e` (which is undocumented on macos) to interpret backslash escapes - Here I am using a variable for clarity, but that is really not necessary.
- Can either just use variables, or keep them together in a class as shown here.
```bash _cx="\033[0m" _cr="\033[1;31m"

echo -e “${_cr}RED${_cx}”

</div><div class="column w" markdown="1">
```python
class _c:
    x = '\033[0m'
    r = '\033[1;31m'
print(f'{_c.r}RED{_c.x}')

Simple string manipulation

- Slicing strings is remarkably similar!
- Python has many string functions which are a lot more usable e.g. `find()`, `index()`, `join()`, `replace()`, `split()`, `strip()`, etc.
```bash a="this is a long string" echo ${#a} # 21 echo ${a:8} # "a long string" echo ${a:8:4} # "a lo" echo ${a::4} # "this" echo ${a: -6} # "string" ```
```python a="this is a long string" print(len(a)) print(a[8:]) print(a[8:12]) print(a[:4]) print(a[-6:]) ```

Checking for console redirection

- Check for redirection using `-t` e.g. `./script.sh > x.txt` or `./script.sh | cat`. - And force output to TTY `/dev/tty`.
- Output is buffered, so to ensure output is in order, make sure to `flush=True` or `sys.stdout.flush()` where needed. - To ignore redirection, `print` to the file `os.ttyname(0)`.
if [[ -t 1 ]]
then
    echo "Stdout is terminal"
else
    echo "Stdout is redirected"
fi

echo “Always output to terminal (TTY)” >/dev/tty

import os, sys
if sys.stdout.isatty():
    print('Stdout is terminal', flush=True)
else:
    print('Stdout is redirected', flush=True)
import os
with open(os.ttyname(0), 'wt') as f:
    print('Always output to terminal (TTY)', file=f)

Cleanly setting line wrap

- Turn off line wrap, and trap program exit to make sure we turn it back on.
```bash tput rmam function wrap() { tput smam } trap wrap EXIT ```
```python import os, atexit os.system('tput rmam') def wrap(): os.system('tput smam') atexit.register(wrap) ```

Command line arguments

- After `shift`-ing, the remainder arguments are left behid, so use `$1`, `$2`, `$3`, etc. to get the first, second, third arguments...
- `getopt` raises an error when there are incorrect arguments - And returns the remaining options in `arguments`, containing a list (`arguments[1]`, `arguments[2]`...]`).
while getopts "hi:" args; do
 case $args in
  h) 
    echo "Help..." 
    exit
    ;;
  i) echo "Hi $OPTARG!" ;;
  *) echo "Error in arguments" && exit
 esac
done

shift $((OPTIND-1)) echo “Remainder: $*”

import getopt, sys
try:
    options, arguments = getopt.getopt(sys.argv[1:], 'hi:')
except getopt.GetoptError as e:
    print(f'Error in arguments: {e.msg}')
    sys.exit()
for opt, value in options:
    if opt == '-h':
        print('Help...')
        sys.exit()
    if opt == '-i':
        print(f'Hi {value}!')
print(f'Remainder: {arguments}')

Single line if-then-else

- I realize using AND/OR to check return codes is not the same thing as a ternary operator (?:) - In Shell, one has to know exactly what one is trying to do, and then choose the best method, see [this StackOverflow answer](https://stackoverflow.com/questions/3953645/ternary-operator-in-bash)
a=1
b=2
[[ a == b ]] && c="yes" || c="no"
```python a = 1 b = 2 c = "yes" if a == b else "no" ```

Filtering text files

- Check for text file(s) (via `ls` return code) - If there is, then get the most recent text (first line) - Finds matching lines with `grep` - Reads each line as input, and split into an array at delimiter `=`
- `sorted(list)` returns a new list, while `list.sort()` does so in-place - When reading from a file, Python always returns the trailing newline, hence the `rstrip('\n')`
ls -1t /temp/*.txt 2>/dev/null 
if [[ $? -eq 0 ]]; then
    file="$(ls -1t /temp/*.txt | head -n1)"
    pattern='.*=.*'
    while read -r line
    do
        IFS='=' read -ra part <<< "$line"
        echo Key [${part[0]}] has value [${part[1]}]
    done < <(grep -i "$pattern" "$file")
fi
```python import re, os from pathlib import Path files = sorted(Path('/temp').glob('*.txt'), key=os.path.getmtime, reverse=True) if len(files): pattern = '.*=.*' re.compile(pattern, re.IGNORECASE) for i, line in enumerate(open(files[0])): if re.search(pattern, line): part = line.rstrip('\n').split('=') print(f'Key [{part[0]}] has value [{part[1]}]') ```