1
1
mirror of https://github.com/wader/fq.git synced 2024-11-30 18:08:16 +03:00
fq/pkg/interp/interp.jq

517 lines
14 KiB
Plaintext
Raw Normal View History

include "internal";
include "funcs";
include "options";
include "args";
include "repl";
2021-09-21 17:42:35 +03:00
# generated decode functions per format and format helpers
include "formats";
2020-06-08 03:29:51 +03:00
# optional user init
include "@config/init?";
# try to be same exit codes as jq
# TODO: jq seems to halt processing inputs on JSON decode error but not IO errors,
# seems strange.
# jq '(' <(echo 1) <(echo 2) ; echo $? => 3 and no inputs processed
# jq '.' missing <(echo 2) ; echo $? => 2 and continues process inputs
# jq '.' <(echo 'a') <(echo 123) ; echo $? => 4 and stops process inputs
# jq '.' missing <(echo 'a') <(echo 123) ; echo $? => 2 ???
# jq '"a"+.' <(echo '"a"') <(echo 1) ; echo $? => 5
# jq '"a"+.' <(echo 1) <(echo '"a"') ; echo $? => 0
2021-08-09 13:47:20 +03:00
def _exit_code_args_error: 2;
2020-06-08 03:29:51 +03:00
def _exit_code_input_io_error: 2;
def _exit_code_compile_error: 3;
def _exit_code_input_decode_error: 4;
def _exit_code_expr_error: 5;
2021-09-20 18:05:24 +03:00
# null input means done, otherwise {approx_read_bytes: 123, total_size: 123}
# TODO: decode provide even more detailed progress, post-process sort etc?
def _decode_progress:
# _input_filenames is remaning files to read
( (_input_filenames | length) as $inputs_len
| ( options.filenames | length) as $filenames_len
2021-09-21 18:34:02 +03:00
| _ansi.clear_line
, "\r"
2021-09-20 18:05:24 +03:00
, if . != null then
( if $filenames_len > 1 then
"\($filenames_len - $inputs_len)/\($filenames_len) \(_input_filename) "
else empty
end
, "\((.approx_read_bytes / .total_size * 100 | _numbertostring(1)))%"
)
else empty
end
| stderr
);
def decode($name; $opts):
( options as $opts
| (null | stdout) as $stdout
| _decode(
$name;
$opts + {
_progress: (
if $opts.decode_progress and $opts.repl and $stdout.is_terminal then
"_decode_progress"
else null
end
)
}
)
);
def decode($name): decode($name; {});
def decode: decode(options.decode_format; {});
2020-06-08 03:29:51 +03:00
# next valid input
def input:
def _input($opts; f):
( _input_filenames
| if length == 0 then error("break") end
| [.[0], .[1:]] as [$h, $t]
| _input_filenames($t)
| _input_filename(null) as $_
| $h
| try
( open
| _input_filename($h) as $_
| .
)
catch
( . as $err
| _input_io_errors(. += {($h): $err}) as $_
| $err
| (_error_str | _errorln)
, _input($opts; f)
)
| try f
catch
( . as $err
| _input_decode_errors(. += {($h): $err}) as $_
| "\($h): failed to decode (\($opts.decode_format)), try -d FORMAT to force"
| (_error_str | _errorln)
, _input($opts; f)
)
);
# TODO: don't rebuild options each time
( options as $opts
2021-09-03 04:30:52 +03:00
# TODO: refactor into def
# this is a bit strange as jq for --raw-string can return string instead
# with data from multiple inputs
| if $opts.string_input then
( _input_strings_lines
| if . then
2021-09-03 04:30:52 +03:00
# we're already iterating lines
if length == 0 then error("break")
else
( [.[0], .[1:]] as [$h, $t]
| _input_strings_lines($t)
| $h
)
end
else
( [_repeat_break(_input($opts; tobytes | tostring))]
2021-09-13 13:12:34 +03:00
| . as $chunks
| if $opts.slurp then
# jq --raw-input combined with --slurp reads all inputs into a string
# make next input break
( _input_strings_lines([]) as $_
2021-09-13 13:12:34 +03:00
| $chunks
| join("")
)
else
# TODO: different line endings?
# jq strips last newline, "a\nb" and "a\nb\n" behaves the same
# also jq -R . <(echo -ne 'a\nb') <(echo c) produces "a" and "bc"
2021-09-13 13:12:34 +03:00
if ($chunks | length) > 0 then
( _input_strings_lines(
( $chunks
| join("")
| rtrimstr("\n")
| split("\n")
)
) as $_
| input
)
else error("break")
end
end
)
end
2020-06-08 03:29:51 +03:00
)
else _input($opts; decode($opts.decode_format))
end
2020-06-08 03:29:51 +03:00
);
# iterate all valid inputs
def inputs: _repeat_break(input);
2020-06-08 03:29:51 +03:00
def input_filename: _input_filename;
2021-08-15 18:11:34 +03:00
def var: _variables;
def var($k; f):
2021-08-15 18:11:34 +03:00
( . as $c
| if ($k | _is_ident | not) then error("invalid variable name: \($k)") end
| _variables(.[$k] |= f)
2021-08-15 18:11:34 +03:00
| empty
);
def var($k): . as $c | var($k; $c);
2021-08-15 18:11:34 +03:00
def _cli_expr_on_error:
( . as $err
| _cli_last_expr_error($err) as $_
| (_error_str | _errorln)
);
def _cli_expr_on_compile_error:
( _eval_compile_error_tostring
| halt_error(_exit_code_compile_error)
);
# _cli_expr_eval halts on compile errors
def _cli_expr_eval($expr; $filename; f):
_eval($expr; $filename; f; _cli_expr_on_error; _cli_expr_on_compile_error);
def _cli_expr_eval($expr; $filename):
_eval($expr; $filename; .; _cli_expr_on_error; _cli_expr_on_compile_error);
2020-06-08 03:29:51 +03:00
def _main:
def _formats_list:
[ ( formats
2020-06-08 03:29:51 +03:00
| to_entries[]
| [(.key+" "), .value.description]
2020-06-08 03:29:51 +03:00
)
]
| table(
.;
map(
( . as $rc
| .string
| if $rc.column != 1 then rpad(" "; $rc.maxwidth) end
)
) | join("")
);
def _opts:
2020-06-08 03:29:51 +03:00
{
"arg": {
long: "--arg",
description: "Set variable $NAME to string VALUE",
pairs: "NAME VALUE"
},
"argjson": {
long: "--argjson",
description: "Set variable $NAME to JSON",
pairs: "NAME JSON"
},
2021-08-11 16:55:43 +03:00
"compact": {
short: "-c",
2021-09-05 14:24:11 +03:00
long: "--compact-output",
2021-08-11 16:55:43 +03:00
description: "Compact output",
2020-06-08 03:29:51 +03:00
bool: true
},
"color_output": {
short: "-C",
long: "--color-output",
description: "Force color output",
bool: true
},
2020-06-08 03:29:51 +03:00
"decode_format": {
short: "-d",
long: "--decode",
description: "Decode format (probe)",
2020-06-08 03:29:51 +03:00
string: "NAME"
},
"decode_file": {
long: "--decode-file",
description: "Set variable $NAME to decode of file",
pairs: "NAME PATH"
},
"expr_file": {
2020-06-08 03:29:51 +03:00
short: "-f",
2021-09-05 14:24:11 +03:00
long: "--from-file",
2021-08-11 16:55:43 +03:00
description: "Read EXPR from file",
2020-06-08 03:29:51 +03:00
string: "PATH"
},
"show_formats": {
2021-08-11 16:55:43 +03:00
long: "--formats",
description: "Show supported formats",
2020-06-08 03:29:51 +03:00
bool: true
},
"show_help": {
2021-08-11 16:55:43 +03:00
short: "-h",
long: "--help",
description: "Show help",
2020-06-08 03:29:51 +03:00
bool: true
},
"join_output": {
short: "-j",
long: "--join-output",
description: "No newline between outputs",
bool: true
},
2021-08-14 20:50:17 +03:00
"include_path": {
short: "-L",
long: "--include-path",
description: "Include search path",
2021-09-05 14:38:13 +03:00
array: "PATH"
2021-08-14 20:50:17 +03:00
},
2020-06-08 03:29:51 +03:00
"null_output": {
short: "-0",
long: "--null-output",
2021-09-05 13:46:21 +03:00
# for jq compatibility
aliases: ["--nul-output"],
2020-06-08 03:29:51 +03:00
description: "Null byte between outputs",
bool: true
},
2021-08-11 16:55:43 +03:00
"null_input": {
short: "-n",
long: "--null-input",
description: "Null input (use input/0 and inputs/0 to read input)",
2021-08-11 16:55:43 +03:00
bool: true
},
"monochrome_output": {
short: "-M",
long: "--monochrome-output",
description: "Force monochrome output",
bool: true
},
"option": {
2020-06-08 03:29:51 +03:00
short: "-o",
long: "--option",
description: "Set option, eg: color=true",
object: "KEY=VALUE",
},
"show_options": {
long: "--options",
description: "Show all options",
bool: true
2020-06-08 03:29:51 +03:00
},
2021-09-01 16:01:13 +03:00
"string_input": {
short: "-R",
long: "--raw-input",
description: "Read raw input strings (don't decode)",
bool: true
},
"rawfile": {
long: "--rawfile",
description: "Set variable $NAME to string content of file",
pairs: "NAME PATH"
},
"raw_string": {
2021-08-11 16:55:43 +03:00
short: "-r",
2021-09-01 16:01:13 +03:00
# for jq compat, is called raw string internally, "raw output" is if
# we can output raw bytes or not
2021-08-11 16:55:43 +03:00
long: "--raw-output",
description: "Raw string output (without quotes)",
bool: true
},
"repl": {
short: "-i",
long: "--repl",
description: "Interactive REPL",
bool: true
},
"slurp": {
short: "-s",
long: "--slurp",
description: "Read (slurp) all inputs into an array",
bool: true
},
"show_version": {
2021-08-11 16:55:43 +03:00
short: "-v",
long: "--version",
description: "Show version",
2021-08-11 16:55:43 +03:00
bool: true
},
2020-06-08 03:29:51 +03:00
};
def _banner:
2021-08-21 19:52:13 +03:00
( "fq - jq for files"
, "Tool, language and format decoders for exploring binary data."
2021-08-14 01:11:57 +03:00
, "For more information see https://github.com/wader/fq"
);
def _usage($arg0):
2021-08-13 20:35:15 +03:00
"Usage: \($arg0) [OPTIONS] [--] [EXPR] [FILE...]";
2020-06-08 03:29:51 +03:00
( . as {$version, $args, args: [$arg0]}
2021-09-01 16:01:13 +03:00
| (null | [stdin, stdout]) as [$stdin, $stdout]
2020-06-08 03:29:51 +03:00
# make sure we don't unintentionally use . to make things clearer
| null
| ( try _args_parse($args[1:]; _opts)
2021-08-09 13:47:20 +03:00
catch halt_error(_exit_code_args_error)
) as {parsed: $parsed_args, $rest}
| _build_default_fixed_options as $default_fixed_opts
# combine --args and -o key=value args
| ( $default_fixed_opts
+ $parsed_args
+ ($parsed_args.option | _to_options)
) as $args_opts
2020-06-08 03:29:51 +03:00
| _options_stack(
[ $args_opts
+ ( {
argjson: (
( $args_opts.argjson
| if . then
map(
( . as $a
| .[1] |=
try fromjson
catch
( "--argjson \($a[0]): \(.)"
| halt_error(_exit_code_args_error)
)
)
)
end
)
),
color: (
if $args_opts.monochrome_output == true then false
elif $args_opts.color_output == true then true
end
),
decode_file: (
( $args_opts.decode_file
| if . then
map(
( . as $a
| .[1] |=
try (open | decode($args_opts.decode_format))
catch
( "--decode-file \($a[0]): \(.)"
| halt_error(_exit_code_args_error)
)
)
)
end
)
),
expr: (
# if -f was used, all rest non-args are filenames
# otherwise first is expr rest is filesnames
( $args_opts.expr_file
| if . then
try (open | tobytes | tostring)
catch halt_error(_exit_code_args_error)
else $rest[0] // null
end
)
),
expr_eval_path: $args_opts.expr_file,
filenames: (
( if $args_opts.expr_file then $rest
else $rest[1:]
end
| if . == [] then null end
)
),
join_string: (
if $args_opts.join_output then ""
elif $args_opts.null_output then "\u0000"
else null
end
),
null_input: (
( if $args_opts.expr_file then $rest
else $rest[1:]
end
| if . == [] and $args_opts.repl then true
else null
end
)
),
rawfile: (
( $args_opts.rawfile
| if . then
( map(.[1] |=
try (open | tobytes | tostring)
catch halt_error(_exit_code_args_error)
)
)
end
)
),
raw_string: (
if $args_opts.raw_string
or $args_opts.join_output
or $args_opts.null_output
then true
else null
end
)
}
| with_entries(select(.value != null))
)
2020-06-08 03:29:51 +03:00
]
) as $_
| options as $opts
| if $opts.show_help then
( _banner
2021-08-13 20:35:15 +03:00
, ""
, _usage($arg0)
, args_help_text(_opts)
2020-06-08 03:29:51 +03:00
) | println
elif $opts.show_version then
2020-06-08 03:29:51 +03:00
$version | println
elif $opts.show_formats then
2020-06-08 03:29:51 +03:00
_formats_list | println
elif $opts.show_options then
$opts | display
2021-09-01 16:01:13 +03:00
elif
( ($rest | length) == 0 and
($opts.repl | not) and
($opts.expr_file | not) and
$stdin.is_terminal and $stdout.is_terminal
) then
( (( _usage($arg0), "\n") | stderr)
2021-08-14 01:11:57 +03:00
, null | halt_error(_exit_code_args_error)
)
2020-06-08 03:29:51 +03:00
else
2021-08-19 19:11:37 +03:00
# use _finally as display etc prints and results in empty
_finally(
# store some globals
2021-09-05 14:38:13 +03:00
( _include_paths($opts.include_path) as $_
| _input_filenames($opts.filenames) as $_
| _variables(
( $opts.arg +
$opts.argjson +
$opts.rawfile +
$opts.decode_file
| map({key: .[0], value: .[1]})
| from_entries
)
)
2021-09-03 04:30:52 +03:00
| ( def _inputs:
( if $opts.null_input then null
# note jq --slurp --raw-string is special, will be just
# a string not an array
elif $opts.string_input then inputs
elif $opts.slurp then [inputs]
else inputs
end
);
if $opts.repl then
( [_inputs]
2021-08-19 19:11:37 +03:00
| map(_cli_expr_eval($opts.expr; $opts.expr_eval_path))
| _repl({})
2021-09-01 16:01:13 +03:00
)
2021-09-03 04:30:52 +03:00
else
( _inputs
# iterate all inputs
2021-08-19 19:11:37 +03:00
| _cli_last_expr_error(null) as $_
| _cli_expr_eval($opts.expr; $opts.expr_eval_path; _repl_display)
2021-09-01 16:01:13 +03:00
)
2021-09-03 04:30:52 +03:00
end
)
2021-08-13 01:55:29 +03:00
)
; # finally
2021-08-13 20:27:38 +03:00
( if _input_io_errors then
2021-08-13 01:55:29 +03:00
null | halt_error(_exit_code_input_io_error)
end
2021-08-13 20:27:38 +03:00
| if _input_decode_errors then
2021-08-13 01:55:29 +03:00
null | halt_error(_exit_code_input_decode_error)
end
2021-08-13 20:27:38 +03:00
| if _cli_last_expr_error then
2021-08-13 01:55:29 +03:00
null | halt_error(_exit_code_expr_error)
end
)
2020-06-08 03:29:51 +03:00
)
end
);