0

I've got a bash script which outputs strings in the format Hostname IP MacAddr and is read by my script in written in C. I am trying to split these 3 up into an array, and make it so that I'm able to store them into a Json-c object to produce something that looks like {Clients: [{Hostname: Value, IP: Value, MacAddr: Value}]}.

Currently my program is able to read each string line by line and store it into an array (The array is initialised wrong just for testing purposes, I'm going to change that):

int get_list_of_connected_clients(json_object *input, json_object *output) {
    FILE *fp;
    char path[1035];
    int i = 0;
    char a[2][100];
    fp = popen("./Sample_Bash_Script_Test.sh", "r");
    if (fp == NULL) {
        printf("Failed To Run Script \n");
        exit(1);
    }
    while (fgets(path, sizeof(path) - 1, fp) != NULL) {
        stpcpy(a[i], path);
        i++;
    }
    pclose(fp);
}

Is anyone able to help me with this and steer me in the right direction? String manipulation in C is relatively new to me and I'm still trying to get my head round it!

Edit:

My function now looks like this:

int get_list_of_connected_clients(json_object* input, json_object* output){
    FILE *filepath;
    char output_line[1035];
    int index=0;
    char arr_clients[30][100];

    filepath = popen("./Sample_Bash_Script_Test.sh", "r");
    if (filepath == NULL){
        printf("Failed To Run Script \n");
        exit(1);
    }
    while (fgets(output_line, sizeof(output_line)-1, filepath) != NULL){
        stpcpy(arr_clients[index], output_line);
        index++;
    }
    pclose(filepath);

    /*Creating a json object*/
    json_object * jobj = json_object_new_object();

    /*Creating a json array*/
    json_object *jarray = json_object_new_array();

    json_object *jstring1[2][2];

    for (int y=0; y < 2; y++) {
        int x = 0;
        char *p = strtok(arr_clients[y], " ");
        char *array[2][3];

        while (p != NULL) {
            array[y][x++] = p;
            p = strtok(NULL, " ");
        }

        for (x = 0; x < 3; ++x) {
            jstring1[y][x] = json_object_new_string(array[y][x]);
            /*Adding the above created json strings to the array*/
            json_object_array_add(jarray,jstring1[y][x]);

        }
    }
    /*Form the json object*/
    json_object_object_add(jobj,"Clients", jarray);

    /*Now printing the json object*/
    printf ("%s",json_object_to_json_string(jobj));

    return 0;
    }

The output looks like this when I run it: { "Clients": [ "Hostname", "192.168.1.18", "XX:XX:XX:XX", "Hostname", "192.168.1.13", "XX:XX:XX:XX" ] }

Does anyone have any ideas what I'm doing wrong to stop it from breaking the list after every client? i.e

{
  "Clients" : [
    {
      "Hostname" : "example.com",
      "IP" : "127.0.0.1",
      "MacAddr" : "mactonight"
    },
    {
      "Hostname" : "foo.biz",
      "IP" : "0.0.0.0",
      "MacAddr" : "12:34:56:78"
    }
  ]
}
Tom Bradely
  • 81
  • 1
  • 1
  • 11
  • For JSON look into a library such as [json-glib](https://developer.gnome.org/json-glib/stable/). – Schwern Apr 12 '19 at 18:26
  • 1
    C is kinda overkill for this kind of translation. Could you instead use another language like Python to translate the bash output into JSON? – Schwern Apr 12 '19 at 18:27
  • Yeah you're right i think C is a bit rough for this, but this was going on an embedded device with not much storage. So rather Python, I could try Lua. I think that could be lightweight and easy enough. – Tom Bradely Apr 12 '19 at 18:31
  • With snprintf you should be able to do it. @Schwern I don't think it is an overkill. It is, by far, the simplest programming language that exists (IMO), and therefore it is legitimate to try to do anything he may want to do with it. – alx - recommends codidact Apr 12 '19 at 18:35
  • @CacahueteFrito Simple doesn't mean easy. – Schwern Apr 12 '19 at 18:36
  • @Schwern For me it does. That's very subjective. I can write 1000 lines of C in the time I could probably write just 50 lines of Python because there are many things to worry about in Python (or C++, or any high level language). I would not advise someone against C. – alx - recommends codidact Apr 12 '19 at 18:41
  • @CacahueteFrito So deciding to go with sprintf, I'm actually stuck with the whole parsing the array into that specific format. Any ideas? – Tom Bradely Apr 12 '19 at 18:54
  • Did you try? `snprintf(json_str, ARRAY_SIZE(json_str), "{Clients: [{Hostname: %s, IP: %s, MacAddr: %s}]}", hostname, ip, mac_addr);` – alx - recommends codidact Apr 12 '19 at 19:11

1 Answers1

2

Rather than trying to build JSON as strings, use a library such as json-glib to build the JSON for you. This is more flexible and will handle all sorts of edge cases. It provides JsonBuilder to build up JSON structures.

We start by taking a file pointer, something else should open the file. Then we start a JsonBuilder and begin building the JSON structure declaring the { "Clients" object and starting the array.

JsonNode *bash_connected_clients_to_json(FILE *fp) {
    JsonBuilder *builder = json_builder_new();

    // { "Clients": [ ...
    json_builder_begin_object(builder);
    json_builder_set_member_name(builder, "Clients");
    json_builder_begin_array(builder);

Now we read each line and send it and the builder into a function to process the line and add it to the open array.

    char line[1024];
    while (fgets(line, sizeof(line), fp) != NULL) {
        bash_connected_clients_line_to_json(line, builder);
    }

Finally close the array and the object and return the JsonNode we've just built.

    // ... ] }
    json_builder_end_array(builder);
    json_builder_end_object(builder);

    return json_builder_get_root(builder);
}

Then the JsonNode can be printed.

int main() {
    JsonNode *json = bash_connected_clients_to_json(stdin);
    printf("%s", json_to_string(json, TRUE));
}

Processing each line starts by parsing it. This can be done in various was. sscanf works fine.

void bash_connected_clients_line_to_json( const char *line, JsonBuilder *builder ) {    
    char hostname[1024], ip[1024], macaddr[1024];
    if( sscanf(line, "%1023s %1023s %1023s", hostname, ip, macaddr) != 3 ) {
        fprintf(stderr, "Could not parse line: '%s'\n", line);
        return;
    }

Then we add a JSON object to the array we already have open, add each of our elements to the object, and close the object.

    // { "Hostname": "foo", "IP", "bar", "MacAddr", "baz" }
    json_builder_begin_object(builder);

    json_builder_set_member_name(builder, "Hostname");
    json_builder_add_string_value(builder, hostname);

    json_builder_set_member_name(builder, "IP");
    json_builder_add_string_value(builder, ip);

    json_builder_set_member_name(builder, "MacAddr");
    json_builder_add_string_value(builder, macaddr);

    json_builder_end_object(builder);
}

$ cat > test.txt
example.com 127.0.0.1 mactonight
foo.biz 0.0.0.0 12:34:56:78

$ ./test < test.txt
{
  "Clients" : [
    {
      "Hostname" : "example.com",
      "IP" : "127.0.0.1",
      "MacAddr" : "mactonight"
    },
    {
      "Hostname" : "foo.biz",
      "IP" : "0.0.0.0",
      "MacAddr" : "12:34:56:78"
    }
  ]
}

Or you can do it in a few lines of Ruby.

require 'json'

clients = []

STDIN.each do |line|
  fields = line.split(/\s+/)
  clients << {
    Hostname: fields[0],
    IP: fields[1],
    MacAddr: fields[2]
  }
end

connections = {}
connections[:Clients] = clients
puts connections.to_json

For json-c it's basically the same. The major difference is instead of bash_connected_clients_line_to_json adding a JSON object to a builder, it returns a JSON object.

json_object* bash_connected_clients_line_to_json( const char *line ) {    
    char hostname[1024], ip[1024], macaddr[1024];
    if( sscanf(line, "%1023s %1023s %1023s", hostname, ip, macaddr) != 3 ) {
        fprintf(stderr, "Could not parse line: '%s'\n", line);
        return NULL;
    }

    json_object *json = json_object_new_object();

    json_object_object_add(json, "Hostname", json_object_new_string(hostname));
    json_object_object_add(json, "IP", json_object_new_string(ip));
    json_object_object_add(json, "MacAddr", json_object_new_string(macaddr));

    return json;
}

This is then added to a JSON array of clients.

json_object *bash_connected_clients_to_json(FILE *fp) {
    json_object *clients = json_object_new_array();

    char line[1024];
    while (fgets(line, sizeof(line), fp) != NULL) {
        json_object_array_add(
            clients,
            bash_connected_clients_line_to_json(line)
        );
    }

    json_object *json = json_object_new_object();
    json_object_object_add(json, "Clients", clients);

    return json;
}
Schwern
  • 153,029
  • 25
  • 195
  • 336
  • Ah, this is exactly what i was looking for! I've been using Json-C for other parts of the script. So i'm going to try and stick with that and translate the stuff you've done and but use that library instead. I don't think it should differ too much, unless – Tom Bradely Apr 13 '19 at 12:25
  • Hey, so I've attempted to do this with the Json-C lib but to no avail. Was wondering if i could get your help. Right now my code looks like in my OP (I've added the Edit). It currently outputs a little awkward and not what I hoped for. Any chance you might have any ideas where I'm going wrong? thanks! – Tom Bradely Apr 13 '19 at 20:36
  • @Syn I'll look at your C code for educational purposes. What I'd recommend is you use the Ruby script to translate your bash script's output into your desired JSON. Then have your C program pipe the bash output into the Ruby translation script `popen("./sample.sh | bash_format_to_json.rb")` and read the JSON. Doing text translation in C is fragile overkill. – Schwern Apr 13 '19 at 20:53
  • @Syn I've added a json-c version. The problem in your version is you're just pushing all the values onto a single JSON array. You need a new JSON object for each line and push that object onto an array. Note that I don't use a intermediate array to hold the lines; working line-by-line is more efficient, less code, and less bugs. – Schwern Apr 13 '19 at 21:14
  • 1
    Hey thanks for this, really helped me. I can definitely see what I was doing wrong now. I'm going to try get it working! But I think realistically I'm going to change to the Ruby version you kindly provided. It does seem overkill with C. Thanks again! – Tom Bradely Apr 13 '19 at 21:39