All links of one day
in a single page.
<Previous day - Next day>

rss_feedDaily RSS Feed
floral_left The Daily Shaarli floral_right
——————————— Sunday 05, April 2020 ———————————

Copy the contents of a file into the clipboard without displaying its contents - Unix & Linux Stack Exchange

Copier tout le contenu d'un fichier dans le presse-papier en ligne de commande : xsel -b < <fichier>

sponge(1): soak up stdin/write to file - Linux man page

Qui n'a pas déjà voulu qu'une commande GNU/Linux enregistre son résultat dans le fichier d'origine, genre sort MonFichier > MonFichier ?

Évidemment, ça ne fonctionne pas car, à cause de la redirection (« > »), le shell vide le fichier « MonFichier » puis l'ouvre avec l'identifiant « 1 » (stdout) dans la table des descripteurs de fichiers du processus, puis il se duplique avec l'appel système « fork() » (un fork() conserve la table des descripteurs de fichiers) puis remplace le code de sa copie par celui du programme demandé (sort, dans notre exemple) avec la famille d'appels système « exec() », ce qui en lance l'exécution. Donc, dès le début, le fichier d'origine est vidé.

La plupart des commandes permettent d'écrire leur résultat dans un fichier genre sort -o <fichier> <fichier>. Mais pas toutes.

C'est là que sponge intervient : cette commande attend la fin de l'écoulement des données sur stdin puis elle ouvre le fichier passé en argument et enregistre tout ce qu'elle a reçu depuis stdin.
Pratique quand une commande ne permet pas d'enregistrer son travail dans un fichier (genre jq) et qu'on ne veut pas créer xxx fichiers temporaires (genre « commande fichierN > fichierN+1 »).

Exemple d'utilisation : jq '.' monFichier | sponge monFichier.

jq : gsub() échoue sur certains caractères Unicode

Voir mon introduction à la prise en main de jq, le logiciel de manipulation de JSON en ligne de commande.

Soit le fichier JSON africa-coast-to-europe-ace.json.

Avec jq version 1.5.1 Debian GNU/Linux Buster, je veux récupérer la valeur de l'attribut « owners » sous la forme d'une unique chaîne de caractère. Je voudrais que la séparation entre chaque propriétaire ne soit plus « , », mais « \n * » (afin de produire une liste à puces).

Facile, il suffit d'utiliser la fonction gsub(), greedy sub(), qui permet de remplacer des bouts de chaînes de caractères. Sauf que…

$ jq -r '.owners | " * \(gsub(", ";"\n *"))"' africa-coast-to-europe-ace.json
 * Orange
 * Dolphin Telecom
 * Cote d’Ivoire Telec
 *
 * Gambia Submarine Cable Company
 * MTN
[…]

Hum, il y a un raté pour Cote d'Ivoire Telecom.

