I have my opinions on API design

So I’m going to write about them.

In this context I’m really talking about REST APIs: those wonderful HTTP requests between you and some application that allow you to do all sorts of great things with your data.  Most projects that I have the most fun with involve working with an API; reading about it, testing against it, building the solution with it and coming up with other crazy sh*t to do with it.

Quickly, about REST

REST APIs provide a simple interface to an application over – normally – HTTP.  It follows the familiar standards of other architectures: create: POST, read: GET, update: PUT/PATCH and delete: DELETE.  You use these methods to interact with ‘resources’ at different ‘endpoints’ of the service.  These endpoints will return and/or accept data to achieve a desired result.

That’s the high level overview.

From there you will start encountering a wide range of variations and differences.  Some APIs will allow XML with your requests, but not JSON.  Some work with JSON, but not XML.  APIs may require you to explicitly declare the media type you’re going to interact with.  You might have one API that accepts HTTP basic authentication while others have a token based authentication workflow (like OAuth/OAuth2).  There is a lot of variance in the designs from service to service.

Which brings us to the opinions on design

The APIs I do the most work with currently are for the JSS (of course), JIRA and HipChat, but I’ve also poked around in CrashPlan and Box on the side.  There are a lot of things that I like about all of these APIs and, frankly, some things that really irk me.  And, I mean, really irk me.  Those experiences started me in the direction of learning what it was like to create my own.

If you know me at all you know that I have a real passion about Python.  My current obsession has been the Flask, a microframework for Python that allows you to write web applications.  I’ve been using it for HipChat add-ons that I’m developing, but I was really excited to get into Flask because I could start building my own REST APIs and dig into how they are designed.

Between working with established APIs and the reading and experimenting as I work on my own, I’ve determined there are a number of design choices I would want implemented in any API I worked with.

But it’s in the interface…

Two years ago I had the opportunity to attend Dreamforce.  That year was a biggie as Salesforce was transitioning their development platform and announced their intention to be mobile first and API first.  It was a pretty “phenomenal” keynote.  There were tons of sessions during the mega-conference devoted to the plethora of new and revamped APIs that now made up the Salesforce platform.  My big take away was a slide that provided an extremely high overview of the new stack.  All of Salesforce’s apps and services sat above a unified API layer.

I can’t say why that stuck with me so much at the time since I didn’t even know how to write a simple Python script, but it did.  This was the first big idea that I held onto about API design: implement your features at the API level first, document the implementation and then use that to build onto the end-user solution.

There are plenty of examples out there of services that segregate their user interface from their API and I’ve seen forums with a lot of developers or IT professionals asking why something was implemented in the GUI but inaccessible through their API which prevented an app/integration/automation from advancing.  So, as Salesforce put it, API first.

Documented without the docs

I’ve seen a lot of great examples of API documentation out there.  CrashPlan, JIRA and HIpChat are at the top of my “how to do it right” examples in that they provides representations of data for each supported request method for an endpoint, returned HTTP status codes and potential error messages with their causes.  This is invaluable information for anyone who is writing a script or application against an API, but they all share the same weakness: they’re docs that exist outside the API.

A robust API can provide all the information a developer requires through through the same HTTP methods that – allowing for automated discovery of the API’s capabilities without scrolling around web pages and then flipping back to your console.

There’s an HTTP method I’ve read about, but not one I’ve seen in any of the docs for these APIs as supported.  That would be the OPTIONS method.  It’s a great idea!  Want to know what you can do to a resource?  Pass OPTIONS as the method and in the response there will be a header “Allow” that will list them.

This could be extended to be a contextual method based upon the level of access the provided credentials have.  Say a resource supports GET, POST, PUT, PATCH and DELETE but our user account only supports creating and updating resources.  An admin would return all five in the response header, but our user would only have GET, PUT and PATCH as valid options.

So ok, there’s an HTTP method in the REST standard that allows us to discovery how we can interact with our resources.  Now how do we determine what the valid format of our data in our requests is supposed to be?  JIRA actually implements a solution this for ‘Issues.’  Check out the following endpoints:

