Search code examples
stringsortinggo

Sort strings by key value - Golang


I have the following string blob in Go:

  AWS_FIREHOSE_US_WEST_2_ENDPOINT: https://foobar.firehose.us-west-2.amazonaws.com
  BOT_NAME: mybot
  DEV_MODE: false
  GITHUB_APP_ID: 1234
  GITHUB_INSTALLATION_ID: 1234
  GITHUB_ORG_NAME: myorg
  SERVICE_WHO_IS_WHO_DEFAULT_HOST: foo.bar
  SERVICE_WHO_IS_WHO_DEFAULT_PORT: 80
  SERVICE_WHO_IS_WHO_DEFAULT_PROTO: http
  TZ: UTC
  _APP_NAME: myapp
  _BUILD_ID: 12345
  _DEPLOYMENT_ID: dev--foo.d53
  _DEPLOY_ENV: dev
  _OTEL_COLLECTOR_URL: tcp://localhost:4317
  _POD_ACCOUNT: 12345
  _POD_ID: a6127c9c-2be8-4078-8c00-132d29fbecfc
  _POD_REGION: us-west-1
  _POD_SHORTNAME: us-west-1-dev-default
  _TEAM_OWNER: eng-infra
  _TRACING_ENABLED: false
  force_restart: 2024-01-02T19:00:59Z
  GITHUB_PRIVATE_KEY: -----BEGIN RSA PRIVATE KEY-----
  fofofofofofofofofofofofofofoofofofofofofofofofoffof
  barbabaabababababababababababababababababaababababa
  batbababtbabttbtbabtbtabtbtbtbtbtbabtbtbbtbtbtbttbt
  -----END RSA PRIVATE KEY-----

This is a single string in Golang, separated by "\n", so please notice, it's not sorted by the key: value pair. I need to sort it out by the primary key, what would be the best way to accomplish this?

I've tried the following method:

// sortLines sorts the lines in a blob of text.
func sortLines(blob string) string {
    var sorted sort.StringSlice
    sorted = strings.Split(blob, "\n")
    sorted.Sort()
    return strings.Join(sorted, "\n")
}

Please notice trying to sort by line won't work due to the fact the GITHUB_PRIVATE_KEY: contains multiple lines.


Solution

  • EDIT: Revised to handle all forms of ASCII Armor.


    Your string is not a blob. It has a well-defined structure. It contains an ASCII Armor value:

    GITHUB_PRIVATE_KEY: -----BEGIN RSA PRIVATE KEY-----
    fofofofofofofofofofofofofofoofofofofofofofofofoffof
    barbabaabababababababababababababababababaababababa
    batbababtbabttbtbabtbtabtbtbtbtbtbabtbtbbtbtbtbttbt
    -----END RSA PRIVATE KEY-----
    

    IETF RFC 4880

    6.2. Forming ASCII Armor

    6.6. Example of an ASCII Armored Message

       -----BEGIN PGP MESSAGE-----
       Version: OpenPrivacy 0.99add ASCII Armor example to test data
    
       yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzS
       vBSFjNSiVHsuAA==
       =njUN
       -----END PGP MESSAGE-----
    

    A case insensitive sort (insert ASCII Armor example into test data):

      AWS_FIREHOSE_US_WEST_2_ENDPOINT: https://foobar.firehose.us-west-2.amazonaws.com
      BOT_NAME: mybot
      DEV_MODE: false
      force_restart: 2024-01-02T19:00:59Z
      GITHUB_APP_ID: 1234
      GITHUB_INSTALLATION_ID: 1234
      GITHUB_ORG_NAME: myorg
      GITHUB_PRIVATE_KEY: -----BEGIN RSA PRIVATE KEY-----
      fofofofofofofofofofofofofofoofofofofofofofofofoffof
      barbabaabababababababababababababababababaababababa
      batbababtbabttbtbabtbtabtbtbtbtbtbabtbtbbtbtbtbttbt
      -----END RSA PRIVATE KEY-----
      IETF_RFC4880_ASCII_ARMOR_EXAMPLE: -----BEGIN PGP MESSAGE-----
      Version: OpenPrivacy 0.99
    
      yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzS
      vBSFjNSiVHsuAA==
      =njUN
      -----END PGP MESSAGE-----
      SERVICE_WHO_IS_WHO_DEFAULT_HOST: foo.bar
      SERVICE_WHO_IS_WHO_DEFAULT_PORT: 80
      SERVICE_WHO_IS_WHO_DEFAULT_PROTO: http
      TZ: UTC
      _APP_NAME: myapp
      _BUILD_ID: 12345
      _DEPLOYMENT_ID: dev--foo.d53
      _DEPLOY_ENV: dev
      _OTEL_COLLECTOR_URL: tcp://localhost:4317
      _POD_ACCOUNT: 12345
      _POD_ID: a6127c9c-2be8-4078-8c00-132d29fbecfc
      _POD_REGION: us-west-1
      _POD_SHORTNAME: us-west-1-dev-default
      _TEAM_OWNER: eng-infra
      _TRACING_ENABLED: false
    

    https://go.dev/play/p/B3x_AbAvZHs

    func sortConfig(config string) (string, error) {
        type KL struct {
            key, lines string
        }
    
        var kls []KL
        scnr := bufio.NewScanner(strings.NewReader(config))
        inArmor := false // ASCII Armor (IETF RFC4880)
        for scnr.Scan() {
            line := scnr.Text() + "\n"
    
            if inArmor {
                kls[len(kls)-1].lines += line
                armor := strings.TrimSpace(line)
                inArmor = !strings.HasPrefix(armor, "-----END ") ||
                    !strings.HasSuffix(armor, "-----")
                continue
            }
    
            if len(strings.TrimSpace(line)) == 0 {
                continue
            }
    
            key, value, pair := strings.Cut(line, ":")
            if !pair {
                key, value = "", key
            }
    
            if pair {
                armor := strings.TrimSpace(value)
                inArmor = strings.HasPrefix(armor, "-----BEGIN ") &&
                    strings.HasSuffix(armor, "-----")
            }
    
            key = strings.TrimSpace(key)
            key = strings.ToUpper(key) // case insensitive
            kls = append(kls, KL{key: key, lines: line})
        }
        if err := scnr.Err(); err != nil {
            return "", err
        }
    
        slices.SortStableFunc(kls, func(a, b KL) int {
            return cmp.Compare(a.key, b.key)
        })
    
        var b strings.Builder
        b.Grow(len(config))
        for _, kl := range kls {
            b.WriteString(kl.lines)
        }
        return b.String(), nil
    }