ezBookkeeping API Tools supports formatting response to table

This commit is contained in:
MaysWind
2026-02-16 01:14:21 +08:00
parent 22e4738b7a
commit 2e97d699e7
2 changed files with 433 additions and 24 deletions
+258 -9
View File
@@ -13,6 +13,9 @@ param(
[Parameter(Mandatory=$false)]
[string]$tzOffset = "",
[Parameter(Mandatory=$false)]
[switch]$rawResponse = $false,
[Parameter(ValueFromRemainingArguments=$true)]
[string[]]$CommandArgs
)
@@ -40,6 +43,10 @@ $API_CONFIGS = @(
" }"
"]"
)
PrettyResponse = @{
Type = "simple_array_to_markdown_table"
Columns = @("tokenId", "tokenType", "userAgent", "lastSeen", "isCurrent")
}
}
@{
Name = "tokens-revoke"
@@ -74,7 +81,7 @@ $API_CONFIGS = @(
" {"
" `"id`": `"string (Account ID)`","
" `"name`": `"string (Account name)`","
" `"parentId`": `"string (Parent account ID)`","
" `"parentId`": `"string (Parent account ID, 0 for primary account)`","
" `"category`": `"integer (Account category, 1: Cash, 2: Checking Account, 3: Credit Card, 4: Virtual Account, 5: Debt Account, 6: Receivables, 7: Investment Account, 8: Savings Account, 9: Certificate of Deposit)`","
" `"type`": `"integer (Account type, 1: Single Account, 2: Multiple Sub-accounts)`","
" `"icon`": `"string (Account icon ID)`","
@@ -91,6 +98,11 @@ $API_CONFIGS = @(
" }"
"]"
)
PrettyResponse = @{
Type = "hierarchical_array_to_markdown_table"
Columns = @("category", "type", "parentId", "id", "name", "currency", "balance", "hidden", "comment")
ChildKey = "subAccounts"
}
}
@{
Name = "accounts-add"
@@ -161,8 +173,8 @@ $API_CONFIGS = @(
" {"
" `"id`": `"string (Transaction category ID)`","
" `"name`": `"string (Transaction category name)`","
" `"parentId`": `"string (Parent transaction category ID)`","
" `"type`": `"integer (Transaction category type)`","
" `"parentId`": `"string (Parent transaction category ID, 0 for primary category)`","
" `"type`": `"integer (Transaction category type, 1: Income, 2: Expense, 3: Transfer)`","
" `"icon`": `"string (Transaction category icon ID)`","
" `"color`": `"string (Transaction category icon color, hex color code RRGGBB)`","
" `"comment`": `"string (Transaction category description)`","
@@ -173,6 +185,11 @@ $API_CONFIGS = @(
" ]"
"}"
)
PrettyResponse = @{
Type = "hierarchical_object_to_markdown_table"
Columns = @("type", "parentId", "id", "name", "hidden", "comment")
ChildKey = "subCategories"
}
}
@{
Name = "transaction-categories-add"
@@ -234,6 +251,10 @@ $API_CONFIGS = @(
" }"
"]"
)
PrettyResponse = @{
Type = "simple_array_to_markdown_table"
Columns = @("groupId", "id", "name", "hidden")
}
}
@{
Name = "transaction-tags-add"
@@ -333,6 +354,15 @@ $API_CONFIGS = @(
" `"totalCount`": `"integer (The total count of transactions)`""
"}"
)
PrettyResponse = @{
Type = "nested_array_to_markdown_table"
Columns = @("id", "type", "time", "utcOffset", "categoryId", "sourceAccountId", "sourceAmount", "destinationAccountId", "destinationAmount", "tagIds", "geoLocation", "comment")
DataPath = "items"
Metadata = @(
@{ Field = "totalCount"; Label = "Total Count" },
@{ Field = "nextTimeSequenceId"; Label = "Next Time Sequence ID" }
)
}
}
@{
Name = "transactions-list-all"
@@ -396,6 +426,10 @@ $API_CONFIGS = @(
" }"
"]"
)
PrettyResponse = @{
Type = "simple_array_to_markdown_table"
Columns = @("id", "type", "time", "utcOffset", "categoryId", "sourceAccountId", "sourceAmount", "destinationAccountId", "destinationAmount", "tagIds", "geoLocation", "comment")
}
}
@{
Name = "transactions-add"
@@ -422,12 +456,12 @@ $API_CONFIGS = @(
}
ParamDescriptions = @{
"type" = "integer (Transaction type, 1: Balance Modification, 2: Income, 3: Expense, 4: Transfer)"
"categoryId" = "string (Transaction category ID)"
"categoryId" = "string (Transaction category ID, supports secondary category)"
"time" = "integer (Transaction unix time)"
"utcOffset" = "integer (Transaction time zone offset minutes)"
"sourceAccountId" = "string (Source account ID)"
"sourceAccountId" = "string (Source account ID, supports account without sub-accounts or sub-account)"
"sourceAmount" = "integer (Source amount, supports up to two decimals. For example, a value of `"1234`" represents an amount of `"12.34`")"
"destinationAccountId" = "string (Destination account ID)"
"destinationAccountId" = "string (Destination account ID, supports account without sub-accounts or sub-account)"
"destinationAmount" = "integer (Destination amount, supports up to two decimals. For example, a value of `"1234`" represents an amount of `"12.34`")"
"hideAmount" = "boolean (Whether to hide amount)"
"tagIds" = "string (Transaction tag IDs, separated by comma, e.g. `"tagid1,tagid2`")"
@@ -484,6 +518,16 @@ $API_CONFIGS = @(
" ]"
"}"
)
PrettyResponse = @{
Type = "nested_array_to_markdown_table"
Columns = @("currency", "rate")
DataPath = "exchangeRates"
Metadata = @(
@{ Field = "dataSource"; Label = "Data Source" },
@{ Field = "baseCurrency"; Label = "Base Currency" },
@{ Field = "updateTime"; Label = "Update Time" }
)
}
}
@{
Name = "server-version"
@@ -821,6 +865,211 @@ function Get-ApiConfig {
return $null
}
function Get-PrettyResponseConfig {
param([string]$commandName)
foreach ($config in $API_CONFIGS) {
if ($config.Name -eq $commandName) {
return $config.PrettyResponse
}
}
return $null
}
function Flatten-HierarchicalData {
param(
[Parameter(Mandatory=$true)]
$Data,
[string]$ChildKey
)
$result = @()
$items = @()
if ($Data -is [Array]) {
$items = $Data
} elseif ($Data -is [PSCustomObject] -or $Data -is [Hashtable]) {
foreach ($prop in $Data.PSObject.Properties) {
if ($prop.Value -is [Array]) {
$items += $prop.Value
}
}
}
foreach ($item in $items) {
$parent = @{}
foreach ($prop in $item.PSObject.Properties) {
if ($prop.Name -ne $ChildKey) {
$parent[$prop.Name] = $prop.Value
}
}
$result += [PSCustomObject]$parent
if ($item.PSObject.Properties[$ChildKey] -and $item.$ChildKey) {
foreach ($child in $item.$ChildKey) {
$result += $child
}
}
}
return $result
}
function Write-Markdown-Table {
param(
[Parameter(Mandatory=$true)]
$Data,
[string[]]$Columns
)
if (-not $Data -or ($Data -is [Array] -and $Data.Count -eq 0)) {
Write-Host "No data to display"
return
}
if (-not $Columns -or $Columns.Count -eq 0) {
$Data | ConvertTo-Json -Depth 10 -Compress | Format-Json
return
}
$tableData = @()
if ($Data -is [Array]) {
foreach ($item in $Data) {
$row = [ordered]@{}
foreach ($col in $Columns) {
if ($item.PSObject.Properties[$col]) {
$value = $item.$col
if ($value -is [bool]) {
$row[$col] = $value.ToString().ToLower()
} elseif ($value -is [string] -and $value -eq "") {
$row[$col] = ""
} elseif ($value -is [string]) {
$row[$col] = $value -replace "`r", "\n" -replace "`n", "\n"
} elseif ($value -is [Array] -and $value.Count -eq 0) {
$row[$col] = "[]"
} elseif ($value -is [Array] -or $value -is [PSCustomObject] -or $value -is [Hashtable]) {
$row[$col] = ($value | ConvertTo-Json -Depth 10 -Compress)
} elseif ($null -eq $value) {
$row[$col] = "-"
} else {
$row[$col] = $value
}
} else {
$row[$col] = "-"
}
}
$tableData += [PSCustomObject]$row
}
} else {
$row = [ordered]@{}
foreach ($col in $Columns) {
if ($Data.PSObject.Properties[$col]) {
$value = $Data.$col
if ($value -is [bool]) {
$row[$col] = $value.ToString().ToLower()
} elseif ($value -is [string] -and $value -eq "") {
$row[$col] = ""
} elseif ($value -is [string]) {
$row[$col] = $value -replace "`r", "\n" -replace "`n", "\n"
} elseif ($value -is [Array] -and $value.Count -eq 0) {
$row[$col] = "[]"
} elseif ($value -is [Array] -or $value -is [PSCustomObject] -or $value -is [Hashtable]) {
$row[$col] = ($value | ConvertTo-Json -Depth 10 -Compress)
} elseif ($null -eq $value) {
$row[$col] = "-"
} else {
$row[$col] = $value
}
} else {
$row[$col] = "-"
}
}
$tableData += [PSCustomObject]$row
}
if ($tableData.Count -gt 0) {
$header = "| " + (($Columns -join " | ")) + " |"
Write-Host $header
$separator = "| " + ((1..$Columns.Count | ForEach-Object { "---" }) -join " | ") + " |"
Write-Host $separator
foreach ($item in $tableData) {
$values = @()
foreach ($col in $Columns) {
$values += $item.$col
}
$row = "| " + (($values -join " | ")) + " |"
Write-Host $row
}
}
}
function Write-Result {
param(
[string]$CommandName,
$ResultData,
[bool]$RawResponse = $false
)
if ($RawResponse) {
$ResultData | ConvertTo-Json -Depth 10 -Compress | Format-Json
return
}
$prettyConfig = Get-PrettyResponseConfig -commandName $CommandName
if (-not $prettyConfig) {
$ResultData | ConvertTo-Json -Depth 10 -Compress | Format-Json
return
}
$displayType = $prettyConfig.Type
$columns = $prettyConfig.Columns
switch ($displayType) {
"simple_array_to_markdown_table" {
Write-Markdown-Table -Data $ResultData -Columns $columns
}
"hierarchical_array_to_markdown_table" {
$childKey = $prettyConfig.ChildKey
$flattened = Flatten-HierarchicalData -Data $ResultData -ChildKey $childKey
Write-Markdown-Table -Data $flattened -Columns $columns
}
"hierarchical_object_to_markdown_table" {
$childKey = $prettyConfig.ChildKey
$flattened = Flatten-HierarchicalData -Data $ResultData -ChildKey $childKey
Write-Markdown-Table -Data $flattened -Columns $columns
}
"nested_array_to_markdown_table" {
$dataPath = $prettyConfig.DataPath
if ($dataPath) {
$nestedData = $ResultData.$dataPath
} else {
$nestedData = $ResultData
}
if ($prettyConfig.Metadata) {
foreach ($meta in $prettyConfig.Metadata) {
$value = $ResultData.($meta.Field)
if ($null -ne $value) {
Write-Host "$($meta.Label): $value"
}
}
Write-Host ""
}
Write-Markdown-Table -Data $nestedData -Columns $columns
}
default {
$ResultData | ConvertTo-Json -Depth 10 -Compress | Format-Json
}
}
}
function Show-Help {
$exampleTimezoneName = Get-ExampleTimezoneName
$exampleTimezoneOffset = Get-ExampleTimezoneOffset
@@ -830,7 +1079,7 @@ function Show-Help {
Write-Host "A command-line tool for calling ezBookkeeping APIs"
Write-Host ""
Write-Host "Usage:"
Write-Host " ebktools.ps1 [-tzName <name>] [-tzOffset <offset>] <command> [command-options]"
Write-Host " ebktools.ps1 [-tzName <name>] [-tzOffset <offset>] [-rawResponse] <command> [command-options]"
Write-Host ""
Write-Host "Environment Variables (Required):"
Write-Host " EBKTOOL_SERVER_BASEURL ezBookkeeping server base URL (e.g., http://localhost:8080)"
@@ -839,6 +1088,7 @@ function Show-Help {
Write-Host "Global Options:"
Write-Host " -tzName <name> The IANA timezone name of current timezone. For example, for Beijing Time it is 'Asia/Shanghai'."
Write-Host " -tzOffset <offset> The offset in minutes of the current timezone from UTC. For example, for Beijing Time which is UTC+8, the value is '480'. If both '-tzName' and '-tzOffset' are set, '-tzName' takes priority. If neither is set, the current system time zone is used by default."
Write-Host " -rawResponse Display the response in raw JSON format instead of formatted table."
Write-Host ""
Write-Host "Commands:"
Write-Host " list List all available API commands"
@@ -1133,8 +1383,7 @@ function Invoke-Api {
if ($response.success -eq $true) {
Write-Host "Response Result:"
if ($response.PSObject.Properties.Name -contains "result") {
$jsonOutput = ConvertTo-Json -Depth 10 -Compress $response.result | Format-Json
Write-Host $jsonOutput
Write-Result -CommandName $commandName -ResultData $response.result -RawResponse $script:rawResponse
} else {
Write-Host "Success: true (No result data)"
}
+175 -15
View File
@@ -25,7 +25,11 @@ API_CONFIGS='[
" \"isCurrent\": \"boolean (Whether the session is current)\"",
" }",
"]"
]
],
"PrettyResponse": {
"Type": "simple_array_to_markdown_table",
"Columns": ["tokenId", "tokenType", "userAgent", "lastSeen", "isCurrent"]
}
},
{
"Name": "tokens-revoke",
@@ -60,7 +64,7 @@ API_CONFIGS='[
" {",
" \"id\": \"string (Account ID)\",",
" \"name\": \"string (Account name)\",",
" \"parentId\": \"string (Parent account ID)\",",
" \"parentId\": \"string (Parent account ID, 0 for primary account)\",",
" \"category\": \"integer (Account category, 1: Cash, 2: Checking Account, 3: Credit Card, 4: Virtual Account, 5: Debt Account, 6: Receivables, 7: Investment Account, 8: Savings Account, 9: Certificate of Deposit)\",",
" \"type\": \"integer (Account type, 1: Single Account, 2: Multiple Sub-accounts)\",",
" \"icon\": \"string (Account icon ID)\",",
@@ -76,7 +80,12 @@ API_CONFIGS='[
" \"subAccounts\": [\"each sub-account object like an account object\"]",
" }",
"]"
]
],
"PrettyResponse": {
"Type": "hierarchical_array_to_markdown_table",
"Columns": ["category", "type", "parentId", "id", "name", "currency", "balance", "hidden", "comment"],
"ChildKey": "subAccounts"
}
},
{
"Name": "accounts-add",
@@ -147,8 +156,8 @@ API_CONFIGS='[
" {",
" \"id\": \"string (Transaction category ID)\",",
" \"name\": \"string (Transaction category name)\",",
" \"parentId\": \"string (Parent transaction category ID)\",",
" \"type\": \"integer (Transaction category type)\",",
" \"parentId\": \"string (Parent transaction category ID, 0 for primary category)\",",
" \"type\": \"integer (Transaction category type, 1: Income, 2: Expense, 3: Transfer)\",",
" \"icon\": \"string (Transaction category icon ID)\",",
" \"color\": \"string (Transaction category icon color, hex color code RRGGBB)\",",
" \"comment\": \"string (Transaction category description)\",",
@@ -158,7 +167,12 @@ API_CONFIGS='[
" }",
" ]",
"}"
]
],
"PrettyResponse": {
"Type": "hierarchical_object_to_markdown_table",
"Columns": ["type", "parentId", "id", "name", "hidden", "comment"],
"ChildKey": "subCategories"
}
},
{
"Name": "transaction-categories-add",
@@ -219,7 +233,11 @@ API_CONFIGS='[
" \"hidden\": \"boolean (Whether the transaction tag is hidden)\"",
" }",
"]"
]
],
"PrettyResponse": {
"Type": "simple_array_to_markdown_table",
"Columns": ["groupId", "id", "name", "hidden"]
}
},
{
"Name": "transaction-tags-add",
@@ -318,7 +336,16 @@ API_CONFIGS='[
" \"nextTimeSequenceId\": \"integer (The next cursor '"'"'max_time'"'"' parameter when requesting older data)\",",
" \"totalCount\": \"integer (The total count of transactions)\"",
"}"
]
],
"PrettyResponse": {
"Type": "nested_array_to_markdown_table",
"Columns": ["id", "type", "time", "utcOffset", "categoryId", "sourceAccountId", "sourceAmount", "destinationAccountId", "destinationAmount", "tagIds", "geoLocation", "comment"],
"DataPath": ".items",
"Metadata": [
{"Field": "totalCount", "Label": "Total Count"},
{"Field": "nextTimeSequenceId", "Label": "Next Time Sequence ID"}
]
}
},
{
"Name": "transactions-list-all",
@@ -381,7 +408,11 @@ API_CONFIGS='[
" \"editable\": \"boolean (Whether the transaction is editable)\"",
" }",
"]"
]
],
"PrettyResponse": {
"Type": "simple_array_to_markdown_table",
"Columns": ["id", "type", "time", "utcOffset", "categoryId", "sourceAccountId", "sourceAmount", "destinationAccountId", "destinationAmount", "tagIds", "geoLocation", "comment"]
}
},
{
"Name": "transactions-add",
@@ -408,12 +439,12 @@ API_CONFIGS='[
},
"ParamDescriptions": {
"type": "integer (Transaction type, 1: Balance Modification, 2: Income, 3: Expense, 4: Transfer)",
"categoryId": "string (Transaction category ID)",
"categoryId": "string (Transaction category ID, supports secondary category)",
"time": "integer (Transaction unix time)",
"utcOffset": "integer (Transaction time zone offset minutes)",
"sourceAccountId": "string (Source account ID)",
"sourceAccountId": "string (Source account ID, supports account without sub-accounts or sub-account)",
"sourceAmount": "integer (Source amount, supports up to two decimals. For example, a value of \\"1234\\" represents an amount of \\"12.34\\")",
"destinationAccountId": "string (Destination account ID)",
"destinationAccountId": "string (Destination account ID, supports account without sub-accounts or sub-account)",
"destinationAmount": "integer (Destination amount, supports up to two decimals. For example, a value of \\"1234\\" represents an amount of \\"12.34\\")",
"hideAmount": "boolean (Whether to hide amount)",
"tagIds": "string (Transaction tag IDs, separated by comma, e.g. \"tagid1,tagid2\")",
@@ -469,7 +500,17 @@ API_CONFIGS='[
" }",
" ]",
"}"
]
],
"PrettyResponse": {
"Type": "nested_array_to_markdown_table",
"Columns": ["currency", "rate"],
"DataPath": ".exchangeRates",
"Metadata": [
{"Field": "dataSource", "Label": "Data Source"},
{"Field": "baseCurrency", "Label": "Base Currency"},
{"Field": "updateTime", "Label": "Update Time"}
]
}
},
{
"Name": "server-version",
@@ -492,6 +533,7 @@ API_CONFIGS='[
TIMEZONE_NAME=""
TIMEZONE_OFFSET=""
RAW_RESPONSE="false"
echo_red() {
printf '\033[31m%s\033[0m\n' "$1"
@@ -576,6 +618,119 @@ parse_api_config() {
echo "$API_CONFIGS" | jq -r --arg name "$api_name" '.[] | select(.Name == $name)'
}
get_pretty_response_config() {
command_name="$1"
echo "$API_CONFIGS" | jq -r --arg name "$command_name" '.[] | select(.Name == $name) | .PrettyResponse // null'
}
flatten_hierarchical_data() {
data="$1"
child_key="$2"
printf "%s\n" "$data" | jq -r --arg childKey "$child_key" '
if type == "array" then
[.[] | . as $parent | [$parent | del(.[$childKey])] + (.[$childKey] // [])]
elif type == "object" then
[.[] | .[] | . as $parent | [$parent | del(.[$childKey])] + (.[$childKey] // [])]
else
[]
end | flatten
'
}
print_markdown_table() {
data="$1"
columns="$2"
if [ -z "$data" ] || [ "$data" = "null" ] || [ "$data" = "[]" ]; then
echo "No data to display"
return
fi
cols_array="$columns"
if [ -z "$cols_array" ] || [ "$cols_array" = "null" ]; then
printf "%s\n" "$data" | jq '.'
return
fi
header="$(echo "$cols_array" | jq -r 'join(" | ")')"
separator="$(echo "$cols_array" | jq -r '[.[] | "---"] | join(" | ")')"
rows="$(printf "%s\n" "$data" | jq -r --argjson cols "$cols_array" '
if type == "array" then
.[] | [$cols[] as $col | .[$col] | if . == null then "-" elif type == "string" then gsub("\r"; "\\r") | gsub("\n"; "\\n") else tostring end] | join(" | ")
else
[$cols[] as $col | .[$col] | if . == null then "-" elif type == "string" then gsub("\r"; "\\r") | gsub("\n"; "\\n") else tostring end] | join(" | ")
end
' 2>/dev/null)"
if [ -z "$rows" ]; then
printf "%s\n" "$data" | jq '.'
return
fi
echo "| $header |"
echo "| $separator |"
printf "%s\n" "$rows" | while IFS= read -r row; do
printf "%s\n" "| $row |"
done
}
print_result() {
command_name="$1"
result_data="$2"
if [ "$RAW_RESPONSE" = "true" ]; then
printf "%s\n" "$result_data" | jq '.'
return
fi
pretty_config="$(get_pretty_response_config "$command_name")"
if [ -z "$pretty_config" ] || [ "$pretty_config" = "null" ]; then
printf "%s\n" "$result_data" | jq '.'
return
fi
display_type="$(echo "$pretty_config" | jq -r '.Type')"
columns="$(echo "$pretty_config" | jq -c '.Columns')"
case "$display_type" in
simple_array_to_markdown_table)
print_markdown_table "$result_data" "$columns"
;;
hierarchical_array_to_markdown_table)
child_key="$(echo "$pretty_config" | jq -r '.ChildKey')"
flattened="$(flatten_hierarchical_data "$result_data" "$child_key")"
print_markdown_table "$flattened" "$columns"
;;
hierarchical_object_to_markdown_table)
child_key="$(echo "$pretty_config" | jq -r '.ChildKey')"
flattened="$(flatten_hierarchical_data "$result_data" "$child_key")"
print_markdown_table "$flattened" "$columns"
;;
nested_array_to_markdown_table)
data_path="$(echo "$pretty_config" | jq -r '.DataPath // "."')"
nested_data="$(printf "%s\n" "$result_data" | jq -r "$data_path")"
metadata="$(echo "$pretty_config" | jq -r '.Metadata // null')"
if [ -n "$metadata" ] && [ "$metadata" != "null" ]; then
echo "$metadata" | jq -r --arg result "$result_data" '
($result | fromjson) as $data |
.[] | select($data[.Field] != null) | "\(.Label): \($data[.Field])"
'
echo ""
fi
print_markdown_table "$nested_data" "$columns"
;;
*)
printf "%s\n" "$result_data" | jq '.'
;;
esac
}
show_help() {
example_timezone_name="$(get_example_timezone_name)"
example_timezone_offset="$(get_example_timezone_offset)"
@@ -586,7 +741,7 @@ ezBookkeeping API Tools
A command-line tool for calling ezBookkeeping APIs
Usage:
ebktools.sh [--tz-name <name>] [--tz-offset <offset>] <command> [command-options]
ebktools.sh [--tz-name <name>] [--tz-offset <offset>] [--raw-response] <command> [command-options]
Environment Variables (Required):
EBKTOOL_SERVER_BASEURL ezBookkeeping server base URL (e.g., http://localhost:8080)
@@ -595,6 +750,7 @@ Environment Variables (Required):
Global Options:
--tz-name <name> The IANA timezone name of current timezone. For example, for Beijing Time it is 'Asia/Shanghai'.
--tz-offset <offset> The offset in minutes of the current timezone from UTC. For example, for Beijing Time which is UTC+8, the value is '480'. If both '--tz-name' and '--tz-offset' are set, '--tz-name' takes priority. If neither is set, the current system time zone is used by default.
--raw-response Display the response in raw JSON format instead of formatted table.
Commands:
list List all available API commands
@@ -956,7 +1112,7 @@ call_api() {
echo "Response Result:"
result=$(printf "%s\n" "$response" | jq '.result // "null"')
if [ "$result" != "null" ]; then
printf "%s\n" "$response" | jq '.result'
print_result "$command_name" "$result"
else
echo "Success: true (No result data)"
fi
@@ -992,6 +1148,10 @@ main() {
TIMEZONE_OFFSET="$2"
shift 2
;;
--raw-response)
RAW_RESPONSE="true"
shift
;;
--help | -h)
show_help
exit 0