/rest/api/2/issue/createmeta
/rest/api/2/issue/{issueIdOrKey}/editmeta

Text The ‘createmeta’ endpoint will return a wealth of data including available projects, issues types, fields and what is required when creating a new issue.  That’s a goldmine of data that’s specific to my JIRA instance!  Then it gets even better when parameters are passed to filter it down even further to better identify what you need to do.  Like this:

/rest/api/2/issue/createmeta?projectIds=10201&issuetypeIds=3

That will return all of the required fields I require to create a new ‘Task’ within the ‘Information Technology’ project in my JIRA board.  If I create a task and then want to update it I can call the second endpoint to reveal all of the fields relevant to this issue, which are required and acceptable values for input.

Despite how great the above is, that’s about all we get for the discovery through JIRA’s API.  We still need to go back to the online docs to reference the other endpoints.

Something I read on RESTful API Design struck a note on this topic.  The idea pitched here is to use forms to provide back to the client a representation of a valid request for the endpoint by passing the appropriate MIME type (for example: ‘application/x-form+json’).  This isn’t something you could expect to have a uniform definition of, but that wouldn’t matter!  You could still programmatically obtain information about any API endpoint by passing the the MIME type for the desired format.

Here’s an example of what a response might look like to such a request:

curl http://my.api.com/users -H "content-type: application/x-form+json" -X POST

{
    "method": "POST",
    "type": "user",
    "fields": {
        "name": {
            "type": "string",
            "required": true
        },
        "email": {
            "type": "string",
            "required": false
        },
        "is_admin": {
            "type": "bool",
            "required": false
        }
    }
}

They can do a lot more work for you

Usually if you’re making a request to an object there will be references, links, within the data to other objects that you can make calls to.  Sometimes this is as simple as an ID or other unique value that can be used to build another request to retrieve that resource.  Seems like an unnecessary amount of code to handle this on the part of the client.

There are two ways of improving this.  The first is to include the full URL to the linked resource as a part of the parent.

curl http://my.api.com/users -H "content-type: application/json"

{
    'name': 'Bryson',
    'email': 'bryson.tyrrell@gmail.com,
    'computers': [
        {
            'id': 1,
            'name': 'USS-Enterprise',
            'url': 'https://my.api.com/computers/1'
        }
    ]
}

The second can build upon this by allowing parameters to be passed that tell the API to return linked objects that are expanded to include all of the data in one request.  JIRA’s API does this for nearly every endpoint.

curl http://my.api.com/users?expand=computers -H "content-type: application/json"

{
    'name': 'Bryson',
    'email': 'bryson.tyrrell@gmail.com,
    'is_admin': true,
    'computers': [
        {
            'id': 1,
            'name': 'USS-Enterprise',
            'url': 'https://my.api.com/computers/1'
            'uuid': 'FBFF2117-B5A2-41D7-9BDF-D46866FB9A54',
            'serial': 'AA1701B11A2B',
            'mac_address': '12:A3:45:B6:7C:DE',
            'model': '13-inch Retina MacBook Pro',
            'os_version': '10.10.2'
        }
    ]
}

Versions are a good thing

All APIs change over time.  Under the hood bug fixes that don’t affect how the client interacts with the service aren’t much to advertise, but additions or changes to endpoints need to be handled in a way that can (potentially) preserve compatibility.

The most common kind of versioning I interact with has it directly in the URL.  I’m going to reference HipChat on this one:

api.hipchat.com/v1
api.hipchat.com/v2

The v1 API was deprecated some time ago as HipChat migrated to their newer and more robust v2 API.  While the v1 API is still accessible it has limitations compared to v2, is lacking many of the endpoints and is no longer supported which means that a lot of integrations that were written using v1 are steadily being phased out.

The differences between the two versions of the API are huge, especially when it comes to authentication, but even after its release the v2 API has had a number of changes and additions made to it.  Unless you’re watching for them they would be easy to miss.

