Meus votos pra 2025: o ano do Linux no desktop
25 de Dezembro de 2024, 15:02 - sem comentários aindaE como sempre faço em todo fim de ano, renovo meus votos de que 2025 será o ano do Linux no desktop.
Aumentando o swap com zfs no Ubuntu 24.04
16 de Dezembro de 2024, 13:34 - sem comentários aindaAcabei tendo problemas de memória no laptop de trabalho. Então resolvi aumentar o tamanho do swap. Ou melhor, adicionar um volume lógico no zfs pro swap.
Após uma pesquisa rápida, achei a forma e fazer isso. Sem grandes surpresas, extremamente fácil.
root@silverhand$ zfs create -V 64G \
-b (getconf PAGESIZE) \
-o compression=zstd-fast \
-o logbias=throughput \
-o sync=always -o primarycache=metadata \
-o secondarycache=none \
-o com.sun:auto-snapshot=false \
rpool/swap
root@silverhand$ mkswap /dev/zvol/rpool/swap
root@silverhand$ swapon /dev/zvol/rpool/swap
O resultado foi rápido e fácil. Com 32 GB de RAM eu não esperava precisar tanto assim de swap. Mas o chrome insiste em tomar toda memória livre possível.
Não necessariamente relacionado, mas fiz upgrade do meu laptop pro Ubuntu 24.10. Como também está com zfs, fiz snapshots de cada volume antes de atualizar. Então fica fácil voltar se algo der errado. Por enquanto parece estar funcionando como esperado. E devo escrever sobre isso em breve.
Capturas da webcam atualizado pra 2024
27 de Novembro de 2024, 14:41 - sem comentários aindaEsse é um update to artigo usando python pra capturar a webcam. E com o uso do obamawatched 2021. Eu atualizei recentemente o obamawatcher pra rodar com PySide6 e está aqui funcionando no meu laptop atual de trabalho. No meu pessoal também. Ao menos acho que está funcionando.
O resultado final já está no vídeo acima. Como são várias imagens, os comandos que listei antes já não funcionam muito bem. Eu precisei primeiro redimensionar as imagens pra 640x360 para ficar em widescreen (16:9). As fotos mais antigas saíram em formato quadrado porque era o que a Webcam suportava na época. Então precisei cortar pra ficarem no aspecto correto. E pra isso eu usei python com pillow.
#! /usr/bin/env python3
import os
import re
import argparse
try:
from PIL import Image
except ModuleNotFoundError as e:
raise Exception("missing pillow - run: pip install Pillow") from e
golden_rate = 1280/720
default_size_x = 640
default_size_y = 360
parse = argparse.ArgumentParser(description="Script to resize pictures from a specific directory to the same size")
parse.add_argument("--directory", required=True, help="directory with images jpg")
args = parse.parse_args()
for filename in sorted(os.listdir(args.directory)):
if not re.search("jpg", filename):
continue
with Image.open(args.directory + "/" + filename) as image:
width, height = image.size
rate = float(width)/float(height)
is_golden = rate == golden_rate
is_correted = False
rate_from_default = width / default_size_x
if rate_from_default == 1:
pass
elif rate_from_default > 1:
image = image.resize((default_size_x, int(height/rate_from_default)))
is_correted = True
else:
image = image.resize((default_size_x, int(height/rate_from_default)), Image.Resampling.LANCZOS)
is_correted = True
if not is_golden:
image = image.crop((0, 0, default_size_x, default_size_y))
image.save(filename)
print(f"{filename}: {width}x{height}, golden: {is_golden}, corrected: {is_correted}")
print(f"Golden rate: {golden_rate}")
Tendo as imagens no mesmo formato, basta ordernar e usar ffmpeg pra montar o vídeo.
#! /usr/bin/env bash
die() {
echo "$1" >&2
exit 1
}
counter=0
for img in [0-9]*.jpg
do
serial=$(printf "%06d" $counter)
new_name="G${serial}.JPG"
counter=$(expr $counter + 1)
if [ -f "$new_name" ]; then
echo "$new_name already exists"
continue
fi
echo "$img => $new_name"
mv $img $new_name
done
case $(uname -s) in
Linux)
echo "Merging images into single video file: output.mp4"
ffmpeg -r 8 -i G%06d.JPG -c:v h264 -b:v 5M output.mp4 || \
die "Failed to render output.mp4"
;;
Darwin)
echo "Merging images into single video file: output.mp4"
ffmpeg -hwaccel auto -r 8 -i G%06d.JPG -c:v h264_videotoolbox -b:v 5M output.mp4 || \
die "Failed to render output.mp4"
esac
Eu não testei o código de macOS, então pode ser que não funcione. Meu PC atual, um Lenovo Thinkpad, é processador e GPU Intel. Eu tentei usar -hwaccel vaapi
mas as cores saiam erradas, no estilo negativo de filme. Então deixei rodar no processador mesmo.
E pra comparar o resultado em mpeg comparado com o mesmo em gif:
░▒▓ …/tmp/imagens-webcam/Webcam2 via v3.12.3 (venv) 15:33
❯ gm convert -delay 20 *JPG output.gif
░▒▓ …/tmp/imagens-webcam/Webcam2 via v3.12.3 (venv) 15:38
❯ ls -l output.*
.rw-rw-r-- 209M helio 27 Nov 15:38 output.gif
.rw-rw-r-- 103M helio 27 Nov 13:54 output.mp4
A geração do gif demorou uma eternidade, 5 minutos. O ffmpeg foram só alguns segundos. O tamanho foi o dobro no gif. Pra ver a imagem, teria de carregar tudo e só depois ver o resultando. Como mpeg, vai enviando o vídeo aos poucos.
Eu finalizei o vídeo no kdenlive, fazendo o merge com o vídeo anterior e adicionando a música. Ficou uma nostalgia gostosa.
E com certeza atualizarei daqui alguns anos.
Filtrando uma resposta json em ansible
23 de Novembro de 2024, 15:05 - sem comentários aindaAbri um grupo novo, SRE - Site Reliability Engineer, e uma categoria nova sobre Ansible já que agora esse é meu mundo. Estou trabalhando nessa área mantendo servidores rodando na empresa nova.
Eu estou migrando um sistema que cria VMs de python, pela falta de suporte na biblioteca, pra outra forma via linha de comando. Mas essa parte não interessa muito. O interessante é sobre como fiz parte do trabalho.
E estou escrevendo a respeito porque foi difícil achar informação sobre isso na Internet. A maioria estava ou errada ou não funcionava como esperado.
Pra começar, vamos dar uma olhada na estrutura de dados que recebo do programa. Pra simular o mesmo no Ansible, vou trocar o comando por um: cat server.json
(onde server.json contém esse dado). Meu objetivo aqui é pegar o IPv4 da interface pública pra depois provisionar o sistema com os programas que vamos usar.
{
"core_number": "1",
"hostname": "helio-testing.loureiro.eng.br",
"license": 0,
"memory_amount": "1024",
"plan": "1xCPU-1GB",
"progress": "0",
"state": "started",
"tags": [],
"title": "helio-testing.loureiro.eng.br (created via ansible)",
"uuid": "87271929-a137-4910-9648-75d05b6d3ecb",
"zone": "sweden-stockholm",
"boot_order": "disk",
"firewall": "off",
"host": 12345678,
"ip_addresses": [
{
"access": "internal",
"address": "192.168.0.1",
"family": "IPv4",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"mac": "",
"floating": "no",
"zone": ""
},
{
"access": "public",
"address": "1.2.3.4",
"family": "IPv4",
"part_of_plan": "yes",
"ptr_record": "",
"server": "",
"mac": "",
"floating": "no",
"zone": ""
}
],
"labels": {
"label": []
},
"metadata": "yes",
"nic_model": "virtio",
"networking": {
"interfaces": [
{
"index": 1,
"ip_addresses": [
{
"access": "",
"address": "1.2.3.4",
"family": "IPv4",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"mac": "",
"floating": "no",
"zone": ""
}
],
"mac": "a1:b2:c3:e4:f5:01",
"network": "fb29cb51-5dd7-4674-baac-b0253a725305",
"type": "public",
"bootable": "no",
"source_ip_filtering": "yes"
},
{
"index": 2,
"ip_addresses": [
{
"access": "",
"address": "192.168.0.1",
"family": "IPv4",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"mac": "",
"floating": "no",
"zone": ""
}
],
"mac": "a1:b2:c3:e4:f5:02",
"network": "92351420-66ec-46e9-98ee-149e3a588bdf",
"type": "utility",
"bootable": "no",
"source_ip_filtering": "yes"
}
]
},
"server_group": "",
"simple_backup": "no",
"storage_devices": [
{
"address": "virtio:0",
"storage": "fe128b6b-876c-4f7b-a070-3a8d6cec6159",
"storage_size": 10,
"storage_tier": "maxiops",
"storage_title": "helio-testing.loureiro.eng.br-OS",
"type": "disk",
"boot_disk": "0"
}
],
"timezone": "UTC",
"video_model": "vga",
"remote_access_enabled": "no",
"remote_access_type": "vnc",
"remote_access_host": "",
"remote_access_password": "abracadabra",
"remote_access_port": "0"
}
Então vamos começar com como pegar a saída desse comando no ansible. Pra isso basta chamar uma parte simples que registra o resultado.
---
- debug: msg="running the command and getting json as result"
- name: read json file
command: >
cat /tmp/server.json
register: result
failed_when: result.rc != 0
- debug:
msg="{{ result }}"
Não tem muito segredo aqui. Eu rodo o comando, que aqui eu troquei pelo cat
e recebo o resultando em result
. Eu olho se result.rc
é diferente de zero pra saber se deu certo (não zero significa erro).
- name: parsing as json
set_fact:
json_result: "{{ result.stdout }}"
- debug:
msg=" from json_result "
msg="{{ json_result}}"
Aqui foi o primeiro pulo do gato. Pra receber o dado como json bastava só pegar de result.stdout
. Em muitos lugares achei receitas exotéricas como {{ result | to_json }}
. Todas erradas.
- name: getting IPs
set_fact:
json_ip_addresses: "{{ json_result.ip_addresses }}"
- debug: msg="{{ json_ip_addresses }}"
Do código json que recebo, eu filtro somente ip_addresses
, que é a parte que me interessa.
- name: filtering public IPs
set_fact:
public_ip_addresses: "{{ json_ip_addresses | json_query(query) }}"
vars:
query: "[?access=='public']"
- debug: msg="{{ public_ip_addresses }}"
Em seguida eu aplico filtro de json usando a sintaxe de jpath. Na parte da query, que tem um array, eu quero somente aquela que tem a propriedade "access" com o valor "public". A query vai dentro de vars
por causa das aspas simples e duplas.
- name: filtering IPv4 only
set_fact:
public_ipv4_address: "{{ public_ip_addresses | json_query(query) }}"
vars:
query: "[?family=='IPv4'].address"
- debug: msg="{{ public_ipv4_address }}"
No exemplo que tenho eu já não tenho mais nada, mas em alguns casos eu tenho um array com 2 elementos: IPv4 e IPv6. Então essa segunda query é pra filtrar qual estrutura tem a propriedade "family" com valor "IPv4". Dessa estrutura eu quero o valor de "address". E pronto! Está aí o IPv4 filtrado. O valor é 1.2.3.4, que é o esperado.
Agora rodando com Ansbile:
❯ ansible-playbook deploy.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [get JSON] ***************************************************************************************************************************************************************************
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "running from roles/getjson/tasks"
}
TASK [getjson : read json file] ***********************************************************************************************************************************************************
changed: [localhost]
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": {
"changed": true,
"cmd": [
"cat",
"../server.json"
],
"delta": "0:00:00.002636",
"end": "2024-11-23 16:02:30.099466",
"failed": false,
"failed_when_result": false,
"msg": "",
"rc": 0,
"start": "2024-11-23 16:02:30.096830",
"stderr": "",
"stderr_lines": [],
"stdout": "{\n \"core_number\": \"1\",\n \"hostname\": \"helio-testing.loureiro.eng.br\",\n \"license\": 0,\n \"memory_amount\": \"1024\",\n \"plan\": \"1xCPU-1GB\",\n \"progress\": \"0\",\n \"state\": \"started\",\n \"tags\": [],\n \"title\": \"helio-testing.loureiro.eng.br (created via ansible)\",\n \"uuid\": \"87271929-a137-4910-9648-75d05b6d3ecb\",\n \"zone\": \"sweden-stockholm\",\n \"boot_order\": \"disk\",\n \"firewall\": \"off\",\n \"host\": 12345678,\n \"ip_addresses\": [\n {\n \"access\": \"internal\",\n \"address\": \"192.168.0.1\",\n \"family\": \"IPv4\",\n \"part_of_plan\": \"no\",\n \"ptr_record\": \"\",\n \"server\": \"\",\n \"mac\": \"\",\n \"floating\": \"no\",\n \"zone\": \"\"\n },\n {\n \"access\": \"public\",\n \"address\": \"1.2.3.4\",\n \"family\": \"IPv4\",\n \"part_of_plan\": \"yes\",\n \"ptr_record\": \"\",\n \"server\": \"\",\n \"mac\": \"\",\n \"floating\": \"no\",\n \"zone\": \"\"\n }\n ],\n \"labels\": {\n \"label\": []\n },\n \"metadata\": \"yes\",\n \"nic_model\": \"virtio\",\n \"networking\": {\n \"interfaces\": [\n {\n \"index\": 1,\n \"ip_addresses\": [\n {\n \"access\": \"\",\n \"address\": \"1.2.3.4\",\n \"family\": \"IPv4\",\n \"part_of_plan\": \"no\",\n \"ptr_record\": \"\",\n \"server\": \"\",\n \"mac\": \"\",\n \"floating\": \"no\",\n \"zone\": \"\"\n }\n ],\n \"mac\": \"a1:b2:c3:e4:f5:01\",\n \"network\": \"fb29cb51-5dd7-4674-baac-b0253a725305\",\n \"type\": \"public\",\n \"bootable\": \"no\",\n \"source_ip_filtering\": \"yes\"\n },\n {\n \"index\": 2,\n \"ip_addresses\": [\n {\n \"access\": \"\",\n \"address\": \"192.168.0.1\",\n \"family\": \"IPv4\",\n \"part_of_plan\": \"no\",\n \"ptr_record\": \"\",\n \"server\": \"\",\n \"mac\": \"\",\n \"floating\": \"no\",\n \"zone\": \"\"\n }\n ],\n \"mac\": \"a1:b2:c3:e4:f5:02\",\n \"network\": \"92351420-66ec-46e9-98ee-149e3a588bdf\",\n \"type\": \"utility\",\n \"bootable\": \"no\",\n \"source_ip_filtering\": \"yes\"\n }\n ]\n },\n \"server_group\": \"\",\n \"simple_backup\": \"no\",\n \"storage_devices\": [\n {\n \"address\": \"virtio:0\",\n \"storage_encrypted\": \"yes\",\n \"part_of_plan\": \"\",\n \"storage\": \"fe128b6b-876c-4f7b-a070-3a8d6cec6159\",\n \"storage_size\": 50,\n \"storage_tier\": \"maxiops\",\n \"storage_title\": \"helio-testing.loureiro.eng.br-OS\",\n \"type\": \"disk\",\n \"boot_disk\": \"0\"\n }\n ],\n \"timezone\": \"UTC\",\n \"video_model\": \"vga\",\n \"remote_access_enabled\": \"no\",\n \"remote_access_type\": \"vnc\",\n \"remote_access_host\": \"\",\n \"remote_access_password\": \"abracadabra\",\n \"remote_access_port\": \"0\"\n}",
"stdout_lines": [
"{",
" \"core_number\": \"1\",",
" \"hostname\": \"helio-testing.loureiro.eng.br\",",
" \"license\": 0,",
" \"memory_amount\": \"1024\",",
" \"plan\": \"1xCPU-1GB\",",
" \"progress\": \"0\",",
" \"state\": \"started\",",
" \"tags\": [],",
" \"title\": \"helio-testing.loureiro.eng.br (created via ansible)\",",
" \"uuid\": \"87271929-a137-4910-9648-75d05b6d3ecb\",",
" \"zone\": \"sweden-stockholm\",",
" \"boot_order\": \"disk\",",
" \"firewall\": \"off\",",
" \"host\": 12345678,",
" \"ip_addresses\": [",
" {",
" \"access\": \"internal\",",
" \"address\": \"192.168.0.1\",",
" \"family\": \"IPv4\",",
" \"part_of_plan\": \"no\",",
" \"ptr_record\": \"\",",
" \"server\": \"\",",
" \"mac\": \"\",",
" \"floating\": \"no\",",
" \"zone\": \"\"",
" },",
" {",
" \"access\": \"public\",",
" \"address\": \"1.2.3.4\",",
" \"family\": \"IPv4\",",
" \"part_of_plan\": \"yes\",",
" \"ptr_record\": \"\",",
" \"server\": \"\",",
" \"mac\": \"\",",
" \"floating\": \"no\",",
" \"zone\": \"\"",
" }",
" ],",
" \"labels\": {",
" \"label\": []",
" },",
" \"metadata\": \"yes\",",
" \"nic_model\": \"virtio\",",
" \"networking\": {",
" \"interfaces\": [",
" {",
" \"index\": 1,",
" \"ip_addresses\": [",
" {",
" \"access\": \"\",",
" \"address\": \"1.2.3.4\",",
" \"family\": \"IPv4\",",
" \"part_of_plan\": \"no\",",
" \"ptr_record\": \"\",",
" \"server\": \"\",",
" \"mac\": \"\",",
" \"floating\": \"no\",",
" \"zone\": \"\"",
" }",
" ],",
" \"mac\": \"a1:b2:c3:e4:f5:01\",",
" \"network\": \"fb29cb51-5dd7-4674-baac-b0253a725305\",",
" \"type\": \"public\",",
" \"bootable\": \"no\",",
" \"source_ip_filtering\": \"yes\"",
" },",
" {",
" \"index\": 2,",
" \"ip_addresses\": [",
" {",
" \"access\": \"\",",
" \"address\": \"192.168.0.1\",",
" \"family\": \"IPv4\",",
" \"part_of_plan\": \"no\",",
" \"ptr_record\": \"\",",
" \"server\": \"\",",
" \"mac\": \"\",",
" \"floating\": \"no\",",
" \"zone\": \"\"",
" }",
" ],",
" \"mac\": \"a1:b2:c3:e4:f5:02\",",
" \"network\": \"92351420-66ec-46e9-98ee-149e3a588bdf\",",
" \"type\": \"utility\",",
" \"bootable\": \"no\",",
" \"source_ip_filtering\": \"yes\"",
" }",
" ]",
" },",
" \"server_group\": \"\",",
" \"simple_backup\": \"no\",",
" \"storage_devices\": [",
" {",
" \"address\": \"virtio:0\",",
" \"storage_encrypted\": \"yes\",",
" \"part_of_plan\": \"\",",
" \"storage\": \"fe128b6b-876c-4f7b-a070-3a8d6cec6159\",",
" \"storage_size\": 50,",
" \"storage_tier\": \"maxiops\",",
" \"storage_title\": \"helio-testing.loureiro.eng.br-OS\",",
" \"type\": \"disk\",",
" \"boot_disk\": \"0\"",
" }",
" ],",
" \"timezone\": \"UTC\",",
" \"video_model\": \"vga\",",
" \"remote_access_enabled\": \"no\",",
" \"remote_access_type\": \"vnc\",",
" \"remote_access_host\": \"\",",
" \"remote_access_password\": \"abracadabra\",",
" \"remote_access_port\": \"0\"",
"}"
]
}
}
TASK [getjson : parsing as json] **********************************************************************************************************************************************************
ok: [localhost]
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": {
"boot_order": "disk",
"core_number": "1",
"firewall": "off",
"host": 12345678,
"hostname": "helio-testing.loureiro.eng.br",
"ip_addresses": [
{
"access": "internal",
"address": "192.168.0.1",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"zone": ""
},
{
"access": "public",
"address": "1.2.3.4",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "yes",
"ptr_record": "",
"server": "",
"zone": ""
}
],
"labels": {
"label": []
},
"license": 0,
"memory_amount": "1024",
"metadata": "yes",
"networking": {
"interfaces": [
{
"bootable": "no",
"index": 1,
"ip_addresses": [
{
"access": "",
"address": "1.2.3.4",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"zone": ""
}
],
"mac": "a1:b2:c3:e4:f5:01",
"network": "fb29cb51-5dd7-4674-baac-b0253a725305",
"source_ip_filtering": "yes",
"type": "public"
},
{
"bootable": "no",
"index": 2,
"ip_addresses": [
{
"access": "",
"address": "192.168.0.1",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"zone": ""
}
],
"mac": "a1:b2:c3:e4:f5:02",
"network": "92351420-66ec-46e9-98ee-149e3a588bdf",
"source_ip_filtering": "yes",
"type": "utility"
}
]
},
"nic_model": "virtio",
"plan": "1xCPU-1GB",
"progress": "0",
"remote_access_enabled": "no",
"remote_access_host": "",
"remote_access_password": "abracadabra",
"remote_access_port": "0",
"remote_access_type": "vnc",
"server_group": "",
"simple_backup": "no",
"state": "started",
"storage_devices": [
{
"address": "virtio:0",
"boot_disk": "0",
"part_of_plan": "",
"storage": "fe128b6b-876c-4f7b-a070-3a8d6cec6159",
"storage_encrypted": "yes",
"storage_size": 50,
"storage_tier": "maxiops",
"storage_title": "helio-testing.loureiro.eng.br-OS",
"type": "disk"
}
],
"tags": [],
"timezone": "UTC",
"title": "helio-testing.loureiro.eng.br (created via ansible)",
"uuid": "87271929-a137-4910-9648-75d05b6d3ecb",
"video_model": "vga",
"zone": "sweden-stockholm"
}
}
TASK [getjson : getting IPs] **************************************************************************************************************************************************************
ok: [localhost]
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": [
{
"access": "internal",
"address": "192.168.0.1",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"zone": ""
},
{
"access": "public",
"address": "1.2.3.4",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "yes",
"ptr_record": "",
"server": "",
"zone": ""
}
]
}
TASK [getjson : filtering public IPs] *****************************************************************************************************************************************************
ok: [localhost]
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": [
{
"access": "public",
"address": "1.2.3.4",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "yes",
"ptr_record": "",
"server": "",
"zone": ""
}
]
}
TASK [getjson : filtering IPv4 only] ******************************************************************************************************************************************************
ok: [localhost]
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": [
"1.2.3.4"
]
}
TASK [getjson : cloud server IP] ********************************************************************************************************************************************************
ok: [localhost]
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "1.2.3.4"
}
PLAY RECAP *********************************************************************************************************************************************************************************
localhost : ok=15 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Bom ansible and happy coding 😄
Adicionando uma pergunta em shell script
6 de Novembro de 2024, 16:16 - sem comentários aindaEsse é um código que estamos usando bastante aqui na firma nova. Ajuda a fazer seu script decidir se segue em frente ou para. E basta apenas apertar uma teclar, sem enter.
#! /usr/bin/env bash
read -p "Deseja continuar (s/n)? " -n 1 -r resposta
if [[ "$resposta" =~ [sS] ]];then
echo "Resposta foi sim"
else
echo "Resposta foi não"
fi