Reading and setting multiple finder labels

  1. last year

    Bruno F

    16 Oct 2018 Pre-Release Testers, Xojo Pro Canada

    I currently use FinderLabelMBS to read and set a folderitem Finder label. However, sometimes a file has multiple labels assigned. I'm looking for a way (with or without MBS) to read all the labels currently assigned to a folderitem. I also need to assign several labels to folderitem.

    Googling around I found an Applescript to do it, but I would prefer to avoid using Applescript if possible.

    Ok so I ended up going the command-line way using "xattr". It allows to display or change file extended attributes. Labels are one of the extended attributes.

    I can't describe all parts in details, but I'll try to explain what I can.as once everything worked, I have not documented what each component does. But in short,

    Reading the labels:

    xattr -p com.apple.metadata:_kMDItemUserTags /path/to/file | xxd -r -p | plutil -convert json - -o -

    In more details:

    xattr -p com.apple.metadata:_kMDItemUserTags /path/to/file
    xattr allows to read and manipulate extended attributes. Labels are one of them. It outputs the result as a hex dump.

    • -p : Print the value associated with the attribute name
    • com.apple.metadata:_kMDItemUserTags is the attribute that stores the labels
    • /path/to/file : valid shell path (properly escaped)

    xxd -r -p
    xxd is a convert utility from hex dump to binary data

    • -r : reverse convert (from hex dump to binary)
    • -p : output as plain text

    plutil -convert json - -o -
    plutil is a utility to work with property lists and will perform the final conversion in json so it's easier to work with.

    • -convert json : convert the property list to json
    • - : take standard in as input
    • -o : allows to specify a path for output (in our case, will be redirected)
    • - : output is redirected to standard out, which will end up in the result property of the Xojo shell.

    The final output is a string in json format which looks like:

    ["Yellow\n5","Red\n6","Work"]

    Each item contains the label name followed by the color code (if there's a color). The name and color are separated by the "\n" string. The color codes are: 0=none | 1=gray | 2=green | 3=purple | 4=blue | 5=yellow | 6=red

    Writing labels

    To set the tags, rebuild the string in the same format. The same utilities are used in reverse order with some different options, but you can basically figure out the process. Any new label will still be assigned and shown, but not available in the label popup list in the finder.

    xattr -xw com.apple.metadata:_kMDItemUserTags $(echo '[labelname1,labelname2,labelname3]' | plutil -convert binary1 - -o - | xxd -p -c 256 -u) /path/to/file

    My needs were to replicate the labels from one file to another. I have an audio converter application and I needed the converted files to have the same labels as the originals. So I have created a small class that reads the labels, and then sets them on another file. It does not allow to manually define the labels, but it would be trivial to add.

    Here's some sample code (adapted from my class but not tested)

    Private Sub ReadLabels(targetFile as FolderItem)
    // READ FILE LABELS
      dim theLabels() as string
      dim theColors() as integer
      
      if targetFile <> nil and targetFile.Exists = true then
        dim s as new shell
        s.Execute "xattr -p com.apple.metadata:_kMDItemUserTags " + targetFile.ShellPath + " | xxd -r -p | plutil -convert json - -o -"
        if s.ErrorCode = 0 then
          
          dim rawResult as string = s.Result
          dim jsonLabels as new JSONItem(rawResult)
          
          dim lastItem as integer
          lastItem = jsonLabels.Count - 1
          
          for i as integer = 0 to lastItem
            
            dim item as string
            item = jsonLabels.Value(i).StringValue
            
            dim parts() as string
            parts = item.Split(&u0A)
            
            theLabels.Append parts(0)
    
            if parts.Ubound > 0 then
              theColors.Append(parts(1).val)
            else
              theColors.Append(0)
            end if
            
          next
          
        end if
      end if
      
    // do your stuff with the labels and the color codes
    
    // theLabels contains an array of label names
    // theColors contains the matching color codes
    
    End Sub

    To write the labels for a given file, create a properly formatted string as: ["hot\n1","projectX","mytag1"]

    Private Sub SetLabels(targetFile as FolderItem,jsonlist as string)  
      // WRITE FILE LABELS
      dim cmd as string = "xattr -xw com.apple.metadata:_kMDItemUserTags $(echo '" + _
      jsonlist + "' | plutil -convert binary1 - -o - | xxd -p -c 256 -u) " + _
      targetFile.ShellPath
      
      dim s as new Shell
      s.Execute cmd
      
    End Sub

    In terminal, use "man" if you want to learn more about the different tools used:

    man xattr
    man xxd
    man plutil
  2. 12 months ago

    Hi Bruno,
    I have run into the same problem. Files end up with multiple labels, and user-defined labels cannot be cleared by setting f.FinderLabelMBS = 0. Did you ever find a solution?

  3. Bruno F

    19 Dec 2018 Pre-Release Testers, Xojo Pro Canada

    Yes I did, let me dig for it and I'll post it back later today.

  4. Bruno F

    19 Dec 2018 Pre-Release Testers, Xojo Pro Answer Canada

    Ok so I ended up going the command-line way using "xattr". It allows to display or change file extended attributes. Labels are one of the extended attributes.

    I can't describe all parts in details, but I'll try to explain what I can.as once everything worked, I have not documented what each component does. But in short,

    Reading the labels:

    xattr -p com.apple.metadata:_kMDItemUserTags /path/to/file | xxd -r -p | plutil -convert json - -o -

    In more details:

    xattr -p com.apple.metadata:_kMDItemUserTags /path/to/file
    xattr allows to read and manipulate extended attributes. Labels are one of them. It outputs the result as a hex dump.

    • -p : Print the value associated with the attribute name
    • com.apple.metadata:_kMDItemUserTags is the attribute that stores the labels
    • /path/to/file : valid shell path (properly escaped)

    xxd -r -p
    xxd is a convert utility from hex dump to binary data

    • -r : reverse convert (from hex dump to binary)
    • -p : output as plain text

    plutil -convert json - -o -
    plutil is a utility to work with property lists and will perform the final conversion in json so it's easier to work with.

    • -convert json : convert the property list to json
    • - : take standard in as input
    • -o : allows to specify a path for output (in our case, will be redirected)
    • - : output is redirected to standard out, which will end up in the result property of the Xojo shell.

    The final output is a string in json format which looks like:

    ["Yellow\n5","Red\n6","Work"]

    Each item contains the label name followed by the color code (if there's a color). The name and color are separated by the "\n" string. The color codes are: 0=none | 1=gray | 2=green | 3=purple | 4=blue | 5=yellow | 6=red

    Writing labels

    To set the tags, rebuild the string in the same format. The same utilities are used in reverse order with some different options, but you can basically figure out the process. Any new label will still be assigned and shown, but not available in the label popup list in the finder.

    xattr -xw com.apple.metadata:_kMDItemUserTags $(echo '[labelname1,labelname2,labelname3]' | plutil -convert binary1 - -o - | xxd -p -c 256 -u) /path/to/file

    My needs were to replicate the labels from one file to another. I have an audio converter application and I needed the converted files to have the same labels as the originals. So I have created a small class that reads the labels, and then sets them on another file. It does not allow to manually define the labels, but it would be trivial to add.

    Here's some sample code (adapted from my class but not tested)

    Private Sub ReadLabels(targetFile as FolderItem)
    // READ FILE LABELS
      dim theLabels() as string
      dim theColors() as integer
      
      if targetFile <> nil and targetFile.Exists = true then
        dim s as new shell
        s.Execute "xattr -p com.apple.metadata:_kMDItemUserTags " + targetFile.ShellPath + " | xxd -r -p | plutil -convert json - -o -"
        if s.ErrorCode = 0 then
          
          dim rawResult as string = s.Result
          dim jsonLabels as new JSONItem(rawResult)
          
          dim lastItem as integer
          lastItem = jsonLabels.Count - 1
          
          for i as integer = 0 to lastItem
            
            dim item as string
            item = jsonLabels.Value(i).StringValue
            
            dim parts() as string
            parts = item.Split(&u0A)
            
            theLabels.Append parts(0)
    
            if parts.Ubound > 0 then
              theColors.Append(parts(1).val)
            else
              theColors.Append(0)
            end if
            
          next
          
        end if
      end if
      
    // do your stuff with the labels and the color codes
    
    // theLabels contains an array of label names
    // theColors contains the matching color codes
    
    End Sub

    To write the labels for a given file, create a properly formatted string as: ["hot\n1","projectX","mytag1"]

    Private Sub SetLabels(targetFile as FolderItem,jsonlist as string)  
      // WRITE FILE LABELS
      dim cmd as string = "xattr -xw com.apple.metadata:_kMDItemUserTags $(echo '" + _
      jsonlist + "' | plutil -convert binary1 - -o - | xxd -p -c 256 -u) " + _
      targetFile.ShellPath
      
      dim s as new Shell
      s.Execute cmd
      
    End Sub

    In terminal, use "man" if you want to learn more about the different tools used:

    man xattr
    man xxd
    man plutil
  5. Thanks Bruno, this is extremely useful. Much appreciated!

    I'm almost there, but curiously, if a file has 2 or more labels calling SetLabels with jsonlist = "" or "[]" removes all but one label. I cannot remove all labels. Even a 2nd call won't do it.

    And even xattr -d com.apple.metadata:_kMDItemUserTags myFile won't do it (even tho xattr -l then reports that the com.apple.metadata:_kMDItemUserTags attribute is in fact gone, but the label in Finder remains, and must be removed manually from the Finder).

    Do you know of a way to remove ALL labels from a file in one shot?

    Thanks again
    Peter.

  6. Bruno F

    19 Dec 2018 Pre-Release Testers, Xojo Pro Canada

    Hmmm. Let me check this tonight, I'll get back with my findings.

  7. An addendum: I just noticed that if a user manually sets a label from the Finder, this label:

    1. cannot be removed by a call to SetLabels with jsonlist = ""
    2. is not even reported by ReadLabels ie. rawResult = "[]" even tho the label is clearly showing in the Finder.

    So it almost seems like if you set the labels in code, you can read them and remove them. If you set them manually in the Finder, they are inaccessible to Read/SetLabels methods.

    Very weird!

    P.

  8. Bruno F

    19 Dec 2018 Pre-Release Testers, Xojo Pro Canada

    ... I couldn't help but check immediately :-) There is a legacy finder info attribute that stores single labels (and other info). Meaning that sometimes, the label info can be stored there instead of the new com.apple.metadata:_kMDItemUserTags attribute.

    To remove all labels with a simpler command:

    xattr -d com.apple.metadata:_kMDItemUserTags /path/to/file

    This will leave the last single label if stored in com.apple.FinderInfo. To remove it:

    xattr -d com.apple.FinderInfo /path/to/file

    BUT com.apple.FinderInfo also stores other info. Although it's legacy info from the Classic environment, you should check if there's anything useful for you. One example is the 4 character file type code. Here's a link to a page that documents this attribute

    From my (non scientific) experiments, it seems that setting the first label from the Finder will set it using com.apple.FinderInfo IF this attribute is present. After removing it, if you set it again in the Finder, it's then correctly stored in the new attribute.

    I haven't poked around to decode the com.apple.FinderInfo attribute to extract the label set there, but it should be feasible.

  9. thanks Bruno, this makes sense and explains the odd behavior. Just for the record, creating a manual label from the Finder will resist clearance by xattr -d com.apple.metadata:_kMDItemUserTags. You must also execute xattr -d com.apple.FinderInfo in addition. This is on a freshly created file from TextEdit so no chance of some old legacy attributes hanging around. Odd that Apple would store labels under different com.apple.XXX files.

    Best
    Peter.

  10. Bruno F

    19 Dec 2018 Pre-Release Testers, Xojo Pro Canada

    Yeah, my tests are similar with new files created on Mojave. Sorry if I misled you in thinking that old files were the culprit. It seems that sometimes the Finder still creates the old attribute, so probably some legacy code buried in MacOS :-) I would guess that the first label uses the old attribute, and that any additional label get set with the new one.

    Glad it works for you. I'm going to make a more elegant class to read set and clear the labels. I can send it to you if you're interested.

  11. Thanks Bruno, I'd appreciate looking at your class. One other point I just noticed during testing: seems that xattr is a python script and unfortunately, compared to FinderLabelMBS, it is extremely slow, at least on a large folder tree (3000 files: 15 min and still running :-( ) on a remote server. So there is no easy way to efficiently manage ALL labels it seems. Just an FYI if your application also includes complex hierarchies.

    Cheers,
    Peter.

  12. Bruno F

    19 Dec 2018 Pre-Release Testers, Xojo Pro Canada

    My guess is that the slow process comes from instantiating a shell with every call. Try to make a call with a wildcard to process a batch of files. I did a quick test locally directly in terminal and processing 539 files with xattr -d com.apple.metadata:_kMDItemUserTags took 148ms and xattr -d com.apple.FinderInfo took 151ms. I know a file server adds quite a bit of lag but maybe you'll want to see if a wildcard helps. Try it directly in the terminal, transpose in a shell in Xojo after.

  13. That's not the problem: I ran a single command with the -r flag and finally had to kill my app after 20 min.

  14. But you're right about the remote volume issue: 3000 files on a local drive took 1 second with -r.

  15. Bruno F

    20 Dec 2018 Pre-Release Testers, Xojo Pro Canada

    So the command itself is not the cause of the delay. If the MBS plugin take less time, then we'll have to bow before our master Christian once again :-))

  16. Yes indeed, however, FinderLabelMBS only gives you access to one "set" of labels unfortunately. Christian, is there a way for MBS to handle them all?

  17. Bruno F

    20 Dec 2018 Pre-Release Testers, Xojo Pro Canada

    So MBS does work faster? (just curious)

  18. If I extract a list of items in a folder and set each one at a time using FinderLabelMBS it's faster than xattr with the -r flag, BUT FinderLabelMBS only seems to manipulate the classic labels, not the com.apple.metadata:_kMDItemUserTags, so it's not that useful because you end up with dual labels, a mix of new and old, and it becomes a mess. I wish we could just pass an array of numbers (= color indexes) to a function that would stamp none, one or several color labels to a folderItem, and quickly, taking care of new and old sets at once.

  19. Bruno F

    20 Dec 2018 Pre-Release Testers, Xojo Pro Canada

    I have searched a bit but the only other way I found (at a high level anyways) is using Applescript. I doubt it'll be faster, but it may be worth a try.

    At this point, you may want to contact Christian about this. He may be willing to update its classes to support both old and new attributes.

    As for me, I'll work on a clean class during the Christmas Holiday (for fun). If it can be useful, even if it's slow, I'll happily pass it along. It can work as you described, but it'll still be slow on a remote volume.

  20. Scott C

    28 Dec 2018 Aurora, IL

    @Bruno F As for me, I'll work on a clean class during the Christmas Holiday (for fun). If it can be useful, even if it's slow, I'll happily pass it along. It can work as you described, but it'll still be slow on a remote volume.

    I would also be interested in seeing your class. I only need read ability, but I can see how we ability would be useful too.

  21. Newer ›

or Sign Up to reply!