Going the route of maintaining the version of the API in the URL, I found this example:

my.api.com/ < Points to the latest version of the API
my.api.com/2/ < Points to latest version of the v2 API
my.api.com/2.0/ < Points to a specific version of the v2 API

On the backend the objects would need to track which version a field or endpoint was added (or even removed) and handle the response to a request based upon the version passed in the URL.  Anything requested that falls outside of the version would prompt the appropriate 4XX response.

Another method of versioning is used with GitHub’s API.  By default your API requests are made against the latest version of the API, but you you can specify a previous version by having it passed as a part of the ‘Accept’ header:

curl https://api.github.com/users/brysontyrrell -H "Accept: application/vnd.github.v3.full+json"

I’ve read about pros and cons for both approaches, but they serve the purpose of identifying changes in an API as it evolves while providing a means for compatibility with existing clients.

Multiple formats isn’t a sin

My personal preference for any REST API I work with is JSON.  JSON is easy to me, it makes sense, it just works.  I can think of one glaring example off the top of my head of an API I frequently work with that lets me read back objects in JSON but only accepts XML for POST/PUT requests.  Frustrating.

Still, JSON is my preference.  Plenty of people prefer XML.  In some cases XML may be easier to work with than JSON (such as parsing in shell scripts) or be the better data set for an application.  Structurally XML and JSON can be very interchangeable depending upon the data that is being accessed.

If the object can be converted to multiple formats then it may be a good idea to support it.  By passing the appropriate MIME type the API can return data in the requested format.  If no MIME type is passed there should be a default type that is always returned or accepted.

Wrap-up

It’s late now and I’ve dumped a lot of words onto the page.  There’s a PyCharm window open with the shell of my sample API project that attempts to implement all of the design ideas I describe above.  Once I finish it I’ll throw it up on GitHub and see about incorporating some of the requests/responses to it into the article.

Advertisements

Bryson’s Bash Favorites

We recently had our JAMF Nation User Conference here in Minneapolis.  I spent a lot of time with a lot of brilliant sysadmins from around the world.  After three nights of mingling it became pretty clear that despite how much I enjoy talking about technology solutions (and I will continue to talk as people hand me Martinis), I don’t feed a lot of that back into Mac admin community, and it was made clear that I should give it a try.

So, this will be my first installment of three posts sharing some of what I’ve learned.  This first will focus on Bash scripting.  The second will serve as an introduction to Python for the Bash scripter (let me be up front about this: Python is so hot…) and some of the benefits that it offers to us as administrators.  The last post will be entirely about the JSS API, interacting the the data in both Bash and Python as well as some examples of what you can accomplish with it.

So, on to Bash…

For the last two years as a Mac administrator I’ve learned quite a bit on the subject of shell scripting and have been shown a wealth of handy tricks that have helped refine my skill (there are a lot of wizards at JAMF).  In this post I want to show some of my favorite scripting solutions and techniques that have become staples to my writing style.  Now, if you’re reading this I’m going to assume you’re already pretty familiar with using a Mac via the command line and basic scripting (if the word shebang doesn’t conjure up the image of a a number sign with an exclamation mark, you might not be ready).

Error Checking and Handling

As admins we write a lot of code that interacts with the Mac in ways that can certainly ruin a person’s day (if not their life, or so they would claim).  When executing commands there is a variable always readily available to view:

USS-Enterprise:~ brysontyrrell$ echo $?
0

The ‘$?’ represents the result of the last command that was run.  If there were no errors you  receive a zero (0) in return.  For anything else there will be a value greater than zero which represents your error code.  Visit the manpage of any number of commands on your Mac and you will usually find a section devoted to what an error code represents (if not, we have Google).  Error codes are critical for figuring out what went wrong.

In many cases we might need to kill a script if a command failed.  After all, if data is being manipulated or moved around there’s not a whole lot of sense in executing the remainder of the script when some key actions did not perform correctly.  We can do this with a simple IF statement that triggers an ‘exit’:

