4

I'm looking to understand how custom file descriptors work in Python for input, output, defaults setup, and final closing. I have a file in Bash that does exactly what I want to do in Python. Can anyone tell me how this would be done in Python? I'm using Python 2.7.5, Bash 4.2, and executing on CentOS 7.3.

setup

$ echo "input string" > input

bash_fd.sh

#!/bin/bash

# Demonstration of custom file descriptors in Bash
#     3  script input (scrin)
#     4  script output (scrout)
#     5  script error (screrr)
#     6  script data (scrdata, demo: JSON return payload)
#     7  script log (scrlog)

fd_open()
{
   ### Provide defaults for file descriptors 3-7 ONLY if the FDs are undefined
   { >&3; } 2>/dev/null || exec 3<&0            # dup scrin   to stdin
   { >&4; } 2>/dev/null || exec 4>&1            # dup scrout  to stdout
   { >&5; } 2>/dev/null || exec 5>&2            # dup screrr  to stderr
   { >&6; } 2>/dev/null || exec 6>/dev/null     # set scrdata to /dev/null
   { >&7; } 2>/dev/null || exec 7>/dev/null     # set scrlog  to /dev/null
}

fd_close()
{
   # Close all file descriptors
   exec 3>&-
   exec 4>&-
   exec 5>&-
   exec 6>&-
   exec 7>&-
}

main()
{
   fd_open                                      # Ensure 

   echo "[$(date)] Program beginning" >&7       # scrlog

   echo -n 'Enter a message: ' >&4              # scrout
   read MSG <&3                                 # scrin

   echo "Read message $MSG" >&4                 # scrout
   echo "[screrr] Read message $MSG" >&5        # screrr

   echo "{\"msg\": \"$MSG\"}" >&6               # scrdata: return JSON payload
   echo "[$(date)] Program finishing: $MSG" >&7 # scrlog

   fd_close

   return ${1:-0}                               # return status code
}

# For demonstration purposes, $1 is the return code returned when calling main
main "$1"

invocation

$ ./bash_fd.sh 37 3<input 4>scrout 5>screrr 6>scrdata 7>scrlog
$

return code

$ echo $?
37

generated files

$ cat scrout
Enter a message: Read message input string

$ cat screrr
[screrr] Read message input string

$ cat scrdata
{"msg": "input string"}

$ cat scrlog
[Wed Jun 14 21:33:24 EDT 2017] Program beginning
[Wed Jun 14 21:33:24 EDT 2017] Program finishing: input string

Any help in translating the above Bash script to Python will really help me understand Python and custom file descriptors and will be greatly appreciated.

Steve Amerige
  • 1,309
  • 1
  • 12
  • 28

1 Answers1

3

Python 2's file object is a fairly thin wrapper over C's stdio FILE structure, which itself contains the corresponding descriptor (an integer). It's not a coincidence that in many places, the docs refer to the underlying/related stdio things.

  • Each time you create a file object (open()), a descriptor corresponding to the file is opened and used in all I/O operations with the object.

    • You can get it with <file>.fileno().
    • conversely, if you have a raw descriptor, you can wrap it with a file object with os.fdopen().
      • e.g. if you commanded bash to redirect specific descriptors of your script, it has opened the corresponding descriptors for your subprocess.
  • When a file object is closed or garbage-collected, the underlying descriptor is closed, too.

  • the os module has a few other functions to work with descriptors that mirror corresponding C functions such as os.dup().

Generally, you should use file objects and don't bother with their underlying descriptors. You can do this even with functions that return raw descriptors like with os.pipe().


Examples:

(entities in angle brackets are pseudocode, showing what is to be inserted there)

Check if a descriptor exists:

While How to check if a given file descriptor stored in a variable is still valid? suggests (UNIX-only) fcntl or (portable) dup as the least intrusive ways, since you're going to use it via a file object, it's best to just attempt to:

import os,errno    
<...>
try: f = os.fdopen(<fd>)
except OSError as e:
    if e.errno!=errno.EBADF: raise
    else:
        # actions when doesn't exist, maybe create `f' some other way
else:
    #actions when exists
# use `f'

Duplicate FDs

Not really needed - you can just assign e.g. f = sys.stdin depending on a condition and use f. The only case where you really need this is if you must provide the extra FDs to other processes.

E.g. to duplicate an FD of a file object and create another file object over the duplicate:

os.dup2(old_f.fileno(),<new_fd>)
new_f = os.fdopen(<new_fd>)

Reading from/Writing to/closing an FD

Read from/write to/close/whatever the file object wrapping that FD. See Reading and Writing files - Python tutorial, the only difference is if you have a raw FD, you create the file object with os.fdopen() instead of open().

ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152
  • Can you provide me with a script example as I'm not a strong Python programmer? In particular, the essential items from my demonstration script above are: (1) Determine if a FD is open, (2) Duping FDs, (3) Writing to a FD, and (4) Closing a FD. In the Bash script, the syntax for input FDs vs. output FDs is slightly different. I'm especially interested in seeing how this is done in Python. I appreciate the information above, but something closer to a translation of my script above would be immensely helpful. Thanks! – Steve Amerige Jun 15 '17 at 08:50
  • Just another comment: the script will be doing no opening if any FD (3-7 in the example) is already open. An open is done only if one of the custom FDs is not opened. In the default case, the FDs 3, 4, and 5 are duped to 0, 1, and 2; and, FDs 6 and 7 are opened to /dev/null. – Steve Amerige Jun 15 '17 at 09:02
  • @SteveAmerige here you go. I strived to make examples generic so as not to imply any constraints that do not really exist. Check the docs for fns used for all options and the linked tutorial for more basic explanations. – ivan_pozdeev Jun 15 '17 at 19:48
  • Thanks for the additional information... I'm visiting in Scotland and will try this out on Monday. You might ask, why is this guy coding while visiting Scotland?!! I have no real excuse other than the love of coding! Tomorrow, I'm heading out to Loch Ness. Provided Nessie doesn't get me, I'll upvote this on Monday! – Steve Amerige Jun 16 '17 at 21:31