Printing via smbclient on Linux command line

After I updated to Fedora 32, my life with my Dell XPS 15” work laptop became so much easier. No random boot locks or I do not want to connect to that Bluetooth device problems. But one thing broke, and that was printing using printers connected through a SAMBA server.

There are a couple of ways to solve this, either go CUPS or run the program smbclient straight from the command line and to be honest, the later speaks to me on a primal level. Running the client is relatively easy; we simply need to type the command:

smbclient <SERVER>/<PRINTER> -U <USER> -W <DOMAIN> -c print <FILE>

Where <SERVER> is the URL for the SAMBA server, <PRINTER> is the name of the printer you want to print from and more specifically the name of the printer on the SAMBA server. <USER> is your username for that SAMBA server, <DOMAIN> is the work-group domain of your user on the SAMBA server, and <FILE> is the name of the file you want to print. Okay, it may not be simple, and you have to remember all those details. Next, what about print settings like printing a duplex? Well, for that you need another tool, and the one I found works best is pdftops. With this pdftops you can change the format of the file you want to print, if you want to change an A4 document to duplex, you will need to run pdftops using the following parameters:

pdftops --paper A4 -duplex <INPUT> <OUTPUT>

Where <INPUT> is the path to the input file, I have tested with .odt and .pdf files, and <OUTPUT> is the path of the generate postscript file. Yes, it generates an additional file. Then you will run the smbclient command with the output file as <FILE> input. But that is a lot of work, so can we script our self out of this? Well, naturally, my young padawan we can!

So first, what do we need? Well, we need to be able to call systems command. What else? Well nothing really, but it would be nice not to have to remember the server URL, printer names, and also where the print is. Okay, seems fair, what scripting/programming language do we use? Well, whatever we want and for this example let us go with Python and let us use a JSON file to keep information on the server, printers, and more.

Let us start with the JSON file and let us assume it has this format:

{
    "server": "SERVER_URL",
    "domain": "DOMAIN",
    "printers": [
        {
            "name": "PRINTER_NAME", 
            "location": "WHERE IS THIS THING"
        }
    ]
}

Here printers is a list of the printers you want to be able to print from. Remember every time you have to add a new printer, and you simply add the information to the JSON file. I have not included the username for security reasons, so you will have to type the username and password every time you run the script. Additionally, I will assume that the first printer in the list is our default printer.

So first, how do we list printers?

import json 

configFilePath = 'PATH_TO_CONFIG_FILE'

def listPrinters():
    with open(configFilePath, 'r') as configFile:
        printers = (json.loads(configFile))['printers']
        
        for index, printer in enumerate(printers):
            print('{!s}: {!s} - ${!s}'.format(index, printer['name'], printer['location']))

So how do we select a printer? Well notice how I printed the index of the printer, let us use that index, and remember how I assumed that default printer was the first in the printers list.


def getPrinter(index=0):
    with open(configFilePath, 'r') as configFile:
        return ((json.loads(configFile))['printers'])[0]['name']

Okay, now we can get the printer information that we need. So now how do we make a function for running the duplex command? Here I prefer to use the subprocess for tasks like this, so we will define a function that takes a file and make a Postscript version, in the same path as the input file. We will assume for now that we only handle A4 paper documents.

import os 
import subprocess

def generateDuplex(file):
    psFile = '{!s}.ps'.format(os.path.splitext(file)[0])
    
    subrocess.call(['pdftops', '-paper', 'A4', '-duplex', file, psFile], encoding='utf-8')
    
    return psFile

This creates the Postscript file with the same name as the input file but with a .ps as an extension. As a state, the output file will be created in the same directory as the input file, so you may want to clean this up after print.

Next step is to print the file, right? Yes, but how do we get the server and the domain? Like all the rest of the configurations, I just like having a function for it. So let us make that one quickly.

def getServerAndDomain():
    with open(configFilePath) as configFile:
        data = json.loads(configFile)
        return data['server'], data['domain']

Now we are ready to print, and we will again use subprocess to call the system command, this time for smbclient.

import shlex