cp /path/to/source /path/to/destination
if [ $? -ne 0 ]; then
    exit
fi

Now, this will exit the script if our copy operation failed, but it isn’t that great.  The script will exit, but we won’t have any tangible information about how or why.  Let’s add a few things into this to make it more helpful to us:

cp /path/to/source /path/to/destination
if [ $? -ne 0 ]; then
    echo "There was a problem with the copy operation. Error code $?"
    exit 1
fi

Now we’re getting somewhere.  With this the script will not only output onto the Terminal that there was an error, but it will return the error code and also exit our script with a value greater than zero which will be reported as a failure!  We can have error codes that mean different things. ‘1’ is just the default.  Any numerical value can be used to represent an error (or types of error if we’re lumping them together) and its meaning can be recorded either within the script or in other documentation:

# Exit 1 for general error
# Exit 10 for copy operation error
cp /path/to/source /path/to/destination
if [ $? -ne 0 ]; then
    echo "There was a problem with the copy operation. Error code $?"
    exit 10
fi

Using ‘echo’ for the output is great if we’re running the script manually, but what we’re writing will be run remotely and we will not be watching it execute live.  We’re going to need something that will allow us to go back at a later time to review what transpired:

cp /path/to/source /path/to/destination
if [ $? -eq 0 ]; then
    log "The copy operation was successful."
else
    log "There was a problem with the copy operation. Error code $?" 10
fi

The log() call you see here is actually a function that I use for almost everything I write.  We’re going to cover what exactly it does a little later, but the basic of the above script is that it will output a message upon both the success of the command as well as the failure and exit the script after the error.  Never underestimate how important it is that your scripts are telling you what they are doing.  Your life will be better for it.

Operators for Shortcuts

Our above examples all allow you to execute multiple actions in response to the success or failure of a command based upon the result.  Sometimes we might only need to trigger one command in response to an action.  We can use operators  to achieve this effect without writing out an entire IF statement:

# A copy operation using an IF statement to execute a file removal
cp /path/to/source /path/to/destination
if [ $? -eq 0 ]; then
    rm /path/to/source
fi

# The above operation using the '&&' operator
cp /path/to/source /path/to/destination && rm /path/to/source

In the second example the source file we are copying is deleted so long as the ‘cp’ command left of the ‘AND’ operator returned zero.  If there had been an error then the code on the right side won’t execute.  Both examples achieve the same result but using the operator acts as a short cut and allows you to cut down on the amount of code you need to write.  If you need to achieve the same effect but when the result is not zero we can turn to the ‘OR’ operator:

# A copy operation using an IF statement to exit upon failure
cp /path/to/source /path/to/destination
if [ $? -ne 0 ]; then
    exit 10
fi

# The same copy operation but using '||' to trigger the 'exit'
cp /path/to/source /path/to/destination || exit 10

Its a TRAP

This one is a personal favorite.  Usage of the Bash builtin ‘trap’ is actually pretty new to me, and it is one of the hands down coolest (if you’re like me and think scripting is, you know, cool) things I’ve seen.  A ‘trap’ give you the ability to determine actions that are performed when your script terminates or when commands throw errors!  Let me demonstrate with a very basic example:

# Here we define the function that will contain our commands for an 'exit'
onExit() {
rm -rf /private/tmp/tempfiles
}

# Here we set the 'trap' to execute upon an EXIT signal
trap onExit EXIT