Je t'épargne l'identification du problème avec hexdump -C et jq -a (afficher le code séquence d'un caractère non-ASCII) : ce modèle d'apostrophe n'est pas un caractère ASCII mais Unicode.

Normalement, ça ne devrait pas poser de problème puisque jq travaille, par défaut, en UTF-8 (d'après son manuel). De plus, file -i est formel, le fichier utilisé est bien encodé en UTF-8. Même si j'utilise iconv -t UTF-8 africa-coast-to-europe-ace.json -o africa-coast-to-europe-ace.json pour m'en assurer, ça change rien.

J'ai essayé avec d'autres caractères Unicode : ça fonctionne avec « » » et des caractères Hangûl, mais pas avec le caractère « ⚧ »…

J'en déduis que la fonction gsub() de jq foire avec certains caractères Unicode.

J'ai rien trouvé de mieux que de remplacer l'aspostrophe problématique par l'apostrophe ASCII avec sed -i "s/’/'/" africa-coast-to-europe-ace.json avant son traitement par jq

Ce bug semble être corrigé dans la version 1.6 Debian GNU/Linux de jq.

Construire une carte des câbles sous-marins d'Internet avec OpenStreetMap

TL;DR : carte OpenStreetMap des câbles sous-marins d'Internet / Internet submarine cables map OpenStreetMap. Ci-dessous, pourquoi et comment construire une telle carte.

ÉDIT DU 29/08/2023 : depuis le 15/11/2022, TeleGeography a supprimé son dépôt git et ne diffuse plus les données géographiques (« We no longer maintain and update a repository on GitHub for our data or our source code » dans l'infobulle « about » sur leur site web). :( Cette carte n'est donc plus à jour. Dernière version des données géographiques. FIN DE L'ÉDIT.


Introduction

Loin d'être virtuel, Internet repose sur des infrastructures physiques dont des câbles sous-marins en fibre optique (qu'il faut entretenir contre l'usure, les événements naturels ou humains, les sabotages, etc.).

La liste de ces câbles, leurs stations d'atterrissement et leur tracé est environ public (environ car le tracé ne l'est pas de manière précise). Nous disposons de l'excellent site web Submarine Cable Map.

Néanmoins, cet existant, cette carte, me pose deux problèmes :

  • Elle repose sur le service Maps de la multinationale Google. Ça pose les habituelles questions de dépendance vis-à-vis d'un acteur unique hors contrat, de pérennité et de respect de la vie privée ;

  • Elle est plutôt unique, donc elle peut disparaître promptement. À titre d'exemple, les repreneurs du site web historique en la matière, cablemap.info, demandent une inscription préalable, donc, en pratique, cette carte publique a disparu. J'ai vu que les sources du site web sont disponibles (donc que quiconque peut en héberger une copie), mais node.js, yarn et compagnie, ça sera sans moi (et ça ne résout pas la dépendance à Google Maps).

Pour l'heure, les données (le tracé des câbles et les coordonnées géographiques des stations d'atterrissement) de la carte « Submarine Cable Map » sont disponibles dans un dépôt git public sous une licence environ libre (CC BY-NC-SA).

Et si on les utilisait pour construire une carte basée sur OpenStreetMap, l'outil de cartographie libre et communautaire (OSM) ?

C'est chose faite : carte OpenStreetMap des câbles sous-marins d'Internet / Internet submarine cables map OpenStreetMap.
J'actualiserai cette carte au fil de l'eau, en me basant sur le dépôt git sus-cité.

Ci-dessous, un retour sur la façon dont j'ai procédé pour construire cette carte.


Que faut-il pour créer cette carte ?

Pour réaliser cette carte, il faut deux choses.

D'un côté, il faut pouvoir créer une carte personnalisée en utilisant un fond de carte OpenStreetMap, c'est-à-dire une carte sur laquelle on peut représenter ce que l'on veut : un itinéraire, un parcours de trail, un marqueur sur un lieu avec une éventuelle infobulle pour donner des infos, plusieurs marqueurs afin d'identifier plusieurs lieux (pour un festival décentralisé, par exemple), etc.

Le site web officiel d'OpenStreetMap permet de tracer des itinéraires et de partager une carte avec un marqueur sur un lieu (sans infobulle, sans marqueur personnalisé), mais ça s'arrête là.

Le logiciel web umap pemet tout le reste sus-cité. On peut installer sa propre instance ou utiliser celles qui existent. Exemples : instance umap OSM France, instance umap Framasoft.

Vu mon maigre besoin, j'ai décidé d'utiliser une instance existante. J'ai choisi celle de Framasoft, une association française qui œuvre pour des services numériques éthiques et la culture libre.

Pour débuter, je recommande vivement cet exellent tutoriel umap. D'autres tutoriels, et même des tutoriels vidéos sont disponibles sur le wiki officiel OSM France.

D'un autre côté, il faut retravailler les données géographiques disponibles :

  • Les tracés des câbles sont dans un fichier JSON alors que les informations sur les câbles (nom, longueur, date de mise en service, etc.) sont stockées dans un autre fichier ;

  • On aimerait mettre en forme (gras, liste à puces, liens vers les stations d'atterrissement, etc.) le texte associé à un câble / une station ;

  • On aimerait conserver la coloration des câbles, mais l'attribut JSON utilisé n'est pas prise en charge par umap (et sa valeur n'est pas au format attendu ‒ # ‒).

On pourrait faire ça à la main, mais de nouveaux câbles entrent régulièrement en service pendant que d'autres rendent leur tablier. Une mise à jour à la main ne serait pas gérable. Il faut donc qu'un programme informatique fasse cette transformation des fichiers d'origine prévus pour Google Maps en fichiers compréhensibles par OpenStreetMap en y ajoutant, au passage, nos desiderata de mise en forme.

Dès que l'on évoque la manipulation de JSON, on pense forcément à la commande jq. J'ai écrit un tutoriel pour jq qui est rien de plus que le retour de l'expérience acquise pour construire cette carte des câbles sous-marins d'Internet.

Voici le script que j'ai pondu : transform_carte_cables_sous-marins.sh.

  • Les logiciels git et jq doivent être installés ;

  • Il suffit de le lancer. Aucun argument attendu ;

  • Il va récupérer le dépôt git, créer un espace de travail temporaire, travailler, détruire l'espace de travail temporaire, indiquer où trouver les fichiers résultants de son traitement ;

  • Deux variables en début de script permettent d'ajouter des paramètres umap sur les câbles et les stations (opacité, forme, interaction, etc.). Pour l'instant, tous les paramètres peuvent être définis au niveau du calque, donc ces variables ne servent pas.


Construire la carte

Une fois que l'on a exécuté ce script afin de traiter les données, comment créer la carte et y importer les données ?

  • Créer une carte sur une instance umap ;

  • Dans l'entête, à côté de « Carte sans nom », il y a une icône en forme de crayon. Cliquer dessus :

    • Nom = « Submarine Cable Map » ;

    • Description = contenu du fichier summary.txt ;

    • Options d'interface :
      • Afficher le bouton de plein écran = jamais ;

      • Afficher le bouton de localisation = jamais ;

      • Afficher le bouton pour ouvrir l'éditeur d'OpenStreetMap = jamais (je ne souhaite pas donner de faux espoirs au pékin : ce ne sont pas les données relatives aux câbles qui seront modifiables par ce biais) ;

      • Afficher le bouton d'accès rapide aux couches de données = caché ;

      • Voulez-vous afficher un panneau latéral au chargement? = légende.
    • Limites géographiques (pour limiter les déplacements sur la carte aux endroits où il y a des câbles afin d'éviter les fausses manips) :

      • Sud = -90 ;

      • Ouest = -250 ;

      • Nord = 90 ;

      • Est = 250.
    • Crédits
  • Cliquer sur le bouton « Enregistrer ». Garder impérativement le lien de modification (tant que le cookie est conservé par ton navigateur web, ça va, mais sinon il constitue le seul moyen de pouvoir modifier la carte) ;

  • Cliquer sur le bouton « calques » :

    • Supprimer le calque 1 ;

    • Ajouter un calque :

      • Nom = « landings » ;

      • Description = « Landing stations » ;

      • Propriétés de la forme :

        • Forme de l'icône = cercle (elle attire moins l'œil / monopolise moins l'espace disponible).
      • Options d'interaction :
        • Forme de popup = panneau latéral (plus pratique que les infobulles + comportement par défaut d'OSM).
    • Ajouter un calque :

      • Nom = « cables » ;

      • Description = « submarine cables » ;

      • Propriétés de la forme :

        • Opacité = 1 (curseur le plus à droite possible).
      • Options d'interaction :
        • Forme de popup = panneau latéral.
  • Cliquer sur le bouton « Enregistrer » ;

  • Cliquer sur le bouton « importer des données » :

    • Importer des données = choisir le fichier landings.json produit par le script sus-mentionné ;

    • Choisir le calque de données pour l'import = landings ;

    • Cliquer sur le bouton « Importer ».
  • Cliquer sur le bouton « Enregistrer » ;

  • Cliquer sur le bouton « importer des données » :
    • Importer des données = choisir le fichier cables.json produit par le script sus-mentionné ;

    • Choisir le calque de données pour l'import = cables ;

    • Cliquer sur le bouton « Importer ».
  • Cliquer sur le bouton « Enregistrer » ;

  • À la fin de l'URL, ajouter « #2/-0.2/0.0 » et valider ;

  • Cliquer sur le bouton « enregistrer le zoom et le centre actuels » ;

  • Cliquer sur le bouton « exporter et partager la carte » :
    • Télécharger les données = données complètes de la carte ;

    • Cliquer sur le bouton « télécharger les données ». Conserver ce fichier de sauvegarde.



Je note quelques limites d'umap :

  • Contrairement à la version originale de la carte, je ne suis pas parvenu à créer un lien vers un câble sous-marin dans le panneau latéral d'une station d'atterrissement. De même, pour avoir un lien vers une station d'atterrissement dans le panneau latéral d'un câble, j'ai dû ruser en utilisant ses coordonnées géographiques plutôt que son nom afin de former une URL valide au sens OSM (#<niveau_de_zoom>/<latitude>/<longitude>). Dit autrement : je n'ai pas trouvé un moyen de faire un lien vers un objet. Sur OSM, les objets (le tracé d'une ligne de bus, par exemple) ont un identifiant interne public, donc on peut construire une URL de la forme « /relation/<identifiant> ». Avec umap, je n'ai pas trouvé d'identifiant, y compris dans l'export ;

  • Contrairement à la version originale de la carte, je n'ai pas trouvé comment mettre en évidence / surligner l'ensemble du tracé d'un câble quand on clique dessus, puisque, comme indiqué au point précédent, je n'ai pas trouvé comment pointer un objet par son nom afin de créer un lien pointant sur lui dans son intégralité.


Annexe : premier script, méconnaissance de jq, optimisation et autocritique

Le script présenté plus haut permettant de traiter les données géographiques avant importation dans umap n'est pas le premier que j'ai écrit. Le premier, c'est celui-ci : transform_carte_cables_sous-marins.original.sh.

Quelles différences ?

  • Je n'avais pas compris que jq peut mettre le contenu d'un (ou plusieurs) fichier secondaire dans une (ou des) variable afin d'enrichir le fichier principal avec des informations du (des) fichier secondaire ;

  • Je ne voulais pas admettre qu'une fois qu'on a sélectionné / demander l'affichage d'un attribut qui contient un tableau (« '.monAttribut[]' »), alors on itère sur ce tableau jusqu'à la fin de la commande jq. Tout filtre ajouté à la chaîne sera exécuté autant de fois qu'il y a d'éléments dans le tableau parcouru. Cela signifie aussi que l'on perd tout le contenu JSON qui était autour de ce tableau. Oui, c'est aussi idiot que de se plaindre qu'avec un grep | xargs <maCommande>, maCommande n'accède pas à ce qui n'a pas été retenu par grep. Mais, du coup, ça me fait perdre l'entête et le pied du fichier JSON d'origine, donc ça produit un JSON invalide. Si je n'itère pas sur le tableau, je ne peux pas accéder aux attributs de chaque élément du tableau, donc il m'est impossible, par exemple, de transvaser la couleur d'un câble dans un attribut compréhensible par umap (genre jq '.features[].properties += { "_umap_options": { "color": .color } }' cables.json, ça ne fonctionne pas). Comme je n'avais pas compris que jq permet de travailler sur plusieurs fichiers, je n'avais pas perçu que l'on peut reconstruire le fichier JSON d'origine ;

  • Comme je n'avais pas compris non plus que l'on peut circonscrire une itération sur un tableau au sein d'une opération bornée / finie comme une affectation, je n'avais pas perçu que ça pouvait être une autre solution au problème du point précédent. C'est finalement la solution que j'ai retenue.

Ces trois points sont détaillés dans mon shaarli dédié à la prise en main de jq par des exemples pratiques.

En conséquence, je faisais une boucle « for » avec mon shell. La fin de l'itération était le nombre d'éléments dans le tableau - 1. Ainsi, j'avais un indice de tableau. Donc je pouvais accéder à l'élément que je voulais dans le tableau, récupérer son identifiant (permettant de récupérer ses infos dans le fichier JSON séparé) et le modifier.

  • L'inconvénient, c'est que je lançais plusieurs jq (récupérer l'identifiant d'un élément, récupérer les infos dans le deuxième fichier, modifier le JSON avec tout ça) ‒ ce qui implique des fork() et des exec() en pagaille ‒, que j'écrivais autant de fichiers temporaires qu'il y a d'éléments parcourus, et que jq renvoi l'intégralité du JSON à chaque passe. Tout ça consomme du CPU, de la RAM et des IO. Ce premier jet s'exécute en 1 minute 54 (moyenne sur 3 exécutions, récupération du dépôt git exclue). Et, encore, je suis en RAM (tmpfs).

  • L'avantage de cette première version, c'est que, puisque je récupère les informations (sur un câble ou une station) avec une commande jq distincte, je peux appliquer très facilement un grep -v 'null' afin de dégager de la sortie les attributs JSON non-remplis (genre un câble qui n'a pas de site web associé, par exemple). EDIT DU 21/08/2021 : pour afficher un texte pré-défini quand la valeur d'un attribut JSON est null, jq dispose d'un opérateur de remplacement, « // ». FIN DE L'ÉDIT.

  • Autre avantage : pour faire passer le temps d'exécution, j'affiche un joli compteur avec printf() genre « Working on cables: 001/469… […] 142/469… ». Ça fait bien longtemps que je n'avais pas écrit un code générant un affichage aussi kikoo. :D

La version actuelle du script utilise une seule commande jq (mais avec des filtres supplémentaires) afin de réaliser l'itération, l'enrichissement du JSON et la modification du flux JSON.

  • Cette dernière version s'exécute en 3,6 secondes (conditions de test identiques). 31 fois plus rapide ! :O

  • Inconvénient : la commande jq commence à devenir touffue et très compliquée à comprendre, donc à maintenir dans le temps. Oui, je pourrais virer des variables, mais guère plus, je pense.

  • Le premier avantage de la première version devient un inconvénient : je ne peux plus virer aussi facilement les attributs JSON non-remplis. Probablement que ça peut se faire avec une condition jq mais au prix de la lisibilité. EDIT DU 21/08/2021 : pour afficher un texte pré-défini quand la valeur d'un attribut JSON est null, jq dispose d'un opérateur de remplacement, « // ». FIN DE L'ÉDIT.



Merci Johndescs pour la relecture et la correction des scripts.

How to increase the Android app VLC’s volume to 200% - Quora

Autoriser l'amplification du volume audio sur la version Android de VLC : menu -> Preferences -> Video -> Audio boost.
Durant la lecture d'un média, il suffira d'augmenter le volume comme d'habitude (glisser bas vers haut du côté droit), mais, cette fois-ci, il pourra dépasser 100 %, comme sur la version PC GNU/Linux de VLC.

Attention, pour ce faire, il faut au moins la version 3.0 de VLC (source).
Si vous avez installé VLC avec l'apk distribué sur le site web officiel (car VLC n'était plus disponible sur F-Droid à une époque, par exemple), F-Droid dira que le logiciel est à jour, ce qui n'est pas le cas. Il faut désinstaller VLC puis l'installer avec F-Droid.

Manipuler du JSON avec le logiciel jq

jq (paquet logiciel du même nom) est un outil en ligne de commande pour manipuler du JSON (quelle idée !).

Il m'apparaît que le meilleur moyen d'apprendre à utiliser jq, c'est de pratiquer. C'est ce que nous allons faire dans ce shaarli.

Fichiers utilisés dans les exemples ci-dessous : cables.json et cablesDefs.json.



L'usage le plus simple de jq est de rendre lisible (indentation, coloration syntaxique, etc.) un bout de JSON qui ne l'est pas (on dit aussi prettifyer, forrmatter) : jq '.' cables.json.

  • Note : « . » est l'identité. Elle désigne l'élément / l'objet courant. Au début, elle pointe sur l'objet racine du fichier JSON, mais ça peut évoluer : lors d'une itération dans un tableau, par exemple, l'identité pointera sur l'élément actuellement parcouru.



Afficher la valeur / le contenu de l'attribut « features » de l'entité / objet racine : jq '.features' cables.json.

  • Comme la valeur de cet attribut est un tableau, tout son contenu est affiché. Si l'on avait afficher la valeur de l'attribut « type » de l'objet racine avec jq '.type' cables.json, le retour serait une unique chaîne de caractères.



Afficher uniquement l'attribut « slug » de l'attribut « properties » de chaque élément du tableau features « features » : jq '.features[].properties.slug' cables.json.

  • Sorte de grep.



Afficher l'attribut « name » de l'attribut « properties » de chaque élément du tableau « features ». Afficher « n/a » (not available) si l'attribut n'existe pas ou a « null » comme valeur : jq '.features[].properties.name // "n/a"'.



Notons que jq propose des paramètres intéressants.

  • « -r » (raw) permet de ne pas afficher les guillemets autour d'une chaîne de caractères ;

  • « -a » (ascii) remplace les caractères non-ASCII par leur séquence échappée, ce qui est pratique pour identifier un problème lié à l'encodage.



Ce que nous avons mis entre apostrophes dans les exemples précédents se nomme un filtre. Il est possible d'enchaîner les filtres. Pour ce faire, deux opérateurs existent :

  • « , » exécute les deux filtres et concatène leur résultat (comme « ; » dans un shell). Afficher le contenu de l'attribut « slug » de chaque élément du tableau « features » puis afficher le contenu de leur attribut « color » : jq '.features[].properties.slug , .features[].properties.color' cables.json ;

  • « | » a le même rôle que dans un shell : la sortie du filtre de gauche est l'entrée du filtre de droite. Travailler sur le slug de chaque élément du tableau : jq '.features[].properties.slug | .' cables.json.



Tout peut être stocké dans une variable à tout moment : jq '.features[].properties as $props | .' cables.json.

  • Plus loin dans l'enchaînement de filtres, on pourra utiliser la variable avec $props | <traitement_ici> ou $props.<nom_attribut> (exemple : « $props.color »).

  • Forcément, si le résultat d'un filtre est stocké dans une variable, alors ce filtre enverra rien sur l'entrée d'un filtre qui lui serait chaîné. C'est le résultat du dernier filtre sans stockage dans une variable qui sera injecté dans l'entrée du filtre chaîné.



Si un filtre retourne plusieurs résultats et qu'un ou plusieurs filtres lui sont chaînés, ces derniers filtres s'appliqueront / seront exécutés pour chaque résultat retourné par le premier filtre. Il est donc possible de travailler sur chaque élément d'un tableau. Nous le faisons depuis le début : jq '.features[].properties.slug' cables.json = jq '.features[] | .properties.slug' cables.json. Il est donc possible d'exécuter un traitement sur chaque élément du tableau, comme une sorte de grep | xargs.

  • Comme c'est confusant, je conseille vivement d'écrire tranquillement son filtre sur un seul objet à la fois avec jq '.features[0] | .properties.slug' cables.json (« [0] » demande à travailler sur le premier élément du tableau) avant de le généraliser ;

  • Si l'on fait jq '.features[] | ., c'est-à-dire « affiche-moi chaque élément du tableau « features » », on se rend compte que l'on perd l'entête (« { "type": "FeatureCollection", "features": [ ») et le pied (« ] } ») de notre fichier. Il faut comprendre qu'on les retrouvera jamais, qu'on peut jamais remonter au niveau supérieur. Ce serait comme utiliser grep essai | xargs <maCommande> et vouloir que maCommande accède à ce qui n'a pas été grep-é (retenu par grep). Si l'on tente d'enregistrer la racine dans une variable (« . as $racine ») et que l'on tente de l'afficher dans une itération (exemple : jq '. as $racine | .features[] | .properties.slug | $racine' cables.json), l'ensemble du fichier JSON sera affiché autant de fois qu'il y a d'éléments dans le tableau « features », ce qui n'est pas ce que l'on veut. Bref, n'essaye pas. On ne sort pas d'une itération, tout enchaînement de filtres après un filtre qui émet plusieurs éléments JSON sera exécuté autant de fois qu'il y a d'éléments ;

  • En revanche, on peut contenir une telle itération au sein d'une opération bornée, comme une affectation. Ainsi, tout le résultat produit par l'enchaînement des filtres de l'itération sera affecté à une variable. Nous verrons selon un peu plus loin, car il nous manque encore des connaissances.



Recherche sur la valeur d'un attribut / afficher l'élément du tableau « features » dont la valeur de l'attribut « slug » est « adria-1 » : jq '.features[] | select(.properties.slug=="adria-1")' cables.json.

  • Afficher uniquement la valeur de l'attribut « properties.color » de cet objet ? jq '.features[] | select(.properties.slug=="adria-1") | .properties.color' cables.json ou, plus simplement : jq '.features[] | select(.properties.slug=="adria-1").properties.color' cables.json.

  • Rechercher une sous-chaîne dans la valeur d'un attribut avec une regex ? jq '.features[] | select(.properties.slug | test("adria-1")). Il est possible de positionner des drapeaux pour des recherches gourmandes (greedy), pour des recherches insensibles à la case, etc. Exemple concret d'utilisation.



On peut mettre en forme /formater la sortie (afficher plusieurs attributs, ajouter du texte libre, etc.). Pour chaque élément du tableau « features », je veux afficher une ligne de la forme \*\*SLUG\*\*: <slug> ~ \*COLOR\*: <color>.. Les « * » sont du formatage Markdown (**gras**, *italique*). Le texte en majuscule, le caractère « ~ » et le caractère final (« . ») sont des bouts de texte arbitraires que je souhaite afficher. jq '.features[] | "**SLUG**: \(.properties.slug) ~ *COLOR*: \(.properties.color)."' cables.json.

  • Les guillemets permettent de saisir des chaînes de caractères. « \() » est l'opérateur qui indique à jq d'interpréter une expression située au milieu d'une chaîne de caractères.



Et si je veux utiliser le contenu d'une variable shell dans un filtre ?

  • Normalement, on écrit un filtre jq entre apostrophes afin que le shell n'interprète pas la chaîne de caractères. Mais, on peut aussi l'écrire entre guillemets afin quil l'interpète. Exemple : maVariable='{ "test": "toto" }' ; jq ".features[].properties += $maVariable" cables.json. Cela signifie aussi que tu vas devoir échapper tout ce que le shell pourrait interpréter : le « $ » d'une variable interne à jq, les guillemets, etc. Pour peu que ton enchaînement de filtres jq soit un peu costaud, ça va le rendre illisible. Je déconseille ;

  • jq propose les arguments --arg <nom_variable_interne> <contenu> (le contenu sera enregistré comme une chaîne de caractères) et --argjson <nom_variable_interne> <bout_de_JSON> (attention : le bout de JSON doit avoir une syntaxe valide !). Reprise de l'exemple précédent : maVariable='{ "test": "toto" }' ; jq --argjson varInterne "$maVariable" '.features[].properties += $varInterne' cables.json.



Et si l'on veut modifier le flux JSON ? Il existe les opérateurs « |= » et « += ». Le premier modifie la valeur d'un attribut en écrasant l'exisant. Le second ajoute un élément dans la valeur d'un attribut.

  • Je veux changer / remplacer la valeur de l'attribut « properties.color » de chaque élément du tableau « features » pour « yellow » : jq '.features[] | .properties.color |= "yellow"' cables.json ;

  • Je veux ajouter un attribut « toto » avec la valeur « titi » dans l'attribut « properties » de chaque élément du tableau « features » : jq '.features[] | .properties += { "toto": "titi" } ' cables.json ;

  • Je veux renommer l'attribut « properties.color » de chaque élément du tableau « features » en « properties.toto » tout en conservant sa valeur : jq '.features[] | .properties += { "toto": .properties.color } ' cables.json ;

  • Je veux déplacer l'attribut « properties.color » de chaque élément du tableau « features » dans un nouvel attribut « properties.toto » tout en conservant sa valeur : jq '.features[] | .properties += { "toto": { "color": .properties.color } }' cables.json.



Normalement, si tu suis, tu me traites de tricheur : les deux dernières commandes du point précédent n'ont pas renommé / déplacé l'attribut, mais l'ont simplement copié. Pour finir le travail, jq propose la fonction native « del() » qui, comme son nom l'indique, permet de supprimer un attribut. Démo sur les deux exemples précédents : jq '.features[] | .properties += { "toto": .properties.color } | del(.properties.color)' cables.json et jq '.features[] | .properties += { "toto": { "color": .properties.color } } | del(.properties.color)' cables.json.



jq ne propose pas un argument (genre « -o ») permettant d'enregistrer le résultat de son traitement dans un fichier. Donc, si l'on veut enregistrer le résultat dans le fichier source / d'origine, il faut utiliser l'outil sponge (une redirection shell, >, viderait le fichier avant d'exécuter jq).



Il est possible de travailler sur plusieurs fichiers JSON en même temps. Soit en les concaténant (cat fichier1 fichier2 | jq '.'). Oui, ça va générer un fichier JSON invalide (plusieurs entités racines…), mais jq est tolérant. Soit en ayant un fichier principal et des fichiers secondaires accessibles via des variables. Cette deuxième possibilité est très pratique quand on veut agrémenter / enrichir un fichier principal avec des informations disponibles dans un autre fichier (sans polluer le fichier principal avec une concaténation, donc).

  • Exemple pratique. Le fichier cables.json contient les coordonnées géographiques des éléments et le fichier cablesDefs.json contient des informations sur ces éléments (comme leur nom). Dans le fichier cablesDefs, on veut récupérer le nom de chaque élément du fichier cables.json afin de l'insérer dans un attribut « name » dans l'attribut « properties » de l'élément. On veut donc enrichir cables.json avec cablesDefs.json. Notons que les éléments ne sont pas forcément dans le même ordre dans les deux fichiers. Il y a une correspondance id = slug. Démo : jq --slurpfile maVariable cablesDefs.json '.features[] | . as $cable | $maVariable[] | select(.id==$cable.properties.slug).name as $cableName | $cable | .properties += { "name": $cableName }' cables.json.

  • Explication : pour chaque élément du tableau « features », on le stocke dans la variable $cable puis, dans le contenu de maVariable (donc du fichier cableDefs.json, dont chaque élément racine a été importé comme case d'un tableau), on cherche un objet dont l'attribut « id » correspond à l'attribut « slug » de notre élément, on stocke la valeur de l'attribut « nom » de cet objet dans $cableName. On affiche l'élément de notre tableau « features » et on ajoute un attribut dans son attribut « properties ».



Plus haut, j'écris que si l'on itère sur le contenu d'un attribut avec jq, alors on perd les attributs qui entourent celui sur lequel on itère. Avec ce "mode multi-fichiers", on peut reconstruire le JSON d'origine (sauf si trop d'élements sont passés à la trappe, bien entendu). Exemple (je reprends le cas cité ci-dessus malgré son absence de représentativité) :

jq '.features[] | .' cables.json > /tmp/json.tmp # dans json.tmp, on aurait donc uniquement les éléments du tableau « features », tout le reste aura disparu
cat <<EOF > /tmp/minJSONStruct # dans ce fichier, on met l'entête et le pied du fichier d'origine (cables.json), précisement ce qui a disparu
{
  "type": "FeatureCollection",
  "features": [
  ]
}
EOF
jq --slurpfile featuresList /tmp/json.tmp '.features += $featuresList' /tmp/minJSONStruct > cables.json # on ajoute les ex-éléments du tableau « features » au tableau « features » de l'entête/pied JSON, donc on reconstitue notre fichier cables.json d'origine.

Oui, je préfère passer par un fichier temporaire que par une variable (avec « --arg ») afin d'éviter les problèmes d'échappement de caractères pour le shell.

On peut aussi préserver la structure JSON autour du tableau. Pour ce faire, il faut contenir l'itération sur le tableau au sein d'une affectation. Exemple : pour renommer l'attribut « properties.color » d'un objet membre d'un tableau en « properties.toto », il faut que j'itère sur ce tableau sinon je ne peux pas récupérer la valeur de l'attribut afin de la transvaser. Pour que cette opération ne soit pas destructrice, je l'encadre dans une affectation : jq '. as $racine | .featuresDup |= $racine.features | del(.features) | .features += [ .featuresDup[] | .properties += { "toto": .properties.color } | del(.properties.color) ] | del(.featuresDup)' cables.json. Si je n'avais pas dupliqué l'attribut « features », alors ce tableau contiendrait deux fois chaque élément : l'original et la version modifiée. Cette copie me permet de vider l'attribut « features » et d'utiliser sa copie pour itérer dessus et remplir « features » avec les objets modifiés.



Appliquer le même traitement à tous les éléments d'un tableau. Pour chaque élément du fichier cablesDefs.json, je veux récupérer une unique chaîne de caractères contenant tous les éléments du tableau « landing_points » sous la forme * <nom1>: <latitude+longitude>\n * <nom2>: <latitude+longitude>\n […]. Premier essai : jq '.landing_points[] | " * \(.name): \(.latlon)"' cablesDefs.json. Bien vu, mais on ne peut pas obtenir une unique chaîne avec join() puisque ce qui ressort du filtre, c'est des chaînes de caractères et qu'un join() ne peut être appliqué sur ce type d'objets.

  • Comme dans beaucoup de langage de programmation (Python, Ruby, etc.), la fonction map() permet d'appliquer un même traitement à chaque élément d'un tableau, donc de produire un tableau en sortie… tableau sur lequel on pourra utiliser join(). Démo : jq '.landing_points | map(" * \(.name) :\(.latlon)") | join("\n")' cablesDefs.json.



Il reste encore beaucoup de choses à découvrir : les boucles, les conditions, les fonctions persos, etc. Voir le manuel officiel de jq. Pour ma part, j'ai pu réaliser ce que je voulais (construire une carte des câbles sous-marins d'Internet) donc je m'arrête là.

Bloquer autre chose (des IPs, des ports, etc.) que des applications Android avec AFWall+

Sur mon ordinateur de poche Android, j'utilise le pare-feu Android AFWall+ (en fait, c'est juste une interface graphique pour le pare-feu natif du noyau Linux, Netfilter).
Ça me permet de contrôler quelles applications peuvent émettre et sur quel(s) réseau(x) (Wi-Fi, 4G, VPN) elles le peuvent.

J'utilise également un VPN de confiance (avec le logiciel Android OpenVPN For Android qui n'est pas l'implémentation officielle du projet OpenVPN) afin de me protéger des craderies de mon opérateur mobile lorsque je suis en 4G (on a vu Bouygues Telecom modifier les réponses DNS, on a vu SFR modifier les réponses web, etc.) et des points d'accès Wi-Fi malveillants (notamment ceux tenus par des potes farceurs). Mais également pour ne pas dévoiler ma vie privée à des gens en qui je n'ai pas confiance (opérateurs mobiles, fournisseurs de Wi-Fi ouverts, etc.).

Lorsqu'on utilise un VPN dans un tel contexte, le minimum est d'interdire tout trafic qui voudrait sortir en dehors du VPN, sauf le VPN lui-même et DHCP.
De même, lorsqu'un VPN est établi, il convient d'interdire tout trafic DNS sortant vers les serveurs DNS récursifs de l'opérateur mobile / fournisseur Wi-Fi, car certaines applications les utilisent au lieu de ceux proposés par le service de VPN, ce qui fait fuiter une partie de la vie privée.



AFWall+ permet uniquement d'autoriser / bloquer des logiciels. Le premier besoin est satisfait (bloquer tout en sortie, autoriser quelques logiciels via le VPN), pas le deuxième. Pour le satisfaire, il faut utiliser des scripts personnalisés.

On peut saisir un script perso directement dans l'interface d'AFWall+, sauf que c'est pénible et long. Je choisis donc de l'écrire dans un fichier séparé. Voici mon script pour bloquer le trafic à destination des serveurs DNS récursifs de Numericable :

#!/system/bin/sh

# Necessary at the beginning of each script!
IPTABLES=/system/bin/iptables

# Now add your own rules...
$IPTABLES -I "afwall-vpn" -d 89.2.0.1/32 -j "afwall-reject"
$IPTABLES -I "afwall-vpn" -d 89.2.0.2/32 -j "afwall-reject"

# Don't
# exit 0

Quelques commentaires :

  • Ne pas oublier le shebang ;

  • La variable « IPTABLES » (et son pendant IPv6 « IP6TABLES ») n'est pas obligatoire, elle ajoute de la simplicité si le chemin vers le binaire iptables change un jour ;

  • Par défaut, AFWall+ exécute le script avant d'insérer ses règles de blocage d'applications. Donc, les règles du script seront ajoutées dans une chaîne avant qu'une application soit autorisée par la même chaîne. C'est bien ce que nous voulons : si les règles étaient ajoutées après, elles seraient inefficaces, l'application ayant été autorisée à émettre par une règle précédente. Par défaut, tout est donc OK. Néanmoins, je décide d'insérer mes règles en début de chaîne (« -I ») plutôt qu'à la fin (« -A ») afin de me prémunir de tout changement futur de comportement d'AFWall+ qui rendrait caduque mon filtrage ;

  • La chaîne « afwall-vpn » s'applique uniquement à ce qui sort par un VPN. D'après la doc', il y existe également les chaînes « afwall-wifi », « afwall-3g », etc. Toutes ne sont pas documentées. Donc je recommande d'exécuter un « /system/bin/iptables -L -n -v » depuis un shell root (commande su) Android (j'utilise le classique Terminal Emulator pour y accéder) pour voir les chaînes disponibles à un instant T ;

  • Je pourrais être plus fin et bloquer uniquement le protocole DNS (udp/53 et tcp/53) à destination de 89.2.0.[1-2], mais ces machines Numericable n'ont probablement pas d'autres usages, et sûrement pas d'usages qui m'intéressent. Donc, autant faire simple et performant en bloquant par IP. Je pourrais aussi bloquer tout trafic DNS qui n'est pas à destination des serveurs DNS récursifs de mon fournisseur de VPN, ce qui serait beaucoup plus sain et sécurisé (aucun risque d'oubli, quel que soit le réseau auquel je suis connecté), sauf que je veux me laisser la possibilité d'utiliser d'autres récursifs DNS au besoin ;

  • Je rappelle que, dans ce contexte, pour un confort utilisateur, il vaut mieux rejeter explicitement le trafic plutôt que de le jeter silencieusement. L'utilisation de la chaîne « afwall-reject » permet en sus la journalisation, utile en cas de problèmes ;

  • set -e (voir ici) ne semble pas être disponible. Dommage. Plus surprenant, un « exit 0 » en fin de script pour signifier que tout s'est bien passé conduit AFWall+ à déclarer que l'application des règles de filtrage a échoué…



Avec le gestionnaire de fichiers Android Ghost commander, je déplace ce script depuis ma carte SD vers le dossier /storage/emulated/0/afwall. Pourquoi ? 1) Je n'ai pas trouvé de dossier nommé à peu près afwall dans /data/data ; 2) C'est ici qu'AFWall+ stocke ses exportations de règles (menu -> « Export ») donc, c'est lui qui a créé le dossier, donc, en l'utilisant, je me prémunis de problèmes de droits d'accès.

Plutôt qu'une carte SD, j'aurais pu utiliser adb, oui.

Ne pas laisser ce script sur la carte SD, car, si elle n'est pas insérée lors d'un démarrage de l'ordiphone, alors les règles de filtrage ne seront pas appliquées, donc le pare-feu sera désactivé.

Dans le menu d'AFWall+ -> « Set custom script » -> « Enter custom script below », je saisis . /storage/emulated/0/afwall/<nom_script>. Attention au point au début !

Menu AFWall+ -> « Apply ». C'est terminé. \o/

-