├── nukecli.py └── nukepy /nukecli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nukepy 2 | # copyright Luma Pictures 3 | ''' 4 | Module for using command-line strings to build, save, and execute Nuke scripts. 5 | 6 | Basic usage syntax is: 7 | 8 | nukecli - [knob1 value1] [knob2 value2]... [knobN valueN] [- [arg1]...] 9 | 10 | is the name of a Nuke node class (grade, read, write, merge, etc.). Node classes 11 | can be case-insensitive, or even partial class names if the partial string will match 12 | exactly one class. Knobs are always given in name/value pairs. 13 | 14 | one the following actions: set, push, save, execute 15 | 16 | --------------------------- 17 | Nodes and Knobs 18 | --------------------------- 19 | 20 | Knobs 21 | ===== 22 | 23 | Knobs whose value requires multiple components should have the value list passed in curly 24 | braces. An example of a 4-component "blackpoint" value on a Grade node: 25 | 26 | -grade blackpoint {.015 .016 .109 .1} 27 | 28 | Multi-dimensional knob values simply use nested brace-sets. Here's an example of setting a 29 | Camera's Local Matrix knob: 30 | 31 | -camera useMatrix true matrix {{.1 .2 .3 .4} {.5 .6 .7 .8} {1 1 2 2} {3 3 4 4}} 32 | 33 | Connections 34 | =========== 35 | 36 | Nuke uses the concept of a stack to build node networks. The most recently created node is placed 37 | at the top of the stack (index 0), pushing previously created nodes down. 38 | When a node is created, its default inputs are filled by the available nodes in the stack: 39 | the node at stack 0 is connected to input 0, stack 1 to input 1, and so on. 40 | 41 | A Merge, for example, has 1 default input, so by default only the last created node will be 42 | connected to it. To use the last 2 created nodes as its inputs, simply add the 'inputs' knob 43 | to the node command along with an integer value. For example, to "plus" the output of the last 44 | two created nodes, you would use: 45 | 46 | -merge operation plus inputs 2 47 | 48 | --------------------------- 49 | Commands 50 | --------------------------- 51 | 52 | There are also several unique keyword commands that can be used to perform other actions: 53 | 54 | -set 55 | ==== 56 | 57 | The most recently created node can be stored to a named variable using the "-set" 58 | command followed by the desired variable name. The following code creates a Grade node with 59 | a mono blackpoint of .015 and stores it in the variable "mygrade:" 60 | 61 | -grade blackpoint .015 -set mygrade 62 | 63 | 64 | -push 65 | ===== 66 | 67 | Named variables can be recalled at any time using the "-push" command. This will 68 | push the corresponding node to the top of the node stack, making it available as the input 69 | for the next node created. To recall our previous grade node for later use, we would simply call: 70 | 71 | -push mygrade 72 | 73 | You can also push 0 (zero), which can be used to keep one of the inputs of a node from 74 | connecting to anything. For example, a ScanlineRender node can take 3 inputs, but typically 75 | only the Camera and Obj/Scene inputs are used (input indices 1 and 2). Thus, to avoid 76 | unintentionally connecting your last-created node to the BG input (index 0) or screwing up 77 | the input connections, you would typically want to "push 0" immediately before creating one. 78 | The following would create a simple 3D setup, but leave the "BG" input of the ScanlineRender 79 | node disconnected: 80 | 81 | -camera -set renderCam -card -scene inputs 1 -set renderScene -push renderCam 82 | -push renderScene -push 0 -scanlinerender inputs 3 83 | 84 | 85 | -execute 86 | ======== 87 | 88 | Any node that can normally be executed in the Nuke GUI can be run as an executable 89 | node using this command-line interface as well. To execute a node, simply add the "-execute" 90 | command after the node's definition command and arguments, followed by a valid frame range: 91 | 92 | -write /path/to/my/file.%04d.exr -execute 1-10 93 | 94 | If no frame range is specified, Nuke will attempt to use the node's "first" and "last" knob 95 | values, so another way of executing the same process as above would be: 96 | 97 | -write /path/to/my/file.%04d.exr first 1 last 10 -execute 98 | 99 | NOTE: All "-execute" commands are stored in the order they are entered and run in succession 100 | after the entire script is built. Keep this in mind if you need to run something like a 101 | CurveTool node before writing out image files. 102 | 103 | ALSO NOTE: Only Write node execution has been tested so far. 104 | 105 | 106 | -save 107 | ===== 108 | 109 | This can be used to save the Nuke script. The syntax is simply "-save" followed by a 110 | valid path to a file. The target path's directory must already exist. If a file with the 111 | given path already exists, a warning will be printed, and by default, the save will be skipped. 112 | However, you can add an optional "force" argument to force the script to be overwritten: 113 | 114 | -save /path/to/a/script.nk force 115 | 116 | NOTE: Save commands are added to the command stack immediately as they are found. Therefore, 117 | inserting two save statements at different points in the command list with different target 118 | files will result in two different .nk files, each containing the script as it existed when 119 | the "save" command was inserted. 120 | ''' 121 | 122 | import os 123 | import random 124 | import re 125 | import subprocess 126 | import sys 127 | 128 | import nuke 129 | import pynuke 130 | 131 | from zlib import crc32 132 | 133 | # List of valid node classes, excluding *Reader and *Writer plugins 134 | allPlugins = pynuke.getPluginList(['^.+Reader$', '^.+Writer$'], inclusiveREs=False) 135 | 136 | def getNukeNode(inString): 137 | ''' 138 | Figures out what node class to use based on 'inString.' 139 | 140 | The resolution order is: 141 | 1) Exact match 142 | 2) Case-insensitive match 143 | 3) Single partial match 144 | 4) Error 145 | 146 | Class names that differ only by a trailing version digit are 147 | prioritized to return the highest version. 148 | ''' 149 | rawVerRE = r'^%s\d?$' % inString 150 | 151 | # Exact match 152 | matches = [] 153 | searchRE = re.compile(rawVerRE) 154 | for item in allPlugins: 155 | if searchRE.match(item): 156 | matches.append(item) 157 | if matches: 158 | matches.sort() 159 | return matches[-1] 160 | 161 | # Case-insensitive match 162 | matches = [] 163 | searchRE = re.compile(rawVerRE, flags=re.IGNORECASE) 164 | for item in allPlugins: 165 | if searchRE.match(item): 166 | matches.append(item) 167 | if matches: 168 | matches.sort() 169 | return matches[-1] 170 | 171 | # Single partial match 172 | matches = [] 173 | searchRE = re.compile(r'^%s' % inString, flags=re.IGNORECASE) 174 | for item in allPlugins: 175 | if searchRE.search(item): 176 | matches.append(item) 177 | if matches: 178 | if len(matches) > 1: 179 | raise ValueError("Input argument '%s' partially matched the following node classes:\n\t%s" % (inString, ', '.join(matches))) 180 | return matches[0] 181 | 182 | # You fucked up now 183 | raise ValueError("Input argument '%s' could not be matched to a node class" % inString) 184 | 185 | def generateNodeID(varName): 186 | ''' 187 | Generates a CRC32 hash from a combination of 'varName' and a 188 | pseudo-random number 189 | ''' 190 | return 'N%s' % str(crc32(varName + str(random.random()))).replace('-', '_') 191 | 192 | def parseLine(rawLine, regexObj): 193 | ''' 194 | Uses the specified 'regexObj' to parse 'rawLine' and return a 195 | command-args pair as a (string, list) 2-tuple. 196 | ''' 197 | if isinstance(regexObj, basestring): 198 | regexObj = re.compile(regexObj) 199 | parsedLine = regexObj.findall(rawLine) 200 | lineCmd = parsedLine.pop(0) 201 | return (lineCmd, parsedLine) 202 | 203 | def parseCLI(rawInput): 204 | ''' 205 | Converts a command-line-style string of arguments into a 206 | string of Nuke-style TCL commands that can be passed directly 207 | to nuke.tcl() 208 | ''' 209 | rawInput = ' %s' % rawInput 210 | cmdStack = [] 211 | execNodes = [] 212 | varMap = {'0':'0'} 213 | argRE = re.compile('(\{[{}.0-9\s]+\}|[^\s]+)') 214 | cli = [i.strip() for i in rawInput.split(' -') if i] 215 | 216 | for line in cli: 217 | cmd, args = parseLine(line, argRE) 218 | if cmd == 'set' and len(args) == 1: 219 | varName = args[0] 220 | varID = generateNodeID(varName) 221 | varMap[varName] = varID 222 | cmdStack.append('set %s [stack 0]' % varID) 223 | elif cmd == 'push' and len(args) == 1: 224 | varName = args[0] 225 | varID = varMap[varName] 226 | if varName != '0': 227 | cmdStack.append('push $%s' % varID) 228 | else: 229 | cmdStack.append('push 0') 230 | elif cmd == 'execute': 231 | varID = generateNodeID(cmd) 232 | execNodes.append((varID, args[0] if args else None)) 233 | cmdStack.append('set %s [stack 0]' % varID) 234 | elif cmd == 'save': 235 | saveFile = args.pop(0) 236 | if os.path.isdir(saveFile): 237 | print "WARNING: Script save path '%s' is a directory. Ignoring." % saveFile 238 | continue 239 | if os.path.exists(saveFile): 240 | print "WARNING: Script save path already exists: '%s'" % saveFile 241 | if not args: 242 | print "WARNING: No force command found. Skipping script save." 243 | continue 244 | if args[0] != 'force': 245 | print "WARNING: Invalid 'save' syntax. Use '-save force' to force-overwrite a save target. Skipping script save." 246 | continue 247 | else: 248 | print "Forcing script save." 249 | cmdStack.append('script_save {%s}' % saveFile) 250 | else: 251 | node = getNukeNode(cmd) 252 | nodeArgs = ' '.join(args) if args else '' 253 | cmdStack.append('%s {%s}' % (node, nodeArgs)) 254 | 255 | # If we have nodes to execute, append those commands to the end of the stack 256 | for node, range in execNodes: 257 | if range is not None: 258 | cmdStack.append('execute $%s %s' % (node, range)) 259 | else: 260 | cmdStack.append('execute $%s [value $%s.first]-[value $%s.last]' % (node, node, node)) 261 | 262 | tclString = ';'.join(cmdStack) + ';' 263 | return tclString 264 | 265 | if __name__ == '__main__': 266 | try: 267 | parsedCmd = parseCLI(' '.join(sys.argv[1:])) 268 | except ValueError: 269 | import traceback 270 | traceback.print_exc() 271 | else: 272 | print '\n%s TCL COMMAND STRING %s' % ('='*15, '='*15) 273 | print '%s' % parsedCmd 274 | print '%s\n' % ('='*50,) 275 | nuke.tcl("""%s""" % parsedCmd) 276 | -------------------------------------------------------------------------------- /nukepy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Wrapper for Nuke -t that behaves more like a normal python binary. 4 | 5 | - adds support for -c flag to pass a string of python code to execute 6 | - expands symbolic links 7 | - can be used as the interpreter in executable python scripts (e.g. #!/usr/bin/env nukepy) 8 | """ 9 | 10 | from __future__ import with_statement 11 | 12 | import sys 13 | import os 14 | import subprocess 15 | import tempfile 16 | 17 | newArgsList = [] 18 | nextIsPyCmd = False 19 | tempFileName = None 20 | try: 21 | for arg in sys.argv[1:]: 22 | if nextIsPyCmd: 23 | nextIsPyCmd = False 24 | fd, tempFileName = tempfile.mkstemp(suffix='.py', 25 | prefix='nukepyCommand', 26 | text=True) 27 | with os.fdopen(fd, 'w') as tempFileHandle: 28 | tempFileHandle.write(arg) 29 | newArgsList.append(tempFileName) 30 | elif arg == '-c': 31 | if tempFileName is not None: 32 | raise Exception('-c argument may only be given once') 33 | nextIsPyCmd = True 34 | elif os.path.islink(arg): 35 | newArgsList.append(os.path.realpath(arg)) 36 | else: 37 | newArgsList.append(arg) 38 | 39 | procArgs = ["Nuke", "-c", "4G", "-t", "--"] + newArgsList 40 | p = subprocess.Popen(procArgs) 41 | os.waitpid(p.pid, 0)[1] 42 | finally: 43 | if tempFileName: 44 | os.remove(tempFileName) 45 | 46 | # this also works but exits in a slightly different way 47 | #/bin/tcsh 48 | #Nuke -t < $* 49 | --------------------------------------------------------------------------------