Bash to Python Converter

tl;dr

Ever start a bash script, then wish you’d started it in python?

Use this Docker image to convert your script.

Introduction

I routinely use both bash and python to quickly whip up tools for short and long-term uses.

Generally I start with a bash script because it’s so fast to get going, but as time goes on I add features, and then wish I had started it in python so that I could access all the modules and functionality that’s harder to get to in bash.

I found a bash2py tool, which looked good, but came as a zipped source download (not even in a git repo!).

I created a Docker image to convert it, and have used it a couple of times. With a little bit of effort you can quickly convert your bash script to a python one and move ahead.

Example

I’m going to use an artificially simple but realistic bash script to walk through a conversion process.

Let’s say I’ve written this bash script to count the number of lines in a list of files, but want to expand this to do very tricky things based on the output:

#!/bin/bash
if [ $# -lt 1 ]
then
echo "Usage: $0 file ..."
exit 1
fi
echo "$0 counts the lines of code"
l=0
for f in $*
do
l=`wc -l $f | sed 's/^\([0-9]*\).*$/\1/'`
echo "$f: $l"
done
Here's a conversion session:
imiell@Ians-Air:/space/git/work/bin$ docker run -ti imiell/bash2py
Unable to find image 'imiell/bash2py:latest' locally
latest: Pulling from imiell/bash2py
357ea8c3d80b: Already exists
98b473a7fa6a: Pull complete
a7f8553161b4: Pull complete
a1dc4858a149: Pull complete
752a5d408084: Pull complete
cf7fa7bc103f: Pull complete
Digest: sha256:110450838816d2838267c394bcc99ae00c99f8162fa85a1daa012cff11c9c6c2
Status: Downloaded newer image for imiell/bash2py:latest
root@89e57c8c3098:/opt/bash2py-3.5# vi a.sh
root@89e57c8c3098:/opt/bash2py-3.5# ./bash2py a.sh
root@89e57c8c3098:/opt/bash2py-3.5# python a.sh.py
Usage: a.sh.py file ...
root@89e57c8c3098:/opt/bash2py-3.5# python a.sh.py afile
a.sh.py counts the lines of code
afile: 16
So that's nice, I now have a working python script I can continue to build on!

Simplify

Before you get too excited, unfortunately it's not magically working out which python modules to import and cleverly converting everything from bash to python. However, what's convenient about this is that you can adjust the script where you care about it, and build from there.
To work through this example, here is the raw conversion:
#! /usr/bin/env python
from __future__ import print_function

import sys,os

class Bash2Py(object):
__slots__ = ["val"]
def __init__(self, value=''):
self.val = value
def setValue(self, value=None):
self.val = value
return value
def GetVariable(name, local=locals()):
if name in local:
return local[name]
if name in globals():
return globals()[name]
return None
def Make(name, local=locals()):
ret = GetVariable(name, local)
if ret is None:
ret = Bash2Py(0)
globals()[name] = ret
return ret
def Array(value):
if isinstance(value, list):
return value
if isinstance(value, basestring):
return value.strip().split(' ')
return [ value ]
class Expand(object):
@staticmethod
def at():
if (len(sys.argv) < 2):
return []
return sys.argv[1:]
@staticmethod
def star(in_quotes):
if (in_quotes):
if (len(sys.argv) < 2):
return ""
return " ".join(sys.argv[1:])
return Expand.at()
@staticmethod
def hash():
return len(sys.argv)-1
if (Expand.hash() < 1 ):
print("Usage: "+__file__+" file ...")
exit(1)
print(__file__+" counts the lines of code")
l=Bash2Py(0)

for Make("f").val in Expand.star(0):
Make("l").setValue(os.popen("wc -l "+str(f.val)+" | sed \"s/^\\([0-9]*\\).*$/\\1/\"").read().rstrip("\n"))
print(str(f.val)+": "+str(l.val))
The guts of the code is in the for loop at the bottom.
bash2py does some safe conversion and wrapping of the bash script into some methods such as 'Make', 'Array' et al that we can get rid of with a little work.
By replacing:
  • Bash2Py(0) with 0
  • Make(“f”).val with f
  • and Make(“l”) with l etc
  • f.val with f
  • and l.val with l etc
54,57c27,30
< l=Bash2Py(0)
< for Make("f").val in Expand.star(0):
< Make("l").setValue(os.popen("wc -l "+str(f.val)+" | sed \"s/^\\([0-9]*\\).*$/\\1/\"").read().rstrip("\n"))
< print(str(f.val)+": "+str(l.val))
---
> l=0
> for f in Expand.star(0):
> l = os.popen("wc -l "+str(f)+" | sed \"s/^\\([0-9]*\\).*$/\\1/\"").read().rstrip("\n")
> print(str(f)+": "+str(l))
I simplify that section.
I can remove the now-unused methods to end up with the simpler:
#! /usr/bin/env python
from __future__ import print_function
import sys,os
class Expand(object):
@staticmethod
def at():
if (len(sys.argv) < 2):
return []
return sys.argv[1:]
@staticmethod
def star(in_quotes):
if (in_quotes):
if (len(sys.argv) < 2):
return ""
return " ".join(sys.argv[1:])
return Expand.at()
@staticmethod
def hash():
return len(sys.argv)-1
if (Expand.hash() < 1 ):
print("Usage: "+__file__+" file ...")
exit(1)
print(__file__+" counts the lines of code")
l=0
for f in Expand.star(0):
l = os.popen("wc -l "+str(f)+" | sed \"s/^\\([0-9]*\\).*$/\\1/\"").read().rstrip("\n")
print(str(f)+": "+str(l))
Note I don't bother with 'Expand' yet, but I can pythonify that later if I choose to.
The Dockerfile is available here.
My book Docker in Practice
DIP
Get 39% off with the code: 39miell
Show your support

Clapping shows how much you appreciated Ian Miell’s story.