Related
I need help with an issue I am having sending the data of my Zigbee thermometers to influx, via telegraf.
this is the path:
Zigbee Sonoff SNZB-02 --> Tasmota ZBBridge --> MQTT --> Telegraf --> InfluxDB
The id of the zigbee thermometer: 0x4EF9 might change since it's randomly assigned to the device, in Tasmota I am able to assign a "friendly name", in this case: ZB_Sonoff_Temp01
With simple tasmota devices I have no issues, I have the single entry of the device in the MQTT Topic and Telegraf plays nicely with those.
My issues is with the data from the Zigbee Bridge, since it has a single topic and the output in Influx is a bit difficult to work with:
Example MQTT message for Zigbee thermometer:
tele/tasmota_ABDCEF/ZB_Sonoff_Temp01/SENSOR {"ZbReceived":{"0x4EF9":{"Device":"0x4EF9","Name":"ZB_Sonoff_Temp01","Humidity":94.84,"Endpoint":1,"LinkQuality":34}}}
the data is in Json format as you can see,
in Telegraf I am using mqtt_consumer, here is the config:
/etc/telegraf/telegraf.d/mqtt.conf
[[inputs.mqtt_consumer]]
servers = ["tcp://192.168.10.10:1883"]
## Topics that will be subscribed to.
topics = [
"tele/tasmota_ABCDEF/ZB_Sonoff_Temp01/SENSOR"
]
qos = 0
connection_timeout = "30s"
username = "user"
password = "password"
data_format = "json"
[[outputs.influxdb]]
urls = ["http://localhost:8086"]
database = "test_temp"
# skip_database_creation = true
and this is my /etc/telegraf/telegraf.conf:
[global_tags]
[agent]
logfile = "/var/log/telegraf/telegraf.log"
interval = "10s"
round_interval = true
metric_batch_size = 1000
metric_buffer_limit = 10000
collection_jitter = "0s"
flush_interval = "10s"
flush_jitter = "0s"
precision = ""
hostname = ""
omit_hostname = false
this is the data in influx:
time ZbReceived_0x4EF9_Endpoint ZbReceived_0x4EF9_Humidity ZbReceived_0x4EF9_LinkQuality ZbReceived_0x4EF9_Temperature host topic
---- -------------------------- -------------------------- ----------------------------- ----------------------------- ---- -----
2021-12-24T15:43:55.26962955Z 1 99.99 26 32.24 influxdb-test tele/tasmota_ABCDEF/ZB_Sonoff_Temp01/SENSOR
2021-12-24T15:43:55.560162845Z 1 21 25.18 influxdb-test tele/tasmota_ABDCEF/ZB_Sonoff_Temp01/SENSOR
which could be ok, since I am able to choose the device via the "topic" field, but the problems is that the field "ZbReceived_0x4EF9_Temperature" is not "sane" in case the devices change ID when re-associating with zigbee, which might happen..
the workaround I found is to add a rename for the fields:
[[processors.rename]]
[[processors.rename.replace]]
field = "ZbReceived_0x4EF9_Temperature"
dest = "Temperature"
[[processors.rename.replace]]
field = "ZbReceived_0x4EF9_Humidity"
dest = "Humidity"
[[processors.rename.replace]]
field = "ZbReceived_0x4EF9_Endpoint"
dest = "Endpoint"
[[processors.rename.replace]]
field = "ZbReceived_0x4EF9_LinkQuality"
dest = "LinkQuality"
which changes the fields as I want (there is no Humidity but it's not always pushed, so it's ok, I am dropping the db between changes):
time Endpoint LinkQuality Temperature host topic
---- -------- ----------- ----------- ---- -----
2021-12-24T15:47:09.992947108Z 1 21 23 influxdb-test tele/tasmota_ABCDEF/ZB_Sonoff_Temp01/SENSOR
2021-12-24T15:47:25.868416967Z 1 13 27.06 influxdb-test tele/tasmota_ABCDEF/ZB_Sonoff_Temp01/SENSOR
I don't like this solution very much since it has the device ids hardcoded in telegraf config, so when I add or change a sensor I need to edit telegraf.
the problem I now have is that I would like to find a wildcard or a method to change the fields independently from the device id, like:
[[processors.rename]]
[[processors.rename.replace]]
field = "*_Temperature"
dest = "Temperature"
but I am not able to find it, I've read all the docs of the methods (also strings) but I could not find a way to achieve that..
do you have any tip that could help me?
thank you very much and happy holidays!
I needed to use processors.regex.field_rename, but I had an older version of telegraf than the latest ((1.21.1-1) over (1.20.2-1)), and so field_rename was not available.
The regex I've used:
[[processors.regex]]
[[processors.regex.field_rename]]
pattern = '(^ZbReceived_)\w+_'
replacement = "${2}"
the result:
time Endpoint Humidity LinkQuality Temperature host topic
---- -------- -------- ----------- ----------- ---- -----
2021-12-24T23:51:48.289677669Z 1 0 25.26 influxdb-test tele/tasmota_ABCDEF/ZB_Sonoff_Temp01/SENSOR
Using wire shark, how can capture or filter communication between two devices on a larger network. Eg, say we have this system:
PC1 ----|
|
| ______
PC2 ----|---| |--- Special Device
| |Router|
| |______|
PC3 ----|
If i want the packets of communication between PC1 (ip 139.136.59.13) and the Special device (ip 139.136.59.14). What is the filter command?
While you can use filters such as
(ip.src == 139.136.59.13 && ip.dst == 139.136.59.14) ||
(ip.dst == 139.136.59.13 && ip.src == 139.136.59.14)
This is complex. Better is to use ip.addr which will match on either src or dst:
(ip.addr == 139.136.59.13 && ip.addr == 139.136.59.14)
I'm trying to create aggregation (map | reduce) with UDF but something is wrong on the very begining. In Aerospike I have a set with bin 'u' (secondary index) and bin 'v' which is a list of objects (auctions with transactions lists and other auction data) and I have a stream UDF to aggregate internal structure of 'v':
function trans_sum_by_years(s)
local function transform(rec)
local l = map()
local x = map()
local trans, auctions = 0, 0
for i in list.iterator(rec['v'] or list()) do
auctions = auctions + 1
for t in list.iterator(i['t'] or list()) do
trans = trans + 1
date = os.date("*t", t['ts'])
if l[date['year']] ~= nil then
l[date['year']] = l[date['year']] + t['price'] * t['qty']
else
l[date['year']] = t['price'] * t['qty']
end
end
end
x.auctions = auctions
x.trans = trans
x.v = l
return x
end
return s : map(transform)
end
The problem is that output is very diffrent depending on setting output on table or json. In first case it seems everything is OK:
{"trans":594, "auctions":15, "v":{2010:1131030}}
{"trans":468, "auctions":68, "v":{2011:1472976, 2012:5188}}
......
On second I get empty object from internal record aggregation.
{
"trans_sum_b...": {
"trans": 389,
"auctions": 89,
"v": {}
}
},
{
"trans_sum_b...": {
"trans": 542,
"auctions": 30,
"v": {}
}
}
.....
I prefer json output and wasted couple hours to find out why I get empty 'v' field without success. So my question is "what the hell is going on" ;-) If my code is correct, what is wrong with the json output, that I don't see the results. If my code is wrong, why it's wrong and why table output results with what I need.
#user1875438 Your code is correct. It seems that there is bug in aql.
My result is the same as yours, the field of v is empty when using json mode.
I used tcpdump to grab the responses of aerospike-server when running these two commands, and found out the responses are the same, so I think it's very possible there is bug in aql tool.
159 0x0050: 0001 0000 0027 0113 0007 5355 4343 4553 .....'....SUCCES
160 0x0060: 5383 a603 7472 616e 7301 a903 6175 6374 S...trans...auct
161 0x0070: 696f 6e73 01a2 0376 81cd 07ce 01 ions...v.....
162 01:57:38.255065 IP localhost.hbci > localhost.57731: Flags [P.], seq 98:128, ack 144, win 42853, options [nop,nop,TS val 976630236 ecr 976630223], length 30
163 0x0000: 4500 0052 55f8 4000 4006 0000 7f00 0001 E..RU.#.#.......
I just posted an issue here.
The answer is simple as hell. But I'm new in Aerospike/Lua and I don't trust my knowledge so I searched for error everywhere but within AQL/UDF area. The problem is more fundamental and interferes with the specification of the JSON itself.
Keys in JSON have to be strings! So tostring(date['year']) solves problem.
Other question is does it is a bug or a feature :-) If Aerospike's map type allow integer keys should there be automatic key conversion from integer to string to satisfy JSON specification or not? IMHO there should be but probably some people disagree claiming that map type is not for integer keys...
I am trying to create a scanner for a compiler which reads a simple language. I created a test file called program, which contains:
z := 2;
if z < 3 then
z := 1
end
To run the program, I use terminal, and run the command line:
python3 scanner.py program tokens
I want the output to be put into the text file tokens, but nothing appears when I do this. During run time, the program runs but does not do anything. I tried to put <> around program but I got a ValueError: need more than 1 value to unpack.
My codes is as follows:
import re
import sys
class Scanner:
'''The interface comprises the methods lookahead and consume.
Other methods should not be called from outside of this class.'''
def __init__(self, input_file):
'''Reads the whole input_file to input_string, which remains constant.
current_char_index counts how many characters of input_string have
been consumed.
current_token holds the most recently found token and the
corresponding part of input_string.'''
# source code of the program to be compiled
self.input_string = input_file.read()
# index where the unprocessed part of input_string starts
self.current_char_index = 0
# a pair (most recently read token, matched substring of input_string)
self.current_token = self.get_token()
def skip_white_space(self):
'''Consumes all characters in input_string up to the next
non-white-space character.'''
if (self.current_char_index >= len(self.input_string) - 1):
return
while self.input_string[self.current_char_index].isspace():
self.current_char_index += 1
def get_token(self):
'''Returns the next token and the part of input_string it matched.
The returned token is None if there is no next token.
The characters up to the end of the token are consumed.'''
self.skip_white_space()
# find the longest prefix of input_string that matches a token
token, longest = None, ''
for (t, r) in Token.token_regexp:
match = re.match(r, self.input_string[self.current_char_index:])
if match and match.end() > len(longest):
token, longest = t, match.group()
# consume the token by moving the index to the end of the matched part
self.current_char_index += len(longest)
return (token, longest)
def lookahead(self):
'''Returns the next token without consuming it.
Returns None if there is no next token.'''
return self.current_token[0]
def consume(self, *tokens):
'''Returns the next token and consumes it, if it is in tokens.
Raises an exception otherwise.
If the token is a number or an identifier, its value is returned
instead of the token.'''
current = self.current_token
if (len(self.input_string[self.current_char_index:]) == 0):
self.current_token = (None, '') # catches the end-of-file errors so lookahead returns none.
else:
self.current_token = self.get_token() # otherwise we consume the token
if current[0] in tokens: # tokens could be a single token, or it could be group of tokens.
if current[0] is Token.ID or current[0] is Token.NUM: # if token is ID or NUM
return current[1] # return the value of the ID or NUM
else: # otherwise
return current[0] # return the token
else: # if current_token is not in tokens
raise Exception('non-token detected') # raise non-token error
class Token:
# The following enumerates all tokens.
DO = 'DO'
ELSE = 'ELSE'
READ = 'READ'
WRITE = 'WRITE'
END = 'END'
IF = 'IF'
THEN = 'THEN'
WHILE = 'WHILE'
SEM = 'SEM'
BEC = 'BEC'
LESS = 'LESS'
EQ = 'EQ'
GRTR = 'GRTR'
LEQ = 'LEQ'
NEQ = 'NEQ'
GEQ = 'GEQ'
ADD = 'ADD'
SUB = 'SUB'
MUL = 'MUL'
DIV = 'DIV'
LPAR = 'LPAR'
RPAR = 'RPAR'
NUM = 'NUM'
ID = 'ID'
# The following list gives the regular expression to match a token.
# The order in the list matters for mimicking Flex behaviour.
# Longer matches are preferred over shorter ones.
# For same-length matches, the first in the list is preferred.
token_regexp = [
(DO, 'do'),
(ELSE, 'else'),
(READ, 'read'),
(WRITE, 'write'),
(END, 'end'),
(IF, 'if'),
(THEN, 'then'),
(WHILE, 'while'),
(SEM, ';'),
(BEC, ':='),
(LESS, '<'),
(EQ, '='),
(NEQ, '!='),
(GRTR, '>'),
(LEQ, '<='),
(GEQ, '>='),
(ADD, '[+]'), # + is special in regular expressions
(SUB, '-'),
(MUL, '[*]'),
(DIV, '[/]'),
(LPAR, '[(]'), # ( is special in regular expressions
(RPAR, '[)]'), # ) is special in regular expressions
(ID, '[a-z]+'),
(NUM, '[0-9]+'),
]
def indent(s, level):
return ' '*level + s + '\n'
# Initialise scanner.
scanner = Scanner(sys.stdin)
# Show all tokens in the input.
token = scanner.lookahead()
test = ''
while token != None:
if token in [Token.NUM, Token.ID]:
token, value = scanner.consume(token)
print(token, value)
else:
print(scanner.consume(token))
token = scanner.lookahead()
Sorry if this is poorly explained. Any help on what is going wrong would be wonderful. Thanks.
Solution 1a
I figured out why it was not printing to the file tokens. I needed to change my test code to this
while token != None:
print(scanner.consume(token))
token = scanner.lookahead()
the only problem now is I cannot read when it is an ID or a NUM, it only prints out the identifies or the number without stating which it is. Right now, it prints out this:
z
BEC
2
SEM
IF
z
LESS
3
THEN
z
BEC
1
END
And I need it to print out this
NUM z
BEC
ID 2
SEM
IF
ID z
LESS
NUM 3
THEN
ID z
BEC
NUM 1
END
I am thinking of adding an if statement which states that if it's a NUM, then print NUM followed by the token, and likewise for if it's an ID.
Solution 1b
I simply added an if and elif statement to consume to print NUM and ID. For example, If current[0] is Token.ID then return "ID " + current[1].
I havent altered anything but whitespace and consume and im having difficulties getting it to run...
def skip_white_space(self):
'''Consumes all characters in input_string up to the next
non-white-space character.'''
while self.input_string[self.current_char_index] == '\s':
self.current_char_index += 1
def consume(self, *tokens):
'''Returns the next token and consumes it, if it is in tokens.
Raises an exception otherwise.
If the token is a number or an identifier, not just the token
but a pair of the token and its value is returned.'''
current = self.current_token
if current[0] in tokens:
if current[0] in Token.ID:
return 'ID' + current[1]
elif current[0] in Token.NUM:
return 'NUM' + current[1]
else:
return current[0]
else:
raise Exception('Error in compiling non-token(not apart of token list)')
... Im particularly having trouble trying to get the python3 scanner.py < program > tokens to work, any guidance would help me alot, thanx
I'd like to have a function that I can pass a whitespace trimmed string to and it will return 0 for error (not a string) 1 for ipv4 2 for ipv6 3 for a string thats not an ip.
Ipv6 has these rules:
Ipv6 is represented by 8 groups of 16-bit hexadecimal values separated by colons (:)
The hexadecimal digits are case-insensitive
Abbreviation rules:
1: Omit leading zeroes in a 16-bit value
2: Replace one or more groups of consecutive zeroes by a double colon
wiki example showing 3 ways that are all the same ipv6:
fe80:0000:0000:0000:0202:b3ff:fe1e:8329
fe80:0:0:0:202:b3ff:fe1e:8329
fe80::202:b3ff:fe1e:8329
I'm reasonably sure for ipv4 you just check for three . then check the string is all numbers and the .'s are counted as numbers and the last check for just a string would be at the end of an if statement so if its not ipv4/6 and its a string then it returns 3
Mike's solution is good, but it can be improved on in several ways. In its current form it doesn't get to ipv6 address check, but it's easy to fix. The ipv6 check fails on things like "1050!0!0+0-5#600$300c#326b" and "1050:0:0:0:5:600:300c:326babcdef" (recognizing both as valid addresses) and "1050:::600:5:1000::" (recognizing it as string).
Here is the improved version (IPv4 are assumed to be decimal numbers and IPv6 are assumed to be hexadecimal numbers):
function GetIPType(ip)
local R = {ERROR = 0, IPV4 = 1, IPV6 = 2, STRING = 3}
if type(ip) ~= "string" then return R.ERROR end
-- check for format 1.11.111.111 for ipv4
local chunks = {ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$")}
if #chunks == 4 then
for _,v in pairs(chunks) do
if tonumber(v) > 255 then return R.STRING end
end
return R.IPV4
end
-- check for ipv6 format, should be 8 'chunks' of numbers/letters
-- without leading/trailing chars
-- or fewer than 8 chunks, but with only one `::` group
local chunks = {ip:match("^"..(("([a-fA-F0-9]*):"):rep(8):gsub(":$","$")))}
if #chunks == 8
or #chunks < 8 and ip:match('::') and not ip:gsub("::","",1):match('::') then
for _,v in pairs(chunks) do
if #v > 0 and tonumber(v, 16) > 65535 then return R.STRING end
end
return R.IPV6
end
return R.STRING
end
The script to check:
local IPType = {[0] = "Error", "IPv4", "IPv6", "string"}
local ips = {
"128.1.0.1", -- ipv4
"223.255.254.254", -- ipv4
"999.12345.0.0001", -- invalid ipv4
"1050:0:0:0:5:600:300c:326b", -- ipv6
"1050!0!0+0-5#600$300c#326b", -- string
"1050:0:0:0:5:600:300c:326babcdef", -- string
"1050:0000:0000:0000:0005:0600:300c:326b", -- ipv6
"fe80:0000:0000:0000:0202:b3ff:fe1e:8329", -- ipv6
"fe80:0:0:0:202:b3ff:fe1e:8329", -- ipv6
"fe80::202:b3ff:fe1e:8329", -- ipv6
"1050:::600:5:1000::", -- contracted ipv6
"::", -- ipv6
"::1", -- ipv6
"::1::", -- string
"129.garbage.9.1", -- string
"xxx127.0.0.0", -- error
"xxx1050:0000:0000:0000:0005:0600:300c:326b", -- string
129.10 -- error
}
for k,v in pairs(ips) do
print(v, IPType[GetIPType(v)])
end
And the output:
128.1.0.1 IPv4
223.255.254.254 IPv4
999.12345.0.0001 string
1050:0:0:0:5:600:300c:326b IPv6
1050!0!0+0-5#600$300c#326b string
1050:0:0:0:5:600:300c:326babcdef string
1050:0000:0000:0000:0005:0600:300c:326b IPv6
fe80:0000:0000:0000:0202:b3ff:fe1e:8329 IPv6
fe80:0:0:0:202:b3ff:fe1e:8329 IPv6
fe80::202:b3ff:fe1e:8329 IPv6
1050:::600:5:1000:: IPv6
:: IPv6
::1 IPv6
::1:: string
129.garbage.9.1 string
xxx127.0.0.0 string
xxx1050:0000:0000:0000:0005:0600:300c:326b string
129.1 Error
Updated on 9/6/2018 to add handling of garbage before/after addresses and checking for contracted ipv6, which allows for fewer than 8 groups with one empty group of two consecutive colons.
this seems like a pretty basic problem to solve. i think this function does what you need...
function GetIPType(ip)
-- must pass in a string value
if ip == nil or type(ip) ~= "string" then
return 0
end
-- check for format 1.11.111.111 for ipv4
local chunks = {ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")}
if (#chunks == 4) then
for _,v in pairs(chunks) do
if (tonumber(v) < 0 or tonumber(v) > 255) then
return 0
end
end
return 1
else
return 0
end
-- check for ipv6 format, should be 8 'chunks' of numbers/letters
local _, chunks = ip:gsub("[%a%d]+%:?", "")
if chunks == 8 then
return 2
end
-- if we get here, assume we've been given a random string
return 3
end
tested it with this code:
local IPType = {
[0] = "Error",
[1] = "IPv4",
[2] = "IPv6",
[3] = "string",
}
local ips = {
"128.1.0.1", -- ipv4
"223.255.254.254", -- ipv4
"999.12345.0.0001", -- invalid ipv4
"1050:0:0:0:5:600:300c:326b", -- ipv6
"1050:0000:0000:0000:0005:0600:300c:326b", -- ipv6
"1050:::600:5:1000::", -- contracted ipv6
"129.garbage.9.1", -- string
129.10 -- error
}
for k,v in pairs(ips) do
print(v, IPType[GetIPType(v)])
end
which generated this output:
128.1.0.1 IPv4
223.255.254.254 IPv4
1050:0:0:0:5:600:300c:326b IPv6
1050:0000:0000:0000:0005:0600:300c:326b IPv6
129.garbage.9.1 string
129.1 Error
in the future, you'll get more helpful feedback if you actually post the code you've attempted to write to solve your particular problem, and let us know where you need help. SO isn't a personal code writing service, as stated in the faq. however, i'll give you the benefit of the doubt since you look new and this is something that could potentially benefit other people. the code above is basic, so feel free to update it if it doesn't catch fringe test cases i don't know about.
This seems as something that could be easily done by using regular expressions. There is plenty of regex libraries for lua.
If, however, you are not willing or are unable to use them, I would do something like this:
Start in ipv4 state
Take a character until string ends
switch(state)
ipv4:
if it's a dot, check if we loaded at least one number
if it's a number, check if it isn't the 4th in row
if it's anything else, set state to ipv6 and proceed in this state
ipv6:
if it's a ':', check if we didnt exceed maximum number of segments
if it's a number or letter<a;f> check if it isn't 5th in row
in case anything breaks, return 3
end
I'm not posting complete lua code, because it looks like homework/learning excercise and full answer would harm you more than it would help you.
Interestingly, none of the above answers takes the test examples of the original question into account, because using them, all of the above checks would fail (because of #3):
fe80:0000:0000:0000:0202:b3ff:fe1e:8329
fe80:0:0:0:202:b3ff:fe1e:8329
fe80::202:b3ff:fe1e:8329 (!)
IPv6 representation rules say:
One or more consecutive groups of zero value may be replaced with a single empty group using two consecutive colons (::),1 but the substitution may only be applied once in the address, because multiple occurrences would create an ambiguous representation.
https://en.wikipedia.org/wiki/IPv6_address#Representation
As Lua patterns do not have support for Alternation, it is not possible to check IPv6 with a single pattern. You may see David M. Syzdek answer on the complexity of IPv6 Regex: https://stackoverflow.com/a/17871737/1895269
Still, a more standards conforming approach is the following improvement of Paul Kulchenko's answer:
function GetIPType(ip)
local R = {ERROR = 0, IPV4 = 1, IPV6 = 2, STRING = 3}
if type(ip) ~= "string" then return R.ERROR end
-- check for format 1.11.111.111 for ipv4
local chunks = { ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") }
if (#chunks == 4) then
for _,v in pairs(chunks) do
if tonumber(v) > 255 then return R.STRING end
end
return R.IPV4
end
-- check for ipv6 format, should be max 8 'chunks' of numbers/letters
local addr = ip:match("^([a-fA-F0-9:]+)$")
if addr ~= nil and #addr > 1 then
-- address part
local nc, dc = 0, false -- chunk count, double colon
for chunk, colons in addr:gmatch("([^:]*)(:*)") do
if nc > (dc and 7 or 8) then return R.STRING end -- max allowed chunks
if #chunk > 0 and tonumber(chunk, 16) > 65535 then
return R.STRING
end
if #colons > 0 then
-- max consecutive colons allowed: 2
if #colons > 2 then return R.STRING end
-- double colon shall appear only once
if #colons == 2 and dc == true then return R.STRING end
if #colons == 2 and dc == false then dc = true end
end
nc = nc + 1
end
return R.IPV6
end
return R.STRING
end
The script to check:
local IPType = {[0] = "Error", "IPv4", "IPv6", "string"}
local ips = {
"128.1.0.1", -- ipv4
"223.255.254.254", -- ipv4
"999.12345.0.0001", -- invalid ipv4
"1050:0:0:0:5:600:300c:326b", -- ipv6
"1050!0!0+0-5#600$300c#326b", -- string
"1050:0:0:0:5:600:300c:326babcdef", -- string
"1050:0000:0000:0000:0005:0600:300c:326b", -- ipv6
"1050:::600:5:1000::", -- contracted ipv6 (invalid)
"fe80::202:b3ff:fe1e:8329", -- shortened ipv6
"fe80::202:b3ff::fe1e:8329", -- shortened ipv6 (invalid)
"fe80:0000:0000:0000:0202:b3ff:fe1e:8329:abcd", -- too many groups
"::1", -- valid IPv6
"::", -- valid IPv6
":", -- string
"129.garbage.9.1", -- string
129.10 -- error
}
for k,v in pairs(ips) do
print(v, IPType[GetIPType(v)])
end
And the output:
128.1.0.1 IPv4
223.255.254.254 IPv4
999.12345.0.0001 string
1050:0:0:0:5:600:300c:326b IPv6
1050!0!0+0-5#600$300c#326b string
1050:0:0:0:5:600:300c:326babcdef string
1050:0000:0000:0000:0005:0600:300c:326b IPv6
1050:::600:5:1000:: string
fe80::202:b3ff:fe1e:8329 IPv6
fe80::202:b3ff::fe1e:8329 string
fe80:0000:0000:0000:0202:b3ff:fe1e:8329:abcd string
::1 IPv6
:: IPv6
: string
129.garbage.9.1 string
129.1 Error
As Lua's regular expressions are not sufficiently expressive, you must proceed with an iterative algorithm.
I suggest you to check the one that I posted on Italian Wikipedia (which have been fully tested):
local R = {ERROR = 0, IPV4 = 1, IPV6 = 2, STRING = 3}
function is_ipv4(str)
local s = str:gsub("/[0-9]$", ""):gsub("/[12][0-9]$", ""):gsub("/[3][0-2]$", "")
if not s:find("^%d+%.%d+%.%d+%.%d+$") then
return nil
end
for substr in s:gmatch("(%d+)") do
if not substr:find("^[1-9]?[0-9]$")
and not substr:find("^1[0-9][0-9]$")
and not substr:find( "^2[0-4][0-9]$")
and not substr:find("^25[0-5]$") then
return nil
end
end
return R.IPV4
end
function is_ipv6(str)
local s = str
if not (s:find("^%w+:%w+:%w+:%w+:%w+:%w+:%w+:%w+$") -- there are exactly seven ":"
or (s:find("^%w*:%w*:%w*:?%w*:?%w*:?%w*:?%w*$") -- otherwise there are two to six sei ":"
and s:find("::"))) -- and there must be the substring "::"
or s:find("::.*::") -- but there cannot be neither two substrings "::"
or s:find(":::") then -- nor a substring ":::"
return nil
end
for substr in s:gmatch("(%w+)") do
if not substr:find("^[0-9A-Fa-f][0-9A-Fa-f]?[0-9A-Fa-f]?[0-9A-Fa-f]?$") then
return nil
end
end
return R.IPV6
end
function ip_type(str)
if type(str) ~= "string" then
return R.ERROR
else
return is_ipv4(str) or is_ipv6(str) or R.STRING
end
end
Edit: I altered the ip_type() function's output as requested by the OP.