for file in /private/tmp/tempfiles/*; do
    cp "${file}" /path/to/destination/
done

# This 'exit' command will send an EXIT signal which will trigger the 'trap'
exit 0

As you can see in the above example, ‘trap’ is very easy to use.  The syntax for ‘trap’ is:

trap 'command(s)' signal(s)

We created an onExit() function containing the actions we wanted to perform.  This became the command in the ‘trap’ line. Once triggered, the temporary directory that we were copying files from is automatically purged once the script is complete. This makes cleanup much simpler and easier on us.  It also allows far more control over the state of the system upon an error requiring we kill the script in process.  I had mentioned in my introduction that we could have traps for both terminations and errors, did I not?  Let’s expand upon that first example and make it a bit more robust:

onExit() {
rm -rf /private/tmp/tempfiles
}

# This function contains commands we want to execute every time there is an ERROR
onError() {
errorCode=$?
cmdLine=$1
cmdName=$2
echo "ERROR: ${cmdName} on line ${cmdLine} gave error code: ${errorCode}"
exit 1
}

trap onExit EXIT

# Here we introduce a second 'trap' for ERRORs
trap 'onError $LINENO $BASH_COMMAND' ERR

for file in /private/tmp/tempfiles/*; do
    cp "${file}" /path/to/destination/
done

exit 0

This one certainly has a lot more going on.  The way I approach these ‘traps’ is pretty simple: my EXIT performs a cleanup of any working files while my ERROR handles outputting the information I will need to determine what went wrong.  In this example I have an ‘exit’ command included inside the onError() function so the cleanup onExit() function is still called in the first event of an error.  That’s not a practice I’m recommending, but I am showing that it is an option.  There are plenty of cases out there where you would want the script to continue on even if an error occurs in the middle of a copy operation (user account migration, anyone?).  Those are the times when you will want to be explicit about where in your script certain errors trigger an ‘exit.’

Let’s break down that onError() function:

onError() {
# Our first action is to capture the error code of the command (remember, this changes after EVERY command executed)
errorCode=$?
# This variable is from $LINENO which tells us the number line the command resides on in the script
cmdLine=$1
# The last variable is from $BASH_COMMAND which is the name of the command itself that gave an error
cmdName=$2
# Our tidy statement here puts it all together in a human-readable form we can use to troubleshoot
echo "ERROR: ${cmdName} on line ${cmdLine} gave error code: ${errorCode}"
exit 1
}

# In this 'trap' we call not just our function, but we also pass two parameters along to it
# The $LINENO and $BASH_COMMAND variables are called 'Internal Variables' to the Bash shell
trap 'onError $LINENO $BASH_COMMAND' ERR

We’re going to make this onError() function even more powerful a little later by visiting the log() function I had mentioned.  Before that, let’s go back to the onExit() function.  This ‘trap’ ideally is where we want to perform all of our cleanup actions, and the basic example I gave it wiping out a temporary directory of working files.  While our scratch space is removed in this process it does not address any actions we may have made in other areas of the system.  So, do we want to write all of that into the onExit() function even if they may not be relevant to when the script terminated?

I’m a big fan of the idea: “If I don’t HAVE to do this, then I don’t want to.”  The meaning of this is I don’t want to execute commands on a system (especially when I’m masquerading around as root) if they’re unnecessary.  We can write our onExit() function to behave following that ideology.  I don’t quite remember where I first saw this on the internet, but it was damned impressive:

# This is an array data type
cleanup=()

onExit() {
# Once again we're capturing our error code right away in a unique variable
exitCode=$?
rm -rf /private/tmp/tempfiles
# If we exit with a value greater than zero and the 'cleanup' array has values we will now execute themif [ $exitCode -ne 0 ] && [ "${#cleanup[@]}" -gt 0 ]; thenfor i in"${cleanup[@]}"; do# The 'eval' builtin takes a string as an argument (executing it)eval"$i"donefi
echo"EXIT: Script error code: $exitCode"
}

onError() {
errorCode=$?
cmdLine=$1
cmdName=$2
echo"ERROR: ${cmdName} on line ${cmdLine} gave error code: ${errorCode}"exit 1
}

trap onExit EXIT
trap'onError $LINENO $BASH_COMMAND' ERR

for file in /private/tmp/tempfiles/*; docp"${file}" /path/to/destination/
    # After each successful copy operation we add a 'rm' command for that file into our 'cleanup' array
    cleanup+=('rm /path/to/destination/"${file##*/}"')