def printDoc(printer, user, file):
    server, domain = getServerAndDomain()
    subprocess.call(['smbclient', '{!s}{!s}'.format(server, printer), 
                      '-U', user, '-W', domain, '-c', 'print ' + shlex.quote(file)], encoding='utf-8')

I think a few comments are needed here. First, what does shlex.quote do? It fix quotations around the file name for us, so we do not have to handle it. Secondly, I do not check the output of the smbclient call to see if it succeed. I should do that, and it is in the future plans, right now I just needed to get the script to work. But it is fairly easy to get the response, and we would use subprocess.check_call(...) to get the needed information. Finally, where does the user come from? Well, where does the printer come from? Those are command-line arguments, which are given by you! Now let us make the entry point part of the script. To handle user inputs, I will use the argparse package, and I will explain this part of the code in, well, parts.

First, we set up the argument parser and the inputs it should be able to handle, as shown in the code block below. Now a few things that have always annoyed me with argparse was that I could not find a way to provide an argument after the argument name. As an example, when listing printers, I do not want to have to type python print_doc --printers 1. I just want to type python print_doc --printers. During the development of this script, I talked to a friend about this, and he informed me about action='store_true' parameter to argparse which I was unaware off. I may have missed while I read the documentation. So as --printers and --duplex do not need an input parameter, we configure these two in that way.

import argparse
if __name__ == '__main__':

    parser = argparse.ArgumentParser()
    parser.add_argument('--printer', '-p', help='set the printer')
    parser.add_argument('--document', help='set the documnet')
    parser.add_argument('--user', '-u', help='set the user')
    parser.add_argument('--duplex', '-d', help='set the user', action='store_true')
    parser.add_argument('--printers', help='print list of all printers', action='store_true')
    args = parser.parse_args()

Next, I will assume that --printers are run just to list printers, and no other parameters should be acted upon if that argument is present and only list the printers.

import sys

if args.printers:
    listPrinters()
    sys.exit(1)

Next, if we are not looking for printers, we check for the user and document.

    if not args.document:
        print('You have not provided a document to print')
        system.exit(1)
    
    if not args.user:
        print('You have not provided a user')
        system.exit(1)

Next, we select the printer; if we provide no printer, we choose the default printer.

    printer = ``
    if args.printer:
        printer = getPrinter(int(args.printer))
    else:
        printer = getPrinter()

Next, we check for enabled duplex and generate the duplex version of the document if needed. We keep the path of the document, such that we can replace it with the duplex document path if needed.

document = args.document
if args.duplex:
    document = generateDuplex(document)

Finally, we print the document and remove the duplex version if duplex was enabled.

printDoc(printer, args.user, document)

if args.duplex: 
    subprocess.call(['rm', document], encoding='utf-8')

So now we can print using this script by typing python SCRIPT_NAME.py -u <USERNAME> --document <PATH_TO_DOCUMENT> if want to enable duplex python SCRIPT_NAME.py -u <USERNAME> --document <PATH_TO_DOCUMENT> -d, and if we want to set a printer python SCRIPT_NAME.py -u <USERNAME> --document <PATH_TO_DOCUMENT> -p <PRINTER_NAME>. The printDoc will prompt you for your user name if needed.

Now finally if we want to be fancy and not have to type python <PATH_TO_SCRIPT> every time we print. But want to type, let us say, smbprint instead, we make an alias in our Terminal configuration. The name of the configuration file varies, but the command we add should be structure the same for BASH, ZSH, and most other common terminals.

alias smbprint="python <PATH_TO_SCRIPT>"

This is how I handle the print using SAMBA printers problem, that has come with Fedora 32, for me. If you want the full script, I have made it available on GitLab, and the version I have presented in this post is tagged as 1.0.0.

Future plans

I have a few plans and dreams for this script.

  • Enable other paper sizes
  • Change document orientation
  • Check the output from subprocess call to smbclient
  • Get a list of printers from the SAMBA server

Known bugs

There is one known bug and which is that some files with white space(s) in their name cause a problem with the print statement. I have the issue both with the script and the raw smbclient command.