doneexit 0

We have now transformed our onExit() function into one giant UNDO command.  The IF statement within it will always remove the temporary working directory (which we always want, no matter what the exit status is) but will now run additional commands out of our handy ‘cleanup’ array.  Effectively, unless the script successfully completes the entire copy operation it will, on the first error, remove every file that did make it into the destination.  This leaves the system pretty much in the same state as it was before our script ran.  We can take this concept further in much larger scripts by adding new commands into a ‘cleanup’ array as we complete sections of our code.

Logging is Awesome

I’m finally getting around to explaining that log() function from earlier.  Logs are fantastic for troubleshooting as they generally contain a lot of data that helps point us towards the source of the issue.  You can approach logging of your own scripts in two ways: append and existing log or use your own customized one.  In my case, I append all of my script log output into the Mac’s system.log using the ‘logger’ command.  This command allows you to do some pretty cool things (like message priority), but my use is fairly simple.

log () {
if [ -z "$2" ]; then
    logger -t "it-logs: My Script" "${1}"
else
    logger -t "it-logs: My Script" "${1}"
    logger -t "it-logs: My Script" "Exiting with error code $2"
    exit $2
fi
}

log "This is a log entry"

***OUTPUT IN SYSTEM.LOG AS SHOWN IN CONSOLE.APP***
Nov 10 12:00:00 USS-Enterprise.local it-logs: My Script[1701]: This is a log entry

You’re probably piecing together how this function works.  the ‘-t’ flag in the command creates a tag for the log entry.  In my case I have a universal prefix for the tag I use in all of my scripts (here I’m using ‘it-logs:’ but its similar) and then I follow it with the name of the script/package for easy reference (you read right: everything I write about in this post I use for preinstall and postinstall scripts in my packages as well).  The tagging allows me to grab the system.log from a machine and filter all entries containing ‘it-logs’ to see everything of mine that has executed, or I can narrow it down to a specific script and/or package by writing the full tag.  Its really nice.

Right after the tag inside the brackets is the process ID, and then we have our message.  If you scroll back up to the example where I used the log() function you’ll see that in code that triggered on a failure I included a ’10’ as a second parameter.  That is the error code to use with an ‘exit.’  If present, log() will write the message first and then write a second entry stating that the script is existing with an error code and  ‘exit’ with that code (and trigger our onExit() trap function).

If you want to maintain your own log, instead of writing into the system.log, you can easily do so with a similar function:

# You must ensure that the file you wish to write to exists
touch /path/to/log.log

log () {
if [ -z "$2" ]; then
    # Here 'echo' commands will output the text message that is '>>' appended to the log
    echo $(date +"%Y %m %d %H:%M")" it-logs: My Script: ${1}" >> /path/to/log.log
else
    echo $(date +"%Y %m %d %H:%M")" it-logs: My Script: ${1}" >> /path/to/log.log
    echo $(date +"%Y %m %d %H:%M")" it-logs: My Script: Exiting with error code $2" >> /path/to/log.log
    exit $2
fi
}

***OUTPUT IN LOG.LOG AS SHOWN IN CONSOLE.APP***
2013 11 10 20:38 it-logs: My Script: This is a log entry

The end result is just about the same, and with a .log extension it will automatically open in the Console.app and still use the filtering.

Now I’m going to take the copy operation from above and write in the log() function so you can see how the whole package fits together:

log () {
if [ -z "$2" ]; then
    logger -t "it-logs: My Script" "${1}"
else
    logger -t "it-logs: My Script" "${1}"
    logger -t "it-logs: My Script" "Exiting with error code $2"
    exit $2
fi
}

cleanup=()

onExit() {
exitCode=$?
log "CLEANUP: rm -rf /private/tmp/tempfiles"
rm -rf /private/tmp/tempfiles
if [ $exitCode -ne 0 ] && [ "${#cleanup[@]}" -gt 0 ]; then
    for i in "${cleanup[@]}"; do
        log "ADD-CLEANUP: $i"
        eval "$i"
    done
fi
}

onError() {
errorCode=$?
cmdLine=$1
cmdName=$2
log "ERROR: ${cmdName} on line ${cmdLine} gave error code: ${errorCode}" 1
}

trap onExit EXIT
trap 'onError $LINENO $BASH_COMMAND' ERR

for file in /private/tmp/tempfiles/*; do
    cp "${file}" /path/to/destination/
    log "COPY: ${file} complete"
    cleanup+=('rm /path/to/destination/"${file##*/}"')
done

exit 0

Interacting With the User Stuff

There are a ton of examples out there for grabbing the name of the currently logged in user and finding their existing home directory.  Both items are very important when we’re executing our scripts as the root user.  To get the name of the logged in user I’ve found these three methods:

# Though the simplest I have found that this method does not work in a package preinstall/postinstall script
USS-Enterprise:~ brysontyrrell$ echo $USER
brysontyrrell

# This one is also very simple but it relies upon a command that may not be present in future OS X builds
USS-Enterprise:~ brysontyrrell$ logname
brysontyrrell

# The following is used pretty widely and very solid (the use of 'ls' and 'awk' nearly future-proofs this method)
USS-Enterprise:~ brysontyrrell$ ls -l /dev/console | awk '{print $3}'
brysontyrrell

One step beyond this is to then find the user’s home directory so we can move and/or manipulate data in there.  One piece of advice I’ve been given is to never assume I know what the environment is.  Users are supposed to be in the ‘/Users/’ directory, but that doesn’t mean they are.  If you’ve never played around much with the directory services command line utility (‘dscl’), I’m happy to introduce you:

USS-Enterprise:~ brysontyrrell$ dscl . read /Users/brysontyrrell | awk '/NFSHomeDirectory:/ {print $2}'
/Users/brysontyrrell

‘dscl’ is incredibly powerful and gives use easy access to a lot of data concerning our end-users’ accounts.  In fact, you can take that above command and change out the regular expression ‘awk’ is using to pull out all sorts of data individually:

USS-Enterprise:~ brysontyrrell$ dscl . read /Users/brysontyrrell | awk '/GeneratedUID:/ {print $2}'
123A456B-7DE8-9101-1FA1-2131415B16C1

USS-Enterprise:~ brysontyrrell$ dscl . read /Users/brysontyrrell | awk '/UniqueID:/ {print $2}'
501

USS-Enterprise:~ brysontyrrell$ dscl . read /Users/brysontyrrell | awk '/PrimaryGroupID:/ {print $2}'
20

USS-Enterprise:~ brysontyrrell$ dscl . read /Users/brysontyrrell | awk '/UserShell:/ {print $2}'
/bin/bash

Alternative Substitution

Kind of an odd header, but you’ll get it in a moment.  One of the bread ‘n butter techniques of scripting is to take the output of one command and capture it into a variable.  This process is known as “command substitution” where instead of displaying the entered command we are shown the result.  The traditional way of doing this is to enclose the command you are capturing in `backticks`.  Instead of using backticks, use a $(dollar and parenthesis) so your editor of choice still highlights the command syntax correctly.

Check out this example which will pull out the type of GPU of the Mac:

gpuType=`system_profiler SPDisplaysDataType | /awk -F': ' '/Chipset Model/ {print $2}' | tail -1`
gpuType=$(system_profiler SPDisplaysDataType | awk -F': ' '/Chipset Model/ {print $2}' | tail -1)

Functionally, both of these statements are identical, but now as we write our scripts we have an easier time going back and identifying what is going on at a glance.  Let’s take two of the examples from above for obtaining information about the logged in user and write them the same way for a script:

userName=$(ls -l /dev/console | awk '{print $3}')
userHome=$(dscl . read /Users/"${userName}" | awk '/NFSHomeDirectory:/ {print $2}')

cp /private/tmp/tempfiles/somefile "${userHome}"/Library/Preferences/